Skip to main content

hashiverse_lib/protocol/payload/
payload.rs

1//! # RPC request and response payloads
2//!
3//! This is the catalogue of *what* peers say to each other. Every application-level
4//! operation — ping, bootstrap, peer announce, submit post (claim + commit), submit
5//! feedback, get post bundle, get post bundle feedback, heal, cache, fetch URL preview,
6//! trending hashtags — has a versioned request and response type defined here, plus a
7//! `u16` discriminator in [`PayloadRequestKind`] / [`PayloadResponseKind`] that routes
8//! packets to the right handler on the receiving side.
9//!
10//! ## Design notes
11//!
12//! - **Tokens**: Two-phase operations (submit post, heal, cache) exchange server-signed
13//!   token types (`SubmitPostClaimTokenV1`, `CacheRequestTokenV1`,
14//!   `HealPostBundleClaimTokenV1`) so the second phase can prove admission was granted
15//!   without the server needing to keep per-client state. Each token embeds the peer's
16//!   own [`Peer`] record so verification is self-contained.
17//! - **Bulk payloads** (`GetPostBundleResponseV1`, `GetPostBundleFeedbackResponseV1`)
18//!   use a custom length-prefixed-JSON + raw-bytes format rather than pure JSON, so
19//!   large opaque blobs stream through the
20//!   [`crate::tools::bytes_gatherer::BytesGatherer`] without being re-serialised.
21//! - **PoW** on any request that carries weight (posting, feedback, fetching URLs) is
22//!   carried inline via [`crate::protocol::peer::ClientPow`] credentials so the server
23//!   can validate amplification before spending CPU on the payload.
24
25use crate::protocol::peer::Peer;
26use crate::tools::buckets::BucketLocation;
27use crate::tools::time::TimeMillis;
28use crate::tools::types::{Hash, Id, Signature, SignatureKey, VerificationKey, ID_BYTES};
29use crate::tools::{hashing, json, signing, tools, BytesGatherer};
30use bytes::{Buf, BufMut, Bytes, BytesMut};
31use serde::{Deserialize, Serialize};
32use strum_macros::{Display, FromRepr};
33use crate::anyhow_assert_ge;
34use crate::protocol::posting::encoded_post_bundle::EncodedPostBundleHeaderV1;
35use crate::protocol::posting::encoded_post_feedback::EncodedPostFeedbackV1;
36
37/// The wire-level discriminator for every RPC request the protocol supports.
38///
39/// This `u16` value sits in the RPC request header so the server-side dispatcher can route
40/// each incoming [`crate::protocol::rpc::rpc_request::RpcRequestPacketRx`] to the correct handler
41/// without having to partially decode the payload first. The variants cover the three
42/// main subsystems: peer exchange (`PingV1`, `BootstrapV1`, `AnnounceV2`), posting and
43/// retrieval (`GetPostBundleV1`, `SubmitPostClaimV1`, `SubmitPostCommitV1`, feedback, heal,
44/// cache), and secondary services (`FetchUrlPreviewV1`, `TrendingHashtagsFetchV1`).
45///
46/// Every variant has a paired [`PayloadResponseKind`]; backwards-compatible additions go at
47/// the end of the enum so existing `u16` values do not shift.
48#[derive(Debug, Display, PartialEq, Clone, FromRepr)]
49#[repr(u16)]
50pub enum PayloadRequestKind {
51    ErrorV1, // Used solely for testing error handling
52    PingV1,
53    BootstrapV1,
54    RetiredAnnounceV1, // Wire value 3 reserved: was AnnounceV1, replaced by AnnounceV2. Never sent or accepted.
55    GetPostBundleV1,
56    GetPostBundleFeedbackV1,
57    SubmitPostClaimV1,
58    SubmitPostCommitV1,
59    SubmitPostFeedbackV1,
60    HealPostBundleClaimV1,
61    HealPostBundleCommitV1,
62    HealPostBundleFeedbackV1,
63    CachePostBundleV1,
64    CachePostBundleFeedbackV1,
65    FetchUrlPreviewV1,
66    TrendingHashtagsFetchV1,
67    PeerStatsRequestV1,
68    AnnounceV2,
69}
70
71/// Number of variants in [`PayloadRequestKind`]. Manually maintained; the
72/// test `test_PAYLOAD_REQUEST_KIND_COUNT_matches_variants` keeps it honest.
73/// Used to size fixed-length per-kind counter arrays (e.g. on the server).
74pub const PAYLOAD_REQUEST_KIND_COUNT: usize = 18;
75
76impl PayloadRequestKind {
77    pub fn from_u16(value: u16) -> anyhow::Result<Self> {
78        Self::from_repr(value).ok_or_else(|| anyhow::anyhow!("unknown PayloadRequestKind: {}", value))
79    }
80}
81
82/// The wire-level discriminator for every RPC response in the protocol.
83///
84/// Each variant pairs with a [`PayloadRequestKind`] of the same name minus the `Response`
85/// suffix (plus the unpaired `ErrorResponseV1`, which any handler may return on failure).
86/// The discriminator rides in the response header so the client knows which payload
87/// deserializer to use when it parses an [`crate::protocol::rpc::rpc_response::RpcResponsePacketRx`].
88///
89/// Additions go at the end of the enum to preserve backwards compatibility.
90#[derive(Debug, Display, PartialEq, Clone, FromRepr)]
91#[repr(u16)]
92pub enum PayloadResponseKind {
93    ErrorResponseV1, // Can be returned by any RPC call if an error happens on the server
94    PingResponseV1,
95    BootstrapResponseV1,
96    RetiredAnnounceResponseV1, // Wire value 3 reserved: was AnnounceResponseV1, replaced by AnnounceResponseV2. Never sent or accepted.
97    GetPostBundleResponseV1,
98    GetPostBundleFeedbackResponseV1,
99    SubmitPostClaimResponseV1,
100    SubmitPostCommitResponseV1,
101    SubmitPostFeedbackResponseV1,
102    HealPostBundleClaimResponseV1,
103    HealPostBundleCommitResponseV1,
104    HealPostBundleFeedbackResponseV1,
105    CachePostBundleResponseV1,
106    CachePostBundleFeedbackResponseV1,
107    FetchUrlPreviewResponseV1,
108    TrendingHashtagsFetchResponseV1,
109    PeerStatsResponseV1,
110    AnnounceResponseV2,
111}
112
113impl PayloadResponseKind {
114    pub fn from_u16(value: u16) -> anyhow::Result<Self> {
115        Self::from_repr(value).ok_or_else(|| anyhow::anyhow!("unknown PayloadResponseKind: {}", value))
116    }
117}
118
119/// The universal error response body returned when a handler rejects a request.
120///
121/// Any RPC handler may return an `ErrorResponseV1` (packaged in a response with
122/// [`PayloadResponseKind::ErrorResponseV1`]) instead of its usual success shape. The `code`
123/// is a numeric discriminator to let callers branch on error class without string parsing;
124/// the `message` is a human-readable explanation for logs and diagnostics.
125#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
126pub struct ErrorResponseV1 {
127    pub code: u16,
128    pub message: String,
129}
130
131#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
132pub struct PingV1 {}
133
134#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
135pub struct PingResponseV1 {
136    pub peer: Peer,
137}
138
139#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
140pub struct BootstrapV1 {}
141
142#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
143pub struct BootstrapResponseV1 {
144    pub peers_random: Vec<Peer>,
145}
146
147/// The peer announce. Carries an opaque `proof_payload` produced and verified by the
148/// announcer's transport-specific
149/// [`crate::transport::transport_ownership_proof::TransportOwnershipProof`] — for the HTTPS
150/// transport that's the announcer's current ACME-issued TLS chain, for mem / TCP it's the
151/// empty marker. The receiver gates Kademlia admission on the proof verifying; an announcer
152/// that can't produce a valid proof (e.g. an HTTPS server that hasn't completed ACME
153/// because it doesn't control port 443) is dropped without ever being added.
154///
155/// `proof_payload` is `Vec<u8>` rather than `bytes::Bytes` because this struct is wire-
156/// encoded as JSON, and `serde_json` doesn't round-trip `Bytes` (the bytes crate's
157/// `Serialize` calls `serialize_bytes`, but `serde_json`'s `Deserialize` reaches Bytes via
158/// `visit_seq` on the integer-array form — they don't meet). `Vec<u8>` round-trips cleanly.
159#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
160pub struct AnnounceV2 {
161    pub peer_self: Peer,
162    pub proof_payload: Vec<u8>,
163}
164
165/// Announce response: the receiver's own [`Peer`] plus the peers it knows nearest to the
166/// announcer. Proof bytes never flow back in the response, since receivers only validate
167/// proofs at admission time and thereafter store and gossip plain [`Peer`] records.
168#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
169pub struct AnnounceResponseV2 {
170    pub peer_self: Peer,
171    pub peers_nearest: Vec<Peer>,
172}
173
174#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
175pub struct GetPostBundleV1 {
176    pub bucket_location: BucketLocation,
177    pub peers_visited: Vec<Peer>,
178    pub already_retrieved_peer_ids: Vec<Id>,
179}
180
181#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
182pub struct GetPostBundleFeedbackV1 {
183    pub bucket_location: BucketLocation,
184    pub peers_visited: Vec<Peer>,
185    pub already_retrieved_peer_ids: Vec<Id>,
186}
187
188#[derive(Debug, PartialEq, Clone)]
189pub struct GetPostBundleResponseV1 {
190    pub peers_nearer: Vec<Peer>,
191    pub cache_request_token: Option<CacheRequestTokenV1>,
192    pub post_bundles_cached: Vec<Bytes>,  // Cached EncodedPostBundleV1 from intermediate servers
193    pub post_bundle: Option<Bytes>, // Primary EncodedPostBundleV1 from the responsible server
194}
195
196impl GetPostBundleResponseV1 {
197    pub fn to_bytes_gatherer(&self) -> anyhow::Result<BytesGatherer> {
198        let mut bytes_gatherer = BytesGatherer::default();
199
200        tools::write_length_prefixed_json::<Vec<Peer>>(&mut bytes_gatherer, &self.peers_nearer)?;
201
202        match &self.cache_request_token {
203            Some(token) => {
204                bytes_gatherer.put_u8(1);
205                tools::write_length_prefixed_json(&mut bytes_gatherer, token)?;
206            }
207            None => bytes_gatherer.put_u8(0),
208        }
209
210        bytes_gatherer.put_u16(self.post_bundles_cached.len() as u16);
211        for bundle in &self.post_bundles_cached {
212            bytes_gatherer.put_u32(bundle.len() as u32);
213            bytes_gatherer.put_bytes(bundle.clone());
214        }
215
216        match &self.post_bundle {
217            Some(post_bundle) => { bytes_gatherer.put_u8(1); bytes_gatherer.put_bytes(post_bundle.clone()); }
218            None => { bytes_gatherer.put_u8(0); }
219        }
220
221        Ok(bytes_gatherer)
222    }
223
224    pub fn from_bytes(mut bytes: Bytes) -> anyhow::Result<Self> {
225        use bytes::Buf;
226
227        let peers_nearer = tools::read_length_prefixed_json::<Vec<Peer>>(&mut bytes)?;
228
229        if bytes.remaining() < 1 {
230            anyhow::bail!("Invalid buffer: missing cache_request_token presence flag");
231        }
232        let cache_request_token = match bytes.get_u8() {
233            0 => None,
234            1 => Some(tools::read_length_prefixed_json::<CacheRequestTokenV1>(&mut bytes)?),
235            _ => anyhow::bail!("Invalid buffer: unknown cache_request_token presence flag"),
236        };
237
238        anyhow_assert_ge!(bytes.remaining(), 2, "Missing post_bundles_cached count");
239        let cached_count = bytes.get_u16() as usize;
240        let mut post_bundles_cached = Vec::with_capacity(cached_count);
241        for _ in 0..cached_count {
242            anyhow_assert_ge!(bytes.remaining(), 4, "Missing post_bundles_cached entry length");
243            let len = bytes.get_u32() as usize;
244            anyhow_assert_ge!(bytes.remaining(), len, "Truncated post_bundles_cached entry");
245            post_bundles_cached.push(bytes.split_to(len));
246        }
247
248        if bytes.remaining() < 1 {
249            anyhow::bail!("Invalid buffer: missing post_bundle presence flag");
250        }
251        let post_bundle = match bytes.get_u8() {
252            0 => None,
253            1 => Some(bytes.copy_to_bytes(bytes.remaining())),
254            _ => anyhow::bail!("Invalid buffer: unknown post_bundle presence flag"),
255        };
256
257        Ok(Self { peers_nearer, cache_request_token, post_bundles_cached, post_bundle })
258    }
259}
260#[derive(Debug, PartialEq, Clone)]
261pub struct GetPostBundleFeedbackResponseV1 {
262    pub peers_nearer: Vec<Peer>,
263    pub cache_request_token: Option<CacheRequestTokenV1>,
264    pub post_bundle_feedbacks_cached: Vec<Bytes>, // Cached feedback from originators the client hasn't seen yet (filtered by already_retrieved_peer_ids).
265    pub encoded_post_bundle_feedback: Option<Bytes>, // Primary feedback from the responsible server
266}
267
268impl GetPostBundleFeedbackResponseV1 {
269    pub fn to_bytes_gatherer(&self) -> anyhow::Result<BytesGatherer> {
270        let mut bytes_gatherer = BytesGatherer::default();
271
272        tools::write_length_prefixed_json::<Vec<Peer>>(&mut bytes_gatherer, &self.peers_nearer)?;
273
274        match &self.cache_request_token {
275            Some(token) => {
276                bytes_gatherer.put_u8(1u8);
277                tools::write_length_prefixed_json(&mut bytes_gatherer, token)?;
278            }
279            None => bytes_gatherer.put_u8(0u8),
280        }
281
282        bytes_gatherer.put_u16(self.post_bundle_feedbacks_cached.len() as u16);
283        for feedback in &self.post_bundle_feedbacks_cached {
284            bytes_gatherer.put_u32(feedback.len() as u32);
285            bytes_gatherer.put_bytes(feedback.clone());
286        }
287
288        match &self.encoded_post_bundle_feedback {
289            Some(post_bundle_feedback) => {
290                bytes_gatherer.put_u8(1u8);
291                bytes_gatherer.put_bytes(post_bundle_feedback.clone());
292            }
293            None => bytes_gatherer.put_u8(0u8),
294        }
295
296        Ok(bytes_gatherer)
297    }
298
299    pub fn from_bytes(mut bytes: Bytes) -> anyhow::Result<Self> {
300        use bytes::Buf;
301
302        let peers_nearer = tools::read_length_prefixed_json::<Vec<Peer>>(&mut bytes)?;
303
304        if bytes.remaining() < 1 {
305            anyhow::bail!("Invalid buffer: missing cache_request_token presence flag");
306        }
307        let cache_request_token = match bytes.get_u8() {
308            0 => None,
309            1 => Some(tools::read_length_prefixed_json::<CacheRequestTokenV1>(&mut bytes)?),
310            _ => anyhow::bail!("Invalid buffer: unknown cache_request_token presence flag"),
311        };
312
313        anyhow_assert_ge!(bytes.remaining(), 2, "Missing post_bundle_feedbacks_cached count");
314        let cached_count = bytes.get_u16() as usize;
315        let mut post_bundle_feedbacks_cached = Vec::with_capacity(cached_count);
316        for _ in 0..cached_count {
317            anyhow_assert_ge!(bytes.remaining(), 4, "Missing post_bundle_feedbacks_cached entry length");
318            let len = bytes.get_u32() as usize;
319            anyhow_assert_ge!(bytes.remaining(), len, "Truncated post_bundle_feedbacks_cached entry");
320            post_bundle_feedbacks_cached.push(bytes.split_to(len));
321        }
322
323        if bytes.remaining() < 1 {
324            anyhow::bail!("Invalid buffer: missing post_bundle_feedback presence flag");
325        }
326        let post_bundle_feedback = match bytes.get_u8() {
327            0 => None,
328            1 => Some(bytes.copy_to_bytes(bytes.remaining())),
329            _ => anyhow::bail!("Invalid buffer: unknown post_bundle_feedback presence flag"),
330        };
331
332        Ok(Self { peers_nearer, cache_request_token, post_bundle_feedbacks_cached, encoded_post_bundle_feedback: post_bundle_feedback })
333    }
334}
335
336#[derive(Debug, PartialEq, Clone)]
337pub struct SubmitPostClaimV1 {
338    pub bucket_location: BucketLocation,
339    pub referenced_post_header_bytes: Option<Bytes>, // For Sequel posts: the original post's bytes_without_body, used to verify same-author
340    pub referenced_hashtags: Vec<String>,             // Hashtag strings referenced by this post, for trending tracking
341    pub encoded_post_bytes: Bytes,                    // Note that it has just the header of the EncodedPost...
342}
343
344impl SubmitPostClaimV1 {
345    pub fn new_to_bytes(bucket_location: &BucketLocation, referenced_post_header_bytes: Option<&[u8]>, referenced_hashtags: &[String], encoded_post_bytes_without_body: &[u8]) -> anyhow::Result<Bytes> {
346        let mut bytes_gatherer = BytesGatherer::default();
347        tools::write_length_prefixed_json(&mut bytes_gatherer, bucket_location)?;
348        match referenced_post_header_bytes {
349            Some(header_bytes) => {
350                bytes_gatherer.put_u32(header_bytes.len() as u32);
351                bytes_gatherer.put_slice(header_bytes);
352            }
353            None => {
354                bytes_gatherer.put_u32(0);
355            }
356        }
357        tools::write_length_prefixed_json(&mut bytes_gatherer, &referenced_hashtags)?;
358        bytes_gatherer.put_slice(encoded_post_bytes_without_body);
359        Ok(bytes_gatherer.to_bytes())
360    }
361
362    pub fn from_bytes(bytes: &mut Bytes) -> anyhow::Result<Self> {
363        use bytes::Buf;
364        let bucket_location = tools::read_length_prefixed_json::<BucketLocation>(bytes)?;
365
366        anyhow::ensure!(bytes.remaining() >= 4, "SubmitPostClaimV1: missing referenced_post_header_bytes length");
367        let referenced_post_header_bytes_len = bytes.get_u32() as usize;
368        let referenced_post_header_bytes = if referenced_post_header_bytes_len > 0 {
369            anyhow::ensure!(bytes.remaining() >= referenced_post_header_bytes_len, "SubmitPostClaimV1: referenced_post_header_bytes length {} exceeds remaining {}", referenced_post_header_bytes_len, bytes.remaining());
370            Some(bytes.split_to(referenced_post_header_bytes_len))
371        } else {
372            None
373        };
374
375        let referenced_hashtags = tools::read_length_prefixed_json::<Vec<String>>(bytes)?;
376
377        anyhow::ensure!(bytes.has_remaining(), "SubmitPostClaimV1: missing encoded_post_bytes");
378        let encoded_post_bytes = bytes.split_to(bytes.len());
379
380        Ok(Self { bucket_location, referenced_post_header_bytes, referenced_hashtags, encoded_post_bytes })
381    }
382}
383
384#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
385pub struct SubmitPostClaimResponseV1 {
386    pub peers_nearer: Vec<Peer>,
387    pub submit_post_claim_token: Option<SubmitPostClaimTokenV1>,
388}
389
390#[derive(Debug, PartialEq, Clone)]
391pub struct SubmitPostCommitV1 {
392    pub bucket_location: BucketLocation,
393    pub submit_post_claim_token: SubmitPostClaimTokenV1,
394    pub encoded_post_bytes: Bytes,
395}
396
397impl SubmitPostCommitV1 {
398    pub fn new_to_bytes(bucket_location: &BucketLocation, submit_post_claim_token: &SubmitPostClaimTokenV1, encoded_post_bytes: &[u8]) -> anyhow::Result<Bytes> {
399        let mut bytes_gatherer = BytesGatherer::default();
400        tools::write_length_prefixed_json(&mut bytes_gatherer, bucket_location)?;
401        tools::write_length_prefixed_json(&mut bytes_gatherer, submit_post_claim_token)?;
402        bytes_gatherer.put_slice(encoded_post_bytes);
403        Ok(bytes_gatherer.to_bytes())
404    }
405
406    pub fn from_bytes(bytes: &mut Bytes) -> anyhow::Result<Self> {
407        let bucket_location = tools::read_length_prefixed_json::<BucketLocation>(bytes)?;
408        let submit_post_claim_token = tools::read_length_prefixed_json::<SubmitPostClaimTokenV1>(bytes)?;
409        let encoded_post_bytes = bytes.split_to(bytes.len());
410
411        Ok(Self {
412            bucket_location,
413            submit_post_claim_token,
414            encoded_post_bytes,
415        })
416    }
417}
418
419#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
420pub struct SubmitPostCommitResponseV1 {
421    pub submit_post_commit_token: SubmitPostCommitTokenV1,
422}
423
424#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
425pub struct SubmitPostClaimTokenV1 {
426    pub peer: Peer,
427    pub bucket_location: BucketLocation,
428    pub post_id: Id,
429    pub token_signature: Signature,
430}
431
432impl SubmitPostClaimTokenV1 {
433    fn get_hash_for_signing(peer: &Peer, bucket_location: &BucketLocation, post_id: &Id) -> Hash {
434        hashing::hash_multiple(&[peer.id.as_ref(), bucket_location.get_hash_for_signing().as_ref(), post_id.as_ref(), "SubmitPostClaimTokenV1".as_bytes()])
435    }
436
437    pub fn new(peer: Peer, bucket_location: BucketLocation, post_id: Id, signature_key: &SignatureKey) -> Self {
438        let token_signature = signing::sign(signature_key, Self::get_hash_for_signing(&peer, &bucket_location, &post_id).as_ref());
439        Self { peer, bucket_location, post_id, token_signature }
440    }
441
442    pub fn verify(&self) -> anyhow::Result<()> {
443        self.peer.verify()?;
444        let verification_key = VerificationKey::from_bytes(&self.peer.verification_key_bytes)?;
445        signing::verify(&verification_key, &self.token_signature, Self::get_hash_for_signing(&self.peer, &self.bucket_location, &self.post_id).as_ref())
446    }
447}
448
449#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
450pub struct SubmitPostCommitTokenV1 {
451    pub peer: Peer,
452    pub bucket_location: BucketLocation,
453    pub post_id: Id,
454    pub token_signature: Signature,
455}
456
457impl SubmitPostCommitTokenV1 {
458    fn get_hash_for_signing(peer: &Peer, bucket_location: &BucketLocation, post_id: &Id) -> Hash {
459        hashing::hash_multiple(&[peer.id.as_ref(), bucket_location.get_hash_for_signing().as_ref(), post_id.as_ref(), "SubmitPostCommitTokenV1".as_bytes()])
460    }
461
462    pub fn new(peer: Peer, bucket_location: BucketLocation, post_id: Id, signature_key: &SignatureKey) -> Self {
463        let token_signature = signing::sign(signature_key, Self::get_hash_for_signing(&peer, &bucket_location, &post_id).as_ref());
464        Self { peer, bucket_location, post_id, token_signature }
465    }
466
467    pub fn verify(&self) -> anyhow::Result<()> {
468        self.peer.verify()?;
469        let verification_key = VerificationKey::from_bytes(&self.peer.verification_key_bytes)?;
470        signing::verify(&verification_key, &self.token_signature, Self::get_hash_for_signing(&self.peer, &self.bucket_location, &self.post_id).as_ref())
471    }
472}
473
474
475#[derive(Debug, PartialEq, Clone)]
476pub struct SubmitPostFeedbackV1 {
477    pub bucket_location: BucketLocation,
478    pub encoded_post_feedback: EncodedPostFeedbackV1,
479}
480
481impl SubmitPostFeedbackV1 {
482    pub fn new_to_bytes(bucket_location: &BucketLocation, encoded_post_feedback: &EncodedPostFeedbackV1) -> anyhow::Result<Bytes> {
483        let mut bytes_gatherer = BytesGatherer::default();
484        tools::write_length_prefixed_json(&mut bytes_gatherer, bucket_location)?;
485        let mut feedback_bytes = BytesMut::new();
486        encoded_post_feedback.append_encode_to_bytes(&mut feedback_bytes)?;
487        bytes_gatherer.put_bytes(feedback_bytes.freeze());
488        Ok(bytes_gatherer.to_bytes())
489    }
490
491    pub fn from_bytes(bytes: &mut Bytes) -> anyhow::Result<Self> {
492        let bucket_location = tools::read_length_prefixed_json::<BucketLocation>(bytes)?;
493        let encoded_post_feedback = EncodedPostFeedbackV1::decode_from_bytes(bytes)?;
494        Ok(Self { bucket_location, encoded_post_feedback })
495    }
496
497}
498
499#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
500pub struct SubmitPostFeedbackResponseV1 {
501    pub peers_nearer: Vec<Peer>,
502    pub accepted: bool,
503}
504
505
506
507// ---- Heal Post Bundle (two-phase: claim then commit) ----
508
509#[derive(Debug, PartialEq, Clone)]
510pub struct HealPostBundleClaimV1 {
511    pub bucket_location: BucketLocation,
512    pub donor_header: EncodedPostBundleHeaderV1,
513}
514
515impl HealPostBundleClaimV1 {
516    pub fn new_to_bytes(donor_header: &EncodedPostBundleHeaderV1, bucket_location: &BucketLocation) -> anyhow::Result<Bytes> {
517        let mut g = BytesGatherer::default();
518        tools::write_length_prefixed_json(&mut g, bucket_location)?;
519        tools::write_length_prefixed_json(&mut g, donor_header)?;
520        Ok(g.to_bytes())
521    }
522
523    pub fn from_bytes(bytes: &mut Bytes) -> anyhow::Result<Self> {
524        let bucket_location = tools::read_length_prefixed_json::<BucketLocation>(bytes)?;
525        let donor_header = tools::read_length_prefixed_json::<EncodedPostBundleHeaderV1>(bytes)?;
526        Ok(Self { donor_header, bucket_location })
527    }
528}
529
530#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
531pub struct HealPostBundleClaimResponseV1 {
532    pub needed_post_ids: Vec<Id>,
533    pub token: Option<HealPostBundleClaimTokenV1>,
534}
535
536#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
537pub struct HealPostBundleClaimTokenV1 {
538    pub peer: Peer,
539    pub bucket_location: BucketLocation,
540    pub donor_header_signature: Signature,
541    pub needed_post_ids: Vec<Id>,
542    pub token_signature: Signature,
543}
544
545impl HealPostBundleClaimTokenV1 {
546    fn get_hash_for_signing(peer_id: &Id, bucket_location: &BucketLocation, donor_header_signature: &Signature, needed_post_ids: &[Id]) -> Hash {
547        let bucket_location_hash = bucket_location.get_hash_for_signing();
548        let mut hash_input: Vec<&[u8]> = vec![
549            peer_id.as_ref(),
550            bucket_location_hash.as_ref(),
551            donor_header_signature.as_ref(),
552            "HealPostBundleClaimTokenV1".as_bytes(),
553        ];
554        for id in needed_post_ids {
555            hash_input.push(id.as_ref());
556        }
557        hashing::hash_multiple(&hash_input)
558    }
559
560    pub fn new(peer: Peer, bucket_location: BucketLocation, needed_post_ids: Vec<Id>, donor_header_signature: Signature, signature_key: &SignatureKey) -> Self {
561        let hash = Self::get_hash_for_signing(&peer.id, &bucket_location, &donor_header_signature, &needed_post_ids);
562        let token_signature = signing::sign(signature_key, hash.as_ref());
563        Self { peer, bucket_location, donor_header_signature, needed_post_ids, token_signature }
564    }
565
566    pub fn verify(&self) -> anyhow::Result<()> {
567        self.peer.verify()?;
568        self.bucket_location.validate()?;
569        let hash = Self::get_hash_for_signing(&self.peer.id, &self.bucket_location, &self.donor_header_signature, &self.needed_post_ids);
570        let verification_key = VerificationKey::from_bytes(&self.peer.verification_key_bytes)?;
571        signing::verify(&verification_key, &self.token_signature, hash.as_ref())
572    }
573}
574
575#[derive(Debug, PartialEq, Clone)]
576pub struct HealPostBundleCommitV1 {
577    pub token: HealPostBundleClaimTokenV1,
578    pub donor_header: EncodedPostBundleHeaderV1,
579    pub encoded_posts_bytes: Bytes,
580}
581
582impl HealPostBundleCommitV1 {
583    pub fn new_to_bytes(token: &HealPostBundleClaimTokenV1, donor_header: &EncodedPostBundleHeaderV1, encoded_posts_bytes: &[u8]) -> anyhow::Result<Bytes> {
584        let mut bytes_gatherer = BytesGatherer::default();
585        tools::write_length_prefixed_json(&mut bytes_gatherer, token)?;
586        tools::write_length_prefixed_json(&mut bytes_gatherer, donor_header)?;
587        bytes_gatherer.put_slice(encoded_posts_bytes);
588        Ok(bytes_gatherer.to_bytes())
589    }
590
591    pub fn from_bytes(bytes: &mut Bytes) -> anyhow::Result<Self> {
592        let token = tools::read_length_prefixed_json::<HealPostBundleClaimTokenV1>(bytes)?;
593        let donor_header = tools::read_length_prefixed_json::<EncodedPostBundleHeaderV1>(bytes)?;
594        let encoded_posts_bytes = bytes.split_to(bytes.len());
595        Ok(Self { token, donor_header, encoded_posts_bytes })
596    }
597}
598
599#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
600pub struct HealPostBundleCommitResponseV1 {
601    pub accepted: bool,
602}
603
604// ---- Heal Post Bundle Feedback (single-phase) ----
605
606#[derive(Debug, PartialEq, Clone)]
607pub struct HealPostBundleFeedbackV1 {
608    pub location_id: Id,
609    pub encoded_post_feedbacks: Vec<EncodedPostFeedbackV1>,
610}
611
612impl HealPostBundleFeedbackV1 {
613    pub fn new_to_bytes(location_id: &Id, feedbacks: &[EncodedPostFeedbackV1]) -> anyhow::Result<Bytes> {
614        let mut bytes = BytesMut::new();
615        bytes.put_slice(location_id.as_ref());
616        for f in feedbacks {
617            f.append_encode_to_bytes(&mut bytes)?;
618        }
619        Ok(bytes.freeze())
620    }
621
622    pub fn from_bytes(bytes: &mut Bytes) -> anyhow::Result<Self> {
623        use bytes::Buf;
624        anyhow_assert_ge!(bytes.remaining(), ID_BYTES, "Missing location_id in HealPostBundleFeedbackV1");
625        let location_id = Id::from_slice(&bytes.split_to(ID_BYTES))?;
626        let mut encoded_post_feedbacks = Vec::new();
627        while bytes.has_remaining() {
628            encoded_post_feedbacks.push(EncodedPostFeedbackV1::decode_from_bytes(&mut *bytes)?);
629        }
630        Ok(Self { location_id, encoded_post_feedbacks })
631    }
632}
633
634#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
635pub struct HealPostBundleFeedbackResponseV1 {
636    pub accepted_count: u32,
637}
638
639
640/// Issued by an intermediate server during a GetPostBundle/GetPostBundleFeedback miss or stale hit.
641/// The client uploads the bundle to this server after completing its walk.
642#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
643pub struct CacheRequestTokenV1 {
644    pub peer: Peer,
645    pub bucket_location: BucketLocation,
646    pub expires_at: TimeMillis,
647    /// Originator peer IDs already held in this server's cache for this location_id.
648    /// The client should only upload a bundle whose originator peer.id is NOT in this list.
649    pub already_cached_peer_ids: Vec<Id>,
650    pub token_signature: Signature,
651}
652
653impl CacheRequestTokenV1 {
654    fn get_hash_for_signing(peer_id: &Id, bucket_location: &BucketLocation, expires_at: &TimeMillis, already_cached_peer_ids: &[Id]) -> Hash {
655        let expires_at_be = expires_at.encode_be();
656        let bucket_location_hash = bucket_location.get_hash_for_signing();
657        let mut inputs: Vec<&[u8]> = vec![peer_id.as_ref(), bucket_location_hash.as_ref(), expires_at_be.as_ref(), "CacheRequestToken".as_bytes()];
658        for id in already_cached_peer_ids {
659            inputs.push(id.as_ref());
660        }
661        hashing::hash_multiple(&inputs)
662    }
663
664    pub fn new(peer: Peer, bucket_location: BucketLocation, expires_at: TimeMillis, already_cached_peer_ids: Vec<Id>, signature_key: &SignatureKey) -> Self {
665        let token_signature = signing::sign(signature_key, Self::get_hash_for_signing(&peer.id, &bucket_location, &expires_at, &already_cached_peer_ids).as_ref());
666        Self { peer, bucket_location, expires_at, already_cached_peer_ids, token_signature }
667    }
668
669    pub fn verify(&self) -> anyhow::Result<()> {
670        self.peer.verify()?;
671        self.bucket_location.validate()?;
672        let verification_key = VerificationKey::from_bytes(&self.peer.verification_key_bytes)?;
673        signing::verify(&verification_key, &self.token_signature, Self::get_hash_for_signing(&self.peer.id, &self.bucket_location, &self.expires_at, &self.already_cached_peer_ids).as_ref())
674    }
675
676    pub fn is_expired(&self, current_time: TimeMillis) -> bool {
677        current_time > self.expires_at
678    }
679}
680
681#[derive(Debug, PartialEq, Clone)]
682pub struct CachePostBundleV1 {
683    pub token: CacheRequestTokenV1,
684    /// One entry per originator — the client uploads all bundles it collected during its walk.
685    pub encoded_post_bundles: Vec<Bytes>,
686}
687
688impl CachePostBundleV1 {
689    pub fn new_to_bytes(token: &CacheRequestTokenV1, encoded_post_bundles: &[&[u8]]) -> anyhow::Result<Bytes> {
690        let mut bytes_gatherer = BytesGatherer::default();
691        tools::write_length_prefixed_json(&mut bytes_gatherer, token)?;
692        bytes_gatherer.put_u16(encoded_post_bundles.len() as u16);
693        for bundle in encoded_post_bundles {
694            bytes_gatherer.put_u32(bundle.len() as u32);
695            bytes_gatherer.put_slice(bundle);
696        }
697        Ok(bytes_gatherer.to_bytes())
698    }
699
700    pub fn from_bytes(bytes: &mut Bytes) -> anyhow::Result<Self> {
701        let token = tools::read_length_prefixed_json::<CacheRequestTokenV1>(bytes)?;
702        anyhow_assert_ge!(bytes.remaining(), 2, "Missing encoded_post_bundles count");
703        let count = bytes.get_u16() as usize;
704        let mut encoded_post_bundles = Vec::with_capacity(count);
705        for _ in 0..count {
706            anyhow_assert_ge!(bytes.remaining(), 4, "Missing encoded_post_bundle entry length");
707            let len = bytes.get_u32() as usize;
708            anyhow_assert_ge!(bytes.remaining(), len, "Truncated encoded_post_bundle entry");
709            encoded_post_bundles.push(bytes.split_to(len));
710        }
711        Ok(Self { token, encoded_post_bundles })
712    }
713}
714
715#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
716pub struct CachePostBundleResponseV1 {
717    pub accepted: bool,
718}
719
720#[derive(Debug, PartialEq, Clone)]
721pub struct CachePostBundleFeedbackV1 {
722    pub token: CacheRequestTokenV1,
723    pub encoded_post_bundle_feedback_bytes: Bytes,
724}
725
726impl CachePostBundleFeedbackV1 {
727    pub fn new_to_bytes(token: &CacheRequestTokenV1, encoded_post_bundle_feedback_bytes: &[u8]) -> anyhow::Result<Bytes> {
728        let mut bytes_gatherer = BytesGatherer::default();
729        tools::write_length_prefixed_json(&mut bytes_gatherer, token)?;
730        bytes_gatherer.put_slice(encoded_post_bundle_feedback_bytes);
731        Ok(bytes_gatherer.to_bytes())
732    }
733
734    pub fn from_bytes(bytes: &mut Bytes) -> anyhow::Result<Self> {
735        let token = tools::read_length_prefixed_json::<CacheRequestTokenV1>(bytes)?;
736        let encoded_post_bundle_feedback_bytes = bytes.split_to(bytes.len());
737        Ok(Self { token, encoded_post_bundle_feedback_bytes })
738    }
739}
740
741#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
742pub struct CachePostBundleFeedbackResponseV1 {
743    pub accepted: bool,
744}
745
746/// Client → Server: fetch Open Graph metadata for `url`
747#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
748pub struct FetchUrlPreviewV1 {
749    pub url: String,
750}
751
752impl FetchUrlPreviewV1 {
753    pub fn new_to_bytes(url: &str) -> anyhow::Result<Bytes> {
754        let mut bytes_gatherer = BytesGatherer::default();
755        tools::write_length_prefixed_json(&mut bytes_gatherer, &FetchUrlPreviewV1 { url: url.to_string() })?;
756        Ok(bytes_gatherer.to_bytes())
757    }
758
759    pub fn from_bytes(bytes: &mut Bytes) -> anyhow::Result<Self> {
760        tools::read_length_prefixed_json(bytes)
761    }
762}
763
764/// Server → Client: extracted preview metadata
765#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
766pub struct FetchUrlPreviewResponseV1 {
767    pub url: String,
768    pub title: String,
769    pub description: String,
770    pub image_url: String,
771}
772
773impl FetchUrlPreviewResponseV1 {
774    pub fn to_bytes(&self) -> anyhow::Result<Bytes> {
775        json::struct_to_bytes(self)
776    }
777
778    pub fn from_bytes(bytes: &Bytes) -> anyhow::Result<Self> {
779        json::bytes_to_struct(bytes)
780    }
781}
782
783/// Client → Server: fetch trending hashtags
784#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
785pub struct TrendingHashtagsFetchV1 {
786    pub limit: u16,
787}
788
789impl TrendingHashtagsFetchV1 {
790    pub fn new_to_bytes(limit: u16) -> anyhow::Result<Bytes> {
791        let mut bytes_gatherer = BytesGatherer::default();
792        tools::write_length_prefixed_json(&mut bytes_gatherer, &TrendingHashtagsFetchV1 { limit })?;
793        Ok(bytes_gatherer.to_bytes())
794    }
795
796    pub fn from_bytes(bytes: &mut Bytes) -> anyhow::Result<Self> {
797        tools::read_length_prefixed_json(bytes)
798    }
799}
800
801#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
802pub struct TrendingHashtagV1 {
803    pub hashtag: String,
804    pub count: u64,
805}
806
807/// Server → Client: trending hashtags with unique author counts
808#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
809pub struct TrendingHashtagsFetchResponseV1 {
810    pub trending_hashtags: Vec<TrendingHashtagV1>,
811}
812
813impl TrendingHashtagsFetchResponseV1 {
814    pub fn to_bytes(&self) -> anyhow::Result<Bytes> {
815        json::struct_to_bytes(self)
816    }
817
818    pub fn from_bytes(bytes: &Bytes) -> anyhow::Result<Self> {
819        json::bytes_to_struct(bytes)
820    }
821}
822
823/// Client → Server: opt-in peer-introspection request.
824///
825/// Empty payload. The corresponding response carries a signed, compressed JSON
826/// document; per-request PoW is gated by `POW_MINIMUM_PER_PEER_STATS` and the
827/// server caches the response for a short window to absorb bursts of well-PoW'd
828/// callers without re-measuring every time.
829#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
830pub struct PeerStatsRequestV1 {}
831
832impl PeerStatsRequestV1 {
833    pub fn to_bytes(&self) -> anyhow::Result<Bytes> {
834        json::struct_to_bytes(self)
835    }
836
837    pub fn from_bytes(bytes: &Bytes) -> anyhow::Result<Self> {
838        json::bytes_to_struct(bytes)
839    }
840}
841
842/// Server → Client: signed peer-introspection response.
843///
844/// `json_compressed` is an lz4-compressed JSON object (the stats document).
845/// `signature` covers `timestamp.encode_be() || json_compressed` using the
846/// server's Ed25519 signing key — clients that re-share the response amongst
847/// themselves verify against the bytes as transmitted, so no canonical-JSON
848/// dance is required.
849#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
850pub struct PeerStatsResponseV1 {
851    pub peer: Peer,
852    pub timestamp: TimeMillis,
853    pub json_compressed: Bytes,
854    pub signature: Signature,
855}
856
857impl PeerStatsResponseV1 {
858    /// The exact byte sequence that `signature` covers.
859    pub fn signing_input(timestamp: TimeMillis, json_compressed: &[u8]) -> Vec<u8> {
860        let mut buf = Vec::with_capacity(crate::tools::time::TIME_MILLIS_BYTES + json_compressed.len());
861        buf.extend_from_slice(timestamp.encode_be().as_ref());
862        buf.extend_from_slice(json_compressed);
863        buf
864    }
865
866    pub fn to_bytes(&self) -> anyhow::Result<Bytes> {
867        json::struct_to_bytes(self)
868    }
869
870    pub fn from_bytes(bytes: &Bytes) -> anyhow::Result<Self> {
871        json::bytes_to_struct(bytes)
872    }
873}
874
875#[cfg(test)]
876mod tests {
877    use crate::protocol::payload::payload::{GetPostBundleResponseV1, HealPostBundleClaimResponseV1, HealPostBundleClaimTokenV1, HealPostBundleClaimV1, HealPostBundleCommitResponseV1, HealPostBundleCommitV1, SubmitPostClaimTokenV1, SubmitPostCommitTokenV1};
878    use crate::protocol::posting::encoded_post_bundle::{EncodedPostBundleHeaderV1, EncodedPostBundleV1};
879    use crate::tools::server_id::ServerId;
880    use crate::tools::buckets::{BucketLocation, BucketType};
881    use crate::tools::time::{TimeMillis, MILLIS_IN_HOUR};
882    use crate::tools::time_provider::time_provider::{RealTimeProvider, TimeProvider};
883    use crate::tools::{config, json, tools};
884    use crate::tools::types::{Id, Pow, Signature, ID_BYTES};
885    use std::collections::HashSet;
886    use crate::tools::pow_generator::single_threaded_pow_generator::SingleThreadedPowGenerator;
887    use bytes::Bytes;
888
889    #[tokio::test]
890    #[allow(non_snake_case)]
891    async fn test_to_from_GetPostBundleResponseV1() -> anyhow::Result<()> {
892        let time_provider = RealTimeProvider;
893        let pow_generator = SingleThreadedPowGenerator::new();
894        let server_id = ServerId::new("own_pow", &time_provider, Pow(4), true, &pow_generator).await?;
895        let peer = server_id.to_peer(&time_provider)?;
896
897        let mut encoded_posts_ids = Vec::new();
898        let mut encoded_posts_lengths = Vec::new();
899        let mut encoded_posts_bytes = Vec::new();
900
901        for i in 0..16 {
902            let len = (i + 1) * 128;
903            let mut encoded_post_bytes = tools::random_bytes(len);
904            let post_id = Id::from_slice(&encoded_post_bytes[0..ID_BYTES])?;
905            encoded_posts_ids.push(post_id);
906            encoded_posts_lengths.push(encoded_post_bytes.len());
907            encoded_posts_bytes.append(&mut encoded_post_bytes);
908        }
909
910        let header = EncodedPostBundleHeaderV1 {
911            time_millis: time_provider.current_time_millis(),
912            location_id: Id::random(),
913            overflowed: false,
914            sealed: false,
915            num_posts: 0,
916            encoded_post_ids: encoded_posts_ids,
917            encoded_post_lengths: encoded_posts_lengths,
918            encoded_post_healed: HashSet::new(),
919            peer,
920            signature: Signature::zero(),
921        };
922
923        let peers_nearer = {
924            vec![
925                ServerId::new("own_pow", &time_provider, Pow(4), true, &pow_generator).await?.to_peer(&time_provider)?,
926                ServerId::new("own_pow", &time_provider, Pow(4), true, &pow_generator).await?.to_peer(&time_provider)?,
927                ServerId::new("own_pow", &time_provider, Pow(4), true, &pow_generator).await?.to_peer(&time_provider)?,
928            ]
929        };
930
931        let response = GetPostBundleResponseV1 {
932            peers_nearer,
933            cache_request_token: None,
934            post_bundles_cached: vec![],
935            post_bundle: Some((EncodedPostBundleV1 { header, encoded_posts_bytes: Bytes::from(encoded_posts_bytes) }).to_bytes()?),
936        };
937
938        let encoded = response.to_bytes_gatherer()?.to_bytes();
939        let response_decoded = GetPostBundleResponseV1::from_bytes(encoded)?;
940
941        assert_eq!(response_decoded, response);
942
943        Ok(())
944    }
945
946    async fn make_signed_header(time_provider: &RealTimeProvider) -> anyhow::Result<(EncodedPostBundleHeaderV1, ServerId)> {
947        let pow_generator = SingleThreadedPowGenerator::new();
948        let server_id = ServerId::new("own_pow", time_provider, config::SERVER_KEY_POW_MIN, true, &pow_generator).await?;
949        let peer = server_id.to_peer(time_provider)?;
950        let num_posts: u8 = 4;
951        let mut header = EncodedPostBundleHeaderV1 {
952            time_millis: time_provider.current_time_millis(),
953            location_id: Id::random(),
954            overflowed: false,
955            sealed: false,
956            num_posts,
957            encoded_post_ids: (0..num_posts).map(|_| Id::random()).collect(),
958            encoded_post_lengths: (0..num_posts).map(|i| (i as usize + 1) * 64).collect(),
959            encoded_post_healed: HashSet::new(),
960            peer,
961            signature: Signature::zero(),
962        };
963        header.signature_generate(&server_id.keys.signature_key)?;
964        Ok((header, server_id))
965    }
966
967    fn make_bucket_location() -> anyhow::Result<BucketLocation> {
968        BucketLocation::new(BucketType::User, Id::random(), MILLIS_IN_HOUR, TimeMillis(1_700_000_000_000))
969    }
970
971    #[tokio::test]
972    #[allow(non_snake_case)]
973    async fn test_SubmitPostClaimTokenV1_verify() -> anyhow::Result<()> {
974        let time_provider = RealTimeProvider;
975        let pow_generator = SingleThreadedPowGenerator::new();
976        let server_id = ServerId::new("own_pow", &time_provider, Pow(4), true, &pow_generator).await?;
977        let peer = server_id.to_peer(&time_provider)?;
978
979        let token = SubmitPostClaimTokenV1::new(peer, make_bucket_location()?, Id::random(), &server_id.keys.signature_key);
980        token.verify()?;
981        Ok(())
982    }
983
984    #[tokio::test]
985    #[allow(non_snake_case)]
986    async fn test_SubmitPostCommitTokenV1_verify() -> anyhow::Result<()> {
987        let time_provider = RealTimeProvider;
988        let pow_generator = SingleThreadedPowGenerator::new();
989        let server_id = ServerId::new("own_pow", &time_provider, Pow(4), true, &pow_generator).await?;
990        let peer = server_id.to_peer(&time_provider)?;
991
992        let token = SubmitPostCommitTokenV1::new(peer, make_bucket_location()?, Id::random(), &server_id.keys.signature_key);
993        token.verify()?;
994        Ok(())
995    }
996
997    #[tokio::test]
998    #[allow(non_snake_case)]
999    async fn test_to_from_HealPostBundleClaimV1() -> anyhow::Result<()> {
1000        let time_provider = RealTimeProvider;
1001        let (header, _) = make_signed_header(&time_provider).await?;
1002
1003        let bucket_location = make_bucket_location()?;
1004        let encoded = HealPostBundleClaimV1::new_to_bytes(&header, &bucket_location)?;
1005        let mut bytes = encoded;
1006        let decoded = HealPostBundleClaimV1::from_bytes(&mut bytes)?;
1007
1008        assert_eq!(decoded.donor_header, header);
1009        assert_eq!(decoded.bucket_location, bucket_location);
1010        Ok(())
1011    }
1012
1013    #[tokio::test]
1014    #[allow(non_snake_case)]
1015    async fn test_to_from_HealPostBundleClaimResponseV1() -> anyhow::Result<()> {
1016        let time_provider = RealTimeProvider;
1017        let (header, server_id) = make_signed_header(&time_provider).await?;
1018        let peer = server_id.to_peer(&time_provider)?;
1019
1020        let needed_post_ids = header.encoded_post_ids[0..2].to_vec();
1021        let token = HealPostBundleClaimTokenV1::new(
1022            peer,
1023            make_bucket_location()?,
1024            needed_post_ids.clone(),
1025            header.signature,
1026            &server_id.keys.signature_key,
1027        );
1028
1029        let response = HealPostBundleClaimResponseV1 { needed_post_ids, token: Some(token) };
1030
1031        let encoded = json::struct_to_bytes(&response)?;
1032        let decoded = json::bytes_to_struct::<HealPostBundleClaimResponseV1>(&encoded)?;
1033
1034        assert_eq!(decoded, response);
1035        Ok(())
1036    }
1037
1038    #[tokio::test]
1039    #[allow(non_snake_case)]
1040    async fn test_HealPostBundleClaimTokenV1_verify() -> anyhow::Result<()> {
1041        let time_provider = RealTimeProvider;
1042        let (header, server_id) = make_signed_header(&time_provider).await?;
1043        let peer = server_id.to_peer(&time_provider)?;
1044
1045        let token = HealPostBundleClaimTokenV1::new(
1046            peer,
1047            make_bucket_location()?,
1048            header.encoded_post_ids.clone(),
1049            header.signature,
1050            &server_id.keys.signature_key,
1051        );
1052
1053        token.verify()?;
1054        Ok(())
1055    }
1056
1057    #[tokio::test]
1058    #[allow(non_snake_case)]
1059    async fn test_to_from_HealPostBundleCommitV1() -> anyhow::Result<()> {
1060        let time_provider = RealTimeProvider;
1061        let (header, server_id) = make_signed_header(&time_provider).await?;
1062        let peer = server_id.to_peer(&time_provider)?;
1063
1064        let token = HealPostBundleClaimTokenV1::new(
1065            peer,
1066            make_bucket_location()?,
1067            header.encoded_post_ids.clone(),
1068            header.signature,
1069            &server_id.keys.signature_key,
1070        );
1071
1072        let post_bytes = tools::random_bytes(512);
1073        let encoded = HealPostBundleCommitV1::new_to_bytes(&token, &header, &post_bytes)?;
1074        let mut bytes = encoded;
1075        let decoded = HealPostBundleCommitV1::from_bytes(&mut bytes)?;
1076
1077        assert_eq!(decoded.token, token);
1078        assert_eq!(decoded.donor_header, header);
1079        assert_eq!(decoded.encoded_posts_bytes.as_ref(), post_bytes.as_slice());
1080        Ok(())
1081    }
1082
1083    #[tokio::test]
1084    #[allow(non_snake_case)]
1085    async fn test_to_from_HealPostBundleCommitResponseV1() -> anyhow::Result<()> {
1086        for accepted in [true, false] {
1087            let response = HealPostBundleCommitResponseV1 { accepted };
1088            let encoded = json::struct_to_bytes(&response)?;
1089            let decoded = json::bytes_to_struct::<HealPostBundleCommitResponseV1>(&encoded)?;
1090            assert_eq!(decoded, response);
1091        }
1092        Ok(())
1093    }
1094
1095    #[tokio::test]
1096    #[allow(non_snake_case)]
1097    async fn test_CacheRequestTokenV1_verify() -> anyhow::Result<()> {
1098        use crate::protocol::payload::payload::CacheRequestTokenV1;
1099        use crate::tools::time::MILLIS_IN_MINUTE;
1100
1101        let time_provider = RealTimeProvider;
1102        let pow_generator = SingleThreadedPowGenerator::new();
1103        let server_id = ServerId::new("own_pow", &time_provider, Pow(4), true, &pow_generator).await?;
1104        let peer = server_id.to_peer(&time_provider)?;
1105        let bucket_location = make_bucket_location()?;
1106        let expires_at = time_provider.current_time_millis() + MILLIS_IN_MINUTE;
1107        let already_cached_peer_ids = vec![Id::random(), Id::random()];
1108
1109        let token = CacheRequestTokenV1::new(peer, bucket_location, expires_at, already_cached_peer_ids, &server_id.keys.signature_key);
1110        token.verify()?;
1111        Ok(())
1112    }
1113
1114    #[tokio::test]
1115    #[allow(non_snake_case)]
1116    async fn test_to_from_CachePostBundleV1() -> anyhow::Result<()> {
1117        use crate::protocol::payload::payload::{CachePostBundleV1, CacheRequestTokenV1};
1118        use crate::tools::time::MILLIS_IN_MINUTE;
1119
1120        let time_provider = RealTimeProvider;
1121        let pow_generator = SingleThreadedPowGenerator::new();
1122        let server_id = ServerId::new("own_pow", &time_provider, Pow(4), true, &pow_generator).await?;
1123        let peer = server_id.to_peer(&time_provider)?;
1124        let bucket_location = make_bucket_location()?;
1125        let expires_at = time_provider.current_time_millis() + MILLIS_IN_MINUTE;
1126        let token = CacheRequestTokenV1::new(peer, bucket_location, expires_at, vec![], &server_id.keys.signature_key);
1127
1128        // Multiple bundles — one per originator
1129        let bundle_a = tools::random_bytes(512);
1130        let bundle_b = tools::random_bytes(256);
1131        let bundle_c = tools::random_bytes(1024);
1132        let bundles: &[&[u8]] = &[&bundle_a, &bundle_b, &bundle_c];
1133
1134        let encoded = CachePostBundleV1::new_to_bytes(&token, bundles)?;
1135        let mut bytes = encoded;
1136        let decoded = CachePostBundleV1::from_bytes(&mut bytes)?;
1137
1138        assert_eq!(decoded.token, token);
1139        assert_eq!(decoded.encoded_post_bundles.len(), 3);
1140        assert_eq!(decoded.encoded_post_bundles[0].as_ref(), bundle_a.as_slice());
1141        assert_eq!(decoded.encoded_post_bundles[1].as_ref(), bundle_b.as_slice());
1142        assert_eq!(decoded.encoded_post_bundles[2].as_ref(), bundle_c.as_slice());
1143        Ok(())
1144    }
1145
1146    #[tokio::test]
1147    #[allow(non_snake_case)]
1148    async fn test_to_from_CachePostBundleV1_empty() -> anyhow::Result<()> {
1149        use crate::protocol::payload::payload::{CachePostBundleV1, CacheRequestTokenV1};
1150        use crate::tools::time::MILLIS_IN_MINUTE;
1151
1152        let time_provider = RealTimeProvider;
1153        let pow_generator = SingleThreadedPowGenerator::new();
1154        let server_id = ServerId::new("own_pow", &time_provider, Pow(4), true, &pow_generator).await?;
1155        let peer = server_id.to_peer(&time_provider)?;
1156        let bucket_location = make_bucket_location()?;
1157        let expires_at = time_provider.current_time_millis() + MILLIS_IN_MINUTE;
1158        let token = CacheRequestTokenV1::new(peer, bucket_location, expires_at, vec![], &server_id.keys.signature_key);
1159
1160        let encoded = CachePostBundleV1::new_to_bytes(&token, &[])?;
1161        let mut bytes = encoded;
1162        let decoded = CachePostBundleV1::from_bytes(&mut bytes)?;
1163
1164        assert_eq!(decoded.token, token);
1165        assert!(decoded.encoded_post_bundles.is_empty());
1166        Ok(())
1167    }
1168
1169    #[tokio::test]
1170    #[allow(non_snake_case)]
1171    async fn test_to_from_CachePostBundleFeedbackV1() -> anyhow::Result<()> {
1172        use crate::protocol::payload::payload::{CachePostBundleFeedbackV1, CacheRequestTokenV1};
1173        use crate::tools::time::MILLIS_IN_MINUTE;
1174
1175        let time_provider = RealTimeProvider;
1176        let pow_generator = SingleThreadedPowGenerator::new();
1177        let server_id = ServerId::new("own_pow", &time_provider, Pow(4), true, &pow_generator).await?;
1178        let peer = server_id.to_peer(&time_provider)?;
1179        let bucket_location = make_bucket_location()?;
1180        let expires_at = time_provider.current_time_millis() + MILLIS_IN_MINUTE;
1181        let already_cached_peer_ids = vec![Id::random()];
1182        let token = CacheRequestTokenV1::new(peer, bucket_location, expires_at, already_cached_peer_ids, &server_id.keys.signature_key);
1183
1184        let feedback_bytes = tools::random_bytes(256);
1185        let encoded = CachePostBundleFeedbackV1::new_to_bytes(&token, &feedback_bytes)?;
1186        let mut bytes = encoded;
1187        let decoded = CachePostBundleFeedbackV1::from_bytes(&mut bytes)?;
1188
1189        assert_eq!(decoded.token, token);
1190        assert_eq!(decoded.encoded_post_bundle_feedback_bytes.as_ref(), feedback_bytes.as_slice());
1191        Ok(())
1192    }
1193
1194    #[tokio::test]
1195    #[allow(non_snake_case)]
1196    async fn test_to_from_FetchUrlPreviewV1() -> anyhow::Result<()> {
1197        use crate::protocol::payload::payload::FetchUrlPreviewV1;
1198
1199        let url = "https://example.com/article?q=1#section";
1200        let encoded = FetchUrlPreviewV1::new_to_bytes(url)?;
1201        let mut bytes = encoded;
1202        let decoded = FetchUrlPreviewV1::from_bytes(&mut bytes)?;
1203
1204        assert_eq!(decoded.url, url);
1205        Ok(())
1206    }
1207
1208    #[tokio::test]
1209    #[allow(non_snake_case)]
1210    async fn test_to_from_FetchUrlPreviewResponseV1() -> anyhow::Result<()> {
1211        use crate::protocol::payload::payload::FetchUrlPreviewResponseV1;
1212
1213        let response = FetchUrlPreviewResponseV1 {
1214            url: "https://example.com/canonical".to_string(),
1215            title: "Example Title".to_string(),
1216            description: "A short description.".to_string(),
1217            image_url: "https://example.com/image.png".to_string(),
1218        };
1219
1220        let encoded = response.to_bytes()?;
1221        let decoded = FetchUrlPreviewResponseV1::from_bytes(&encoded)?;
1222
1223        assert_eq!(decoded, response);
1224        Ok(())
1225    }
1226
1227    // ── Robustness tests: CachePostBundleV1::from_bytes ──
1228
1229    #[test]
1230    #[allow(non_snake_case)]
1231    fn test_CachePostBundleV1_from_bytes_empty_input() {
1232        use crate::protocol::payload::payload::CachePostBundleV1;
1233        let mut bytes = Bytes::new();
1234        assert!(CachePostBundleV1::from_bytes(&mut bytes).is_err());
1235    }
1236
1237    #[test]
1238    #[allow(non_snake_case)]
1239    fn test_CachePostBundleV1_from_bytes_garbage_input() {
1240        use crate::protocol::payload::payload::CachePostBundleV1;
1241        let mut bytes = Bytes::from_static(&[0xff; 64]);
1242        assert!(CachePostBundleV1::from_bytes(&mut bytes).is_err());
1243    }
1244
1245    #[test]
1246    #[allow(non_snake_case)]
1247    fn test_CachePostBundleV1_from_bytes_truncated_count() {
1248        use crate::protocol::payload::payload::CachePostBundleV1;
1249        // Valid length-prefixed JSON for an empty-ish struct won't work here since CacheRequestTokenV1
1250        // has required fields. But a single byte after a valid-looking length prefix will fail at JSON parse.
1251        // The simplest test: just one byte (not enough for the length prefix u64).
1252        let mut bytes = Bytes::from_static(&[0x01]);
1253        assert!(CachePostBundleV1::from_bytes(&mut bytes).is_err());
1254    }
1255
1256    // ── Robustness tests: GetPostBundleResponseV1::from_bytes ──
1257
1258    #[test]
1259    #[allow(non_snake_case)]
1260    fn test_GetPostBundleResponseV1_from_bytes_empty_input() {
1261        let bytes = Bytes::new();
1262        assert!(GetPostBundleResponseV1::from_bytes(bytes).is_err());
1263    }
1264
1265    #[test]
1266    #[allow(non_snake_case)]
1267    fn test_GetPostBundleResponseV1_from_bytes_garbage_input() {
1268        let bytes = Bytes::from_static(&[0xff; 128]);
1269        assert!(GetPostBundleResponseV1::from_bytes(bytes).is_err());
1270    }
1271
1272    // ── Robustness tests: HealPostBundleClaimV1::from_bytes ──
1273
1274    #[test]
1275    #[allow(non_snake_case)]
1276    fn test_HealPostBundleClaimV1_from_bytes_empty_input() {
1277        let mut bytes = Bytes::new();
1278        assert!(HealPostBundleClaimV1::from_bytes(&mut bytes).is_err());
1279    }
1280
1281    #[test]
1282    #[allow(non_snake_case)]
1283    fn test_HealPostBundleClaimV1_from_bytes_garbage_input() {
1284        let mut bytes = Bytes::from_static(&[0xff; 64]);
1285        assert!(HealPostBundleClaimV1::from_bytes(&mut bytes).is_err());
1286    }
1287
1288    // ── Robustness tests: HealPostBundleCommitV1::from_bytes ──
1289
1290    #[test]
1291    #[allow(non_snake_case)]
1292    fn test_HealPostBundleCommitV1_from_bytes_empty_input() {
1293        let mut bytes = Bytes::new();
1294        assert!(HealPostBundleCommitV1::from_bytes(&mut bytes).is_err());
1295    }
1296
1297    #[test]
1298    #[allow(non_snake_case)]
1299    fn test_HealPostBundleCommitV1_from_bytes_garbage_input() {
1300        let mut bytes = Bytes::from_static(&[0xff; 64]);
1301        assert!(HealPostBundleCommitV1::from_bytes(&mut bytes).is_err());
1302    }
1303
1304    // ── Robustness tests: FetchUrlPreviewV1::from_bytes ──
1305
1306    #[test]
1307    #[allow(non_snake_case)]
1308    fn test_FetchUrlPreviewV1_from_bytes_empty_input() {
1309        use crate::protocol::payload::payload::FetchUrlPreviewV1;
1310        let mut bytes = Bytes::new();
1311        assert!(FetchUrlPreviewV1::from_bytes(&mut bytes).is_err());
1312    }
1313
1314    // ── Robustness tests: FetchUrlPreviewResponseV1::from_bytes ──
1315
1316    #[test]
1317    #[allow(non_snake_case)]
1318    fn test_FetchUrlPreviewResponseV1_from_bytes_empty_input() {
1319        use crate::protocol::payload::payload::FetchUrlPreviewResponseV1;
1320        let bytes = Bytes::new();
1321        assert!(FetchUrlPreviewResponseV1::from_bytes(&bytes).is_err());
1322    }
1323
1324    #[test]
1325    #[allow(non_snake_case)]
1326    fn test_FetchUrlPreviewResponseV1_from_bytes_garbage_input() {
1327        use crate::protocol::payload::payload::FetchUrlPreviewResponseV1;
1328        let bytes = Bytes::from_static(&[0xff; 64]);
1329        assert!(FetchUrlPreviewResponseV1::from_bytes(&bytes).is_err());
1330    }
1331
1332    // ── Robustness tests: SubmitPostClaimV1::from_bytes ──
1333
1334    #[test]
1335    #[allow(non_snake_case)]
1336    fn test_SubmitPostClaimV1_from_bytes_empty_input() {
1337        use crate::protocol::payload::payload::SubmitPostClaimV1;
1338        let mut bytes = Bytes::new();
1339        assert!(SubmitPostClaimV1::from_bytes(&mut bytes).is_err());
1340    }
1341
1342    #[test]
1343    #[allow(non_snake_case)]
1344    fn test_SubmitPostClaimV1_from_bytes_garbage_input() {
1345        use crate::protocol::payload::payload::SubmitPostClaimV1;
1346        let mut bytes = Bytes::from_static(&[0xff; 64]);
1347        assert!(SubmitPostClaimV1::from_bytes(&mut bytes).is_err());
1348    }
1349
1350    // ── Robustness tests: SubmitPostCommitV1::from_bytes ──
1351
1352    #[test]
1353    #[allow(non_snake_case)]
1354    fn test_SubmitPostCommitV1_from_bytes_empty_input() {
1355        use crate::protocol::payload::payload::SubmitPostCommitV1;
1356        let mut bytes = Bytes::new();
1357        assert!(SubmitPostCommitV1::from_bytes(&mut bytes).is_err());
1358    }
1359
1360    // ── Robustness tests: SubmitPostFeedbackV1::from_bytes ──
1361
1362    #[test]
1363    #[allow(non_snake_case)]
1364    fn test_SubmitPostFeedbackV1_from_bytes_empty_input() {
1365        use crate::protocol::payload::payload::SubmitPostFeedbackV1;
1366        let mut bytes = Bytes::new();
1367        assert!(SubmitPostFeedbackV1::from_bytes(&mut bytes).is_err());
1368    }
1369
1370    // ── Robustness tests: GetPostBundleFeedbackResponseV1::from_bytes ──
1371
1372    #[test]
1373    #[allow(non_snake_case)]
1374    fn test_GetPostBundleFeedbackResponseV1_from_bytes_empty_input() {
1375        use crate::protocol::payload::payload::GetPostBundleFeedbackResponseV1;
1376        let bytes = Bytes::new();
1377        assert!(GetPostBundleFeedbackResponseV1::from_bytes(bytes).is_err());
1378    }
1379
1380    // ── Robustness tests: CachePostBundleFeedbackV1::from_bytes ──
1381
1382    #[test]
1383    #[allow(non_snake_case)]
1384    fn test_CachePostBundleFeedbackV1_from_bytes_empty_input() {
1385        use crate::protocol::payload::payload::CachePostBundleFeedbackV1;
1386        let mut bytes = Bytes::new();
1387        assert!(CachePostBundleFeedbackV1::from_bytes(&mut bytes).is_err());
1388    }
1389
1390    #[cfg(not(target_arch = "wasm32"))]
1391    mod bolero_fuzz {
1392        use bytes::Bytes;
1393
1394        #[test]
1395        #[allow(non_snake_case)]
1396        fn fuzz_CachePostBundleV1_from_bytes() {
1397            use crate::protocol::payload::payload::CachePostBundleV1;
1398            bolero::check!().for_each(|data: &[u8]| {
1399                let mut bytes = Bytes::copy_from_slice(data);
1400                let _ = CachePostBundleV1::from_bytes(&mut bytes);
1401            });
1402        }
1403
1404        #[test]
1405        #[allow(non_snake_case)]
1406        fn fuzz_GetPostBundleResponseV1_from_bytes() {
1407            use crate::protocol::payload::payload::GetPostBundleResponseV1;
1408            bolero::check!().for_each(|data: &[u8]| {
1409                let _ = GetPostBundleResponseV1::from_bytes(Bytes::copy_from_slice(data));
1410            });
1411        }
1412
1413        #[test]
1414        #[allow(non_snake_case)]
1415        fn fuzz_GetPostBundleFeedbackResponseV1_from_bytes() {
1416            use crate::protocol::payload::payload::GetPostBundleFeedbackResponseV1;
1417            bolero::check!().for_each(|data: &[u8]| {
1418                let _ = GetPostBundleFeedbackResponseV1::from_bytes(Bytes::copy_from_slice(data));
1419            });
1420        }
1421
1422        #[test]
1423        #[allow(non_snake_case)]
1424        fn fuzz_SubmitPostClaimV1_from_bytes() {
1425            use crate::protocol::payload::payload::SubmitPostClaimV1;
1426            bolero::check!().for_each(|data: &[u8]| {
1427                let mut bytes = Bytes::copy_from_slice(data);
1428                let _ = SubmitPostClaimV1::from_bytes(&mut bytes);
1429            });
1430        }
1431
1432        #[test]
1433        #[allow(non_snake_case)]
1434        fn fuzz_HealPostBundleClaimV1_from_bytes() {
1435            use crate::protocol::payload::payload::HealPostBundleClaimV1;
1436            bolero::check!().for_each(|data: &[u8]| {
1437                let mut bytes = Bytes::copy_from_slice(data);
1438                let _ = HealPostBundleClaimV1::from_bytes(&mut bytes);
1439            });
1440        }
1441
1442        #[test]
1443        #[allow(non_snake_case)]
1444        fn fuzz_HealPostBundleCommitV1_from_bytes() {
1445            use crate::protocol::payload::payload::HealPostBundleCommitV1;
1446            bolero::check!().for_each(|data: &[u8]| {
1447                let mut bytes = Bytes::copy_from_slice(data);
1448                let _ = HealPostBundleCommitV1::from_bytes(&mut bytes);
1449            });
1450        }
1451    }
1452
1453    #[test]
1454    fn test_payload_kind_wire_values_and_display_are_stable() {
1455        // `FromRepr` / `Display` come from strum_macros; the u16 values are on the wire.
1456        use crate::protocol::payload::payload::{PayloadRequestKind, PayloadResponseKind};
1457        assert_eq!(PayloadRequestKind::from_u16(0).unwrap(), PayloadRequestKind::ErrorV1);
1458        assert_eq!(PayloadRequestKind::from_u16(3).unwrap(), PayloadRequestKind::RetiredAnnounceV1, "retired slot must stay reserved so later kinds keep their wire values");
1459        assert_eq!(PayloadResponseKind::from_u16(3).unwrap(), PayloadResponseKind::RetiredAnnounceResponseV1);
1460        assert_eq!(PayloadRequestKind::from_u16(4).unwrap(), PayloadRequestKind::GetPostBundleV1);
1461        assert_eq!(PayloadResponseKind::from_u16(4).unwrap(), PayloadResponseKind::GetPostBundleResponseV1);
1462        assert_eq!(PayloadRequestKind::from_u16(17).unwrap(), PayloadRequestKind::AnnounceV2);
1463        assert_eq!(PayloadResponseKind::from_u16(0).unwrap(), PayloadResponseKind::ErrorResponseV1);
1464        assert_eq!(PayloadResponseKind::from_u16(17).unwrap(), PayloadResponseKind::AnnounceResponseV2);
1465        assert!(PayloadResponseKind::from_u16(18).is_err());
1466        assert_eq!(PayloadRequestKind::GetPostBundleV1.to_string(), "GetPostBundleV1");
1467        assert_eq!(PayloadResponseKind::AnnounceResponseV2.to_string(), "AnnounceResponseV2");
1468    }
1469
1470    #[test]
1471    #[allow(non_snake_case)]
1472    fn test_PAYLOAD_REQUEST_KIND_COUNT_matches_variants() {
1473        use crate::protocol::payload::payload::{PayloadRequestKind, PAYLOAD_REQUEST_KIND_COUNT};
1474        let last_variant = PayloadRequestKind::from_u16((PAYLOAD_REQUEST_KIND_COUNT - 1) as u16)
1475            .expect("PAYLOAD_REQUEST_KIND_COUNT - 1 must decode to a valid variant");
1476        assert_eq!(last_variant, PayloadRequestKind::AnnounceV2, "last variant changed; bump PAYLOAD_REQUEST_KIND_COUNT");
1477        assert!(PayloadRequestKind::from_u16(PAYLOAD_REQUEST_KIND_COUNT as u16).is_err(), "PAYLOAD_REQUEST_KIND_COUNT must equal the variant count");
1478    }
1479
1480    #[test]
1481    #[allow(non_snake_case)]
1482    fn test_to_from_PeerStatsRequestV1() -> anyhow::Result<()> {
1483        use crate::protocol::payload::payload::PeerStatsRequestV1;
1484        let request = PeerStatsRequestV1 {};
1485        let encoded = request.to_bytes()?;
1486        let decoded = PeerStatsRequestV1::from_bytes(&encoded)?;
1487        assert_eq!(decoded, request);
1488        Ok(())
1489    }
1490
1491    #[tokio::test]
1492    #[allow(non_snake_case)]
1493    async fn test_to_from_PeerStatsResponseV1() -> anyhow::Result<()> {
1494        use crate::protocol::payload::payload::PeerStatsResponseV1;
1495        use crate::tools::compression;
1496
1497        let time_provider = RealTimeProvider;
1498        let pow_generator = SingleThreadedPowGenerator::new();
1499        let server_id = ServerId::new("own_pow", &time_provider, Pow(4), true, &pow_generator).await?;
1500        let peer = server_id.to_peer(&time_provider)?;
1501
1502        let doc = serde_json::json!({ "requests": { "PingV1": 7 }, "system": { "memory_total_bytes": 1234 } });
1503        let json_bytes = serde_json::to_vec(&doc)?;
1504        let json_compressed = compression::compress_for_speed(&json_bytes)?.to_bytes();
1505
1506        let timestamp = time_provider.current_time_millis();
1507        let signing_input = PeerStatsResponseV1::signing_input(timestamp, &json_compressed);
1508        let signature = crate::tools::signing::sign(&server_id.keys.signature_key, &signing_input);
1509
1510        let response = PeerStatsResponseV1 { peer, timestamp, json_compressed, signature };
1511        let encoded = response.to_bytes()?;
1512        let decoded = PeerStatsResponseV1::from_bytes(&encoded)?;
1513        assert_eq!(decoded, response);
1514        Ok(())
1515    }
1516}