1use crate::protocol::peer::Peer;
25use crate::protocol::posting::encoded_post_feedback::{EncodedPostFeedbackV1, EncodedPostFeedbackViewV1, ENTRY_SIZE};
26use crate::tools::time::TimeMillis;
27use crate::tools::types::{Hash, Id, Pow, Salt, Signature, SignatureKey, VerificationKey};
28use std::collections::HashMap;
29use crate::tools::{hashing, json, signing};
30use bytes::{Buf, BufMut, Bytes, BytesMut};
31use serde::{Deserialize, Serialize};
32use std::fmt::{Debug, Display};
33use crate::anyhow_assert_eq;
34
35#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
37pub struct EncodedPostBundleFeedbackHeaderV1 {
38 pub time_millis: TimeMillis, pub location_id: Id,
40 pub feedbacks_bytes_hash: Hash, pub peer: Peer,
42
43 pub signature: Signature, }
45
46impl EncodedPostBundleFeedbackHeaderV1 {
47 pub fn get_hash_for_signing(&self) -> Hash {
48 let time_millis_be = self.time_millis.encode_be();
49
50 let hash_input: Vec<&[u8]> = vec![
51 time_millis_be.as_ref(),
52 self.location_id.as_ref(),
53 self.feedbacks_bytes_hash.as_ref(),
54 self.peer.signature.as_ref(),
55 ];
56
57 hashing::hash_multiple(&hash_input)
58 }
59
60 pub fn signature_generate(&mut self, signature_key: &SignatureKey) {
61 let hash = self.get_hash_for_signing();
62 self.signature = signing::sign(signature_key, hash.as_ref());
63 }
64
65 pub fn signature_verify(&self) -> anyhow::Result<()> {
66 let hash = self.get_hash_for_signing();
67 let verification_key = VerificationKey::from_bytes(&self.peer.verification_key_bytes)?;
68 signing::verify(&verification_key, &self.signature, hash.as_ref())
69 }
70
71 pub fn verify(&self) -> anyhow::Result<()> {
72 self.signature_verify()?;
73 Ok(())
74 }
75}
76
77impl Display for EncodedPostBundleFeedbackHeaderV1 {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 write!(f, "EncodedPostBundleFeedbackHeaderV1 [ location_id: {}, time_millis: {} ]", self.location_id, self.time_millis)
80 }
81}
82
83#[derive(Debug, PartialEq, Clone)]
84pub struct EncodedPostBundleFeedbackV1 {
85 pub header: EncodedPostBundleFeedbackHeaderV1,
86 pub feedbacks_bytes: Bytes, }
88impl Display for EncodedPostBundleFeedbackV1 {
89 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90 write!(f, "EncodedPostBundleV1 [ header: {}, length: {} ]", self.header, self.feedbacks_bytes.len())
91 }
92}
93
94impl EncodedPostBundleFeedbackV1 {
95 pub fn to_bytes(&self) -> anyhow::Result<Bytes> {
96 let mut bytes = BytesMut::new();
97
98 let header = json::struct_to_bytes(&self.header)?;
99 bytes.put_u8(1u8); bytes.put_u64(header.len() as u64);
101 bytes.put_u64(self.feedbacks_bytes.len() as u64);
102 bytes.put_slice(header.as_ref());
103 bytes.put_slice(self.feedbacks_bytes.as_ref());
104
105 Ok(bytes.freeze())
106 }
107
108 pub fn from_bytes(mut bytes: Bytes) -> anyhow::Result<Self> {
109 if bytes.remaining() < 1 {
110 anyhow::bail!("Invalid buffer: missing post_bundle version");
111 }
112 let version = bytes.get_u8();
113 if 1 != version {
114 anyhow::bail!("Invalid buffer: unknown post_bundle version");
115 }
116
117 if bytes.remaining() < 16 {
118 anyhow::bail!("Invalid buffer: missing post_bundle lengths");
119 }
120
121 let header_len = bytes.get_u64() as usize;
122 let body_len = bytes.get_u64() as usize;
123 let total_length = header_len.checked_add(body_len).ok_or_else(|| anyhow::anyhow!("total_length overflow"))?;
124 if bytes.remaining() < total_length {
125 anyhow::bail!("Invalid buffer: post_bundle data truncated");
126 }
127 let header_bytes = bytes.copy_to_bytes(header_len);
128 let body_bytes = bytes.copy_to_bytes(body_len);
129
130 let header = json::bytes_to_struct(&header_bytes)?;
131
132 Ok(EncodedPostBundleFeedbackV1 { header, feedbacks_bytes: body_bytes })
133 }
134
135 pub fn verify(&self) -> anyhow::Result<()> {
143 self.verify_envelope()?;
144 self.verify_entries_pow()
145 }
146
147 fn verify_envelope(&self) -> anyhow::Result<()> {
149 self.header.verify()?;
151
152 if !self.feedbacks_bytes.len().is_multiple_of(ENTRY_SIZE) {
154 anyhow::bail!(
155 "feedbacks_bytes length ({}) is not a multiple of ENTRY_SIZE ({})",
156 self.feedbacks_bytes.len(),
157 ENTRY_SIZE
158 );
159 }
160
161 let feedbacks_bytes_hash = hashing::hash(self.feedbacks_bytes.as_ref());
163 anyhow_assert_eq!(feedbacks_bytes_hash, self.header.feedbacks_bytes_hash, "feedbacks_bytes_hash mismatch");
164 Ok(())
165 }
166
167 fn verify_entries_pow(&self) -> anyhow::Result<()> {
169 let num_entries = self.feedbacks_bytes.len() / ENTRY_SIZE;
171 for i in 0..num_entries {
172 let entry_bytes = &self.feedbacks_bytes[i * ENTRY_SIZE..(i + 1) * ENTRY_SIZE];
173 let feedback = EncodedPostFeedbackV1::decode_from_bytes(&mut &entry_bytes[..])
174 .map_err(|e| anyhow::anyhow!("feedback {}: invalid entry: {}", i, e))?;
175 feedback.pow_verify()
176 .map_err(|e| anyhow::anyhow!("feedback {}: pow verification failed: {}", i, e))?;
177 }
178
179 Ok(())
180 }
181
182 pub fn verify_for_location(&self, location_id: &Id) -> anyhow::Result<()> {
189 self.verify_origin_for_location(location_id)?;
190 self.verify_entries_pow()
191 }
192
193 pub fn verify_origin_for_location(&self, location_id: &Id) -> anyhow::Result<()> {
197 self.header.peer.verify().map_err(|e| anyhow::anyhow!("feedback bundle peer {} failed verification: {}", self.header.peer.id, e))?;
198 if self.header.location_id != *location_id {
199 anyhow::bail!("feedback bundle location_id {} does not match location_id {}", self.header.location_id, location_id);
200 }
201 self.verify_envelope()
202 }
203
204 pub fn get_post_pow_for_feedback_type(&self, post_id: &Id, feedback_type: u8) -> Pow {
205 for view in EncodedPostFeedbackViewV1::iter(&self.feedbacks_bytes).flatten() {
206 if view.feedback_type() == feedback_type && view.post_id_bytes() == post_id.as_ref() {
208 return view.pow();
209 }
210 }
211 Pow(0)
212 }
213
214 pub fn merge(bundles: &[Self]) -> Option<Self> {
226 let header = bundles.iter().max_by_key(|b| b.header.time_millis)?.header.clone();
227
228 let mut global_max: HashMap<(Id, u8), EncodedPostFeedbackV1> = HashMap::new();
229 for bundle in bundles {
230 for view in EncodedPostFeedbackViewV1::iter(&bundle.feedbacks_bytes) {
231 let Ok(view) = view else { continue };
232 let Ok(post_id) = Id::from_slice(view.post_id_bytes()) else { continue };
233 let Ok(salt) = Salt::from_slice(view.salt_bytes()) else { continue };
234 let encoded_post_feedback = EncodedPostFeedbackV1::new(post_id, view.feedback_type(), salt, view.pow());
235 if encoded_post_feedback.pow_verify().is_err() {
236 continue;
237 }
238
239 let key = (post_id, encoded_post_feedback.feedback_type);
240 match global_max.entry(key) {
241 std::collections::hash_map::Entry::Vacant(vacant) => {
242 vacant.insert(encoded_post_feedback);
243 }
244 std::collections::hash_map::Entry::Occupied(mut occupied) => {
245 if encoded_post_feedback.pow > occupied.get().pow {
246 occupied.insert(encoded_post_feedback);
247 }
248 }
249 }
250 }
251 }
252
253 let mut feedbacks_bytes_mut = Vec::new();
254 for feedback in global_max.values() {
255 let _ = feedback.append_encode_to_bytes(&mut feedbacks_bytes_mut);
256 }
257
258 Some(Self { header, feedbacks_bytes: Bytes::from(feedbacks_bytes_mut) })
259 }
260
261 pub fn get_post_pows(&self, post_id: &Id) -> [Pow; 256] {
262 let mut result = [Pow(0); 256];
263 for view in EncodedPostFeedbackViewV1::iter(&self.feedbacks_bytes).flatten() {
264 if view.post_id_bytes() == post_id.as_ref() {
265 result[view.feedback_type() as usize] = view.pow();
266 }
267 }
268
269 result
270 }
271}
272
273#[cfg(test)]
274pub(crate) mod test_fixtures {
275 use super::*;
276 use crate::tools::pow;
277 use crate::tools::server_id::ServerId;
278 use crate::tools::time_provider::time_provider::RealTimeProvider;
279
280 pub(crate) fn make_valid_feedback(post_id: Id, feedback_type: u8) -> anyhow::Result<EncodedPostFeedbackV1> {
282 let salt = Salt::random();
283 let (measured_pow, _) = pow::pow_measure(&[post_id.as_bytes(), &[feedback_type]], &salt)?;
284 let encoded_post_feedback = EncodedPostFeedbackV1::new(post_id, feedback_type, salt, measured_pow);
285 encoded_post_feedback.pow_verify()?;
286 Ok(encoded_post_feedback)
287 }
288
289 pub(crate) fn make_forged_feedback(post_id: Id, feedback_type: u8) -> EncodedPostFeedbackV1 {
291 let encoded_post_feedback = EncodedPostFeedbackV1::new(post_id, feedback_type, Salt::random(), Pow(255));
292 assert!(encoded_post_feedback.pow_verify().is_err());
293 encoded_post_feedback
294 }
295
296 pub(crate) fn make_bundle_signed_by(server_id: &ServerId, location_id: Id, time_millis: TimeMillis, encoded_post_feedbacks: &[EncodedPostFeedbackV1]) -> anyhow::Result<EncodedPostBundleFeedbackV1> {
298 let peer = server_id.to_peer(&RealTimeProvider)?;
299
300 let mut feedbacks_bytes_mut = Vec::new();
301 for encoded_post_feedback in encoded_post_feedbacks {
302 encoded_post_feedback.append_encode_to_bytes(&mut feedbacks_bytes_mut)?;
303 }
304 let feedbacks_bytes = Bytes::from(feedbacks_bytes_mut);
305
306 let mut header = EncodedPostBundleFeedbackHeaderV1 {
307 time_millis,
308 location_id,
309 feedbacks_bytes_hash: hashing::hash(feedbacks_bytes.as_ref()),
310 peer,
311 signature: Signature::zero(),
312 };
313 header.signature_generate(&server_id.keys.signature_key);
314 Ok(EncodedPostBundleFeedbackV1 { header, feedbacks_bytes })
315 }
316}
317
318#[cfg(test)]
319mod tests {
320 use super::*;
321 use super::test_fixtures::{make_bundle_signed_by, make_forged_feedback, make_valid_feedback};
322 use crate::tools::server_id::ServerId;
323 use crate::tools::time_provider::time_provider::{RealTimeProvider, TimeProvider};
324 use crate::tools::pow;
325 use crate::tools::pow_generator::pow_generator::PowGenerator;
326 use crate::tools::pow_generator::single_threaded_pow_generator::SingleThreadedPowGenerator;
327
328 async fn make_valid_bundle() -> anyhow::Result<EncodedPostBundleFeedbackV1> {
330 let time_provider = RealTimeProvider;
331 let pow_generator = SingleThreadedPowGenerator::new();
332 let server_id = ServerId::new("own_pow", &time_provider, Pow(0), true, &pow_generator).await?;
333 let peer = server_id.to_peer(&time_provider)?;
334
335 let post_id = Id::random();
336 let feedback_type = 1u8;
337 let data_hash = pow::pow_compute_data_hash(&[post_id.as_bytes(), &[feedback_type]]);
339 let (salt, achieved_pow, _) = pow_generator.generate_best_effort("make_valid_bundle_feedback", 1, Pow(0), data_hash).await?;
340 let feedback = EncodedPostFeedbackV1::new(post_id, feedback_type, salt, achieved_pow);
341
342 let mut feedbacks_bytes_mut = Vec::new();
343 feedback.append_encode_to_bytes(&mut feedbacks_bytes_mut)?;
344 let feedbacks_bytes = Bytes::from(feedbacks_bytes_mut);
345
346 let mut header = EncodedPostBundleFeedbackHeaderV1 {
347 time_millis: time_provider.current_time_millis(),
348 location_id: Id::random(),
349 feedbacks_bytes_hash: hashing::hash(feedbacks_bytes.as_ref()),
350 peer,
351 signature: Signature::zero(),
352 };
353 header.signature_generate(&server_id.keys.signature_key);
354
355 Ok(EncodedPostBundleFeedbackV1 { header, feedbacks_bytes })
356 }
357
358 #[tokio::test]
359 async fn test_verify_valid_bundle() -> anyhow::Result<()> {
360 let bundle = make_valid_bundle().await?;
361 bundle.verify()
362 }
363
364 #[tokio::test]
365 async fn test_verify_bad_header_signature() -> anyhow::Result<()> {
366 let mut bundle = make_valid_bundle().await?;
367 bundle.header.signature = Signature::zero();
368 assert!(bundle.verify().is_err());
369 Ok(())
370 }
371
372 #[tokio::test]
373 async fn test_verify_wrong_feedbacks_hash() -> anyhow::Result<()> {
374 let mut bundle = make_valid_bundle().await?;
375 let pow_generator = SingleThreadedPowGenerator::new();
376 let server_id = ServerId::new("own_pow", &RealTimeProvider, Pow(0), true, &pow_generator).await?;
377 bundle.header.feedbacks_bytes_hash = hashing::hash(b"wrong");
378 bundle.header.signature_generate(&server_id.keys.signature_key); assert!(bundle.verify().is_err());
380 Ok(())
381 }
382
383 #[tokio::test]
384 async fn test_verify_partial_entry() -> anyhow::Result<()> {
385 let mut bundle = make_valid_bundle().await?;
386 let pow_generator = SingleThreadedPowGenerator::new();
387 let server_id = ServerId::new("own_pow", &RealTimeProvider, Pow(0), true, &pow_generator).await?;
388 let mut bytes = bundle.feedbacks_bytes.to_vec();
390 bytes.push(0u8);
391 bundle.feedbacks_bytes = Bytes::from(bytes);
392 bundle.header.feedbacks_bytes_hash = hashing::hash(bundle.feedbacks_bytes.as_ref());
393 bundle.header.signature_generate(&server_id.keys.signature_key);
394 assert!(bundle.verify().is_err());
395 Ok(())
396 }
397
398 #[tokio::test]
399 async fn test_verify_wrong_pow() -> anyhow::Result<()> {
400 let mut bundle = make_valid_bundle().await?;
401 let pow_generator = SingleThreadedPowGenerator::new();
402 let server_id = ServerId::new("own_pow", &RealTimeProvider, Pow(0), true, &pow_generator).await?;
403 let mut bytes = bundle.feedbacks_bytes.to_vec();
405 let last = bytes.last_mut().unwrap();
406 *last = last.wrapping_add(1);
407 bundle.feedbacks_bytes = Bytes::from(bytes);
408 bundle.header.feedbacks_bytes_hash = hashing::hash(bundle.feedbacks_bytes.as_ref());
409 bundle.header.signature_generate(&server_id.keys.signature_key);
410 assert!(bundle.verify().is_err());
411 Ok(())
412 }
413
414 async fn make_bundle_with_feedbacks(time_millis: TimeMillis, feedbacks: &[(Id, u8, Pow)]) -> anyhow::Result<EncodedPostBundleFeedbackV1> {
419 let encoded_post_feedbacks: Vec<EncodedPostFeedbackV1> = feedbacks.iter().map(|(post_id, feedback_type, pow)| EncodedPostFeedbackV1::new(*post_id, *feedback_type, Salt::random(), *pow)).collect();
420 make_bundle_with_encoded_post_feedbacks(time_millis, &encoded_post_feedbacks).await
421 }
422
423
424 async fn make_bundle_with_encoded_post_feedbacks(time_millis: TimeMillis, encoded_post_feedbacks: &[EncodedPostFeedbackV1]) -> anyhow::Result<EncodedPostBundleFeedbackV1> {
425 let server_id = ServerId::new("own_pow", &RealTimeProvider, Pow(0), true, &SingleThreadedPowGenerator::new()).await?;
426 make_bundle_signed_by(&server_id, Id::random(), time_millis, encoded_post_feedbacks)
427 }
428
429
430 #[tokio::test]
431 async fn get_post_pows_reports_pow_per_feedback_type() -> anyhow::Result<()> {
432 let post_a = Id::random();
433 let post_b = Id::random();
434 let bundle = make_bundle_with_feedbacks(TimeMillis(1000), &[
435 (post_a, 1, Pow(10)),
436 (post_a, 2, Pow(20)),
437 (post_b, 1, Pow(30)),
438 ]).await?;
439
440 assert_eq!(Pow(10), bundle.get_post_pow_for_feedback_type(&post_a, 1));
442 assert_eq!(Pow(20), bundle.get_post_pow_for_feedback_type(&post_a, 2));
443 assert_eq!(Pow(0), bundle.get_post_pow_for_feedback_type(&post_a, 3), "absent type => Pow(0)");
444 assert_eq!(Pow(0), bundle.get_post_pow_for_feedback_type(&post_b, 2), "post_b has no type-2 feedback");
445
446 let pows_a = bundle.get_post_pows(&post_a);
448 assert_eq!(Pow(10), pows_a[1]);
449 assert_eq!(Pow(20), pows_a[2]);
450 assert_eq!(Pow(0), pows_a[0]);
451 assert_eq!(Pow(0), pows_a[3]);
452
453 let pows_b = bundle.get_post_pows(&post_b);
454 assert_eq!(Pow(30), pows_b[1]);
455 assert_eq!(Pow(0), pows_b[2]);
456 Ok(())
457 }
458
459 #[tokio::test]
460 async fn merge_keeps_highest_pow_per_post_and_type() -> anyhow::Result<()> {
461 let post_a = Id::random();
462
463 let candidates_for_type_1: Vec<EncodedPostFeedbackV1> = (0..16).map(|_| make_valid_feedback(post_a, 1)).collect::<anyhow::Result<_>>()?;
465 let only_for_type_2 = make_valid_feedback(post_a, 2)?;
466 let expected_type_1_pow = candidates_for_type_1.iter().map(|encoded_post_feedback| encoded_post_feedback.pow).max().unwrap();
467
468 let (first_half, second_half) = candidates_for_type_1.split_at(8);
469 let mut first_bundle_feedbacks = first_half.to_vec();
470 first_bundle_feedbacks.push(only_for_type_2.clone());
471 let bundle_first = make_bundle_with_encoded_post_feedbacks(TimeMillis(1000), &first_bundle_feedbacks).await?;
472 let bundle_second = make_bundle_with_encoded_post_feedbacks(TimeMillis(2000), second_half).await?;
473
474 let merged = EncodedPostBundleFeedbackV1::merge(&[bundle_first, bundle_second]).expect("non-empty input");
475
476 assert_eq!(expected_type_1_pow, merged.get_post_pow_for_feedback_type(&post_a, 1), "the highest pow for (post_a, type 1) must win");
477 assert_eq!(only_for_type_2.pow, merged.get_post_pow_for_feedback_type(&post_a, 2), "the only pow for (post_a, type 2) must be preserved");
478 merged_entries_all_verify(&merged)?;
479 Ok(())
480 }
481
482 #[tokio::test]
486 async fn merge_drops_entries_with_invalid_pow() -> anyhow::Result<()> {
487 let post_a = Id::random();
488 let post_b = Id::random();
489 let valid_for_post_a = make_valid_feedback(post_a, 1)?;
490
491 let poisoned_bundle = make_bundle_with_encoded_post_feedbacks(TimeMillis(1000), &[make_forged_feedback(post_a, 1), valid_for_post_a.clone(), make_forged_feedback(post_b, 2)]).await?;
492 let honest_bundle = make_bundle_with_encoded_post_feedbacks(TimeMillis(2000), &[]).await?;
493
494 let merged = EncodedPostBundleFeedbackV1::merge(&[poisoned_bundle, honest_bundle]).expect("non-empty input");
495
496 assert_eq!(valid_for_post_a.pow, merged.get_post_pow_for_feedback_type(&post_a, 1), "the valid entry must survive alongside a forged one");
497 assert_eq!(Pow(0), merged.get_post_pow_for_feedback_type(&post_b, 2), "a key with only a forged entry must be absent");
498 assert_eq!(merged.feedbacks_bytes.len(), ENTRY_SIZE, "only the one valid entry should remain");
499 merged_entries_all_verify(&merged)?;
500 Ok(())
501 }
502
503 #[tokio::test]
506 async fn merged_bundle_is_a_client_view_not_a_signed_bundle() -> anyhow::Result<()> {
507 let post_a = Id::random();
508 let valid_for_type_1 = make_valid_feedback(post_a, 1)?;
509 let valid_for_type_2 = make_valid_feedback(post_a, 2)?;
510
511 let bundle_first = make_bundle_with_encoded_post_feedbacks(TimeMillis(1000), &[valid_for_type_1.clone()]).await?;
512 let bundle_second = make_bundle_with_encoded_post_feedbacks(TimeMillis(2000), &[valid_for_type_2.clone()]).await?;
513 bundle_first.verify()?;
514 bundle_second.verify()?;
515
516 let merged = EncodedPostBundleFeedbackV1::merge(&[bundle_first, bundle_second]).expect("non-empty input");
517 assert_eq!(valid_for_type_1.pow, merged.get_post_pow_for_feedback_type(&post_a, 1));
518 assert_eq!(valid_for_type_2.pow, merged.get_post_pow_for_feedback_type(&post_a, 2));
519 assert!(merged.verify().is_err(), "the merged union is not covered by any originator's signature");
520
521 let poisoned_bundle = make_bundle_with_encoded_post_feedbacks(TimeMillis(3000), &[make_forged_feedback(post_a, 3), valid_for_type_1.clone()]).await?;
523 poisoned_bundle.header.verify()?;
524 assert!(poisoned_bundle.verify().is_err(), "a bundle with a forged entry must fail full verification");
525 let merged_from_poisoned = EncodedPostBundleFeedbackV1::merge(&[poisoned_bundle]).expect("non-empty input");
526 assert_eq!(valid_for_type_1.pow, merged_from_poisoned.get_post_pow_for_feedback_type(&post_a, 1));
527 assert_eq!(Pow(0), merged_from_poisoned.get_post_pow_for_feedback_type(&post_a, 3));
528 Ok(())
529 }
530
531 #[tokio::test]
533 async fn verify_for_location_rejects_wrong_location_and_unworked_peer() -> anyhow::Result<()> {
534 let location_id = Id::random();
535 let encoded_post_feedbacks = [make_valid_feedback(Id::random(), 1)?];
536
537 let genuine_server_id = ServerId::new("genuine", &RealTimeProvider, crate::tools::config::SERVER_KEY_POW_MIN, true, &SingleThreadedPowGenerator::new()).await?;
538 let genuine_bundle = make_bundle_signed_by(&genuine_server_id, location_id, TimeMillis(1000), &encoded_post_feedbacks)?;
539 genuine_bundle.verify_for_location(&location_id)?;
540 assert!(genuine_bundle.verify_for_location(&Id::random()).is_err(), "a bundle for another location must be rejected");
541
542 let unworked_server_id = loop {
543 let server_id = ServerId::new("unworked", &RealTimeProvider, Pow(0), true, &SingleThreadedPowGenerator::new()).await?;
544 if server_id.pow < crate::tools::config::SERVER_KEY_POW_MIN {
545 break server_id;
546 }
547 };
548 let unworked_bundle = make_bundle_signed_by(&unworked_server_id, location_id, TimeMillis(1000), &encoded_post_feedbacks)?;
549 unworked_bundle.verify()?;
550 assert!(unworked_bundle.verify_for_location(&location_id).is_err(), "a self-signed bundle from a pow-less identity must be rejected");
551 Ok(())
552 }
553
554 #[tokio::test]
555 async fn verify_origin_for_location_tolerates_bad_entries_but_not_bad_origin() -> anyhow::Result<()> {
556 let location_id = Id::random();
557 let genuine_server_id = ServerId::new("genuine", &RealTimeProvider, crate::tools::config::SERVER_KEY_POW_MIN, true, &SingleThreadedPowGenerator::new()).await?;
558 let poisoned_bundle = make_bundle_signed_by(&genuine_server_id, location_id, TimeMillis(1000), &[make_valid_feedback(Id::random(), 1)?, make_forged_feedback(Id::random(), 2)])?;
559 poisoned_bundle.verify_origin_for_location(&location_id)?;
560 assert!(poisoned_bundle.verify_for_location(&location_id).is_err(), "full verification still rejects the forged entry");
561 assert!(poisoned_bundle.verify_origin_for_location(&Id::random()).is_err(), "a bundle for another location must be rejected");
562
563 let mut tampered_bundle = poisoned_bundle.clone();
564 tampered_bundle.feedbacks_bytes = tampered_bundle.feedbacks_bytes.slice(0..ENTRY_SIZE);
565 assert!(tampered_bundle.verify_origin_for_location(&location_id).is_err(), "bytes not covered by the signed hash must be rejected");
566 Ok(())
567 }
568
569 fn merged_entries_all_verify(merged: &EncodedPostBundleFeedbackV1) -> anyhow::Result<()> {
570 for entry_bytes in merged.feedbacks_bytes.chunks_exact(ENTRY_SIZE) {
571 EncodedPostFeedbackV1::decode_from_bytes(entry_bytes)?.pow_verify()?;
572 }
573 Ok(())
574 }
575
576 #[tokio::test]
577 async fn encoded_post_bundle_header_v1_to_from_bytes_roundtrip() -> anyhow::Result<()> {
578 let time_provider = RealTimeProvider;
579 let pow_generator = SingleThreadedPowGenerator::new();
580 let server_id = ServerId::new("own_pow", &time_provider, Pow(0), true, &pow_generator).await?;
581 let peer = server_id.to_peer(&time_provider)?;
582 let feedbacks_bytes = Bytes::new();
583
584 let mut header = EncodedPostBundleFeedbackHeaderV1 {
585 time_millis: TimeMillis::random(),
586 location_id: Id::random(),
587 feedbacks_bytes_hash: hashing::hash(feedbacks_bytes.as_ref()),
588 peer,
589 signature: Signature::zero(),
590 };
591
592 header.signature_generate(&server_id.keys.signature_key);
593 header.verify()?;
594
595 let bundle = EncodedPostBundleFeedbackV1 { header, feedbacks_bytes };
596
597 let bytes1 = bundle.to_bytes()?;
598 let decoded = EncodedPostBundleFeedbackV1::from_bytes(bytes1.clone())?;
599
600 assert_eq!(bundle, decoded);
601
602 let bytes2 = decoded.to_bytes()?;
604 assert_eq!(bytes1, bytes2);
605
606 Ok(())
607 }
608}