1use std::collections::HashSet;
23use crate::{anyhow_assert_eq, anyhow_assert_ge};
24use crate::protocol::peer::Peer;
25use crate::protocol::posting::encoded_post::EncodedPostV1;
26use crate::tools::buckets::BucketLocation;
27use crate::tools::time::TimeMillis;
28use crate::tools::types::{Hash, Id, ID_BYTES, Signature, SignatureKey, VerificationKey};
29use crate::tools::{config, hashing, json, signing};
30use bytes::{Buf, BufMut, Bytes, BytesMut};
31use serde::{Deserialize, Serialize};
32use std::fmt::{Debug, Display};
33
34#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
49pub struct EncodedPostBundleHeaderV1 {
50 pub time_millis: TimeMillis, pub location_id: Id,
52 pub overflowed: bool, pub sealed: bool, pub num_posts: u8,
55 pub encoded_post_ids: Vec<Id>,
56 pub encoded_post_lengths: Vec<usize>,
57 pub encoded_post_healed: HashSet<Id>,
58 pub peer: Peer,
59
60 pub signature: Signature, }
62
63impl EncodedPostBundleHeaderV1 {
64 pub fn get_hash_for_signing(&self) -> anyhow::Result<Hash> {
65 let time_millis_be = self.time_millis.encode_be();
66 let overflowed_bytes = [self.overflowed as u8];
67 let sealed_bytes = [self.sealed as u8];
68 let num_posts_bytes = [self.num_posts];
69 let encoded_post_lengths_be: Vec<[u8; 8]> = self.encoded_post_lengths.iter().map(|&l| (l as u64).to_be_bytes()).collect();
70 let peer_hash = self.peer.signature_hash_generate()?;
71
72 let mut hash_input: Vec<&[u8]> = vec![
73 time_millis_be.as_ref(),
74 self.location_id.as_ref(),
75 &overflowed_bytes,
76 &sealed_bytes,
77 &num_posts_bytes,
78 ];
79
80 for encoded_post_id in &self.encoded_post_ids {
81 hash_input.push(encoded_post_id.as_ref());
82 }
83 for length_be in &encoded_post_lengths_be {
84 hash_input.push(length_be.as_ref());
85 }
86 let mut healed_ids_sorted: Vec<Id> = self.encoded_post_healed.iter().copied().collect();
87 healed_ids_sorted.sort();
88 for healed_id in &healed_ids_sorted {
89 hash_input.push(healed_id.as_ref());
90 }
91 hash_input.push(peer_hash.as_ref());
92
93 Ok(hashing::hash_multiple(&hash_input))
94 }
95
96 pub fn signature_generate(&mut self, signature_key: &SignatureKey) -> anyhow::Result<()> {
97 let hash = self.get_hash_for_signing()?;
98 self.signature = signing::sign(signature_key, hash.as_ref());
99 Ok(())
100 }
101
102 pub fn signature_verify(&self) -> anyhow::Result<()> {
103 let hash = self.get_hash_for_signing()?;
104 let verification_key = VerificationKey::from_bytes(&self.peer.verification_key_bytes)?;
105 signing::verify(&verification_key, &self.signature, hash.as_ref())
106 }
107
108 pub fn verify(&self) -> anyhow::Result<()> {
109 anyhow_assert_eq!(self.num_posts, self.encoded_post_lengths.len() as u8);
110 anyhow_assert_eq!(self.num_posts, self.encoded_post_ids.len() as u8);
111 for healed_id in &self.encoded_post_healed {
112 if !self.encoded_post_ids.contains(healed_id) {
113 anyhow::bail!("encoded_post_healed contains id not in encoded_post_ids: {}", healed_id);
114 }
115 }
116 self.signature_verify()?;
117 Ok(())
118 }
119}
120
121impl Display for EncodedPostBundleHeaderV1 {
122 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123 write!(f, "EncodedPostBundleHeaderV1 [ location_id: {}, time_millis: {}, num_posts: {}, overflowed: {}, sealed: {} ]", self.location_id, self.time_millis, self.num_posts, self.overflowed, self.sealed)
124 }
125}
126
127#[derive(Debug, PartialEq, Clone)]
128pub struct EncodedPostBundleV1 {
129 pub header: EncodedPostBundleHeaderV1,
130 pub encoded_posts_bytes: Bytes,
131}
132
133impl Display for EncodedPostBundleV1 {
134 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135 write!(f, "EncodedPostBundleV1 [ header: {}, length: {} ]", self.header, self.encoded_posts_bytes.len())
136 }
137}
138
139impl EncodedPostBundleV1 {
140 pub fn to_bytes(&self) -> anyhow::Result<Bytes> {
141 let mut bytes = BytesMut::new();
142
143 let json_post_bundle_header = json::struct_to_bytes(&self.header)?;
144 bytes.put_u8(1u8); bytes.put_u64(json_post_bundle_header.len() as u64);
146 bytes.put_u64(self.encoded_posts_bytes.len() as u64);
147 bytes.put_slice(json_post_bundle_header.as_ref());
148 bytes.put_slice(self.encoded_posts_bytes.as_ref());
149
150 Ok(bytes.freeze())
151 }
152
153 pub fn from_bytes(mut bytes: Bytes, decode_body: bool) -> anyhow::Result<Self> {
154 anyhow_assert_ge!(bytes.remaining(), 1, "Missing version");
155 let version = bytes.get_u8();
156 anyhow_assert_eq!(1, version, "Invalid version");
157
158 anyhow_assert_ge!(bytes.remaining(), 8, "Missing header length");
159 let header_len = bytes.get_u64() as usize;
160 anyhow_assert_ge!(bytes.remaining(), 8, "Missing body length");
161 let body_len = bytes.get_u64() as usize;
162
163 let total_length = header_len.checked_add(body_len).ok_or_else(|| anyhow::anyhow!("header_len + body_len overflow"))?;
164 anyhow_assert_ge!(bytes.remaining(), total_length, "Truncated post bundle data");
165
166 let header_bytes = bytes.copy_to_bytes(header_len);
167 let header = json::bytes_to_struct(&header_bytes)?;
168
169 let body = match decode_body {
170 true => {
171 let body_bytes = bytes.copy_to_bytes(body_len);
172 anyhow_assert_eq!(bytes.remaining(), 0, "Excess data");
173 body_bytes
174 },
175 false => Bytes::new(),
176 };
177
178 Ok(EncodedPostBundleV1 {
179 header,
180 encoded_posts_bytes: body,
181 })
182 }
183
184 pub fn verify(&self, base_id: &Id) -> anyhow::Result<()> {
194 self.verify_and_decode_post_headers(base_id)?;
195 Ok(())
196 }
197
198 pub fn verify_for_bucket(&self, bucket_location: &BucketLocation, time_millis_now: TimeMillis) -> anyhow::Result<()> {
211 self.header.peer.verify().map_err(|e| anyhow::anyhow!("bundle peer {} failed verification: {}", self.header.peer.id, e))?;
212
213 if self.header.location_id != bucket_location.location_id {
214 anyhow::bail!("bundle location_id {} does not match bucket location_id {}", self.header.location_id, bucket_location.location_id);
215 }
216
217 let unique_post_ids: HashSet<&Id> = self.header.encoded_post_ids.iter().collect();
218 if unique_post_ids.len() != self.header.encoded_post_ids.len() {
219 anyhow::bail!("bundle contains duplicate post ids");
220 }
221
222 if self.header.time_millis + config::CLIENT_POST_TIMESTAMP_DELTA_THRESHOLD < bucket_location.bucket_time_millis {
223 anyhow::bail!("bundle time_millis {} is before its bucket starts at {}", self.header.time_millis, bucket_location.bucket_time_millis);
224 }
225 if self.header.time_millis > time_millis_now + config::CLIENT_POST_TIMESTAMP_DELTA_THRESHOLD {
226 anyhow::bail!("bundle time_millis {} is in the future (now {})", self.header.time_millis, time_millis_now);
227 }
228
229 let decoded_posts = self.verify_and_decode_post_headers(&bucket_location.base_id)?;
230 for decoded_post in &decoded_posts {
231 decoded_post.check_admissible_to_bucket(bucket_location, self.header.time_millis)?;
232 }
233
234 Ok(())
235 }
236
237 pub fn normalise_flags_for_bucket(&mut self, bucket_location: &BucketLocation, time_millis_now: TimeMillis) {
247 self.header.overflowed |= self.header.num_posts > config::ENCODED_POST_BUNDLE_V1_OVERFLOWED_NUM_POSTS;
248 self.header.sealed = bucket_location.is_sealed_at(self.header.time_millis.min(time_millis_now), self.header.overflowed);
249 }
250
251 fn verify_and_decode_post_headers(&self, base_id: &Id) -> anyhow::Result<Vec<EncodedPostV1>> {
252 self.header.verify()?;
254
255 let total_length: usize = self.header.encoded_post_lengths.iter().sum();
257 if total_length != self.encoded_posts_bytes.len() {
258 anyhow::bail!(
259 "sum of encoded_post_lengths ({}) != encoded_posts_bytes length ({})",
260 total_length,
261 self.encoded_posts_bytes.len()
262 );
263 }
264
265 let mut decoded_posts = Vec::with_capacity(self.header.encoded_post_ids.len());
267 let mut offset = 0usize;
268 for (i, (&length, expected_post_id)) in self.header.encoded_post_lengths.iter().zip(self.header.encoded_post_ids.iter()).enumerate() {
269 let post_bytes = self.encoded_posts_bytes.slice(offset..offset + length);
270
271 if post_bytes.len() < ID_BYTES {
273 anyhow::bail!("post {}: bytes too short to contain post_id", i);
274 }
275 let actual_post_id = Id::from_slice(&post_bytes[..ID_BYTES])?;
276 if actual_post_id != *expected_post_id {
277 anyhow::bail!("post {}: id mismatch — header claims {} but bytes contain {}", i, expected_post_id, actual_post_id);
278 }
279
280 let decoded_post = EncodedPostV1::decode_from_bytes(post_bytes, base_id, true, false)
282 .map_err(|e| anyhow::anyhow!("post {}: failed to verify with base_id: {}", i, e))?;
283 decoded_posts.push(decoded_post);
284
285 offset += length;
286 }
287
288 Ok(decoded_posts)
289 }
290}
291
292
293#[cfg(test)]
294pub(crate) mod test_fixtures {
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::buckets::BucketType;
299 use crate::tools::pow_generator::single_threaded_pow_generator::SingleThreadedPowGenerator;
300 use crate::tools::server_id::ServerId;
301 use crate::tools::time::MILLIS_IN_YEAR;
302 use crate::tools::time_provider::time_provider::{RealTimeProvider, TimeProvider};
303 use std::sync::Arc;
304
305 pub(crate) struct BucketBundleFixture {
307 pub(crate) server_id: ServerId,
308 pub(crate) key_locker: Arc<dyn KeyLocker>,
309 pub(crate) bucket_location: BucketLocation,
310 pub(crate) time_millis_now: TimeMillis,
311 }
312
313 impl BucketBundleFixture {
314 pub(crate) async fn new() -> anyhow::Result<Self> {
315 let time_provider = RealTimeProvider;
316 let pow_generator = SingleThreadedPowGenerator::new();
317 let server_id = ServerId::new("bucket_bundle", &time_provider, config::SERVER_KEY_POW_MIN, true, &pow_generator).await?;
318 let key_locker_manager = MemKeyLockerManager::new().await?;
319 let key_locker: Arc<dyn KeyLocker> = key_locker_manager.create("bucket bundle keyphrase".to_string()).await?;
320 let time_millis_now = time_provider.current_time_millis();
321 let bucket_location = BucketLocation::new(BucketType::User, key_locker.client_id().id, MILLIS_IN_YEAR, time_millis_now)?;
322 Ok(Self { server_id, key_locker, bucket_location, time_millis_now })
323 }
324
325 pub(crate) async fn make_post(&self, timestamp: TimeMillis, linked_base_ids: Vec<Id>) -> anyhow::Result<(Id, Bytes)> {
326 let mut encoded_post = EncodedPostV1::new(self.key_locker.client_id(), timestamp, linked_base_ids, "bucket bundle post");
327 let encoded_post_bytes = encoded_post.encode_to_bytes_direct(&self.key_locker).await?;
328 Ok((encoded_post.post_id, Bytes::copy_from_slice(encoded_post_bytes.bytes())))
329 }
330
331 pub(crate) async fn make_own_posts(&self, num_posts: usize) -> anyhow::Result<Vec<(Id, Bytes)>> {
332 let mut posts = Vec::with_capacity(num_posts);
333 for _ in 0..num_posts {
334 posts.push(self.make_post(self.time_millis_now, vec![self.bucket_location.base_id]).await?);
335 }
336 Ok(posts)
337 }
338
339 pub(crate) fn make_bundle_signed_by(&self, server_id: &ServerId, peer: Peer, time_millis: TimeMillis, posts: &[(Id, Bytes)]) -> anyhow::Result<EncodedPostBundleV1> {
340 let mut encoded_posts_bytes = BytesMut::new();
341 for (_, post_bytes) in posts {
342 encoded_posts_bytes.put_slice(post_bytes);
343 }
344 let mut header = EncodedPostBundleHeaderV1 {
345 time_millis,
346 location_id: self.bucket_location.location_id,
347 overflowed: false,
348 sealed: false,
349 num_posts: posts.len() as u8,
350 encoded_post_ids: posts.iter().map(|(post_id, _)| *post_id).collect(),
351 encoded_post_lengths: posts.iter().map(|(_, post_bytes)| post_bytes.len()).collect(),
352 encoded_post_healed: HashSet::new(),
353 peer,
354 signature: Signature::zero(),
355 };
356 header.signature_generate(&server_id.keys.signature_key)?;
357 Ok(EncodedPostBundleV1 { header, encoded_posts_bytes: encoded_posts_bytes.freeze() })
358 }
359
360 pub(crate) fn make_bundle(&self, posts: &[(Id, Bytes)]) -> anyhow::Result<EncodedPostBundleV1> {
361 let peer = self.server_id.to_peer(&RealTimeProvider)?;
362 self.make_bundle_signed_by(&self.server_id, peer, self.time_millis_now, posts)
363 }
364
365 pub(crate) fn resign(&self, encoded_post_bundle: &mut EncodedPostBundleV1) -> anyhow::Result<()> {
366 encoded_post_bundle.header.signature_generate(&self.server_id.keys.signature_key)
367 }
368
369 pub(crate) async fn new_server_id() -> anyhow::Result<ServerId> {
371 ServerId::new("bucket_bundle", &RealTimeProvider, config::SERVER_KEY_POW_MIN, true, &SingleThreadedPowGenerator::new()).await
372 }
373
374 pub(crate) fn make_bundle_from(&self, server_id: &ServerId, posts: &[(Id, Bytes)]) -> anyhow::Result<EncodedPostBundleV1> {
375 self.make_bundle_signed_by(server_id, server_id.to_peer(&RealTimeProvider)?, self.time_millis_now, posts)
376 }
377 }
378}
379
380#[cfg(test)]
381mod tests {
382 use super::*;
383 use super::test_fixtures::BucketBundleFixture;
384 use crate::client::key_locker::key_locker::{KeyLocker, KeyLockerManager};
385 use crate::client::key_locker::mem_key_locker::MemKeyLockerManager;
386 use std::sync::Arc;
387 use crate::protocol::posting::encoded_post::EncodedPostV1;
388 use crate::tools::buckets::BucketType;
389 use crate::tools::server_id::ServerId;
390 use crate::tools::time::{MILLIS_IN_DAY, MILLIS_IN_MINUTE, MILLIS_IN_SECOND};
391 use crate::tools::time_provider::time_provider::{RealTimeProvider, TimeProvider};
392 use crate::tools::tools;
393 use crate::tools::types::Pow;
394 use crate::tools::pow_generator::single_threaded_pow_generator::SingleThreadedPowGenerator;
395
396 async fn make_valid_bundle(base_id: Id) -> anyhow::Result<EncodedPostBundleV1> {
398 let time_provider = RealTimeProvider;
399 let pow_generator = SingleThreadedPowGenerator::new();
400 let server_id = ServerId::new("own_pow", &time_provider, Pow(0), true, &pow_generator).await?;
401 let peer = server_id.to_peer(&time_provider)?;
402
403 let key_locker_manager = MemKeyLockerManager::new().await?;
404 let key_locker: Arc<dyn KeyLocker> = key_locker_manager.create("test keyphrase".to_string()).await?;
405 let client_id = key_locker.client_id();
406 let timestamp = time_provider.current_time_millis();
407
408 let mut encoded_post = EncodedPostV1::new(client_id, timestamp, vec![base_id], "test post content");
409 let post_bytes_obj = encoded_post.encode_to_bytes_direct(&key_locker).await?;
410 let post_bytes = Bytes::copy_from_slice(post_bytes_obj.bytes());
411
412 let mut header = EncodedPostBundleHeaderV1 {
413 time_millis: timestamp,
414 location_id: Id::random(),
415 overflowed: false,
416 sealed: false,
417 num_posts: 1,
418 encoded_post_ids: vec![encoded_post.post_id],
419 encoded_post_lengths: vec![post_bytes.len()],
420 encoded_post_healed: HashSet::new(),
421 peer,
422 signature: Signature::zero(),
423 };
424 header.signature_generate(&server_id.keys.signature_key)?;
425
426 Ok(EncodedPostBundleV1 { header, encoded_posts_bytes: post_bytes })
427 }
428
429 #[tokio::test]
430 async fn test_verify_valid_bundle() -> anyhow::Result<()> {
431 let base_id = Id::random();
432 let bundle = make_valid_bundle(base_id).await?;
433 bundle.verify(&base_id)
434 }
435
436 #[tokio::test]
437 async fn test_verify_wrong_base_id() -> anyhow::Result<()> {
438 let base_id = Id::random();
439 let bundle = make_valid_bundle(base_id).await?;
440 let wrong_base_id = Id::random();
441 assert!(bundle.verify(&wrong_base_id).is_err());
442 Ok(())
443 }
444
445 #[tokio::test]
446 async fn test_verify_tampered_post_bytes() -> anyhow::Result<()> {
447 let base_id = Id::random();
448 let bundle = make_valid_bundle(base_id).await?;
449 let mut tampered_posts = bundle.encoded_posts_bytes.to_vec();
450 tampered_posts[ID_BYTES + 10] ^= 0xff; let tampered_bundle = EncodedPostBundleV1 {
452 header: bundle.header,
453 encoded_posts_bytes: Bytes::from(tampered_posts),
454 };
455 assert!(tampered_bundle.verify(&base_id).is_err());
456 Ok(())
457 }
458
459 #[tokio::test]
460 async fn test_verify_wrong_post_id_in_header() -> anyhow::Result<()> {
461 let base_id = Id::random();
462 let mut bundle = make_valid_bundle(base_id).await?;
463 let pow_generator = SingleThreadedPowGenerator::new();
464 let server_id = ServerId::new("own_pow", &RealTimeProvider, Pow(0), true, &pow_generator).await?;
465 bundle.header.encoded_post_ids[0] = Id::random(); bundle.header.signature_generate(&server_id.keys.signature_key)?;
467 assert!(bundle.verify(&base_id).is_err());
468 Ok(())
469 }
470
471 #[tokio::test]
472 async fn test_verify_wrong_length_sum() -> anyhow::Result<()> {
473 let base_id = Id::random();
474 let mut bundle = make_valid_bundle(base_id).await?;
475 let pow_generator = SingleThreadedPowGenerator::new();
476 let server_id = ServerId::new("own_pow", &RealTimeProvider, Pow(0), true, &pow_generator).await?;
477 bundle.header.encoded_post_lengths[0] += 1; bundle.header.signature_generate(&server_id.keys.signature_key)?;
479 assert!(bundle.verify(&base_id).is_err());
480 Ok(())
481 }
482
483 #[tokio::test]
484 async fn test_verify_tampered_post_length() -> anyhow::Result<()> {
485 let base_id = Id::random();
488 let bundle = make_valid_bundle(base_id).await?;
489 let original_length = bundle.header.encoded_post_lengths[0];
490 let mut tampered_posts = bundle.encoded_posts_bytes.to_vec();
492 tampered_posts.push(0u8); let mut tampered_header = bundle.header.clone();
494 tampered_header.encoded_post_lengths[0] = original_length + 1;
495 let tampered_bundle = EncodedPostBundleV1 {
497 header: tampered_header,
498 encoded_posts_bytes: Bytes::from(tampered_posts),
499 };
500 assert!(tampered_bundle.verify(&base_id).is_err());
501 Ok(())
502 }
503
504 #[tokio::test]
505 async fn test_verify_bad_header_signature() -> anyhow::Result<()> {
506 let base_id = Id::random();
507 let mut bundle = make_valid_bundle(base_id).await?;
508 bundle.header.signature = Signature::zero(); assert!(bundle.verify(&base_id).is_err());
510 Ok(())
511 }
512
513 #[tokio::test]
516 async fn test_verify_for_bucket_accepts_genuine_bundles() -> anyhow::Result<()> {
517 let fixture = BucketBundleFixture::new().await?;
518 let posts = fixture.make_own_posts(2).await?;
519 fixture.make_bundle(&posts)?.verify_for_bucket(&fixture.bucket_location, fixture.time_millis_now)?;
520
521 fixture.make_bundle(&[])?.verify_for_bucket(&fixture.bucket_location, fixture.time_millis_now)?;
523 Ok(())
524 }
525
526 #[tokio::test]
527 async fn test_verify_for_bucket_rejects_peer_without_server_pow() -> anyhow::Result<()> {
528 let fixture = BucketBundleFixture::new().await?;
529 let posts = fixture.make_own_posts(1).await?;
530
531 let pow_generator = SingleThreadedPowGenerator::new();
533 let unworked_server_id = loop {
534 let server_id = ServerId::new("unworked", &RealTimeProvider, Pow(0), true, &pow_generator).await?;
535 if server_id.pow < config::SERVER_KEY_POW_MIN {
536 break server_id;
537 }
538 };
539 let unworked_peer = unworked_server_id.to_peer(&RealTimeProvider)?;
540 let encoded_post_bundle = fixture.make_bundle_signed_by(&unworked_server_id, unworked_peer, fixture.time_millis_now, &posts)?;
541 encoded_post_bundle.verify(&fixture.bucket_location.base_id)?; assert!(encoded_post_bundle.verify_for_bucket(&fixture.bucket_location, fixture.time_millis_now).is_err()); Ok(())
544 }
545
546 #[tokio::test]
547 async fn test_verify_for_bucket_rejects_peer_impersonating_another_id() -> anyhow::Result<()> {
548 let fixture = BucketBundleFixture::new().await?;
549 let posts = fixture.make_own_posts(1).await?;
550
551 let mut impersonating_peer = fixture.server_id.to_peer(&RealTimeProvider)?;
553 impersonating_peer.id = Id::random();
554 impersonating_peer.sign(&RealTimeProvider, &fixture.server_id.keys.signature_key)?;
555 let encoded_post_bundle = fixture.make_bundle_signed_by(&fixture.server_id, impersonating_peer, fixture.time_millis_now, &posts)?;
556 encoded_post_bundle.verify(&fixture.bucket_location.base_id)?;
557 assert!(encoded_post_bundle.verify_for_bucket(&fixture.bucket_location, fixture.time_millis_now).is_err());
558 Ok(())
559 }
560
561 #[tokio::test]
562 async fn test_verify_for_bucket_rejects_wrong_location() -> anyhow::Result<()> {
563 let fixture = BucketBundleFixture::new().await?;
564 let posts = fixture.make_own_posts(1).await?;
565 let mut encoded_post_bundle = fixture.make_bundle(&posts)?;
566 encoded_post_bundle.header.location_id = Id::random();
567 fixture.resign(&mut encoded_post_bundle)?;
568 assert!(encoded_post_bundle.verify_for_bucket(&fixture.bucket_location, fixture.time_millis_now).is_err());
569 Ok(())
570 }
571
572 #[tokio::test]
573 async fn test_verify_for_bucket_rejects_duplicate_post_ids() -> anyhow::Result<()> {
574 let fixture = BucketBundleFixture::new().await?;
575 let posts = fixture.make_own_posts(1).await?;
576 let duplicated_posts = vec![posts[0].clone(), posts[0].clone()];
577 let encoded_post_bundle = fixture.make_bundle(&duplicated_posts)?;
578 encoded_post_bundle.verify(&fixture.bucket_location.base_id)?;
579 assert!(encoded_post_bundle.verify_for_bucket(&fixture.bucket_location, fixture.time_millis_now).is_err());
580 Ok(())
581 }
582
583 #[tokio::test]
584 async fn test_verify_for_bucket_rejects_implausible_header_time() -> anyhow::Result<()> {
585 let fixture = BucketBundleFixture::new().await?;
586 let peer = fixture.server_id.to_peer(&RealTimeProvider)?;
587
588 let future_time_millis = fixture.time_millis_now + config::CLIENT_POST_TIMESTAMP_DELTA_THRESHOLD + MILLIS_IN_SECOND;
589 let future_encoded_post_bundle = fixture.make_bundle_signed_by(&fixture.server_id, peer.clone(), future_time_millis, &[])?;
590 assert!(future_encoded_post_bundle.verify_for_bucket(&fixture.bucket_location, fixture.time_millis_now).is_err());
591
592 let before_bucket_time_millis = fixture.bucket_location.bucket_time_millis - config::CLIENT_POST_TIMESTAMP_DELTA_THRESHOLD - MILLIS_IN_SECOND;
593 let before_bucket_encoded_post_bundle = fixture.make_bundle_signed_by(&fixture.server_id, peer, before_bucket_time_millis, &[])?;
594 assert!(before_bucket_encoded_post_bundle.verify_for_bucket(&fixture.bucket_location, fixture.time_millis_now).is_err());
595 Ok(())
596 }
597
598 #[tokio::test]
599 async fn test_verify_for_bucket_rejects_inadmissible_posts() -> anyhow::Result<()> {
600 let fixture = BucketBundleFixture::new().await?;
601 let own_posts = fixture.make_own_posts(1).await?;
602
603 let old_post = fixture.make_post(fixture.bucket_location.bucket_time_millis - MILLIS_IN_SECOND, vec![fixture.bucket_location.base_id]).await?;
605 let replayed_encoded_post_bundle = fixture.make_bundle(&[own_posts[0].clone(), old_post])?;
606 replayed_encoded_post_bundle.verify(&fixture.bucket_location.base_id)?;
607 assert!(replayed_encoded_post_bundle.verify_for_bucket(&fixture.bucket_location, fixture.time_millis_now).is_err());
608
609 let post_after_bundle = fixture.make_post(fixture.time_millis_now + config::CLIENT_POST_TIMESTAMP_DELTA_THRESHOLD, vec![fixture.bucket_location.base_id]).await?;
611 let peer = fixture.server_id.to_peer(&RealTimeProvider)?;
612 let stale_header_encoded_post_bundle = fixture.make_bundle_signed_by(&fixture.server_id, peer, fixture.time_millis_now - MILLIS_IN_SECOND, &[post_after_bundle])?;
613 assert!(stale_header_encoded_post_bundle.verify_for_bucket(&fixture.bucket_location, fixture.time_millis_now + config::CLIENT_POST_TIMESTAMP_DELTA_THRESHOLD).is_err());
614 Ok(())
615 }
616
617 #[tokio::test]
618 async fn test_verify_for_bucket_rejects_foreign_author_in_user_bucket() -> anyhow::Result<()> {
619 let fixture = BucketBundleFixture::new().await?;
620
621 let other_key_locker_manager = MemKeyLockerManager::new().await?;
623 let other_key_locker: Arc<dyn KeyLocker> = other_key_locker_manager.create("other keyphrase".to_string()).await?;
624 let mut foreign_post = EncodedPostV1::new(other_key_locker.client_id(), fixture.time_millis_now, vec![fixture.bucket_location.base_id], "not your post");
625 let foreign_post_bytes = foreign_post.encode_to_bytes_direct(&other_key_locker).await?;
626
627 let encoded_post_bundle = fixture.make_bundle(&[(foreign_post.post_id, Bytes::copy_from_slice(foreign_post_bytes.bytes()))])?;
628 encoded_post_bundle.verify(&fixture.bucket_location.base_id)?;
629 assert!(encoded_post_bundle.verify_for_bucket(&fixture.bucket_location, fixture.time_millis_now).is_err());
630 Ok(())
631 }
632
633 #[tokio::test]
634 async fn test_normalise_flags_for_bucket() -> anyhow::Result<()> {
635 let fixture = BucketBundleFixture::new().await?;
636
637 let posts = fixture.make_own_posts(1).await?;
639 let mut prematurely_sealed_encoded_post_bundle = fixture.make_bundle(&posts)?;
640 prematurely_sealed_encoded_post_bundle.header.sealed = true;
641 prematurely_sealed_encoded_post_bundle.normalise_flags_for_bucket(&fixture.bucket_location, fixture.time_millis_now);
642 assert!(!prematurely_sealed_encoded_post_bundle.header.sealed);
643 assert!(!prematurely_sealed_encoded_post_bundle.header.overflowed);
644
645 let mut overflowed_encoded_post_bundle = fixture.make_bundle(&posts)?;
647 overflowed_encoded_post_bundle.header.overflowed = true;
648 overflowed_encoded_post_bundle.normalise_flags_for_bucket(&fixture.bucket_location, fixture.time_millis_now);
649 assert!(overflowed_encoded_post_bundle.header.overflowed);
650 assert!(overflowed_encoded_post_bundle.header.sealed);
651
652 let many_posts = fixture.make_own_posts(config::ENCODED_POST_BUNDLE_V1_OVERFLOWED_NUM_POSTS as usize + 1).await?;
654 let mut understated_encoded_post_bundle = fixture.make_bundle(&many_posts)?;
655 understated_encoded_post_bundle.verify_for_bucket(&fixture.bucket_location, fixture.time_millis_now)?;
656 understated_encoded_post_bundle.normalise_flags_for_bucket(&fixture.bucket_location, fixture.time_millis_now);
657 assert!(understated_encoded_post_bundle.header.overflowed);
658 assert!(understated_encoded_post_bundle.header.sealed);
659
660 let past_bucket_location = BucketLocation::new(BucketType::User, fixture.bucket_location.base_id, MILLIS_IN_DAY, fixture.time_millis_now - MILLIS_IN_DAY.const_mul(2))?;
662 let mut unsealed_past_encoded_post_bundle = fixture.make_bundle(&[])?;
663 unsealed_past_encoded_post_bundle.header.location_id = past_bucket_location.location_id;
664 unsealed_past_encoded_post_bundle.normalise_flags_for_bucket(&past_bucket_location, fixture.time_millis_now);
665 assert!(unsealed_past_encoded_post_bundle.header.sealed);
666
667 let bucket_location = &fixture.bucket_location;
669 let seal_time_millis = bucket_location.bucket_time_millis + bucket_location.duration + config::ENCODED_POST_BUNDLE_V1_ELAPSED_THRESHOLD_MILLIS;
670 let time_millis_before_seal = seal_time_millis - MILLIS_IN_MINUTE.const_mul(5);
671 let mut future_dated_encoded_post_bundle = fixture.make_bundle(&posts)?;
672 future_dated_encoded_post_bundle.header.time_millis = time_millis_before_seal + MILLIS_IN_MINUTE.const_mul(9);
673 future_dated_encoded_post_bundle.header.sealed = true;
674 let mut normalised_before_seal = future_dated_encoded_post_bundle.clone();
675 normalised_before_seal.normalise_flags_for_bucket(bucket_location, time_millis_before_seal);
676 assert!(!normalised_before_seal.header.sealed, "a future-dated header must not seal the bundle early");
677 let mut normalised_after_seal = future_dated_encoded_post_bundle.clone();
678 normalised_after_seal.normalise_flags_for_bucket(bucket_location, seal_time_millis + MILLIS_IN_MINUTE);
679 assert!(normalised_after_seal.header.sealed, "once the bucket has really sealed, the bundle is sealed");
680 Ok(())
681 }
682
683 #[tokio::test]
684 async fn encoded_post_bundle_v1_to_from_bytes_roundtrip() -> anyhow::Result<()> {
685 let time_provider = RealTimeProvider;
686 let pow_generator = SingleThreadedPowGenerator::new();
687 let server_id = ServerId::new("own_pow", &time_provider, Pow(0), true, &pow_generator).await?;
688 let peer = server_id.to_peer(&time_provider)?;
689
690 let num_posts: u8 = 3;
691
692 let mut header = EncodedPostBundleHeaderV1 {
693 time_millis: TimeMillis::random(),
694 location_id: Id::random(),
695 overflowed: true,
696 sealed: false,
697 num_posts,
698 encoded_post_ids: (0..num_posts).map(|_| Id::random()).collect(),
699 encoded_post_lengths: (0..num_posts).map(|_| tools::random_usize_bounded(1024)).collect(),
700 encoded_post_healed: HashSet::new(),
701 peer,
702 signature: Signature::zero(),
703 };
704
705 header.signature_generate(&server_id.keys.signature_key)?;
706 header.verify()?;
707
708 let total_bytes = header.encoded_post_lengths.iter().sum::<usize>();
709 let encoded_posts_bytes = Bytes::from(tools::random_bytes(total_bytes));
710
711 let bundle = EncodedPostBundleV1 {
712 header,
713 encoded_posts_bytes,
714 };
715
716 let bytes1 = bundle.to_bytes()?;
717 let decoded = EncodedPostBundleV1::from_bytes(bytes1.clone(), true)?;
718
719 assert_eq!(bundle, decoded);
720
721 let bytes2 = decoded.to_bytes()?;
723 assert_eq!(bytes1, bytes2);
724
725 Ok(())
726 }
727
728 #[tokio::test]
729 async fn encoded_post_bundle_v1_to_from_bytes_roundtrip_without_body() -> anyhow::Result<()> {
730 let time_provider = RealTimeProvider;
731 let pow_generator = SingleThreadedPowGenerator::new();
732 let server_id = ServerId::new("own_pow", &time_provider, Pow(0), true, &pow_generator).await?;
733 let peer = server_id.to_peer(&time_provider)?;
734
735 let num_posts: u8 = 3;
736
737 let mut header = EncodedPostBundleHeaderV1 {
738 time_millis: TimeMillis::random(),
739 location_id: Id::random(),
740 overflowed: true,
741 sealed: false,
742 num_posts,
743 encoded_post_ids: (0..num_posts).map(|_| Id::random()).collect(),
744 encoded_post_lengths: (0..num_posts).map(|_| tools::random_usize_bounded(1024)).collect(),
745 encoded_post_healed: HashSet::new(),
746 peer,
747 signature: Signature::zero(),
748 };
749
750 header.signature_generate(&server_id.keys.signature_key)?;
751 header.verify()?;
752
753 let total_bytes = header.encoded_post_lengths.iter().sum::<usize>();
754 let encoded_posts_bytes = Bytes::from(tools::random_bytes(total_bytes));
755
756 let bundle = EncodedPostBundleV1 {
757 header,
758 encoded_posts_bytes,
759 };
760
761 let bytes1 = bundle.to_bytes()?;
762 let decoded = EncodedPostBundleV1::from_bytes(bytes1.clone(), false)?;
763
764 assert_eq!(bundle.header, decoded.header);
765 assert!(decoded.encoded_posts_bytes.is_empty());
766
767 Ok(())
768 }
769
770 #[test]
773 fn test_from_bytes_empty() {
774 assert!(EncodedPostBundleV1::from_bytes(Bytes::new(), true).is_err());
775 }
776
777 #[test]
778 fn test_from_bytes_wrong_version() {
779 assert!(EncodedPostBundleV1::from_bytes(Bytes::from_static(&[99u8]), true).is_err());
780 }
781
782 #[test]
783 fn test_from_bytes_truncated_at_header_length() {
784 assert!(EncodedPostBundleV1::from_bytes(Bytes::from_static(&[1u8]), true).is_err());
786 }
787
788 #[test]
789 fn test_from_bytes_truncated_at_body_length() {
790 let mut bytes = BytesMut::new();
792 bytes.put_u8(1); bytes.put_u64(0); assert!(EncodedPostBundleV1::from_bytes(bytes.freeze(), true).is_err());
795 }
796
797 #[test]
798 fn test_from_bytes_header_len_exceeds_remaining() {
799 let mut bytes = BytesMut::new();
800 bytes.put_u8(1); bytes.put_u64(99999); bytes.put_u64(0); assert!(EncodedPostBundleV1::from_bytes(bytes.freeze(), true).is_err());
804 }
805
806 #[test]
807 fn test_from_bytes_overflow_lengths() {
808 let mut bytes = BytesMut::new();
809 bytes.put_u8(1); bytes.put_u64(u64::MAX); bytes.put_u64(1); assert!(EncodedPostBundleV1::from_bytes(bytes.freeze(), true).is_err());
813 }
814
815 #[test]
816 fn test_from_bytes_garbage() {
817 assert!(EncodedPostBundleV1::from_bytes(Bytes::from_static(&[0xff; 128]), true).is_err());
818 }
819
820 #[cfg(not(target_arch = "wasm32"))]
821 mod bolero_fuzz {
822 use bytes::Bytes;
823 use crate::protocol::posting::encoded_post_bundle::EncodedPostBundleV1;
824
825 #[test]
826 fn fuzz_from_bytes() {
827 bolero::check!().for_each(|data: &[u8]| {
828 let _ = EncodedPostBundleV1::from_bytes(Bytes::copy_from_slice(data), true);
829 });
830 }
831
832 #[test]
833 fn fuzz_from_bytes_no_body() {
834 bolero::check!().for_each(|data: &[u8]| {
835 let _ = EncodedPostBundleV1::from_bytes(Bytes::copy_from_slice(data), false);
836 });
837 }
838 }
839}