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::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`, `AnnounceV1`), 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    AnnounceV1,
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}
68
69impl PayloadRequestKind {
70    pub fn from_u16(value: u16) -> anyhow::Result<Self> {
71        Self::from_repr(value).ok_or_else(|| anyhow::anyhow!("unknown PayloadRequestKind: {}", value))
72    }
73}
74
75/// The wire-level discriminator for every RPC response in the protocol.
76///
77/// Each variant pairs with a [`PayloadRequestKind`] of the same name minus the `Response`
78/// suffix (plus the unpaired `ErrorResponseV1`, which any handler may return on failure).
79/// The discriminator rides in the response header so the client knows which payload
80/// deserializer to use when it parses an [`crate::protocol::rpc::RpcResponsePacketRx`].
81///
82/// Additions go at the end of the enum to preserve backwards compatibility.
83#[derive(Debug, Display, PartialEq, Clone, FromRepr)]
84#[repr(u16)]
85pub enum PayloadResponseKind {
86    ErrorResponseV1, // Can be returned by any RPC call if an error happens on the server
87    PingResponseV1,
88    BootstrapResponseV1,
89    AnnounceResponseV1,
90    GetPostBundleResponseV1,
91    GetPostBundleFeedbackResponseV1,
92    SubmitPostClaimResponseV1,
93    SubmitPostCommitResponseV1,
94    SubmitPostFeedbackResponseV1,
95    HealPostBundleClaimResponseV1,
96    HealPostBundleCommitResponseV1,
97    HealPostBundleFeedbackResponseV1,
98    CachePostBundleResponseV1,
99    CachePostBundleFeedbackResponseV1,
100    FetchUrlPreviewResponseV1,
101    TrendingHashtagsFetchResponseV1,
102}
103
104impl PayloadResponseKind {
105    pub fn from_u16(value: u16) -> anyhow::Result<Self> {
106        Self::from_repr(value).ok_or_else(|| anyhow::anyhow!("unknown PayloadResponseKind: {}", value))
107    }
108}
109
110/// The universal error response body returned when a handler rejects a request.
111///
112/// Any RPC handler may return an `ErrorResponseV1` (packaged in a response with
113/// [`PayloadResponseKind::ErrorResponseV1`]) instead of its usual success shape. The `code`
114/// is a numeric discriminator to let callers branch on error class without string parsing;
115/// the `message` is a human-readable explanation for logs and diagnostics.
116#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
117pub struct ErrorResponseV1 {
118    pub code: u16,
119    pub message: String,
120}
121
122#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
123pub struct PingV1 {}
124
125#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
126pub struct PingResponseV1 {
127    pub peer: Peer,
128}
129
130#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
131pub struct BootstrapV1 {}
132
133#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
134pub struct BootstrapResponseV1 {
135    pub peers_random: Vec<Peer>,
136}
137
138#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
139pub struct AnnounceV1 {
140    pub peer_self: Peer,
141}
142
143#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
144pub struct AnnounceResponseV1 {
145    pub peer_self: Peer,
146    pub peers_nearest: Vec<Peer>,
147}
148
149#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
150pub struct GetPostBundleV1 {
151    pub bucket_location: BucketLocation,
152    pub peers_visited: Vec<Peer>,
153    pub already_retrieved_peer_ids: Vec<Id>,
154}
155
156#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
157pub struct GetPostBundleFeedbackV1 {
158    pub bucket_location: BucketLocation,
159    pub peers_visited: Vec<Peer>,
160    pub already_retrieved_peer_ids: Vec<Id>,
161}
162
163#[derive(Debug, PartialEq, Clone)]
164pub struct GetPostBundleResponseV1 {
165    pub peers_nearer: Vec<Peer>,
166    pub cache_request_token: Option<CacheRequestTokenV1>,
167    pub post_bundles_cached: Vec<Bytes>,  // Cached EncodedPostBundleV1 from intermediate servers
168    pub post_bundle: Option<Bytes>, // Primary EncodedPostBundleV1 from the responsible server
169}
170
171impl GetPostBundleResponseV1 {
172    pub fn to_bytes_gatherer(&self) -> anyhow::Result<BytesGatherer> {
173        let mut bytes_gatherer = BytesGatherer::default();
174
175        tools::write_length_prefixed_json::<Vec<Peer>>(&mut bytes_gatherer, &self.peers_nearer)?;
176
177        match &self.cache_request_token {
178            Some(token) => {
179                bytes_gatherer.put_u8(1);
180                tools::write_length_prefixed_json(&mut bytes_gatherer, token)?;
181            }
182            None => bytes_gatherer.put_u8(0),
183        }
184
185        bytes_gatherer.put_u16(self.post_bundles_cached.len() as u16);
186        for bundle in &self.post_bundles_cached {
187            bytes_gatherer.put_u32(bundle.len() as u32);
188            bytes_gatherer.put_bytes(bundle.clone());
189        }
190
191        match &self.post_bundle {
192            Some(post_bundle) => { bytes_gatherer.put_u8(1); bytes_gatherer.put_bytes(post_bundle.clone()); }
193            None => { bytes_gatherer.put_u8(0); }
194        }
195
196        Ok(bytes_gatherer)
197    }
198
199    pub fn from_bytes(mut bytes: Bytes) -> anyhow::Result<Self> {
200        use bytes::Buf;
201
202        let peers_nearer = tools::read_length_prefixed_json::<Vec<Peer>>(&mut bytes)?;
203
204        if bytes.remaining() < 1 {
205            anyhow::bail!("Invalid buffer: missing cache_request_token presence flag");
206        }
207        let cache_request_token = match bytes.get_u8() {
208            0 => None,
209            1 => Some(tools::read_length_prefixed_json::<CacheRequestTokenV1>(&mut bytes)?),
210            _ => anyhow::bail!("Invalid buffer: unknown cache_request_token presence flag"),
211        };
212
213        anyhow_assert_ge!(bytes.remaining(), 2, "Missing post_bundles_cached count");
214        let cached_count = bytes.get_u16() as usize;
215        let mut post_bundles_cached = Vec::with_capacity(cached_count);
216        for _ in 0..cached_count {
217            anyhow_assert_ge!(bytes.remaining(), 4, "Missing post_bundles_cached entry length");
218            let len = bytes.get_u32() as usize;
219            anyhow_assert_ge!(bytes.remaining(), len, "Truncated post_bundles_cached entry");
220            post_bundles_cached.push(bytes.split_to(len));
221        }
222
223        if bytes.remaining() < 1 {
224            anyhow::bail!("Invalid buffer: missing post_bundle presence flag");
225        }
226        let post_bundle = match bytes.get_u8() {
227            0 => None,
228            1 => Some(bytes.copy_to_bytes(bytes.remaining())),
229            _ => anyhow::bail!("Invalid buffer: unknown post_bundle presence flag"),
230        };
231
232        Ok(Self { peers_nearer, cache_request_token, post_bundles_cached, post_bundle })
233    }
234}
235#[derive(Debug, PartialEq, Clone)]
236pub struct GetPostBundleFeedbackResponseV1 {
237    pub peers_nearer: Vec<Peer>,
238    pub cache_request_token: Option<CacheRequestTokenV1>,
239    pub post_bundle_feedbacks_cached: Vec<Bytes>, // Cached feedback from originators the client hasn't seen yet (filtered by already_retrieved_peer_ids).
240    pub encoded_post_bundle_feedback: Option<Bytes>, // Primary feedback from the responsible server
241}
242
243impl GetPostBundleFeedbackResponseV1 {
244    pub fn to_bytes_gatherer(&self) -> anyhow::Result<BytesGatherer> {
245        let mut bytes_gatherer = BytesGatherer::default();
246
247        tools::write_length_prefixed_json::<Vec<Peer>>(&mut bytes_gatherer, &self.peers_nearer)?;
248
249        match &self.cache_request_token {
250            Some(token) => {
251                bytes_gatherer.put_u8(1u8);
252                tools::write_length_prefixed_json(&mut bytes_gatherer, token)?;
253            }
254            None => bytes_gatherer.put_u8(0u8),
255        }
256
257        bytes_gatherer.put_u16(self.post_bundle_feedbacks_cached.len() as u16);
258        for feedback in &self.post_bundle_feedbacks_cached {
259            bytes_gatherer.put_u32(feedback.len() as u32);
260            bytes_gatherer.put_bytes(feedback.clone());
261        }
262
263        match &self.encoded_post_bundle_feedback {
264            Some(post_bundle_feedback) => {
265                bytes_gatherer.put_u8(1u8);
266                bytes_gatherer.put_bytes(post_bundle_feedback.clone());
267            }
268            None => bytes_gatherer.put_u8(0u8),
269        }
270
271        Ok(bytes_gatherer)
272    }
273
274    pub fn from_bytes(mut bytes: Bytes) -> anyhow::Result<Self> {
275        use bytes::Buf;
276
277        let peers_nearer = tools::read_length_prefixed_json::<Vec<Peer>>(&mut bytes)?;
278
279        if bytes.remaining() < 1 {
280            anyhow::bail!("Invalid buffer: missing cache_request_token presence flag");
281        }
282        let cache_request_token = match bytes.get_u8() {
283            0 => None,
284            1 => Some(tools::read_length_prefixed_json::<CacheRequestTokenV1>(&mut bytes)?),
285            _ => anyhow::bail!("Invalid buffer: unknown cache_request_token presence flag"),
286        };
287
288        anyhow_assert_ge!(bytes.remaining(), 2, "Missing post_bundle_feedbacks_cached count");
289        let cached_count = bytes.get_u16() as usize;
290        let mut post_bundle_feedbacks_cached = Vec::with_capacity(cached_count);
291        for _ in 0..cached_count {
292            anyhow_assert_ge!(bytes.remaining(), 4, "Missing post_bundle_feedbacks_cached entry length");
293            let len = bytes.get_u32() as usize;
294            anyhow_assert_ge!(bytes.remaining(), len, "Truncated post_bundle_feedbacks_cached entry");
295            post_bundle_feedbacks_cached.push(bytes.split_to(len));
296        }
297
298        if bytes.remaining() < 1 {
299            anyhow::bail!("Invalid buffer: missing post_bundle_feedback presence flag");
300        }
301        let post_bundle_feedback = match bytes.get_u8() {
302            0 => None,
303            1 => Some(bytes.copy_to_bytes(bytes.remaining())),
304            _ => anyhow::bail!("Invalid buffer: unknown post_bundle_feedback presence flag"),
305        };
306
307        Ok(Self { peers_nearer, cache_request_token, post_bundle_feedbacks_cached, encoded_post_bundle_feedback: post_bundle_feedback })
308    }
309}
310
311#[derive(Debug, PartialEq, Clone)]
312pub struct SubmitPostClaimV1 {
313    pub bucket_location: BucketLocation,
314    pub referenced_post_header_bytes: Option<Bytes>, // For Sequel posts: the original post's bytes_without_body, used to verify same-author
315    pub referenced_hashtags: Vec<String>,             // Hashtag strings referenced by this post, for trending tracking
316    pub encoded_post_bytes: Bytes,                    // Note that it has just the header of the EncodedPost...
317}
318
319impl SubmitPostClaimV1 {
320    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> {
321        let mut bytes_gatherer = BytesGatherer::default();
322        tools::write_length_prefixed_json(&mut bytes_gatherer, bucket_location)?;
323        match referenced_post_header_bytes {
324            Some(header_bytes) => {
325                bytes_gatherer.put_u32(header_bytes.len() as u32);
326                bytes_gatherer.put_slice(header_bytes);
327            }
328            None => {
329                bytes_gatherer.put_u32(0);
330            }
331        }
332        tools::write_length_prefixed_json(&mut bytes_gatherer, &referenced_hashtags)?;
333        bytes_gatherer.put_slice(encoded_post_bytes_without_body);
334        Ok(bytes_gatherer.to_bytes())
335    }
336
337    pub fn from_bytes(bytes: &mut Bytes) -> anyhow::Result<Self> {
338        use bytes::Buf;
339        let bucket_location = tools::read_length_prefixed_json::<BucketLocation>(bytes)?;
340
341        anyhow::ensure!(bytes.remaining() >= 4, "SubmitPostClaimV1: missing referenced_post_header_bytes length");
342        let referenced_post_header_bytes_len = bytes.get_u32() as usize;
343        let referenced_post_header_bytes = if referenced_post_header_bytes_len > 0 {
344            anyhow::ensure!(bytes.remaining() >= referenced_post_header_bytes_len, "SubmitPostClaimV1: referenced_post_header_bytes length {} exceeds remaining {}", referenced_post_header_bytes_len, bytes.remaining());
345            Some(bytes.split_to(referenced_post_header_bytes_len))
346        } else {
347            None
348        };
349
350        let referenced_hashtags = tools::read_length_prefixed_json::<Vec<String>>(bytes)?;
351
352        anyhow::ensure!(bytes.has_remaining(), "SubmitPostClaimV1: missing encoded_post_bytes");
353        let encoded_post_bytes = bytes.split_to(bytes.len());
354
355        Ok(Self { bucket_location, referenced_post_header_bytes, referenced_hashtags, encoded_post_bytes })
356    }
357}
358
359#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
360pub struct SubmitPostClaimResponseV1 {
361    pub peers_nearer: Vec<Peer>,
362    pub submit_post_claim_token: Option<SubmitPostClaimTokenV1>,
363}
364
365#[derive(Debug, PartialEq, Clone)]
366pub struct SubmitPostCommitV1 {
367    pub bucket_location: BucketLocation,
368    pub submit_post_claim_token: SubmitPostClaimTokenV1,
369    pub encoded_post_bytes: Bytes,
370}
371
372impl SubmitPostCommitV1 {
373    pub fn new_to_bytes(bucket_location: &BucketLocation, submit_post_claim_token: &SubmitPostClaimTokenV1, encoded_post_bytes: &[u8]) -> anyhow::Result<Bytes> {
374        let mut bytes_gatherer = BytesGatherer::default();
375        tools::write_length_prefixed_json(&mut bytes_gatherer, bucket_location)?;
376        tools::write_length_prefixed_json(&mut bytes_gatherer, submit_post_claim_token)?;
377        bytes_gatherer.put_slice(encoded_post_bytes);
378        Ok(bytes_gatherer.to_bytes())
379    }
380
381    pub fn from_bytes(bytes: &mut Bytes) -> anyhow::Result<Self> {
382        let bucket_location = tools::read_length_prefixed_json::<BucketLocation>(bytes)?;
383        let submit_post_claim_token = tools::read_length_prefixed_json::<SubmitPostClaimTokenV1>(bytes)?;
384        let encoded_post_bytes = bytes.split_to(bytes.len());
385
386        Ok(Self {
387            bucket_location,
388            submit_post_claim_token,
389            encoded_post_bytes,
390        })
391    }
392}
393
394#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
395pub struct SubmitPostCommitResponseV1 {
396    pub submit_post_commit_token: SubmitPostCommitTokenV1,
397}
398
399#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
400pub struct SubmitPostClaimTokenV1 {
401    pub peer: Peer,
402    pub bucket_location: BucketLocation,
403    pub post_id: Id,
404    pub token_signature: Signature,
405}
406
407impl SubmitPostClaimTokenV1 {
408    fn get_hash_for_signing(peer: &Peer, bucket_location: &BucketLocation, post_id: &Id) -> Hash {
409        hashing::hash_multiple(&[peer.id.as_ref(), bucket_location.get_hash_for_signing().as_ref(), post_id.as_ref(), "SubmitPostClaimTokenV1".as_bytes()])
410    }
411
412    pub fn new(peer: Peer, bucket_location: BucketLocation, post_id: Id, signature_key: &SignatureKey) -> Self {
413        let token_signature = signing::sign(signature_key, Self::get_hash_for_signing(&peer, &bucket_location, &post_id).as_ref());
414        Self { peer, bucket_location, post_id, token_signature }
415    }
416
417    pub fn verify(&self) -> anyhow::Result<()> {
418        self.peer.verify()?;
419        let verification_key = VerificationKey::from_bytes(&self.peer.verification_key_bytes)?;
420        signing::verify(&verification_key, &self.token_signature, Self::get_hash_for_signing(&self.peer, &self.bucket_location, &self.post_id).as_ref())
421    }
422}
423
424#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
425pub struct SubmitPostCommitTokenV1 {
426    pub peer: Peer,
427    pub bucket_location: BucketLocation,
428    pub post_id: Id,
429    pub token_signature: Signature,
430}
431
432impl SubmitPostCommitTokenV1 {
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(), "SubmitPostCommitTokenV1".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
450#[derive(Debug, PartialEq, Clone)]
451pub struct SubmitPostFeedbackV1 {
452    pub bucket_location: BucketLocation,
453    pub encoded_post_feedback: EncodedPostFeedbackV1,
454}
455
456impl SubmitPostFeedbackV1 {
457    pub fn new_to_bytes(bucket_location: &BucketLocation, encoded_post_feedback: &EncodedPostFeedbackV1) -> anyhow::Result<Bytes> {
458        let mut bytes_gatherer = BytesGatherer::default();
459        tools::write_length_prefixed_json(&mut bytes_gatherer, bucket_location)?;
460        let mut feedback_bytes = BytesMut::new();
461        encoded_post_feedback.append_encode_to_bytes(&mut feedback_bytes)?;
462        bytes_gatherer.put_bytes(feedback_bytes.freeze());
463        Ok(bytes_gatherer.to_bytes())
464    }
465
466    pub fn from_bytes(bytes: &mut Bytes) -> anyhow::Result<Self> {
467        let bucket_location = tools::read_length_prefixed_json::<BucketLocation>(bytes)?;
468        let encoded_post_feedback = EncodedPostFeedbackV1::decode_from_bytes(bytes)?;
469        Ok(Self { bucket_location, encoded_post_feedback })
470    }
471
472}
473
474#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
475pub struct SubmitPostFeedbackResponseV1 {
476    pub peers_nearer: Vec<Peer>,
477    pub accepted: bool,
478}
479
480
481
482// ---- Heal Post Bundle (two-phase: claim then commit) ----
483
484#[derive(Debug, PartialEq, Clone)]
485pub struct HealPostBundleClaimV1 {
486    pub bucket_location: BucketLocation,
487    pub donor_header: EncodedPostBundleHeaderV1,
488}
489
490impl HealPostBundleClaimV1 {
491    pub fn new_to_bytes(donor_header: &EncodedPostBundleHeaderV1, bucket_location: &BucketLocation) -> anyhow::Result<Bytes> {
492        let mut g = BytesGatherer::default();
493        tools::write_length_prefixed_json(&mut g, bucket_location)?;
494        tools::write_length_prefixed_json(&mut g, donor_header)?;
495        Ok(g.to_bytes())
496    }
497
498    pub fn from_bytes(bytes: &mut Bytes) -> anyhow::Result<Self> {
499        let bucket_location = tools::read_length_prefixed_json::<BucketLocation>(bytes)?;
500        let donor_header = tools::read_length_prefixed_json::<EncodedPostBundleHeaderV1>(bytes)?;
501        Ok(Self { donor_header, bucket_location })
502    }
503}
504
505#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
506pub struct HealPostBundleClaimResponseV1 {
507    pub needed_post_ids: Vec<Id>,
508    pub token: Option<HealPostBundleClaimTokenV1>,
509}
510
511#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
512pub struct HealPostBundleClaimTokenV1 {
513    pub peer: Peer,
514    pub bucket_location: BucketLocation,
515    pub donor_header_signature: Signature,
516    pub needed_post_ids: Vec<Id>,
517    pub token_signature: Signature,
518}
519
520impl HealPostBundleClaimTokenV1 {
521    fn get_hash_for_signing(peer_id: &Id, bucket_location: &BucketLocation, donor_header_signature: &Signature, needed_post_ids: &[Id]) -> Hash {
522        let bucket_location_hash = bucket_location.get_hash_for_signing();
523        let mut hash_input: Vec<&[u8]> = vec![
524            peer_id.as_ref(),
525            bucket_location_hash.as_ref(),
526            donor_header_signature.as_ref(),
527            "HealPostBundleClaimTokenV1".as_bytes(),
528        ];
529        for id in needed_post_ids {
530            hash_input.push(id.as_ref());
531        }
532        hashing::hash_multiple(&hash_input)
533    }
534
535    pub fn new(peer: Peer, bucket_location: BucketLocation, needed_post_ids: Vec<Id>, donor_header_signature: Signature, signature_key: &SignatureKey) -> Self {
536        let hash = Self::get_hash_for_signing(&peer.id, &bucket_location, &donor_header_signature, &needed_post_ids);
537        let token_signature = signing::sign(signature_key, hash.as_ref());
538        Self { peer, bucket_location, donor_header_signature, needed_post_ids, token_signature }
539    }
540
541    pub fn verify(&self) -> anyhow::Result<()> {
542        self.peer.verify()?;
543        self.bucket_location.validate()?;
544        let hash = Self::get_hash_for_signing(&self.peer.id, &self.bucket_location, &self.donor_header_signature, &self.needed_post_ids);
545        let verification_key = VerificationKey::from_bytes(&self.peer.verification_key_bytes)?;
546        signing::verify(&verification_key, &self.token_signature, hash.as_ref())
547    }
548}
549
550#[derive(Debug, PartialEq, Clone)]
551pub struct HealPostBundleCommitV1 {
552    pub token: HealPostBundleClaimTokenV1,
553    pub donor_header: EncodedPostBundleHeaderV1,
554    pub encoded_posts_bytes: Bytes,
555}
556
557impl HealPostBundleCommitV1 {
558    pub fn new_to_bytes(token: &HealPostBundleClaimTokenV1, donor_header: &EncodedPostBundleHeaderV1, encoded_posts_bytes: &[u8]) -> anyhow::Result<Bytes> {
559        let mut bytes_gatherer = BytesGatherer::default();
560        tools::write_length_prefixed_json(&mut bytes_gatherer, token)?;
561        tools::write_length_prefixed_json(&mut bytes_gatherer, donor_header)?;
562        bytes_gatherer.put_slice(encoded_posts_bytes);
563        Ok(bytes_gatherer.to_bytes())
564    }
565
566    pub fn from_bytes(bytes: &mut Bytes) -> anyhow::Result<Self> {
567        let token = tools::read_length_prefixed_json::<HealPostBundleClaimTokenV1>(bytes)?;
568        let donor_header = tools::read_length_prefixed_json::<EncodedPostBundleHeaderV1>(bytes)?;
569        let encoded_posts_bytes = bytes.split_to(bytes.len());
570        Ok(Self { token, donor_header, encoded_posts_bytes })
571    }
572}
573
574#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
575pub struct HealPostBundleCommitResponseV1 {
576    pub accepted: bool,
577}
578
579// ---- Heal Post Bundle Feedback (single-phase) ----
580
581#[derive(Debug, PartialEq, Clone)]
582pub struct HealPostBundleFeedbackV1 {
583    pub location_id: Id,
584    pub encoded_post_feedbacks: Vec<EncodedPostFeedbackV1>,
585}
586
587impl HealPostBundleFeedbackV1 {
588    pub fn new_to_bytes(location_id: &Id, feedbacks: &[EncodedPostFeedbackV1]) -> anyhow::Result<Bytes> {
589        let mut bytes = BytesMut::new();
590        bytes.put_slice(location_id.as_ref());
591        for f in feedbacks {
592            f.append_encode_to_bytes(&mut bytes)?;
593        }
594        Ok(bytes.freeze())
595    }
596
597    pub fn from_bytes(bytes: &mut Bytes) -> anyhow::Result<Self> {
598        use bytes::Buf;
599        anyhow_assert_ge!(bytes.remaining(), ID_BYTES, "Missing location_id in HealPostBundleFeedbackV1");
600        let location_id = Id::from_slice(&bytes.split_to(ID_BYTES))?;
601        let mut encoded_post_feedbacks = Vec::new();
602        while bytes.has_remaining() {
603            encoded_post_feedbacks.push(EncodedPostFeedbackV1::decode_from_bytes(&mut *bytes)?);
604        }
605        Ok(Self { location_id, encoded_post_feedbacks })
606    }
607}
608
609#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
610pub struct HealPostBundleFeedbackResponseV1 {
611    pub accepted_count: u32,
612}
613
614
615/// Issued by an intermediate server during a GetPostBundle/GetPostBundleFeedback miss or stale hit.
616/// The client uploads the bundle to this server after completing its walk.
617#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
618pub struct CacheRequestTokenV1 {
619    pub peer: Peer,
620    pub bucket_location: BucketLocation,
621    pub expires_at: TimeMillis,
622    /// Originator peer IDs already held in this server's cache for this location_id.
623    /// The client should only upload a bundle whose originator peer.id is NOT in this list.
624    pub already_cached_peer_ids: Vec<Id>,
625    pub token_signature: Signature,
626}
627
628impl CacheRequestTokenV1 {
629    fn get_hash_for_signing(peer_id: &Id, bucket_location: &BucketLocation, expires_at: &TimeMillis, already_cached_peer_ids: &[Id]) -> Hash {
630        let expires_at_be = expires_at.encode_be();
631        let bucket_location_hash = bucket_location.get_hash_for_signing();
632        let mut inputs: Vec<&[u8]> = vec![peer_id.as_ref(), bucket_location_hash.as_ref(), expires_at_be.as_ref(), "CacheRequestToken".as_bytes()];
633        for id in already_cached_peer_ids {
634            inputs.push(id.as_ref());
635        }
636        hashing::hash_multiple(&inputs)
637    }
638
639    pub fn new(peer: Peer, bucket_location: BucketLocation, expires_at: TimeMillis, already_cached_peer_ids: Vec<Id>, signature_key: &SignatureKey) -> Self {
640        let token_signature = signing::sign(signature_key, Self::get_hash_for_signing(&peer.id, &bucket_location, &expires_at, &already_cached_peer_ids).as_ref());
641        Self { peer, bucket_location, expires_at, already_cached_peer_ids, token_signature }
642    }
643
644    pub fn verify(&self) -> anyhow::Result<()> {
645        self.peer.verify()?;
646        self.bucket_location.validate()?;
647        let verification_key = VerificationKey::from_bytes(&self.peer.verification_key_bytes)?;
648        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())
649    }
650
651    pub fn is_expired(&self, current_time: TimeMillis) -> bool {
652        current_time > self.expires_at
653    }
654}
655
656#[derive(Debug, PartialEq, Clone)]
657pub struct CachePostBundleV1 {
658    pub token: CacheRequestTokenV1,
659    /// One entry per originator — the client uploads all bundles it collected during its walk.
660    pub encoded_post_bundles: Vec<Bytes>,
661}
662
663impl CachePostBundleV1 {
664    pub fn new_to_bytes(token: &CacheRequestTokenV1, encoded_post_bundles: &[&[u8]]) -> anyhow::Result<Bytes> {
665        let mut bytes_gatherer = BytesGatherer::default();
666        tools::write_length_prefixed_json(&mut bytes_gatherer, token)?;
667        bytes_gatherer.put_u16(encoded_post_bundles.len() as u16);
668        for bundle in encoded_post_bundles {
669            bytes_gatherer.put_u32(bundle.len() as u32);
670            bytes_gatherer.put_slice(bundle);
671        }
672        Ok(bytes_gatherer.to_bytes())
673    }
674
675    pub fn from_bytes(bytes: &mut Bytes) -> anyhow::Result<Self> {
676        let token = tools::read_length_prefixed_json::<CacheRequestTokenV1>(bytes)?;
677        anyhow_assert_ge!(bytes.remaining(), 2, "Missing encoded_post_bundles count");
678        let count = bytes.get_u16() as usize;
679        let mut encoded_post_bundles = Vec::with_capacity(count);
680        for _ in 0..count {
681            anyhow_assert_ge!(bytes.remaining(), 4, "Missing encoded_post_bundle entry length");
682            let len = bytes.get_u32() as usize;
683            anyhow_assert_ge!(bytes.remaining(), len, "Truncated encoded_post_bundle entry");
684            encoded_post_bundles.push(bytes.split_to(len));
685        }
686        Ok(Self { token, encoded_post_bundles })
687    }
688}
689
690#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
691pub struct CachePostBundleResponseV1 {
692    pub accepted: bool,
693}
694
695#[derive(Debug, PartialEq, Clone)]
696pub struct CachePostBundleFeedbackV1 {
697    pub token: CacheRequestTokenV1,
698    pub encoded_post_bundle_feedback_bytes: Bytes,
699}
700
701impl CachePostBundleFeedbackV1 {
702    pub fn new_to_bytes(token: &CacheRequestTokenV1, encoded_post_bundle_feedback_bytes: &[u8]) -> anyhow::Result<Bytes> {
703        let mut bytes_gatherer = BytesGatherer::default();
704        tools::write_length_prefixed_json(&mut bytes_gatherer, token)?;
705        bytes_gatherer.put_slice(encoded_post_bundle_feedback_bytes);
706        Ok(bytes_gatherer.to_bytes())
707    }
708
709    pub fn from_bytes(bytes: &mut Bytes) -> anyhow::Result<Self> {
710        let token = tools::read_length_prefixed_json::<CacheRequestTokenV1>(bytes)?;
711        let encoded_post_bundle_feedback_bytes = bytes.split_to(bytes.len());
712        Ok(Self { token, encoded_post_bundle_feedback_bytes })
713    }
714}
715
716#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
717pub struct CachePostBundleFeedbackResponseV1 {
718    pub accepted: bool,
719}
720
721/// Client → Server: fetch Open Graph metadata for `url`
722#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
723pub struct FetchUrlPreviewV1 {
724    pub url: String,
725}
726
727impl FetchUrlPreviewV1 {
728    pub fn new_to_bytes(url: &str) -> anyhow::Result<Bytes> {
729        let mut bytes_gatherer = BytesGatherer::default();
730        tools::write_length_prefixed_json(&mut bytes_gatherer, &FetchUrlPreviewV1 { url: url.to_string() })?;
731        Ok(bytes_gatherer.to_bytes())
732    }
733
734    pub fn from_bytes(bytes: &mut Bytes) -> anyhow::Result<Self> {
735        tools::read_length_prefixed_json(bytes)
736    }
737}
738
739/// Server → Client: extracted preview metadata
740#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
741pub struct FetchUrlPreviewResponseV1 {
742    pub url: String,
743    pub title: String,
744    pub description: String,
745    pub image_url: String,
746}
747
748impl FetchUrlPreviewResponseV1 {
749    pub fn to_bytes(&self) -> anyhow::Result<Bytes> {
750        json::struct_to_bytes(self)
751    }
752
753    pub fn from_bytes(bytes: &Bytes) -> anyhow::Result<Self> {
754        json::bytes_to_struct(bytes)
755    }
756}
757
758/// Client → Server: fetch trending hashtags
759#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
760pub struct TrendingHashtagsFetchV1 {
761    pub limit: u16,
762}
763
764impl TrendingHashtagsFetchV1 {
765    pub fn new_to_bytes(limit: u16) -> anyhow::Result<Bytes> {
766        let mut bytes_gatherer = BytesGatherer::default();
767        tools::write_length_prefixed_json(&mut bytes_gatherer, &TrendingHashtagsFetchV1 { limit })?;
768        Ok(bytes_gatherer.to_bytes())
769    }
770
771    pub fn from_bytes(bytes: &mut Bytes) -> anyhow::Result<Self> {
772        tools::read_length_prefixed_json(bytes)
773    }
774}
775
776#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
777pub struct TrendingHashtagV1 {
778    pub hashtag: String,
779    pub count: u64,
780}
781
782/// Server → Client: trending hashtags with unique author counts
783#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
784pub struct TrendingHashtagsFetchResponseV1 {
785    pub trending_hashtags: Vec<TrendingHashtagV1>,
786}
787
788impl TrendingHashtagsFetchResponseV1 {
789    pub fn to_bytes(&self) -> anyhow::Result<Bytes> {
790        json::struct_to_bytes(self)
791    }
792
793    pub fn from_bytes(bytes: &Bytes) -> anyhow::Result<Self> {
794        json::bytes_to_struct(bytes)
795    }
796}
797
798#[cfg(test)]
799mod tests {
800    use crate::protocol::payload::payload::{GetPostBundleResponseV1, HealPostBundleClaimResponseV1, HealPostBundleClaimTokenV1, HealPostBundleClaimV1, HealPostBundleCommitResponseV1, HealPostBundleCommitV1, SubmitPostClaimTokenV1, SubmitPostCommitTokenV1};
801    use crate::protocol::posting::encoded_post_bundle::{EncodedPostBundleHeaderV1, EncodedPostBundleV1};
802    use crate::tools::server_id::ServerId;
803    use crate::tools::buckets::{BucketLocation, BucketType};
804    use crate::tools::time::{TimeMillis, MILLIS_IN_HOUR};
805    use crate::tools::time_provider::time_provider::{RealTimeProvider, TimeProvider};
806    use crate::tools::{config, json, tools};
807    use crate::tools::types::{Id, Pow, Signature, ID_BYTES};
808    use std::collections::HashSet;
809    use crate::tools::parallel_pow_generator::StubParallelPowGenerator;
810    use bytes::Bytes;
811
812    #[tokio::test]
813    #[allow(non_snake_case)]
814    async fn test_to_from_GetPostBundleResponseV1() -> anyhow::Result<()> {
815        let time_provider = RealTimeProvider::default();
816        let pow_generator = StubParallelPowGenerator::new();
817        let server_id = ServerId::new(&time_provider, Pow(4), true, &pow_generator).await?;
818        let peer = server_id.to_peer(&time_provider)?;
819
820        let mut encoded_posts_ids = Vec::new();
821        let mut encoded_posts_lengths = Vec::new();
822        let mut encoded_posts_bytes = Vec::new();
823
824        for i in 0..16 {
825            let len = (i + 1) * 128;
826            let mut encoded_post_bytes = tools::random_bytes(len);
827            let post_id = Id::from_slice(&encoded_post_bytes[0..ID_BYTES])?;
828            encoded_posts_ids.push(post_id);
829            encoded_posts_lengths.push(encoded_post_bytes.len());
830            encoded_posts_bytes.append(&mut encoded_post_bytes);
831        }
832
833        let header = EncodedPostBundleHeaderV1 {
834            time_millis: time_provider.current_time_millis(),
835            location_id: Id::random(),
836            overflowed: false,
837            sealed: false,
838            num_posts: 0,
839            encoded_post_ids: encoded_posts_ids,
840            encoded_post_lengths: encoded_posts_lengths,
841            encoded_post_healed: HashSet::new(),
842            peer,
843            signature: Signature::zero(),
844        };
845
846        let peers_nearer = {
847            vec![
848                ServerId::new(&time_provider, Pow(4), true, &pow_generator).await?.to_peer(&time_provider)?,
849                ServerId::new(&time_provider, Pow(4), true, &pow_generator).await?.to_peer(&time_provider)?,
850                ServerId::new(&time_provider, Pow(4), true, &pow_generator).await?.to_peer(&time_provider)?,
851            ]
852        };
853
854        let response = GetPostBundleResponseV1 {
855            peers_nearer,
856            cache_request_token: None,
857            post_bundles_cached: vec![],
858            post_bundle: Some((EncodedPostBundleV1 { header, encoded_posts_bytes: Bytes::from(encoded_posts_bytes) }).to_bytes()?),
859        };
860
861        let encoded = response.to_bytes_gatherer()?.to_bytes();
862        let response_decoded = GetPostBundleResponseV1::from_bytes(encoded)?;
863
864        assert_eq!(response_decoded, response);
865
866        Ok(())
867    }
868
869    async fn make_signed_header(time_provider: &RealTimeProvider) -> anyhow::Result<(EncodedPostBundleHeaderV1, ServerId)> {
870        let pow_generator = StubParallelPowGenerator::new();
871        let server_id = ServerId::new(time_provider, config::SERVER_KEY_POW_MIN, true, &pow_generator).await?;
872        let peer = server_id.to_peer(time_provider)?;
873        let num_posts: u8 = 4;
874        let mut header = EncodedPostBundleHeaderV1 {
875            time_millis: time_provider.current_time_millis(),
876            location_id: Id::random(),
877            overflowed: false,
878            sealed: false,
879            num_posts,
880            encoded_post_ids: (0..num_posts).map(|_| Id::random()).collect(),
881            encoded_post_lengths: (0..num_posts).map(|i| (i as usize + 1) * 64).collect(),
882            encoded_post_healed: HashSet::new(),
883            peer,
884            signature: Signature::zero(),
885        };
886        header.signature_generate(&server_id.keys.signature_key)?;
887        Ok((header, server_id))
888    }
889
890    fn make_bucket_location() -> anyhow::Result<BucketLocation> {
891        BucketLocation::new(BucketType::User, Id::random(), MILLIS_IN_HOUR, TimeMillis(1_700_000_000_000))
892    }
893
894    #[tokio::test]
895    #[allow(non_snake_case)]
896    async fn test_SubmitPostClaimTokenV1_verify() -> anyhow::Result<()> {
897        let time_provider = RealTimeProvider::default();
898        let pow_generator = StubParallelPowGenerator::new();
899        let server_id = ServerId::new(&time_provider, Pow(4), true, &pow_generator).await?;
900        let peer = server_id.to_peer(&time_provider)?;
901
902        let token = SubmitPostClaimTokenV1::new(peer, make_bucket_location()?, Id::random(), &server_id.keys.signature_key);
903        token.verify()?;
904        Ok(())
905    }
906
907    #[tokio::test]
908    #[allow(non_snake_case)]
909    async fn test_SubmitPostCommitTokenV1_verify() -> anyhow::Result<()> {
910        let time_provider = RealTimeProvider::default();
911        let pow_generator = StubParallelPowGenerator::new();
912        let server_id = ServerId::new(&time_provider, Pow(4), true, &pow_generator).await?;
913        let peer = server_id.to_peer(&time_provider)?;
914
915        let token = SubmitPostCommitTokenV1::new(peer, make_bucket_location()?, Id::random(), &server_id.keys.signature_key);
916        token.verify()?;
917        Ok(())
918    }
919
920    #[tokio::test]
921    #[allow(non_snake_case)]
922    async fn test_to_from_HealPostBundleClaimV1() -> anyhow::Result<()> {
923        let time_provider = RealTimeProvider::default();
924        let (header, _) = make_signed_header(&time_provider).await?;
925
926        let bucket_location = make_bucket_location()?;
927        let encoded = HealPostBundleClaimV1::new_to_bytes(&header, &bucket_location)?;
928        let mut bytes = encoded;
929        let decoded = HealPostBundleClaimV1::from_bytes(&mut bytes)?;
930
931        assert_eq!(decoded.donor_header, header);
932        assert_eq!(decoded.bucket_location, bucket_location);
933        Ok(())
934    }
935
936    #[tokio::test]
937    #[allow(non_snake_case)]
938    async fn test_to_from_HealPostBundleClaimResponseV1() -> anyhow::Result<()> {
939        let time_provider = RealTimeProvider::default();
940        let (header, server_id) = make_signed_header(&time_provider).await?;
941        let peer = server_id.to_peer(&time_provider)?;
942
943        let needed_post_ids = header.encoded_post_ids[0..2].to_vec();
944        let token = HealPostBundleClaimTokenV1::new(
945            peer,
946            make_bucket_location()?,
947            needed_post_ids.clone(),
948            header.signature,
949            &server_id.keys.signature_key,
950        );
951
952        let response = HealPostBundleClaimResponseV1 { needed_post_ids, token: Some(token) };
953
954        let encoded = json::struct_to_bytes(&response)?;
955        let decoded = json::bytes_to_struct::<HealPostBundleClaimResponseV1>(&encoded)?;
956
957        assert_eq!(decoded, response);
958        Ok(())
959    }
960
961    #[tokio::test]
962    #[allow(non_snake_case)]
963    async fn test_HealPostBundleClaimTokenV1_verify() -> anyhow::Result<()> {
964        let time_provider = RealTimeProvider::default();
965        let (header, server_id) = make_signed_header(&time_provider).await?;
966        let peer = server_id.to_peer(&time_provider)?;
967
968        let token = HealPostBundleClaimTokenV1::new(
969            peer,
970            make_bucket_location()?,
971            header.encoded_post_ids.clone(),
972            header.signature,
973            &server_id.keys.signature_key,
974        );
975
976        token.verify()?;
977        Ok(())
978    }
979
980    #[tokio::test]
981    #[allow(non_snake_case)]
982    async fn test_to_from_HealPostBundleCommitV1() -> anyhow::Result<()> {
983        let time_provider = RealTimeProvider::default();
984        let (header, server_id) = make_signed_header(&time_provider).await?;
985        let peer = server_id.to_peer(&time_provider)?;
986
987        let token = HealPostBundleClaimTokenV1::new(
988            peer,
989            make_bucket_location()?,
990            header.encoded_post_ids.clone(),
991            header.signature,
992            &server_id.keys.signature_key,
993        );
994
995        let post_bytes = tools::random_bytes(512);
996        let encoded = HealPostBundleCommitV1::new_to_bytes(&token, &header, &post_bytes)?;
997        let mut bytes = encoded;
998        let decoded = HealPostBundleCommitV1::from_bytes(&mut bytes)?;
999
1000        assert_eq!(decoded.token, token);
1001        assert_eq!(decoded.donor_header, header);
1002        assert_eq!(decoded.encoded_posts_bytes.as_ref(), post_bytes.as_slice());
1003        Ok(())
1004    }
1005
1006    #[tokio::test]
1007    #[allow(non_snake_case)]
1008    async fn test_to_from_HealPostBundleCommitResponseV1() -> anyhow::Result<()> {
1009        for accepted in [true, false] {
1010            let response = HealPostBundleCommitResponseV1 { accepted };
1011            let encoded = json::struct_to_bytes(&response)?;
1012            let decoded = json::bytes_to_struct::<HealPostBundleCommitResponseV1>(&encoded)?;
1013            assert_eq!(decoded, response);
1014        }
1015        Ok(())
1016    }
1017
1018    #[tokio::test]
1019    #[allow(non_snake_case)]
1020    async fn test_CacheRequestTokenV1_verify() -> anyhow::Result<()> {
1021        use crate::protocol::payload::payload::CacheRequestTokenV1;
1022        use crate::tools::time::MILLIS_IN_MINUTE;
1023
1024        let time_provider = RealTimeProvider::default();
1025        let pow_generator = StubParallelPowGenerator::new();
1026        let server_id = ServerId::new(&time_provider, Pow(4), true, &pow_generator).await?;
1027        let peer = server_id.to_peer(&time_provider)?;
1028        let bucket_location = make_bucket_location()?;
1029        let expires_at = time_provider.current_time_millis() + MILLIS_IN_MINUTE;
1030        let already_cached_peer_ids = vec![Id::random(), Id::random()];
1031
1032        let token = CacheRequestTokenV1::new(peer, bucket_location, expires_at, already_cached_peer_ids, &server_id.keys.signature_key);
1033        token.verify()?;
1034        Ok(())
1035    }
1036
1037    #[tokio::test]
1038    #[allow(non_snake_case)]
1039    async fn test_to_from_CachePostBundleV1() -> anyhow::Result<()> {
1040        use crate::protocol::payload::payload::{CachePostBundleV1, CacheRequestTokenV1};
1041        use crate::tools::time::MILLIS_IN_MINUTE;
1042
1043        let time_provider = RealTimeProvider::default();
1044        let pow_generator = StubParallelPowGenerator::new();
1045        let server_id = ServerId::new(&time_provider, Pow(4), true, &pow_generator).await?;
1046        let peer = server_id.to_peer(&time_provider)?;
1047        let bucket_location = make_bucket_location()?;
1048        let expires_at = time_provider.current_time_millis() + MILLIS_IN_MINUTE;
1049        let token = CacheRequestTokenV1::new(peer, bucket_location, expires_at, vec![], &server_id.keys.signature_key);
1050
1051        // Multiple bundles — one per originator
1052        let bundle_a = tools::random_bytes(512);
1053        let bundle_b = tools::random_bytes(256);
1054        let bundle_c = tools::random_bytes(1024);
1055        let bundles: &[&[u8]] = &[&bundle_a, &bundle_b, &bundle_c];
1056
1057        let encoded = CachePostBundleV1::new_to_bytes(&token, bundles)?;
1058        let mut bytes = encoded;
1059        let decoded = CachePostBundleV1::from_bytes(&mut bytes)?;
1060
1061        assert_eq!(decoded.token, token);
1062        assert_eq!(decoded.encoded_post_bundles.len(), 3);
1063        assert_eq!(decoded.encoded_post_bundles[0].as_ref(), bundle_a.as_slice());
1064        assert_eq!(decoded.encoded_post_bundles[1].as_ref(), bundle_b.as_slice());
1065        assert_eq!(decoded.encoded_post_bundles[2].as_ref(), bundle_c.as_slice());
1066        Ok(())
1067    }
1068
1069    #[tokio::test]
1070    #[allow(non_snake_case)]
1071    async fn test_to_from_CachePostBundleV1_empty() -> anyhow::Result<()> {
1072        use crate::protocol::payload::payload::{CachePostBundleV1, CacheRequestTokenV1};
1073        use crate::tools::time::MILLIS_IN_MINUTE;
1074
1075        let time_provider = RealTimeProvider::default();
1076        let pow_generator = StubParallelPowGenerator::new();
1077        let server_id = ServerId::new(&time_provider, Pow(4), true, &pow_generator).await?;
1078        let peer = server_id.to_peer(&time_provider)?;
1079        let bucket_location = make_bucket_location()?;
1080        let expires_at = time_provider.current_time_millis() + MILLIS_IN_MINUTE;
1081        let token = CacheRequestTokenV1::new(peer, bucket_location, expires_at, vec![], &server_id.keys.signature_key);
1082
1083        let encoded = CachePostBundleV1::new_to_bytes(&token, &[])?;
1084        let mut bytes = encoded;
1085        let decoded = CachePostBundleV1::from_bytes(&mut bytes)?;
1086
1087        assert_eq!(decoded.token, token);
1088        assert!(decoded.encoded_post_bundles.is_empty());
1089        Ok(())
1090    }
1091
1092    #[tokio::test]
1093    #[allow(non_snake_case)]
1094    async fn test_to_from_CachePostBundleFeedbackV1() -> anyhow::Result<()> {
1095        use crate::protocol::payload::payload::{CachePostBundleFeedbackV1, CacheRequestTokenV1};
1096        use crate::tools::time::MILLIS_IN_MINUTE;
1097
1098        let time_provider = RealTimeProvider::default();
1099        let pow_generator = StubParallelPowGenerator::new();
1100        let server_id = ServerId::new(&time_provider, Pow(4), true, &pow_generator).await?;
1101        let peer = server_id.to_peer(&time_provider)?;
1102        let bucket_location = make_bucket_location()?;
1103        let expires_at = time_provider.current_time_millis() + MILLIS_IN_MINUTE;
1104        let already_cached_peer_ids = vec![Id::random()];
1105        let token = CacheRequestTokenV1::new(peer, bucket_location, expires_at, already_cached_peer_ids, &server_id.keys.signature_key);
1106
1107        let feedback_bytes = tools::random_bytes(256);
1108        let encoded = CachePostBundleFeedbackV1::new_to_bytes(&token, &feedback_bytes)?;
1109        let mut bytes = encoded;
1110        let decoded = CachePostBundleFeedbackV1::from_bytes(&mut bytes)?;
1111
1112        assert_eq!(decoded.token, token);
1113        assert_eq!(decoded.encoded_post_bundle_feedback_bytes.as_ref(), feedback_bytes.as_slice());
1114        Ok(())
1115    }
1116
1117    #[tokio::test]
1118    #[allow(non_snake_case)]
1119    async fn test_to_from_FetchUrlPreviewV1() -> anyhow::Result<()> {
1120        use crate::protocol::payload::payload::FetchUrlPreviewV1;
1121
1122        let url = "https://example.com/article?q=1#section";
1123        let encoded = FetchUrlPreviewV1::new_to_bytes(url)?;
1124        let mut bytes = encoded;
1125        let decoded = FetchUrlPreviewV1::from_bytes(&mut bytes)?;
1126
1127        assert_eq!(decoded.url, url);
1128        Ok(())
1129    }
1130
1131    #[tokio::test]
1132    #[allow(non_snake_case)]
1133    async fn test_to_from_FetchUrlPreviewResponseV1() -> anyhow::Result<()> {
1134        use crate::protocol::payload::payload::FetchUrlPreviewResponseV1;
1135
1136        let response = FetchUrlPreviewResponseV1 {
1137            url: "https://example.com/canonical".to_string(),
1138            title: "Example Title".to_string(),
1139            description: "A short description.".to_string(),
1140            image_url: "https://example.com/image.png".to_string(),
1141        };
1142
1143        let encoded = response.to_bytes()?;
1144        let decoded = FetchUrlPreviewResponseV1::from_bytes(&encoded)?;
1145
1146        assert_eq!(decoded, response);
1147        Ok(())
1148    }
1149
1150    // ── Robustness tests: CachePostBundleV1::from_bytes ──
1151
1152    #[test]
1153    #[allow(non_snake_case)]
1154    fn test_CachePostBundleV1_from_bytes_empty_input() {
1155        use crate::protocol::payload::payload::CachePostBundleV1;
1156        let mut bytes = Bytes::new();
1157        assert!(CachePostBundleV1::from_bytes(&mut bytes).is_err());
1158    }
1159
1160    #[test]
1161    #[allow(non_snake_case)]
1162    fn test_CachePostBundleV1_from_bytes_garbage_input() {
1163        use crate::protocol::payload::payload::CachePostBundleV1;
1164        let mut bytes = Bytes::from_static(&[0xff; 64]);
1165        assert!(CachePostBundleV1::from_bytes(&mut bytes).is_err());
1166    }
1167
1168    #[test]
1169    #[allow(non_snake_case)]
1170    fn test_CachePostBundleV1_from_bytes_truncated_count() {
1171        use crate::protocol::payload::payload::CachePostBundleV1;
1172        // Valid length-prefixed JSON for an empty-ish struct won't work here since CacheRequestTokenV1
1173        // has required fields. But a single byte after a valid-looking length prefix will fail at JSON parse.
1174        // The simplest test: just one byte (not enough for the length prefix u64).
1175        let mut bytes = Bytes::from_static(&[0x01]);
1176        assert!(CachePostBundleV1::from_bytes(&mut bytes).is_err());
1177    }
1178
1179    // ── Robustness tests: GetPostBundleResponseV1::from_bytes ──
1180
1181    #[test]
1182    #[allow(non_snake_case)]
1183    fn test_GetPostBundleResponseV1_from_bytes_empty_input() {
1184        let bytes = Bytes::new();
1185        assert!(GetPostBundleResponseV1::from_bytes(bytes).is_err());
1186    }
1187
1188    #[test]
1189    #[allow(non_snake_case)]
1190    fn test_GetPostBundleResponseV1_from_bytes_garbage_input() {
1191        let bytes = Bytes::from_static(&[0xff; 128]);
1192        assert!(GetPostBundleResponseV1::from_bytes(bytes).is_err());
1193    }
1194
1195    // ── Robustness tests: HealPostBundleClaimV1::from_bytes ──
1196
1197    #[test]
1198    #[allow(non_snake_case)]
1199    fn test_HealPostBundleClaimV1_from_bytes_empty_input() {
1200        let mut bytes = Bytes::new();
1201        assert!(HealPostBundleClaimV1::from_bytes(&mut bytes).is_err());
1202    }
1203
1204    #[test]
1205    #[allow(non_snake_case)]
1206    fn test_HealPostBundleClaimV1_from_bytes_garbage_input() {
1207        let mut bytes = Bytes::from_static(&[0xff; 64]);
1208        assert!(HealPostBundleClaimV1::from_bytes(&mut bytes).is_err());
1209    }
1210
1211    // ── Robustness tests: HealPostBundleCommitV1::from_bytes ──
1212
1213    #[test]
1214    #[allow(non_snake_case)]
1215    fn test_HealPostBundleCommitV1_from_bytes_empty_input() {
1216        let mut bytes = Bytes::new();
1217        assert!(HealPostBundleCommitV1::from_bytes(&mut bytes).is_err());
1218    }
1219
1220    #[test]
1221    #[allow(non_snake_case)]
1222    fn test_HealPostBundleCommitV1_from_bytes_garbage_input() {
1223        let mut bytes = Bytes::from_static(&[0xff; 64]);
1224        assert!(HealPostBundleCommitV1::from_bytes(&mut bytes).is_err());
1225    }
1226
1227    // ── Robustness tests: FetchUrlPreviewV1::from_bytes ──
1228
1229    #[test]
1230    #[allow(non_snake_case)]
1231    fn test_FetchUrlPreviewV1_from_bytes_empty_input() {
1232        use crate::protocol::payload::payload::FetchUrlPreviewV1;
1233        let mut bytes = Bytes::new();
1234        assert!(FetchUrlPreviewV1::from_bytes(&mut bytes).is_err());
1235    }
1236
1237    // ── Robustness tests: FetchUrlPreviewResponseV1::from_bytes ──
1238
1239    #[test]
1240    #[allow(non_snake_case)]
1241    fn test_FetchUrlPreviewResponseV1_from_bytes_empty_input() {
1242        use crate::protocol::payload::payload::FetchUrlPreviewResponseV1;
1243        let bytes = Bytes::new();
1244        assert!(FetchUrlPreviewResponseV1::from_bytes(&bytes).is_err());
1245    }
1246
1247    #[test]
1248    #[allow(non_snake_case)]
1249    fn test_FetchUrlPreviewResponseV1_from_bytes_garbage_input() {
1250        use crate::protocol::payload::payload::FetchUrlPreviewResponseV1;
1251        let bytes = Bytes::from_static(&[0xff; 64]);
1252        assert!(FetchUrlPreviewResponseV1::from_bytes(&bytes).is_err());
1253    }
1254
1255    // ── Robustness tests: SubmitPostClaimV1::from_bytes ──
1256
1257    #[test]
1258    #[allow(non_snake_case)]
1259    fn test_SubmitPostClaimV1_from_bytes_empty_input() {
1260        use crate::protocol::payload::payload::SubmitPostClaimV1;
1261        let mut bytes = Bytes::new();
1262        assert!(SubmitPostClaimV1::from_bytes(&mut bytes).is_err());
1263    }
1264
1265    #[test]
1266    #[allow(non_snake_case)]
1267    fn test_SubmitPostClaimV1_from_bytes_garbage_input() {
1268        use crate::protocol::payload::payload::SubmitPostClaimV1;
1269        let mut bytes = Bytes::from_static(&[0xff; 64]);
1270        assert!(SubmitPostClaimV1::from_bytes(&mut bytes).is_err());
1271    }
1272
1273    // ── Robustness tests: SubmitPostCommitV1::from_bytes ──
1274
1275    #[test]
1276    #[allow(non_snake_case)]
1277    fn test_SubmitPostCommitV1_from_bytes_empty_input() {
1278        use crate::protocol::payload::payload::SubmitPostCommitV1;
1279        let mut bytes = Bytes::new();
1280        assert!(SubmitPostCommitV1::from_bytes(&mut bytes).is_err());
1281    }
1282
1283    // ── Robustness tests: SubmitPostFeedbackV1::from_bytes ──
1284
1285    #[test]
1286    #[allow(non_snake_case)]
1287    fn test_SubmitPostFeedbackV1_from_bytes_empty_input() {
1288        use crate::protocol::payload::payload::SubmitPostFeedbackV1;
1289        let mut bytes = Bytes::new();
1290        assert!(SubmitPostFeedbackV1::from_bytes(&mut bytes).is_err());
1291    }
1292
1293    // ── Robustness tests: GetPostBundleFeedbackResponseV1::from_bytes ──
1294
1295    #[test]
1296    #[allow(non_snake_case)]
1297    fn test_GetPostBundleFeedbackResponseV1_from_bytes_empty_input() {
1298        use crate::protocol::payload::payload::GetPostBundleFeedbackResponseV1;
1299        let bytes = Bytes::new();
1300        assert!(GetPostBundleFeedbackResponseV1::from_bytes(bytes).is_err());
1301    }
1302
1303    // ── Robustness tests: CachePostBundleFeedbackV1::from_bytes ──
1304
1305    #[test]
1306    #[allow(non_snake_case)]
1307    fn test_CachePostBundleFeedbackV1_from_bytes_empty_input() {
1308        use crate::protocol::payload::payload::CachePostBundleFeedbackV1;
1309        let mut bytes = Bytes::new();
1310        assert!(CachePostBundleFeedbackV1::from_bytes(&mut bytes).is_err());
1311    }
1312
1313    #[cfg(not(target_arch = "wasm32"))]
1314    mod bolero_fuzz {
1315        use bytes::Bytes;
1316
1317        #[test]
1318        #[allow(non_snake_case)]
1319        fn fuzz_CachePostBundleV1_from_bytes() {
1320            use crate::protocol::payload::payload::CachePostBundleV1;
1321            bolero::check!().for_each(|data: &[u8]| {
1322                let mut bytes = Bytes::copy_from_slice(data);
1323                let _ = CachePostBundleV1::from_bytes(&mut bytes);
1324            });
1325        }
1326
1327        #[test]
1328        #[allow(non_snake_case)]
1329        fn fuzz_GetPostBundleResponseV1_from_bytes() {
1330            use crate::protocol::payload::payload::GetPostBundleResponseV1;
1331            bolero::check!().for_each(|data: &[u8]| {
1332                let _ = GetPostBundleResponseV1::from_bytes(Bytes::copy_from_slice(data));
1333            });
1334        }
1335
1336        #[test]
1337        #[allow(non_snake_case)]
1338        fn fuzz_GetPostBundleFeedbackResponseV1_from_bytes() {
1339            use crate::protocol::payload::payload::GetPostBundleFeedbackResponseV1;
1340            bolero::check!().for_each(|data: &[u8]| {
1341                let _ = GetPostBundleFeedbackResponseV1::from_bytes(Bytes::copy_from_slice(data));
1342            });
1343        }
1344
1345        #[test]
1346        #[allow(non_snake_case)]
1347        fn fuzz_SubmitPostClaimV1_from_bytes() {
1348            use crate::protocol::payload::payload::SubmitPostClaimV1;
1349            bolero::check!().for_each(|data: &[u8]| {
1350                let mut bytes = Bytes::copy_from_slice(data);
1351                let _ = SubmitPostClaimV1::from_bytes(&mut bytes);
1352            });
1353        }
1354
1355        #[test]
1356        #[allow(non_snake_case)]
1357        fn fuzz_HealPostBundleClaimV1_from_bytes() {
1358            use crate::protocol::payload::payload::HealPostBundleClaimV1;
1359            bolero::check!().for_each(|data: &[u8]| {
1360                let mut bytes = Bytes::copy_from_slice(data);
1361                let _ = HealPostBundleClaimV1::from_bytes(&mut bytes);
1362            });
1363        }
1364
1365        #[test]
1366        #[allow(non_snake_case)]
1367        fn fuzz_HealPostBundleCommitV1_from_bytes() {
1368            use crate::protocol::payload::payload::HealPostBundleCommitV1;
1369            bolero::check!().for_each(|data: &[u8]| {
1370                let mut bytes = Bytes::copy_from_slice(data);
1371                let _ = HealPostBundleCommitV1::from_bytes(&mut bytes);
1372            });
1373        }
1374    }
1375}