Skip to main content

hashiverse_lib/protocol/rpc/
rpc.rs

1//! # High-level RPC client helpers
2//!
3//! The user-facing side of the RPC layer. Where
4//! [`crate::protocol::rpc::rpc_request`] and
5//! [`crate::protocol::rpc::rpc_response`] deal with packet bytes, this module exposes
6//! end-to-end helpers that:
7//!
8//! 1. **Encode** the request with the right PoW difficulty (higher when the peer is
9//!    unknown, lower when the peer is already in the tracker — see
10//!    [`rpc_server_known_with_requisite_pow`]).
11//! 2. **Dispatch** over the supplied transport.
12//! 3. **Decode** the response, verifying the signature over the request's
13//!    `pow_content_hash` (salted by the server) — so a reply can only have come from the
14//!    specific server we addressed, not a replay from another peer.
15//! 4. **Return** typed payloads or an `ErrorResponseV1`.
16//!
17//! Call sites either use [`rpc_server_unknown`] (first contact / bootstrap) or
18//! [`rpc_server_known`] (everything else). Trust-sensitive handlers can opt into a
19//! higher PoW floor via the `_with_requisite_pow` variant.
20
21use bytes::Bytes;
22use crate::protocol::payload::payload::{ErrorResponseV1, PayloadRequestKind, PayloadResponseKind};
23use crate::protocol::peer::Peer;
24use crate::protocol::rpc::rpc_request::{RpcRequestPacketTx, RpcRequestPacketTxFlags};
25use crate::protocol::rpc::rpc_response::RpcResponsePacketRx;
26use crate::tools::runtime_services::RuntimeServices;
27use crate::tools::types::{Id, PQCommitmentBytes, Pow, VerificationKeyBytes};
28use crate::tools::{config, json};
29
30pub async fn rpc_server_unknown(
31    runtime_services: &RuntimeServices,
32    sponsor_id: &Id,
33    destination_address: &str,
34    payload_request_kind: PayloadRequestKind,
35    payload: Bytes,
36) -> anyhow::Result<RpcResponsePacketRx> {
37    let destination_id = Id::zero();
38    let destination_verification_key = VerificationKeyBytes::zero();
39    let destination_pq_commitment_bytes = PQCommitmentBytes::zero();
40    let flags = RpcRequestPacketTxFlags::COMPRESSED;
41    rpc_server_xxx(
42        runtime_services,
43        config::POW_MINIMUM_PER_RPC_SERVER_UNKNOWN,
44        flags,
45        sponsor_id,
46        destination_address,
47        &destination_id,
48        &destination_verification_key,
49        &destination_pq_commitment_bytes,
50        payload_request_kind,
51        payload,
52    )
53    .await
54}
55
56pub async fn rpc_server_known(
57    runtime_services: &RuntimeServices,
58    sponsor_id: &Id,
59    destination_peer: &Peer,
60    payload_request_kind: PayloadRequestKind,
61    payload: Bytes,
62) -> anyhow::Result<RpcResponsePacketRx> {
63    let flags = RpcRequestPacketTxFlags::COMPRESSED | RpcRequestPacketTxFlags::SERVER_KNOWN;
64    rpc_server_xxx(
65        runtime_services,
66        config::POW_MINIMUM_PER_RPC_SERVER_KNOWN,
67        flags,
68        sponsor_id,
69        &destination_peer.address,
70        &destination_peer.id,
71        &destination_peer.verification_key_bytes,
72        &destination_peer.pq_commitment_bytes,
73        payload_request_kind,
74        payload,
75    )
76    .await
77}
78
79pub async fn rpc_server_known_with_no_compression(
80    runtime_services: &RuntimeServices,
81    sponsor_id: &Id,
82    destination_peer: &Peer,
83    payload_request_kind: PayloadRequestKind,
84    payload: Bytes,
85) -> anyhow::Result<RpcResponsePacketRx> {
86    let flags = RpcRequestPacketTxFlags::SERVER_KNOWN;
87    rpc_server_xxx(
88        runtime_services,
89        config::POW_MINIMUM_PER_RPC_SERVER_KNOWN,
90        flags,
91        sponsor_id,
92        &destination_peer.address,
93        &destination_peer.id,
94        &destination_peer.verification_key_bytes,
95        &destination_peer.pq_commitment_bytes,
96        payload_request_kind,
97        payload,
98    )
99    .await
100}
101
102pub async fn rpc_server_known_with_requisite_pow(
103    runtime_services: &RuntimeServices,
104    sponsor_id: &Id,
105    destination_peer: &Peer,
106    payload_request_kind: PayloadRequestKind,
107    payload: Bytes,
108    requisite_pow: Pow,
109) -> anyhow::Result<RpcResponsePacketRx> {
110    let flags = RpcRequestPacketTxFlags::COMPRESSED | RpcRequestPacketTxFlags::SERVER_KNOWN;
111    rpc_server_xxx(
112        runtime_services,
113        requisite_pow,
114        flags,
115        sponsor_id,
116        &destination_peer.address,
117        &destination_peer.id,
118        &destination_peer.verification_key_bytes,
119        &destination_peer.pq_commitment_bytes,
120        payload_request_kind,
121        payload,
122    )
123    .await
124}
125
126pub async fn rpc_server_known_with_requisite_pow_and_no_compression(
127    runtime_services: &RuntimeServices,
128    sponsor_id: &Id,
129    destination_peer: &Peer,
130    payload_request_kind: PayloadRequestKind,
131    payload: Bytes,
132    requisite_pow: Pow,
133) -> anyhow::Result<RpcResponsePacketRx> {
134    let flags = RpcRequestPacketTxFlags::SERVER_KNOWN;
135    rpc_server_xxx(
136        runtime_services,
137        requisite_pow,
138        flags,
139        sponsor_id,
140        &destination_peer.address,
141        &destination_peer.id,
142        &destination_peer.verification_key_bytes,
143        &destination_peer.pq_commitment_bytes,
144        payload_request_kind,
145        payload,
146    )
147    .await
148}
149
150#[allow(clippy::too_many_arguments)] // protocol layer — each arg is part of the RPC envelope; bundling into a struct would just rename them
151async fn rpc_server_xxx(
152    runtime_services: &RuntimeServices,
153    pow_minimum_per_rpc: Pow,
154    flags: RpcRequestPacketTxFlags,
155    sponsor_id: &Id,
156    destination_address: &str,
157    destination_id: &Id,
158    destination_verification_key_bytes: &VerificationKeyBytes,
159    destination_pq_commitment_bytes: &PQCommitmentBytes,
160    payload_request_kind: PayloadRequestKind,
161    payload: Bytes,
162) -> anyhow::Result<RpcResponsePacketRx> {
163    let rpc_request_packet = RpcRequestPacketTx::encode(runtime_services.time_provider.as_ref(), pow_minimum_per_rpc, flags, payload_request_kind, sponsor_id, destination_verification_key_bytes, destination_pq_commitment_bytes, payload, runtime_services.pow_generator.as_ref()).await?;
164
165    let response_bytes = runtime_services.transport_factory.rpc(destination_address, rpc_request_packet.bytes).await?;
166
167    let rpc_response_packet = RpcResponsePacketRx::decode(destination_id, &rpc_request_packet.pow_content_hash, config::SERVER_KEY_POW_MIN, response_bytes)?;
168
169    // Check if there was a server error
170    if rpc_response_packet.response_request_kind == PayloadResponseKind::ErrorResponseV1 {
171        let response = json::bytes_to_struct::<ErrorResponseV1>(&rpc_response_packet.bytes)?;
172        return Err(anyhow::anyhow!("server error {}: {}", response.code, response.message));
173    }
174
175    Ok(rpc_response_packet)
176}
177
178#[cfg(test)]
179mod tests {
180    use std::sync::Arc;
181    use bytes::Bytes;
182    use crate::protocol::payload::payload::{PayloadRequestKind, PayloadResponseKind};
183    use crate::protocol::rpc::rpc_request::{RpcRequestPacketRx, RpcRequestPacketTx, RpcRequestPacketTxFlags};
184    use crate::protocol::rpc::rpc_response::{RpcResponsePacketRx, RpcResponsePacketTx, RpcResponsePacketTxFlags};
185    use crate::tools::pow_generator::single_threaded_pow_generator::SingleThreadedPowGenerator;
186    use crate::tools::server_id::ServerId;
187    use crate::tools::time_provider::time_provider::RealTimeProvider;
188    use crate::tools::tools;
189    use crate::tools::{types::{Id, PQCommitmentBytes, Pow, VerificationKeyBytes}, BytesGatherer};
190    use log::trace;
191    use crate::tools::runtime_services::RuntimeServices;
192
193    #[tokio::test]
194    async fn rpc_request_packet_txrx() -> anyhow::Result<()> {
195        let runtime_services = RuntimeServices::default_for_testing();
196
197        let pow_min = Pow(12);
198        let pow_minimum_per_rpc = Pow(12);
199
200        let server_id = ServerId::new("own_pow", runtime_services.time_provider.as_ref(), pow_min, true, runtime_services.pow_generator.as_ref()).await?;
201        trace!("server_id.pow={}", server_id.pow);
202        let payload_request_kind = PayloadRequestKind::PingV1;
203        let mut payload_request = [0u8; 1024];
204        tools::random_fill_bytes(&mut payload_request);
205        let request_flags = RpcRequestPacketTxFlags::COMPRESSED | RpcRequestPacketTxFlags::SERVER_KNOWN;
206
207        let rpc_request_packet_tx = RpcRequestPacketTx::encode(
208            runtime_services.time_provider.as_ref(),
209            pow_minimum_per_rpc,
210            request_flags,
211            payload_request_kind.clone(),
212            &server_id.id,
213            &server_id.keys.verification_key_bytes,
214            &server_id.keys.pq_commitment_bytes,
215            Bytes::copy_from_slice(&payload_request),
216            runtime_services.pow_generator.as_ref(),
217        )
218        .await?;
219
220        let rpc_request_packet_rx = RpcRequestPacketRx::decode(&server_id.timestamp, &server_id.keys.verification_key_bytes, &server_id.keys.pq_commitment_bytes, rpc_request_packet_tx.bytes)?;
221        assert_eq!(payload_request_kind, rpc_request_packet_rx.payload_request_kind);
222        assert_eq!(payload_request, rpc_request_packet_rx.bytes.as_ref());
223        assert!(rpc_request_packet_rx.pow_server_known);
224
225        let mut payload_response = [0u8; 1024];
226        tools::random_fill_bytes(&mut payload_response);
227        let response_flags = RpcResponsePacketTxFlags::COMPRESSED;
228
229        let gatherer = RpcResponsePacketTx::encode(
230            &server_id.keys.signature_key,
231            &server_id.keys.verification_key_bytes,
232            &server_id.keys.pq_commitment_bytes,
233            &server_id.sponsor_id,
234            &server_id.timestamp,
235            &server_id.hash,
236            &server_id.salt,
237            &rpc_request_packet_rx.pow_content_hash,
238            response_flags,
239            PayloadResponseKind::PingResponseV1,
240            BytesGatherer::from_bytes(Bytes::copy_from_slice(&payload_response)),
241        )?;
242
243        let rpc_response_packet_rx = RpcResponsePacketRx::decode(&server_id.id, &rpc_request_packet_rx.pow_content_hash, pow_min, gatherer.to_bytes())?;
244
245        assert_eq!(rpc_response_packet_rx.response_request_kind, PayloadResponseKind::PingResponseV1);
246        assert_eq!(rpc_response_packet_rx.bytes.as_ref(), payload_response);
247
248        Ok(())
249    }
250
251    #[tokio::test]
252    async fn rpc_request_packet_txrx_server_unknown() -> anyhow::Result<()> {
253        let time_provider = RealTimeProvider;
254        let pow_generator: Arc<dyn crate::tools::pow_generator::pow_generator::PowGenerator> = Arc::new(SingleThreadedPowGenerator::new());
255
256        let pow_min_for_server_id = Pow(12);
257        let pow_min_for_rpc = Pow(12);
258
259        let server_id = ServerId::new("own_pow", &time_provider, pow_min_for_server_id, true, pow_generator.as_ref()).await?;
260        let payload_request_kind = PayloadRequestKind::PingV1;
261        let mut payload_request = [0u8; 1024];
262        tools::random_fill_bytes(&mut payload_request);
263        let flags = RpcRequestPacketTxFlags::COMPRESSED;
264
265        let rpc_request_packet_tx = RpcRequestPacketTx::encode(&time_provider, pow_min_for_rpc, flags, payload_request_kind.clone(), &Id::zero(), &VerificationKeyBytes::zero(), &PQCommitmentBytes::zero(), Bytes::copy_from_slice(&payload_request), pow_generator.as_ref()).await?;
266
267        let rpc_request_packet_rx = RpcRequestPacketRx::decode(&server_id.timestamp, &server_id.keys.verification_key_bytes, &server_id.keys.pq_commitment_bytes, rpc_request_packet_tx.bytes)?;
268        assert_eq!(payload_request_kind, rpc_request_packet_rx.payload_request_kind);
269        assert_eq!(payload_request, rpc_request_packet_rx.bytes.as_ref());
270        assert!(!rpc_request_packet_rx.pow_server_known);
271
272        let mut payload_response = [0u8; 1024];
273        tools::random_fill_bytes(&mut payload_response);
274        let response_flags = RpcResponsePacketTxFlags::COMPRESSED;
275        let gatherer = RpcResponsePacketTx::encode(
276            &server_id.keys.signature_key,
277            &server_id.keys.verification_key_bytes,
278            &server_id.keys.pq_commitment_bytes,
279            &server_id.sponsor_id,
280            &server_id.timestamp,
281            &server_id.hash,
282            &server_id.salt,
283            &rpc_request_packet_rx.pow_content_hash,
284            response_flags,
285            PayloadResponseKind::PingResponseV1,
286            BytesGatherer::from_bytes(Bytes::copy_from_slice(&payload_response)),
287        )?;
288
289        let rpc_response_packet_rx = RpcResponsePacketRx::decode(&Id::zero(), &rpc_request_packet_rx.pow_content_hash, pow_min_for_server_id, gatherer.to_bytes())?;
290
291        assert_eq!(rpc_response_packet_rx.response_request_kind, PayloadResponseKind::PingResponseV1);
292        assert_eq!(rpc_response_packet_rx.bytes.as_ref(), payload_response);
293
294        Ok(())
295    }
296
297    // ── Salted response signature (C1: servers must not be a signing oracle) ──
298
299    use crate::protocol::rpc::rpc_response::RPC_RESPONSE_PACKET_VERSION;
300    use crate::tools::types::{Hash, Signature, Salt, HASH_BYTES, ID_BYTES, PQ_COMMITMENT_BYTES, SALT_BYTES, SIGNATURE_BYTES, VERIFICATION_KEY_BYTES};
301    use crate::tools::time::TIME_MILLIS_BYTES;
302
303    const RESPONSE_SIGNATURE_OFFSET: usize = 1 + 1 + 2 + VERIFICATION_KEY_BYTES + PQ_COMMITMENT_BYTES + ID_BYTES + TIME_MILLIS_BYTES + HASH_BYTES + SALT_BYTES;
304    const RESPONSE_SALT_OFFSET: usize = RESPONSE_SIGNATURE_OFFSET + SIGNATURE_BYTES;
305
306    fn encode_test_response(server_id: &ServerId, pow_content_hash: &Hash) -> anyhow::Result<Bytes> {
307        let bytes_gatherer = RpcResponsePacketTx::encode(
308            &server_id.keys.signature_key,
309            &server_id.keys.verification_key_bytes,
310            &server_id.keys.pq_commitment_bytes,
311            &server_id.sponsor_id,
312            &server_id.timestamp,
313            &server_id.hash,
314            &server_id.salt,
315            pow_content_hash,
316            RpcResponsePacketTxFlags::COMPRESSED,
317            PayloadResponseKind::ErrorResponseV1,
318            BytesGatherer::from_bytes(Bytes::from_static(b"{}")),
319        )?;
320        Ok(bytes_gatherer.to_bytes())
321    }
322
323    fn extract_response_signature(response_bytes: &Bytes) -> anyhow::Result<Signature> {
324        Ok(Signature(response_bytes[RESPONSE_SIGNATURE_OFFSET..RESPONSE_SIGNATURE_OFFSET + SIGNATURE_BYTES].try_into()?))
325    }
326
327    fn extract_response_salt(response_bytes: &Bytes) -> anyhow::Result<Salt> {
328        Ok(Salt(response_bytes[RESPONSE_SALT_OFFSET..RESPONSE_SALT_OFFSET + SALT_BYTES].try_into()?))
329    }
330
331    async fn test_server_id(runtime_services: &RuntimeServices) -> anyhow::Result<ServerId> {
332        ServerId::new("own_pow", runtime_services.time_provider.as_ref(), Pow(8), true, runtime_services.pow_generator.as_ref()).await
333    }
334
335    #[tokio::test]
336    async fn rpc_response_signature_is_not_a_signing_oracle_for_peer() -> anyhow::Result<()> {
337        let runtime_services = RuntimeServices::default_for_testing();
338        let server_id = test_server_id(&runtime_services).await?;
339
340        // Attacker forges a Peer record for the server: their own address, far-future timestamp,
341        // and the server's genuine pows copied from its real record.
342        let mut forged_peer = server_id.to_peer(runtime_services.time_provider.as_ref())?;
343        forged_peer.address = "6.6.6.6:666".to_string();
344        forged_peer.timestamp = TimeMillis(i64::MAX / 2);
345        let forged_peer_digest = forged_peer.signature_hash_generate()?;
346
347        // Sanity: a signature over the raw digest (the old, unsalted behaviour) would forge it.
348        {
349            let mut unsalted_forged_peer = forged_peer.clone();
350            unsalted_forged_peer.signature = crate::tools::signing::sign(&server_id.keys.signature_key, forged_peer_digest.as_ref());
351            assert!(unsalted_forged_peer.verify().is_ok(), "test setup: raw-digest signature should forge the peer");
352        }
353
354        // The attacker submits the forged digest as pow_content_hash and harvests the response signature.
355        let response_bytes = encode_test_response(&server_id, &forged_peer_digest)?;
356        forged_peer.signature = extract_response_signature(&response_bytes)?;
357        assert!(forged_peer.verify().is_err(), "response signature must not be usable as a Peer signature");
358
359        // ...while the response itself still verifies for the honest caller.
360        RpcResponsePacketRx::decode(&server_id.id, &forged_peer_digest, Pow(8), response_bytes)?;
361        Ok(())
362    }
363
364    #[tokio::test]
365    async fn rpc_response_signature_is_not_a_signing_oracle_for_submit_post_claim_token() -> anyhow::Result<()> {
366        use crate::protocol::payload::payload::SubmitPostClaimTokenV1;
367        use crate::tools::buckets::{BucketLocation, BucketType, BUCKET_DURATIONS};
368
369        let runtime_services = RuntimeServices::default_for_testing();
370        let server_id = test_server_id(&runtime_services).await?;
371        let peer = server_id.to_peer(runtime_services.time_provider.as_ref())?;
372        let bucket_location = BucketLocation::new(BucketType::User, Id::random(), BUCKET_DURATIONS[0], TimeMillis(1_000_000))?;
373        let post_id = Id::random();
374
375        // Mirrors SubmitPostClaimTokenV1::get_hash_for_signing.
376        let claim_token_digest = hashing::hash_multiple(&[peer.id.as_ref(), bucket_location.get_hash_for_signing().as_ref(), post_id.as_ref(), "SubmitPostClaimTokenV1".as_bytes()]);
377
378        // Sanity: the genuine token's signature is exactly sign(claim_token_digest), so the mirror above is correct.
379        let genuine_claim_token = SubmitPostClaimTokenV1::new(peer.clone(), bucket_location.clone(), post_id, &server_id.keys.signature_key);
380        assert_eq!(genuine_claim_token.token_signature, crate::tools::signing::sign(&server_id.keys.signature_key, claim_token_digest.as_ref()));
381
382        let response_bytes = encode_test_response(&server_id, &claim_token_digest)?;
383        let forged_claim_token = SubmitPostClaimTokenV1 { peer, bucket_location, post_id, token_signature: extract_response_signature(&response_bytes)? };
384        assert!(forged_claim_token.verify().is_err(), "response signature must not be usable as a claim token signature");
385        Ok(())
386    }
387
388    #[tokio::test]
389    async fn rpc_response_salt_or_signature_tamper_is_rejected() -> anyhow::Result<()> {
390        let runtime_services = RuntimeServices::default_for_testing();
391        let server_id = test_server_id(&runtime_services).await?;
392        let pow_content_hash = hashing::hash(b"some request");
393        let response_bytes = encode_test_response(&server_id, &pow_content_hash)?;
394
395        RpcResponsePacketRx::decode(&server_id.id, &pow_content_hash, Pow(8), response_bytes.clone())?;
396
397        for tamper_offset in [RESPONSE_SALT_OFFSET, RESPONSE_SALT_OFFSET + SALT_BYTES - 1, RESPONSE_SIGNATURE_OFFSET, RESPONSE_SALT_OFFSET - 1] {
398            let mut tampered_response_bytes = response_bytes.to_vec();
399            tampered_response_bytes[tamper_offset] ^= 0x01;
400            assert!(RpcResponsePacketRx::decode(&server_id.id, &pow_content_hash, Pow(8), Bytes::from(tampered_response_bytes)).is_err(), "tamper at offset {} must be rejected", tamper_offset);
401        }
402        Ok(())
403    }
404
405    #[tokio::test]
406    async fn rpc_response_old_version_is_rejected() -> anyhow::Result<()> {
407        let runtime_services = RuntimeServices::default_for_testing();
408        let server_id = test_server_id(&runtime_services).await?;
409        let pow_content_hash = hashing::hash(b"some request");
410        let response_bytes = encode_test_response(&server_id, &pow_content_hash)?;
411        assert_eq!(response_bytes[0], RPC_RESPONSE_PACKET_VERSION);
412
413        let mut old_version_response_bytes = response_bytes.to_vec();
414        old_version_response_bytes[0] = 1;
415        let error = RpcResponsePacketRx::decode(&server_id.id, &pow_content_hash, Pow(8), Bytes::from(old_version_response_bytes)).err().expect("version 1 must be rejected");
416        assert!(error.to_string().contains("Unsupported RpcResponsePacket version: 1"), "unexpected error: {}", error);
417        Ok(())
418    }
419
420    #[tokio::test]
421    async fn rpc_response_salt_is_fresh_per_response() -> anyhow::Result<()> {
422        let runtime_services = RuntimeServices::default_for_testing();
423        let server_id = test_server_id(&runtime_services).await?;
424        let pow_content_hash = hashing::hash(b"some request");
425
426        let response_bytes_1 = encode_test_response(&server_id, &pow_content_hash)?;
427        let response_bytes_2 = encode_test_response(&server_id, &pow_content_hash)?;
428        assert_ne!(extract_response_salt(&response_bytes_1)?, extract_response_salt(&response_bytes_2)?);
429        assert_ne!(extract_response_signature(&response_bytes_1)?, extract_response_signature(&response_bytes_2)?);
430
431        RpcResponsePacketRx::decode(&server_id.id, &pow_content_hash, Pow(8), response_bytes_1)?;
432        RpcResponsePacketRx::decode(&server_id.id, &pow_content_hash, Pow(8), response_bytes_2)?;
433        Ok(())
434    }
435
436    // ── Robustness tests: RpcRequestPacketRx::decode ──
437
438    use crate::tools::time::TimeMillis;
439    use crate::tools::hashing;
440
441    #[test]
442    fn rpc_request_decode_empty_input() {
443        let timestamp = TimeMillis::zero();
444        let verification_key_bytes = VerificationKeyBytes::zero();
445        let pq_commitment_bytes = PQCommitmentBytes::zero();
446        assert!(RpcRequestPacketRx::decode(&timestamp, &verification_key_bytes, &pq_commitment_bytes, Bytes::new()).is_err());
447    }
448
449    #[test]
450    fn rpc_request_decode_single_byte() {
451        let timestamp = TimeMillis::zero();
452        let verification_key_bytes = VerificationKeyBytes::zero();
453        let pq_commitment_bytes = PQCommitmentBytes::zero();
454        assert!(RpcRequestPacketRx::decode(&timestamp, &verification_key_bytes, &pq_commitment_bytes, Bytes::from_static(&[1u8])).is_err());
455    }
456
457    #[test]
458    fn rpc_request_decode_garbage() {
459        let timestamp = TimeMillis::zero();
460        let verification_key_bytes = VerificationKeyBytes::zero();
461        let pq_commitment_bytes = PQCommitmentBytes::zero();
462        assert!(RpcRequestPacketRx::decode(&timestamp, &verification_key_bytes, &pq_commitment_bytes, Bytes::from_static(&[0xff; 256])).is_err());
463    }
464
465    // ── Robustness tests: RpcResponsePacketRx::decode ──
466
467    #[test]
468    fn rpc_response_decode_empty_input() {
469        let destination_id = Id::zero();
470        let pow_content_hash = hashing::hash(&[0u8]);
471        assert!(RpcResponsePacketRx::decode(&destination_id, &pow_content_hash, Pow(0), Bytes::new()).is_err());
472    }
473
474    #[test]
475    fn rpc_response_decode_single_byte() {
476        let destination_id = Id::zero();
477        let pow_content_hash = hashing::hash(&[0u8]);
478        assert!(RpcResponsePacketRx::decode(&destination_id, &pow_content_hash, Pow(0), Bytes::from_static(&[1u8])).is_err());
479    }
480
481    #[test]
482    fn rpc_response_decode_garbage() {
483        let destination_id = Id::zero();
484        let pow_content_hash = hashing::hash(&[0u8]);
485        assert!(RpcResponsePacketRx::decode(&destination_id, &pow_content_hash, Pow(0), Bytes::from_static(&[0xff; 256])).is_err());
486    }
487
488    #[test]
489    fn rpc_response_decode_header_too_short_for_payload_len() {
490        // The header up to (but not including) the u32 payload_len field is 220 bytes
491        // (212 + 8 bytes of pow_content_hash_salt). Providing header + 2 bytes used to pass
492        // the old `+ 2` minimum-length check but panic on get_u32_le() which needs 4 bytes.
493        // Now correctly rejected.
494        let mut data = vec![0u8; 222];
495        data[0] = crate::protocol::rpc::rpc_response::RPC_RESPONSE_PACKET_VERSION;
496        let destination_id = Id::zero();
497        let pow_content_hash = hashing::hash(&[0u8]);
498        assert!(RpcResponsePacketRx::decode(&destination_id, &pow_content_hash, Pow(0), Bytes::from(data)).is_err());
499    }
500
501    #[cfg(not(target_arch = "wasm32"))]
502    mod bolero_fuzz {
503        use bytes::Bytes;
504        use crate::protocol::rpc::rpc_request::RpcRequestPacketRx;
505        use crate::protocol::rpc::rpc_response::RpcResponsePacketRx;
506        use crate::tools::types::{Id, PQCommitmentBytes, Pow, VerificationKeyBytes};
507        use crate::tools::time::TimeMillis;
508        use crate::tools::hashing;
509
510        #[test]
511        fn fuzz_rpc_request_decode() {
512            bolero::check!().for_each(|data: &[u8]| {
513                let timestamp = TimeMillis::zero();
514                let verification_key_bytes = VerificationKeyBytes::zero();
515                let pq_commitment_bytes = PQCommitmentBytes::zero();
516                let _ = RpcRequestPacketRx::decode(&timestamp, &verification_key_bytes, &pq_commitment_bytes, Bytes::copy_from_slice(data));
517            });
518        }
519
520        #[test]
521        fn fuzz_rpc_response_decode() {
522            bolero::check!().for_each(|data: &[u8]| {
523                let destination_id = Id::zero();
524                let pow_content_hash = hashing::hash(&[0u8]);
525                let _ = RpcResponsePacketRx::decode(&destination_id, &pow_content_hash, Pow(0), Bytes::copy_from_slice(data));
526            });
527        }
528    }
529}