Skip to main content

hashiverse_lib/protocol/posting/
encoded_post.rs

1//! # `EncodedPostV1` — individual post wire format
2//!
3//! The canonical on-the-wire representation of a single user post.
4//! [`EncodedPostV1`] pairs:
5//!
6//! - an [`EncodedPostHeaderV1`] carrying the author's keys, timestamp, linked base IDs,
7//!   and signing-authority choice,
8//! - the post body bytes (compressed + encrypted),
9//! - an Ed25519 signature (or delegated-key signature per `PostSigningAuthorityV1`).
10//!
11//! The `post_id` is the hash of the signature, which gives every post a deterministic,
12//! collision-free identifier derivable from the bytes alone. On the wire both header
13//! and body are compressed and encrypted under passwords derived from the client id
14//! and the set of linked base IDs — so that recipients who already know those keys
15//! can decrypt without an extra key exchange.
16//!
17//! [`EncodedPostV1::bytes_without_body`] returns just the header bytes, so that
18//! previews and timeline skeletons can be delivered before the full body is paid for
19//! in the reader's bandwidth budget.
20
21use crate::client::key_locker::key_locker::KeyLocker;
22use crate::tools::client_id::ClientId;
23use crate::tools::time::TimeMillis;
24use crate::tools::types::{Hash, Id, PQCommitmentBytes, Signature, VerificationKey, VerificationKeyBytes, HASH_BYTES, ID_BYTES, SIGNATURE_BYTES};
25use crate::tools::{compression, encryption, hashing, json, signing};
26use crate::{anyhow_assert_eq, anyhow_assert_ge};
27use bytes::{Buf, BufMut, Bytes, BytesMut};
28use serde::{Deserialize, Serialize};
29use std::fmt::Debug;
30use std::sync::Arc;
31
32#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
33pub struct EncodedPostHeaderSignatureDirectV1 {}
34#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
35pub struct EncodedPostHeaderSignatureEphemeralV1 {
36    // Not yet implemented
37    // ephemeral_salt: Salt,
38    // ephemeral_signature: Signature,
39}
40
41#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
42pub struct EncodedPostHeaderSignatureDelegationV1 {
43    // Not yet implemented
44}
45
46#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
47pub struct EncodedPostHeaderSignatureMechanismV1 {
48    direct: Option<EncodedPostHeaderSignatureDirectV1>,
49    ephemeral: Option<EncodedPostHeaderSignatureEphemeralV1>,
50    delegation: Option<EncodedPostHeaderSignatureDelegationV1>,
51}
52
53#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
54pub struct EncodedPostHeaderV1 {
55    pub verification_key_bytes: VerificationKeyBytes,
56    pub pq_commitment_bytes: PQCommitmentBytes,
57    pub time_millis: TimeMillis,
58    pub post_length: usize,
59    pub linked_base_ids: Vec<Id>,
60    pub signature_mechanism: EncodedPostHeaderSignatureMechanismV1,
61}
62
63impl EncodedPostHeaderV1 {
64    pub fn client_id(&self) -> anyhow::Result<ClientId> {
65        ClientId::new(self.verification_key_bytes, self.pq_commitment_bytes)
66    }
67}
68
69/// A single user post in its canonical, verifiable form as it travels across the network.
70///
71/// `EncodedPostV1` bundles the post body together with everything a recipient needs to
72/// authenticate it: the author's [`EncodedPostHeaderV1`] (verification key, PQ commitment,
73/// timestamp, linked base IDs, and which signature mechanism is in use), the author's
74/// [`Signature`] over the canonical bytes, and a `post_id` which is simply the hash of the
75/// signature (so every post has a deterministic, collision-free identifier).
76///
77/// On the wire the post body is compressed and optionally encrypted; this struct is the
78/// decoded in-memory form. [`EncodedPostV1::bytes_without_body`] exists for callers who only
79/// need the header portion — e.g. a timeline that is showing post previews without having
80/// yet fetched the full body.
81#[derive(Debug, PartialEq, Clone)]
82pub struct EncodedPostV1 {
83    pub post_id: Id, // Simply the hash of the signature
84    pub signature: Signature,
85    pub header: EncodedPostHeaderV1,
86    pub post: String,
87}
88
89impl EncodedPostV1 {
90    /// Returns the header portion of encoded post bytes (post_id + signature + version +
91    /// hashes + lengths + encrypted_header), excluding the encrypted body.
92    /// The result is suitable for passing to `decode_from_bytes(bytes, password, false, false)`.
93    pub fn bytes_without_body(bytes: Bytes) -> anyhow::Result<Bytes> {
94        let fixed_prefix = ID_BYTES + SIGNATURE_BYTES + 1 + HASH_BYTES * 2;
95        anyhow_assert_ge!(bytes.len(), fixed_prefix + 4 + 4, "Bytes too short for header");
96        let header_encrypted_length = u32::from_be_bytes(bytes[fixed_prefix..fixed_prefix + 4].try_into()?) as usize;
97        let length_without_body = fixed_prefix + 4 + 4 + header_encrypted_length;
98        anyhow_assert_ge!(bytes.len(), length_without_body, "Bytes too short for encrypted header");
99        Ok(bytes.slice(..length_without_body))
100    }
101
102    pub fn new(client_id: &ClientId, timestamp: TimeMillis, linked_base_ids: Vec<Id>, post: &str) -> Self {
103        let post = post.to_string();
104        let post_length = post.len();
105
106        Self {
107            post_id: Id::zero(),
108            signature: Signature::zero(),
109            header: EncodedPostHeaderV1 {
110                verification_key_bytes: client_id.verification_key_bytes,
111                pq_commitment_bytes: client_id.pq_commitment_bytes,
112                time_millis: timestamp,
113                post_length,
114                linked_base_ids,
115                signature_mechanism: EncodedPostHeaderSignatureMechanismV1 {
116                    direct: None,
117                    ephemeral: None,
118                    delegation: None,
119                },
120            },
121
122            post,
123        }
124    }
125
126    pub async fn encode_to_bytes_direct(&mut self, key_locker: &Arc<dyn KeyLocker>) -> anyhow::Result<EncodedPostBytesV1> {
127        self.header.signature_mechanism.direct = Some(EncodedPostHeaderSignatureDirectV1 {});
128
129        let mut passwords = Vec::new();
130        {
131            let client_id = ClientId::id_from_parts(&self.header.verification_key_bytes, &self.header.pq_commitment_bytes)?;
132            passwords.push(client_id.as_bytes().to_vec());
133            let mut linked_base_ids = self.header.linked_base_ids.iter().map(|id| id.as_bytes().to_vec()).collect();
134            passwords.append(&mut linked_base_ids);
135        }
136
137        let header = json::struct_to_bytes(&self.header)?;
138        let header_compressed = compression::compress_for_size(&header)?.to_bytes();
139        let header_encrypted = encryption::encrypt_weak(&header_compressed, &passwords)?;
140        let header_encrypted_hash = hashing::hash(&header_encrypted);
141
142        let post_compressed = compression::compress_for_size(self.post.as_bytes())?.to_bytes();
143        let post_encrypted = encryption::encrypt_weak(&post_compressed, &passwords)?;
144        let post_encrypted_hash = hashing::hash(&post_encrypted);
145
146        let hash = hashing::hash_multiple(&[header_encrypted_hash.as_ref(), post_encrypted_hash.as_ref()]);
147        let signature = key_locker.sign(hash.as_ref()).await?;
148        let post_id = Id::from_hash(hashing::hash(signature.as_ref()))?;
149
150        self.signature = signature;
151        self.post_id = post_id;
152
153        let mut bytes = BytesMut::new();
154        bytes.put_slice(post_id.as_ref());
155        bytes.put_slice(signature.as_ref());
156        bytes.put_u8(1u8); // Version
157        bytes.put_slice(header_encrypted_hash.as_ref());
158        bytes.put_slice(post_encrypted_hash.as_ref());
159        bytes.put_u32(header_encrypted.len() as u32);
160        bytes.put_u32(post_encrypted.len() as u32);
161        bytes.put_slice(header_encrypted.as_ref());
162        bytes.put_slice(post_encrypted.as_ref());
163
164        let bytes = bytes.freeze();
165
166        Ok(EncodedPostBytesV1 {
167            length_without_body: bytes.len() - post_encrypted.len(),
168            bytes,
169        })
170    }
171
172    pub fn decode_signature_from_bytes(bytes: &[u8]) -> anyhow::Result<Signature> {
173        anyhow::ensure!(bytes.len() >= SIGNATURE_BYTES, "decode_signature_from_bytes: need {} bytes, got {}", SIGNATURE_BYTES, bytes.len());
174        Signature::from_slice(&bytes[0..SIGNATURE_BYTES])
175    }
176
177    pub fn decode_from_bytes(mut bytes: Bytes, password_base_id: &Id, expect_body: bool, decode_body: bool) -> anyhow::Result<Self> {
178        let password = password_base_id.as_ref();
179
180        anyhow_assert_ge!(bytes.remaining(), ID_BYTES, "Missing post_id");
181        let post_id = Id::from_slice(&bytes.split_to(ID_BYTES))?;
182
183        anyhow_assert_ge!(bytes.remaining(), SIGNATURE_BYTES, "Missing signature");
184        let signature = Signature::from_slice(&bytes.split_to(SIGNATURE_BYTES))?;
185
186        anyhow_assert_ge!(bytes.remaining(), 1, "Missing version");
187        let version = bytes.get_u8();
188        if 1 != version {
189            anyhow::bail!("Invalid buffer: unknown version");
190        }
191
192        anyhow_assert_ge!(bytes.remaining(), HASH_BYTES, "Missing encrypted hashes");
193        let header_encrypted_hash = Hash::from_slice(&bytes.split_to(HASH_BYTES))?;
194        anyhow_assert_ge!(bytes.remaining(), HASH_BYTES, "Missing encrypted hashes");
195        let post_encrypted_hash = Hash::from_slice(&bytes.split_to(HASH_BYTES))?;
196
197        anyhow_assert_ge!(bytes.remaining(), size_of::<u32>(), "Missing encrypted lengths");
198        let header_encrypted_length = bytes.get_u32() as usize;
199        anyhow_assert_ge!(bytes.remaining(), size_of::<u32>(), "Missing encrypted lengths");
200        let post_encrypted_length = bytes.get_u32() as usize;
201
202        anyhow_assert_ge!(bytes.remaining(), header_encrypted_length, "Missing encrypted header");
203        let header_encrypted = bytes.split_to(header_encrypted_length);
204
205        let post_encrypted = match expect_body {
206            true => {
207                anyhow_assert_ge!(bytes.remaining(), post_encrypted_length, "Missing encrypted post");
208                bytes.split_to(post_encrypted_length)
209            }
210            false => Bytes::new(),
211        };
212
213        anyhow_assert_eq!(bytes.remaining(), 0, "Unexpected remaining data");
214
215        // Recreate the header
216        // log::trace!("Decrypting header length {}", header_encrypted.len());
217        let header_compressed = encryption::decrypt(&header_encrypted, password)?;
218        // log::trace!("Decompressing header length {}", header_compressed.len());
219        let header_bytes = compression::decompress(&header_compressed)?.to_bytes();
220        // log::trace!("Done header length {}", header_bytes.len());
221        let header = json::bytes_to_struct::<EncodedPostHeaderV1>(&header_bytes)?;
222
223        // Verify the hashes
224        anyhow_assert_eq!(header_encrypted_hash, hashing::hash(&header_encrypted));
225        if expect_body {
226            anyhow_assert_eq!(post_encrypted_hash, hashing::hash(&post_encrypted));
227        }
228
229        // Verify the signature
230        {
231            let hash = hashing::hash_multiple(&[header_encrypted_hash.as_ref(), post_encrypted_hash.as_ref()]);
232
233            // Direct
234            if header.signature_mechanism.direct.is_some() {
235                let verification_key = VerificationKey::from_bytes(&header.verification_key_bytes)?;
236                signing::verify(&verification_key, &signature, hash.as_ref())?;
237            }
238            // Ephemeral
239            else if header.signature_mechanism.ephemeral.is_some() {
240                anyhow::bail!("Signature verification ephemeral not implemented")
241            }
242            // Delegation
243            else if header.signature_mechanism.delegation.is_some() {
244                anyhow::bail!("Signature verification delegation not implemented")
245            }
246            // None!
247            else {
248                anyhow::bail!("No signature verification mechanisms")
249            }
250        }
251
252        // Verify the post_id
253        {
254            anyhow_assert_eq!(post_id, Id::from_hash(hashing::hash(signature.as_ref()))?, "post_id is not the has of signature");
255        }
256
257        // Recreate the body
258        let post = match expect_body && decode_body {
259            true => {
260                // log::trace!("Decrypting post length {}", post_encrypted.len());
261                let post_compressed = encryption::decrypt(&post_encrypted, password)?;
262                // log::trace!("Decompressing post length {}", post_compressed.len());
263                let post_bytes = compression::decompress(&post_compressed)?.to_bytes();
264                // log::trace!("Done post length {}", post_bytes.len());
265                let post = std::str::from_utf8(&post_bytes)?;
266                if post.len() != header.post_length {
267                    anyhow::bail!("Post length mismatch: header.post_length={}, actual={}", header.post_length, post.len())
268                }
269                post.to_string()
270            }
271            false => String::new(),
272        };
273
274        // Woohoo!
275        Ok(Self { post_id, signature, header, post })
276    }
277}
278
279pub struct EncodedPostBytesV1 {
280    length_without_body: usize,
281    bytes: Bytes,
282}
283
284impl EncodedPostBytesV1 {
285    pub fn bytes_without_body(&self) -> &[u8] {
286        &self.bytes[..self.length_without_body]
287    }
288    pub fn bytes(&self) -> &[u8] {
289        self.bytes.as_ref()
290    }
291}
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296    use crate::client::key_locker::key_locker::{KeyLocker, KeyLockerManager};
297    use crate::client::key_locker::mem_key_locker::MemKeyLockerManager;
298    use crate::tools::time_provider::time_provider::{RealTimeProvider, TimeProvider};
299
300    #[tokio::test]
301    async fn test_post_v1_verification() -> anyhow::Result<()> {
302        let key_locker_manager = MemKeyLockerManager::new().await?;
303        let key_locker: Arc<dyn KeyLocker> = key_locker_manager.create("this is a random keyphrase".to_string()).await?;
304        let time_provider = RealTimeProvider::default();
305        let client_id = key_locker.client_id();
306        let timestamp = time_provider.current_time_millis();
307        let linked_base_ids = vec![client_id.id, Id::random(), Id::random(), Id::random()];
308
309        let password1 = linked_base_ids[0].clone();
310        let password2 = linked_base_ids[1].clone();
311
312        let mut encoded_post = EncodedPostV1::new(client_id, timestamp, linked_base_ids, "this is a test post");
313        let bytes = encoded_post.encode_to_bytes_direct(&key_locker).await?;
314
315        // Test decoding with multiple passwords
316        {
317            {
318                let decoded_post = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(&bytes.bytes()), &password1, true, true)?;
319                assert_eq!(encoded_post, decoded_post);
320            }
321
322            {
323                let decoded_post = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(&bytes.bytes()), &password2, true, true)?;
324                assert_eq!(encoded_post, decoded_post);
325            }
326        }
327
328        // Test decoding without body
329        {
330            let decoded_post = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(&bytes.bytes()), &password2, true, false)?;
331            assert_eq!("", decoded_post.post);
332        }
333
334        // Incorrect password
335        {
336            let wrong_password = Id::random();
337            let decoded_post_attempt = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(&bytes.bytes()), &wrong_password, true, true);
338            if decoded_post_attempt.is_ok() {
339                anyhow::bail!("Decoding with wrong password should fail")
340            }
341        }
342
343        // Tampering with bytes
344        {
345            let mut tampered_bytes = Vec::from(bytes.bytes());
346            tampered_bytes[100] = 0u8;
347            let decoded_post_attempt = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(&tampered_bytes), &password1, true, true);
348            if decoded_post_attempt.is_ok() {
349                anyhow::bail!("Decoding tampered bytes should fail")
350            }
351        }
352
353        Ok(())
354    }
355
356    #[tokio::test]
357    async fn test_header_only_verification() -> anyhow::Result<()> {
358        let key_locker_manager = MemKeyLockerManager::new().await?;
359        let key_locker: Arc<dyn KeyLocker> = key_locker_manager.create("this is a random keyphrase".to_string()).await?;
360        let time_provider = RealTimeProvider::default();
361        let client_id = key_locker.client_id();
362        let timestamp = time_provider.current_time_millis();
363        let linked_base_ids = vec![client_id.id, Id::random(), Id::random(), Id::random()];
364
365        let password1 = linked_base_ids[0].clone();
366        let password2 = linked_base_ids[1].clone();
367
368        let mut encoded_post = EncodedPostV1::new(client_id, timestamp, linked_base_ids, "this is a test post");
369        let bytes = encoded_post.encode_to_bytes_direct(&key_locker).await?;
370
371        {
372            let decoded_post = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(&bytes.bytes_without_body()), &password1, false, false)?;
373            assert_eq!(encoded_post.signature, decoded_post.signature);
374            assert_eq!(encoded_post.header, decoded_post.header);
375            assert_eq!(String::new(), decoded_post.post);
376        }
377
378        {
379            let decoded_post = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(&bytes.bytes_without_body()), &password2, false, false)?;
380            assert_eq!(encoded_post.signature, decoded_post.signature);
381            assert_eq!(encoded_post.header, decoded_post.header);
382            assert_eq!(String::new(), decoded_post.post);
383        }
384
385        // Provide the body when not expected
386        {
387            let decoded_post_attempt = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(&bytes.bytes()), &password2, false, false);
388            if decoded_post_attempt.is_ok() {
389                anyhow::bail!("Decoding with wrong too many bytes should fail")
390            }
391        }
392
393        {
394            let wrong_password = Id::random();
395            let decoded_post_attempt = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(&bytes.bytes_without_body()), &wrong_password, false, false);
396            if decoded_post_attempt.is_ok() {
397                anyhow::bail!("Decoding with wrong password should fail")
398            }
399        }
400
401        {
402            let mut tampered_bytes = Vec::from(bytes.bytes_without_body());
403            tampered_bytes[100] = 0u8;
404            let decoded_post_attempt = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(&tampered_bytes), &password1, false, false);
405            if decoded_post_attempt.is_ok() {
406                anyhow::bail!("Decoding tampered bytes should fail")
407            }
408        }
409
410        Ok(())
411    }
412
413    #[tokio::test]
414    async fn test_post_with_no_linked_base_ids() -> anyhow::Result<()> {
415        let key_locker_manager = MemKeyLockerManager::new().await?;
416        let key_locker: Arc<dyn KeyLocker> = key_locker_manager.create("this is a random keyphrase".to_string()).await?;
417        let time_provider = RealTimeProvider::default();
418        let client_id = key_locker.client_id();
419        let timestamp = time_provider.current_time_millis();
420
421        // No linked_base_ids — only the client_id itself is used as password
422        let password = client_id.id.clone();
423        let mut encoded_post = EncodedPostV1::new(client_id, timestamp, vec![], "post with no linked ids");
424        let bytes = encoded_post.encode_to_bytes_direct(&key_locker).await?;
425
426        // Decrypt with the client_id password succeeds
427        let decoded_post = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(bytes.bytes()), &password, true, true)?;
428        assert_eq!(encoded_post, decoded_post);
429
430        // Wrong password fails
431        let wrong_password = Id::random();
432        let attempt = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(bytes.bytes()), &wrong_password, true, true);
433        if attempt.is_ok() {
434            anyhow::bail!("Decoding with wrong password should fail");
435        }
436
437        Ok(())
438    }
439
440    #[tokio::test]
441    async fn test_bytes_without_body_round_trip() -> anyhow::Result<()> {
442        let key_locker_manager = MemKeyLockerManager::new().await?;
443        let key_locker: Arc<dyn KeyLocker> = key_locker_manager.create("test keyphrase for bytes_without_body".to_string()).await?;
444        let time_provider = RealTimeProvider::default();
445        let client_id = key_locker.client_id();
446        let timestamp = time_provider.current_time_millis();
447        let linked_base_ids = vec![client_id.id, Id::random(), Id::random()];
448
449        let password = linked_base_ids[0].clone();
450
451        let mut encoded_post = EncodedPostV1::new(client_id, timestamp, linked_base_ids, "test post for bytes_without_body");
452        let bytes = encoded_post.encode_to_bytes_direct(&key_locker).await?;
453
454        // bytes_without_body from the static method should match the EncodedPostBytesV1 method
455        let full_bytes = Bytes::copy_from_slice(bytes.bytes());
456        let header_bytes_static = EncodedPostV1::bytes_without_body(full_bytes.clone())?;
457        let header_bytes_original = Bytes::copy_from_slice(bytes.bytes_without_body());
458        assert_eq!(header_bytes_static, header_bytes_original);
459
460        // The header bytes should be decodable with expect_body=false
461        let decoded_header_only = EncodedPostV1::decode_from_bytes(header_bytes_static.clone(), &password, false, false)?;
462        assert_eq!(encoded_post.header, decoded_header_only.header);
463        assert_eq!(encoded_post.signature, decoded_header_only.signature);
464        assert_eq!(encoded_post.post_id, decoded_header_only.post_id);
465        assert_eq!("", decoded_header_only.post);
466
467        // Hex round-trip (simulating the WASM bridge flow)
468        let hex_encoded = hex::encode(&header_bytes_static);
469        let hex_decoded = Bytes::from(hex::decode(&hex_encoded)?);
470        let decoded_from_hex = EncodedPostV1::decode_from_bytes(hex_decoded, &password, false, false)?;
471        assert_eq!(encoded_post.header, decoded_from_hex.header);
472
473        Ok(())
474    }
475
476    // ── Robustness tests: decode_signature_from_bytes ──
477
478    #[test]
479    fn test_decode_signature_from_bytes_empty() {
480        assert!(EncodedPostV1::decode_signature_from_bytes(&[]).is_err());
481    }
482
483    #[test]
484    fn test_decode_signature_from_bytes_too_short() {
485        assert!(EncodedPostV1::decode_signature_from_bytes(&[0u8; SIGNATURE_BYTES - 1]).is_err());
486    }
487
488    #[test]
489    fn test_decode_signature_from_bytes_exact_length() {
490        // Should succeed (signature is all zeros, but structurally valid)
491        assert!(EncodedPostV1::decode_signature_from_bytes(&[0u8; SIGNATURE_BYTES]).is_ok());
492    }
493
494    // ── Robustness tests: decode_from_bytes ──
495
496    #[test]
497    fn test_decode_from_bytes_empty() {
498        let password = Id::random();
499        assert!(EncodedPostV1::decode_from_bytes(Bytes::new(), &password, true, true).is_err());
500    }
501
502    #[test]
503    fn test_decode_from_bytes_too_short_for_post_id() {
504        let password = Id::random();
505        let bytes = Bytes::from_static(&[0u8; ID_BYTES - 1]);
506        assert!(EncodedPostV1::decode_from_bytes(bytes, &password, true, true).is_err());
507    }
508
509    #[test]
510    fn test_decode_from_bytes_garbage() {
511        let password = Id::random();
512        let bytes = Bytes::from_static(&[0xff; 256]);
513        assert!(EncodedPostV1::decode_from_bytes(bytes, &password, true, true).is_err());
514    }
515
516    // ── Robustness tests: bytes_without_body ──
517
518    #[test]
519    fn test_bytes_without_body_empty() {
520        assert!(EncodedPostV1::bytes_without_body(Bytes::new()).is_err());
521    }
522
523    #[test]
524    fn test_bytes_without_body_too_short() {
525        let bytes = Bytes::from_static(&[0u8; 10]);
526        assert!(EncodedPostV1::bytes_without_body(bytes).is_err());
527    }
528
529    #[cfg(not(target_arch = "wasm32"))]
530    mod bolero_fuzz {
531        use crate::protocol::posting::encoded_post::EncodedPostV1;
532        use crate::tools::types::Id;
533        use bytes::Bytes;
534
535        #[test]
536        fn fuzz_decode_from_bytes() {
537            bolero::check!().for_each(|data: &[u8]| {
538                let password = Id::zero();
539                let _ = EncodedPostV1::decode_from_bytes(Bytes::copy_from_slice(data), &password, true, true);
540            });
541        }
542
543        #[test]
544        fn fuzz_decode_signature_from_bytes() {
545            bolero::check!().for_each(|data: &[u8]| {
546                let _ = EncodedPostV1::decode_signature_from_bytes(data);
547            });
548        }
549
550        #[test]
551        fn fuzz_bytes_without_body() {
552            bolero::check!().for_each(|data: &[u8]| {
553                let _ = EncodedPostV1::bytes_without_body(Bytes::copy_from_slice(data));
554            });
555        }
556    }
557}