1use crate::environment::environment::PostBundleMetadata;
24use crate::server::hashiverse_server::HashiverseServer;
25use crate::tools::tools::is_ssrf_protected_ip;
26use bytes::{Bytes, BytesMut};
27use hashiverse_lib::anyhow_assert_eq;
28use hashiverse_lib::protocol::payload::payload::{
29 AnnounceResponseV2, AnnounceV2, BootstrapResponseV1, CachePostBundleFeedbackResponseV1, CachePostBundleFeedbackV1, CachePostBundleResponseV1, CachePostBundleV1, ErrorResponseV1, FetchUrlPreviewResponseV1, FetchUrlPreviewV1,
30 GetPostBundleFeedbackResponseV1, GetPostBundleFeedbackV1, GetPostBundleResponseV1, GetPostBundleV1, HealPostBundleClaimResponseV1, HealPostBundleClaimTokenV1, HealPostBundleClaimV1, HealPostBundleCommitResponseV1, HealPostBundleCommitV1,
31 HealPostBundleFeedbackResponseV1, HealPostBundleFeedbackV1, PayloadRequestKind, PayloadResponseKind, PingResponseV1, PeerStatsRequestV1, PeerStatsResponseV1, SubmitPostClaimResponseV1, SubmitPostClaimTokenV1, SubmitPostClaimV1,
32 SubmitPostCommitResponseV1, SubmitPostCommitTokenV1, SubmitPostCommitV1, SubmitPostFeedbackResponseV1, SubmitPostFeedbackV1, TrendingHashtagV1, TrendingHashtagsFetchResponseV1, TrendingHashtagsFetchV1,
33};
34use hashiverse_lib::protocol::peer::{Peer, PeerPow};
35use hashiverse_lib::protocol::posting::amplification::get_minimum_post_pow;
36use hashiverse_lib::protocol::posting::encoded_post::EncodedPostV1;
37use hashiverse_lib::protocol::posting::encoded_post_bundle::{EncodedPostBundleHeaderV1, EncodedPostBundleV1};
38use hashiverse_lib::protocol::posting::encoded_post_bundle_feedback::{EncodedPostBundleFeedbackHeaderV1, EncodedPostBundleFeedbackV1};
39use hashiverse_lib::protocol::rpc::rpc_request::RpcRequestPacketRx;
40use hashiverse_lib::protocol::rpc::rpc_response::{RpcResponsePacketTx, RpcResponsePacketTxFlags};
41use hashiverse_lib::tools::buckets::{BucketLocation, BucketType, BUCKET_DURATIONS};
42use hashiverse_lib::tools::time::{TimeMillis, MILLIS_IN_SECOND};
43use hashiverse_lib::tools::types::{Id, Signature};
44use hashiverse_lib::tools::{hashing, url_preview};
45use hashiverse_lib::tools::{compression, config, json, signing, BytesGatherer};
46use hashiverse_lib::tools::tools::{self, LeadingAgreementBits};
47use hashiverse_lib::transport::transport::IncomingRequest;
48use futures::FutureExt;
49use log::{error, info, trace, warn};
50use std::collections::HashSet;
51use std::future::Future;
52use std::panic::AssertUnwindSafe;
53use std::sync::atomic::Ordering;
54
55use crate::server::stats::{environment_stats_subtree, kademlia_stats_subtree, request_counts_subtree, system_stats_subtree};
56use tokio::sync::mpsc;
57use tokio_util::sync::CancellationToken;
58
59const TRENDING_HASHTAGS_FALLBACK: &[&str] = &["hashiverse", "news"];
65
66fn normalise_hashtag(hashtag: &str) -> String {
69 let lowercased = hashtag.to_lowercase();
70 match lowercased.strip_prefix('#') {
71 Some(stripped) => stripped.to_string(),
72 None => lowercased,
73 }
74}
75
76fn is_plausible_heal_donor(donor_id: &Id, location_id: &Id, nearest_peer_ids: &[Id], required_count: usize) -> bool {
87 if nearest_peer_ids.len() < required_count {
88 return true;
89 }
90
91 let furthest_nearest_leading_agreement_bits = nearest_peer_ids
92 .iter()
93 .map(|nearest_peer_id| tools::leading_agreement_bits_xor(location_id.as_ref(), nearest_peer_id.as_ref()))
94 .min()
95 .unwrap_or(LeadingAgreementBits::MIN);
96 let donor_leading_agreement_bits = tools::leading_agreement_bits_xor(location_id.as_ref(), donor_id.as_ref());
97
98 donor_leading_agreement_bits >= furthest_nearest_leading_agreement_bits - config::HEAL_DONOR_LEADING_AGREEMENT_BITS_SLACK
99}
100
101fn top_up_trending_hashtags_with_fallback(trending_hashtags: &mut Vec<TrendingHashtagV1>, limit: u16, fallback_hashtags: &[&str]) {
102 let target_length = limit as usize;
103 if trending_hashtags.len() >= target_length {
104 return;
105 }
106
107 let mut existing_normalised_hashtags: HashSet<String> = trending_hashtags.iter()
108 .map(|entry| normalise_hashtag(&entry.hashtag))
109 .collect();
110
111 for fallback_hashtag in fallback_hashtags {
112 if trending_hashtags.len() >= target_length {
113 break;
114 }
115 let normalised_fallback_hashtag = normalise_hashtag(fallback_hashtag);
116 if existing_normalised_hashtags.contains(&normalised_fallback_hashtag) {
117 continue;
118 }
119 trending_hashtags.push(TrendingHashtagV1 {
120 hashtag: (*fallback_hashtag).to_string(),
121 count: 0,
122 });
123 existing_normalised_hashtags.insert(normalised_fallback_hashtag);
124 }
125}
126
127async fn resolve_url_preview_host<I>(host: &str, dns_timeout: std::time::Duration, lookup: impl Future<Output = std::io::Result<I>>) -> anyhow::Result<Vec<std::net::SocketAddr>>
130where
131 I: Iterator<Item = std::net::SocketAddr>,
132{
133 match tokio::time::timeout(dns_timeout, lookup).await {
134 Err(_) => anyhow::bail!("FetchUrlPreviewV1: DNS resolution timed out for {}", host),
135 Ok(Err(e)) => anyhow::bail!("FetchUrlPreviewV1 SSRF: DNS resolution failed for {}: {}", host, e),
136 Ok(Ok(socket_addrs)) => Ok(socket_addrs.collect()),
137 }
138}
139
140async fn catch_panic_as_error<T>(future: impl Future<Output = T>) -> anyhow::Result<T> {
143 match AssertUnwindSafe(future).catch_unwind().await {
144 Ok(value) => Ok(value),
145 Err(panic_payload) => {
146 let panic_message = if let Some(panic_message) = panic_payload.downcast_ref::<&str>() {
147 panic_message.to_string()
148 } else if let Some(panic_message) = panic_payload.downcast_ref::<String>() {
149 panic_message.clone()
150 } else {
151 "<non-string panic payload>".to_string()
152 };
153 Err(anyhow::anyhow!("panic: {}", panic_message))
154 }
155 }
156}
157
158fn complete_incoming_request(incoming: IncomingRequest, dispatch_outcome: anyhow::Result<anyhow::Result<BytesGatherer>>) {
162 match dispatch_outcome {
163 Ok(Ok(bytes)) => {
164 let result = incoming.reply.send(bytes);
165 if result.is_err() { warn!("failed to send reply"); }
166 },
167 Err(handler_panic) => {
168 error!("handler panicked processing packet from {}: {}", incoming.caller_address, handler_panic);
169 incoming.report_bad_request();
170 drop(incoming.reply);
171 },
172 Ok(Err(e)) => {
173 warn!("failed to process packet from {}: {}", incoming.caller_address, e);
174 incoming.report_bad_request();
175 drop(incoming.reply);
176 },
177 }
178}
179
180impl HashiverseServer {
181 pub async fn wrap_and_dispatch_network_envelopes(&self, cancellation_token: CancellationToken, mut rx: mpsc::Receiver<IncomingRequest>) -> Result<(), anyhow::Error> {
182 loop {
183 tokio::select! {
184 _ = cancellation_token.cancelled() => { break },
185
186 receipt = rx.recv() => {
187 match receipt {
188 Some(incoming) => {
189 let dispatch_outcome = catch_panic_as_error(self.wrap_and_dispatch_network_envelope(cancellation_token.clone(), &incoming)).await;
193 complete_incoming_request(incoming, dispatch_outcome);
194 },
195 None => {
196 warn!("channel closed");
197 break;
198 }
199 }
200 }
201 }
202 }
203
204 Ok(())
205 }
206
207 async fn wrap_and_dispatch_network_envelope(&self, cancellation_token: CancellationToken, incoming: &IncomingRequest) -> anyhow::Result<BytesGatherer> {
208 let caller_address = incoming.caller_address.as_str();
209 let current_time_millis = self.runtime_services.time_provider.current_time_millis();
210
211 let rpc_request_packet_rx = RpcRequestPacketRx::decode(¤t_time_millis, &self.server_id.keys.verification_key_bytes, &self.server_id.keys.pq_commitment_bytes, incoming.bytes.clone())?;
213 let payload_request_kind_index = rpc_request_packet_rx.payload_request_kind.clone() as usize;
219 self.request_counters[payload_request_kind_index].fetch_add(1, Ordering::Relaxed);
220 self.request_rate_windows[payload_request_kind_index].lock().record(current_time_millis);
223
224 {
226 if self.seen_salts.contains_key(&rpc_request_packet_rx.pow_salt) {
227 anyhow::bail!("replay detected: salt already seen");
228 }
229 self.seen_salts.insert(rpc_request_packet_rx.pow_salt, ());
230 }
231
232 let pow_content_hash = rpc_request_packet_rx.pow_content_hash;
234
235 let dispatch_result: anyhow::Result<BytesGatherer> = try {
236 let pow = match rpc_request_packet_rx.pow_server_known {
238 true => {
239 let (pow, improved_pow_current_day, improved_pow_current_month) = {
240 let peer_self = self.peer_self.read(); let pow = PeerPow::new(
242 rpc_request_packet_rx.pow_sponsor_id,
243 &peer_self.verification_key_bytes,
244 &peer_self.pq_commitment_bytes,
245 rpc_request_packet_rx.pow_timestamp,
246 rpc_request_packet_rx.pow_content_hash,
247 rpc_request_packet_rx.pow_salt,
248 )?;
249
250 let improved_pow_current_day = pow.pow_decayed_day(current_time_millis) > peer_self.pow_current_day.pow_decayed_day(current_time_millis);
251 let improved_pow_current_month = pow.pow_decayed_month(current_time_millis) > peer_self.pow_current_month.pow_decayed_month(current_time_millis);
252
253 (pow, improved_pow_current_day, improved_pow_current_month)
254 };
255
256 if improved_pow_current_day || improved_pow_current_month {
258 let mut peer_self = self.peer_self.write(); if improved_pow_current_day {
260 trace!("pow_current_day upgraded {} -> {}", peer_self.pow_current_day, pow);
261 peer_self.pow_current_day = pow.clone();
262 }
263 if improved_pow_current_month {
264 trace!("pow_current_month upgraded {} -> {}", peer_self.pow_current_month, pow);
265 peer_self.pow_current_month = pow.clone();
266 }
267
268 peer_self.sign(self.runtime_services.time_provider.as_ref(), &self.server_id.keys.signature_key)?;
269 }
270
271 Some(pow)
272 }
273
274 false => {
275 match rpc_request_packet_rx.payload_request_kind {
277 PayloadRequestKind::BootstrapV1 => {}
278 _ => anyhow::bail!("Anonymous pow not allowed for {}", rpc_request_packet_rx.payload_request_kind),
279 }
280
281 None
282 }
283 };
284
285 let (compress_response, payload_response_kind, payload) = self.dispatch_network_envelope(cancellation_token, pow, rpc_request_packet_rx).await?;
287 let response_flags = match compress_response {
288 true => RpcResponsePacketTxFlags::COMPRESSED,
289 false => RpcResponsePacketTxFlags::empty(),
290 };
291
292 RpcResponsePacketTx::encode(
294 &self.server_id.keys.signature_key,
295 &self.server_id.keys.verification_key_bytes,
296 &self.server_id.keys.pq_commitment_bytes,
297 &self.server_id.sponsor_id,
298 &self.server_id.timestamp,
299 &self.server_id.hash,
300 &self.server_id.salt,
301 &pow_content_hash,
302 response_flags,
303 payload_response_kind,
304 payload,
305 )?
306 };
307
308 match dispatch_result {
309 Ok(results) => Ok(results),
310 Err(e) => {
311 warn!("failed to dispatch packet from {}: {}", caller_address, e);
312 incoming.report_bad_request();
313
314 let payload_response_kind = PayloadResponseKind::ErrorResponseV1;
315 let response = ErrorResponseV1 { code: 0, message: e.to_string() };
316 let payload = BytesGatherer::from_bytes(json::struct_to_bytes(&response)?);
317
318 RpcResponsePacketTx::encode(
320 &self.server_id.keys.signature_key,
321 &self.server_id.keys.verification_key_bytes,
322 &self.server_id.keys.pq_commitment_bytes,
323 &self.server_id.sponsor_id,
324 &self.server_id.timestamp,
325 &self.server_id.hash,
326 &self.server_id.salt,
327 &pow_content_hash,
328 RpcResponsePacketTxFlags::COMPRESSED,
329 payload_response_kind,
330 payload,
331 )
332 }
333 }
334 }
335
336 async fn dispatch_network_envelope(&self, cancellation_token: CancellationToken, pow: Option<PeerPow>, rpc_request_packet_rx: RpcRequestPacketRx) -> anyhow::Result<(bool, PayloadResponseKind, BytesGatherer)> {
337 let compress_response = match rpc_request_packet_rx.payload_request_kind {
339 PayloadRequestKind::GetPostBundleV1 => false, PayloadRequestKind::CachePostBundleV1 => false, _ => true,
342 };
343
344 let (payload_response_kind, payload) = match rpc_request_packet_rx.payload_request_kind {
345 PayloadRequestKind::ErrorV1 => {
346 anyhow::bail!("Received ErrorV1");
347 }
348 PayloadRequestKind::PingV1 => self.dispatch_network_payload_x_PingV1(cancellation_token, rpc_request_packet_rx.payload_request_kind, rpc_request_packet_rx.bytes).await?,
349 PayloadRequestKind::BootstrapV1 => self.dispatch_network_payload_x_BootstrapV1(cancellation_token, rpc_request_packet_rx.payload_request_kind, rpc_request_packet_rx.bytes).await?,
350 PayloadRequestKind::RetiredAnnounceV1 => {
351 anyhow::bail!("AnnounceV1 is retired; use AnnounceV2");
352 }
353 PayloadRequestKind::GetPostBundleV1 => self.dispatch_network_payload_x_GetPostBundleV1(cancellation_token, rpc_request_packet_rx.payload_request_kind, rpc_request_packet_rx.bytes).await?,
354 PayloadRequestKind::GetPostBundleFeedbackV1 => { self.dispatch_network_payload_x_GetPostBundleFeedbackV1(cancellation_token, rpc_request_packet_rx.payload_request_kind, rpc_request_packet_rx.bytes).await? }
355 PayloadRequestKind::SubmitPostClaimV1 => { self.dispatch_network_payload_x_SubmitPostClaimV1(cancellation_token, pow, rpc_request_packet_rx.payload_request_kind, rpc_request_packet_rx.bytes).await? }
356 PayloadRequestKind::SubmitPostCommitV1 => { self.dispatch_network_payload_x_SubmitPostCommitV1(cancellation_token, rpc_request_packet_rx.payload_request_kind, rpc_request_packet_rx.bytes).await? }
357 PayloadRequestKind::SubmitPostFeedbackV1 => { self.dispatch_network_payload_x_SubmitPostFeedbackV1(cancellation_token, pow, rpc_request_packet_rx.payload_request_kind, rpc_request_packet_rx.bytes).await? }
358 PayloadRequestKind::HealPostBundleClaimV1 => { self.dispatch_network_payload_x_HealPostBundleClaimV1(cancellation_token, rpc_request_packet_rx.payload_request_kind, rpc_request_packet_rx.bytes).await? }
359 PayloadRequestKind::HealPostBundleCommitV1 => { self.dispatch_network_payload_x_HealPostBundleCommitV1(cancellation_token, rpc_request_packet_rx.payload_request_kind, rpc_request_packet_rx.bytes).await? }
360 PayloadRequestKind::HealPostBundleFeedbackV1 => { self.dispatch_network_payload_x_HealPostBundleFeedbackV1(cancellation_token, rpc_request_packet_rx.payload_request_kind, rpc_request_packet_rx.bytes).await? }
361 PayloadRequestKind::CachePostBundleV1 => self.dispatch_network_payload_x_CachePostBundleV1(cancellation_token, rpc_request_packet_rx.payload_request_kind, rpc_request_packet_rx.bytes).await?,
362 PayloadRequestKind::CachePostBundleFeedbackV1 => { self.dispatch_network_payload_x_CachePostBundleFeedbackV1(cancellation_token, rpc_request_packet_rx.payload_request_kind, rpc_request_packet_rx.bytes).await? }
363 PayloadRequestKind::FetchUrlPreviewV1 => self.dispatch_network_payload_x_FetchUrlPreviewV1(cancellation_token, pow, rpc_request_packet_rx.payload_request_kind, rpc_request_packet_rx.bytes).await?,
364 PayloadRequestKind::TrendingHashtagsFetchV1 => self.dispatch_network_payload_x_TrendingHashtagsFetchV1(cancellation_token, rpc_request_packet_rx.payload_request_kind, rpc_request_packet_rx.bytes).await?,
365 PayloadRequestKind::PeerStatsRequestV1 => self.dispatch_network_payload_x_PeerStatsRequestV1(cancellation_token, pow, rpc_request_packet_rx.payload_request_kind, rpc_request_packet_rx.bytes).await?,
366 PayloadRequestKind::AnnounceV2 => self.dispatch_network_payload_x_AnnounceV2(cancellation_token, rpc_request_packet_rx.payload_request_kind, rpc_request_packet_rx.bytes).await?,
367 };
368
369 Ok((compress_response, payload_response_kind, payload))
370 }
371
372 #[allow(non_snake_case)]
373 async fn dispatch_network_payload_x_PingV1(&self, _cancellation_token: CancellationToken, payload_request_kind: PayloadRequestKind, _bytes: Bytes) -> anyhow::Result<(PayloadResponseKind, BytesGatherer)> {
374 anyhow_assert_eq!(&PayloadRequestKind::PingV1, &payload_request_kind);
375 let peer = self.peer_self.read().clone();
376 let json = json::struct_to_bytes(&PingResponseV1 { peer })?;
377 Ok((PayloadResponseKind::PingResponseV1, BytesGatherer::from_bytes(json)))
378 }
379
380 #[allow(non_snake_case)]
381 async fn dispatch_network_payload_x_BootstrapV1(&self, _cancellation_token: CancellationToken, payload_request_kind: PayloadRequestKind, _bytes: Bytes) -> anyhow::Result<(PayloadResponseKind, BytesGatherer)> {
382 anyhow_assert_eq!(&PayloadRequestKind::BootstrapV1, &payload_request_kind);
383 let peers_random = self.kademlia.read().get_peers_random(config::BOOTSTRAP_V1_NUM_PEERS);
384 let json = json::struct_to_bytes(&BootstrapResponseV1 { peers_random })?;
385 Ok((PayloadResponseKind::BootstrapResponseV1, BytesGatherer::from_bytes(json)))
386 }
387
388 #[allow(non_snake_case)]
389 async fn dispatch_network_payload_x_AnnounceV2(&self, _cancellation_token: CancellationToken, payload_request_kind: PayloadRequestKind, bytes: Bytes) -> anyhow::Result<(PayloadResponseKind, BytesGatherer)> {
390 anyhow_assert_eq!(&PayloadRequestKind::AnnounceV2, &payload_request_kind);
391
392 let request = json::bytes_to_struct::<AnnounceV2>(&bytes)?;
393
394 let peer = request.peer_self;
395 let peer_id = peer.id;
396 let now = self.runtime_services.time_provider.as_ref().current_time_millis();
397
398 let ownership_proof = self.transport_server.get_transport_ownership_proof();
405 if !ownership_proof.prove(&peer, &request.proof_payload, now) {
406 anyhow::bail!("AnnounceV2 from peer {} at {} failed ownership proof", peer_id, peer.address);
407 }
408 self.add_potential_peer_to_kademlia(peer, now).await;
409
410 let (peers_nearest, _) = self.kademlia.read().get_peers_for_key(&peer_id, config::ANNOUNCE_NUM_PEERS);
411
412 let json = json::struct_to_bytes(&AnnounceResponseV2 {
413 peer_self: self.peer_self.read().clone(),
414 peers_nearest,
415 })?;
416 Ok((PayloadResponseKind::AnnounceResponseV2, BytesGatherer::from_bytes(json)))
417 }
418
419 #[allow(non_snake_case)]
420 async fn dispatch_network_payload_x_GetPostBundleV1(&self, _cancellation_token: CancellationToken, payload_request_kind: PayloadRequestKind, bytes: Bytes) -> anyhow::Result<(PayloadResponseKind, BytesGatherer)> {
421 anyhow_assert_eq!(&PayloadRequestKind::GetPostBundleV1, &payload_request_kind);
422
423 let time_millis = self.runtime_services.time_provider.as_ref().current_time_millis();
424
425 let request = json::bytes_to_struct::<GetPostBundleV1>(&bytes)?;
426 trace!("received GetPostBundleV1: bucket_location={}", request.bucket_location);
427
428 request.bucket_location.validate()?;
430
431 {
433 for peer in request.peers_visited {
434 self.add_potential_peer_to_kademlia(peer, time_millis).await;
435 }
436 }
437
438 let peer_self = self.peer_self.read().clone();
439
440 let (peers_nearer, among_peers_nearer) = self.kademlia.read().get_peers_for_key(&request.bucket_location.location_id, config::REDUNDANT_SERVERS_PER_POST);
442 if !among_peers_nearer {
443 warn!("I am not in peers_nearer {}", peer_self);
444 }
445
446 let post_bundle = match among_peers_nearer {
447 true => {
448 let _post_bundle_lock = self.environment.get_write_lock_for_location_id(&request.bucket_location.location_id);
451
452 let mut encoded_post_bundle_bytes: Option<Bytes> = None;
453
454 let post_bundle_metadata = self.environment.get_post_bundle_metadata(time_millis, &request.bucket_location.location_id)?;
456 if let Some(mut post_bundle_metadata) = post_bundle_metadata {
457 encoded_post_bundle_bytes = self.environment.get_post_bundle_bytes(time_millis, &request.bucket_location.location_id)?;
458
459 if !post_bundle_metadata.sealed {
461 let sealed = request.bucket_location.is_sealed_at(time_millis, false);
462 if sealed {
463 if let Some(encoded_post_bundle_bytes_old) = encoded_post_bundle_bytes {
465 let mut encoded_post_bundle = EncodedPostBundleV1::from_bytes(encoded_post_bundle_bytes_old, true)?;
466 encoded_post_bundle.header.time_millis = time_millis;
467 encoded_post_bundle.header.sealed = true;
468 encoded_post_bundle.header.signature_generate(&self.server_id.keys.signature_key)?;
469 let encoded_post_bundle_bytes_new = encoded_post_bundle.to_bytes()?;
470 self.environment.put_post_bundle_bytes(time_millis, &request.bucket_location.location_id, &encoded_post_bundle_bytes_new)?;
471 encoded_post_bundle_bytes = Some(encoded_post_bundle_bytes_new);
472 }
473
474 post_bundle_metadata.sealed = true;
476 self.environment.put_post_bundle_metadata(time_millis, &request.bucket_location.location_id, &post_bundle_metadata, 0)?;
477 }
478 else {
479 if let Some(encoded_post_bundle_bytes_old) = encoded_post_bundle_bytes {
481 let mut encoded_post_bundle = EncodedPostBundleV1::from_bytes(encoded_post_bundle_bytes_old, true)?;
482 encoded_post_bundle.header.time_millis = time_millis;
483 encoded_post_bundle.header.signature_generate(&self.server_id.keys.signature_key)?;
484 let encoded_post_bundle_bytes_new = encoded_post_bundle.to_bytes()?;
485 encoded_post_bundle_bytes = Some(encoded_post_bundle_bytes_new);
486 }
487 }
488 }
489 };
490
491 if encoded_post_bundle_bytes.is_none() {
494 let sealed = request.bucket_location.is_sealed_at(time_millis, false);
495
496 let mut header = EncodedPostBundleHeaderV1 {
497 time_millis,
498 location_id: request.bucket_location.location_id,
499 overflowed: false,
500 sealed,
501 num_posts: 0,
502 encoded_post_ids: vec![],
503 encoded_post_lengths: vec![],
504 encoded_post_healed: HashSet::new(),
505 peer: peer_self.clone(),
506 signature: Signature::zero(),
507 };
508 header.signature_generate(&self.server_id.keys.signature_key)?;
509
510 let encoded_post_bundle = EncodedPostBundleV1 { header, encoded_posts_bytes: Bytes::new() };
511 encoded_post_bundle_bytes = Some(encoded_post_bundle.to_bytes()?);
512 }
513
514 encoded_post_bundle_bytes
515 }
516 false => None,
517 };
518
519 let cache_result = self.post_bundle_cache.on_get(&request.bucket_location, &request.already_retrieved_peer_ids, &peer_self, &self.server_id, time_millis);
520
521 let get_post_bundle_response = GetPostBundleResponseV1 {
522 peers_nearer,
523 cache_request_token: cache_result.cache_request_token,
524 post_bundles_cached: cache_result.cached_items,
525 post_bundle,
526 };
527 Ok((PayloadResponseKind::GetPostBundleResponseV1, get_post_bundle_response.to_bytes_gatherer()?))
528 }
529
530 #[allow(non_snake_case)]
531 async fn dispatch_network_payload_x_GetPostBundleFeedbackV1(&self, _cancellation_token: CancellationToken, payload_request_kind: PayloadRequestKind, bytes: Bytes) -> anyhow::Result<(PayloadResponseKind, BytesGatherer)> {
532 anyhow_assert_eq!(&PayloadRequestKind::GetPostBundleFeedbackV1, &payload_request_kind);
533
534 let time_millis = self.runtime_services.time_provider.as_ref().current_time_millis();
535
536 let request = json::bytes_to_struct::<GetPostBundleFeedbackV1>(&bytes)?;
537 trace!("received GetPostBundleFeedbackV1");
538
539 {
541 for peer in request.peers_visited {
542 self.add_potential_peer_to_kademlia(peer, time_millis).await;
543 }
544 }
545
546 let (peers_nearer, among_peers_nearer) = self.kademlia.read().get_peers_for_key(&request.bucket_location.location_id, config::REDUNDANT_SERVERS_PER_POST);
548
549 let mut post_bundle_encoded_feedbacks_bytes: Option<Bytes> = None;
550
551 if among_peers_nearer {
552 let post_bundle_metadata = self.environment.get_post_bundle_metadata(time_millis, &request.bucket_location.location_id)?;
554 if post_bundle_metadata.is_some() {
555 post_bundle_encoded_feedbacks_bytes = Some(self.environment.get_post_bundle_encoded_post_feedbacks_bytes(time_millis, &request.bucket_location.location_id)?);
556 }
557 }
558
559 let peer_self = self.peer_self.read().clone();
561 let encoded_post_bundle_feedback = match post_bundle_encoded_feedbacks_bytes {
562 Some(feedbacks_bytes) => {
563
564 let feedbacks_bytes_hash = hashing::hash(feedbacks_bytes.as_ref());
565
566 let mut header = EncodedPostBundleFeedbackHeaderV1 {
567 time_millis,
568 location_id: request.bucket_location.location_id,
569 feedbacks_bytes_hash,
570 peer: peer_self.clone(),
571 signature: Signature::zero(),
572 };
573 header.signature_generate(&self.server_id.keys.signature_key);
574
575 let encoded_post_bundle_feedback = EncodedPostBundleFeedbackV1 {
576 header,
577 feedbacks_bytes,
578 };
579 Some(encoded_post_bundle_feedback.to_bytes()?)
580 }
581 None => None,
582 };
583
584 let cache_result = self.post_bundle_feedback_cache.on_get(&request.bucket_location, &request.already_retrieved_peer_ids, &peer_self, &self.server_id, time_millis);
585
586 let get_post_bundle_feedback_response = GetPostBundleFeedbackResponseV1 {
587 peers_nearer,
588 cache_request_token: cache_result.cache_request_token,
589 post_bundle_feedbacks_cached: cache_result.cached_items,
590 encoded_post_bundle_feedback,
591 };
592 Ok((PayloadResponseKind::GetPostBundleFeedbackResponseV1, get_post_bundle_feedback_response.to_bytes_gatherer()?))
593 }
594
595 #[allow(non_snake_case)]
596 async fn dispatch_network_payload_x_SubmitPostClaimV1(&self, _cancellation_token: CancellationToken, pow: Option<PeerPow>, payload_request_kind: PayloadRequestKind, mut bytes: Bytes) -> anyhow::Result<(PayloadResponseKind, BytesGatherer)> {
597 anyhow_assert_eq!(&PayloadRequestKind::SubmitPostClaimV1, &payload_request_kind);
598
599 let time_millis = self.runtime_services.time_provider.as_ref().current_time_millis();
600
601 let pow = match pow {
602 Some(pow) => pow,
603 None => anyhow::bail!("We need pow for a submit post claim"),
604 };
605
606 let request = SubmitPostClaimV1::from_bytes(&mut bytes)?;
607 trace!("received SubmitPostClaimV1");
608
609 request.bucket_location.validate()?;
611
612 let bucket_duration = {
614 let bucket_duration = BUCKET_DURATIONS.iter().find(|bucket_duration| **bucket_duration == request.bucket_location.duration);
615 match bucket_duration {
616 Some(bucket_duration) => *bucket_duration,
617 None => anyhow::bail!("Unrecognised bucket duration provided"),
618 }
619 };
620
621 let decoded_post = EncodedPostV1::decode_from_bytes(request.encoded_post_bytes, &request.bucket_location.base_id, false, false)?;
622
623 {
625 let pow_minimum = get_minimum_post_pow(decoded_post.header.post_length, decoded_post.header.linked_base_ids.len(), request.bucket_location.duration)?;
626 if pow.pow < pow_minimum {
627 anyhow::bail!("Insufficient proof of work for this post: actual={} < expected={}", pow.pow, pow_minimum);
628 }
629 }
630
631 {
633 let timestamp = BucketLocation::round_down_to_bucket_start(decoded_post.header.time_millis, bucket_duration);
635 if timestamp != request.bucket_location.bucket_time_millis {
636 anyhow::bail!("The post timestamp does not match the bucket");
637 }
638 }
639
640 let client_id = decoded_post.header.client_id()?;
641
642 if !decoded_post.header.linked_base_ids.contains(&request.bucket_location.base_id) {
644 anyhow::bail!("The base_id is not related to the post");
645 }
646
647 if request.bucket_location.bucket_type == BucketType::User && request.bucket_location.base_id != client_id.id {
649 anyhow::bail!("Only the posting user is allowed to post to a bucket of type USER");
650 }
651
652 if matches!(request.bucket_location.bucket_type, BucketType::ReplyToPost | BucketType::Sequel) {
655 let original_header_bytes = request.referenced_post_header_bytes
656 .ok_or_else(|| anyhow::anyhow!("{:?} posts require the original post's header bytes", request.bucket_location.bucket_type))?;
657
658 let original_post = EncodedPostV1::decode_from_bytes(original_header_bytes, &client_id.id, false, false)?;
661
662 if original_post.post_id != request.bucket_location.base_id {
664 anyhow::bail!("Referenced post header's post_id does not match the bucket's base_id");
665 }
666
667 if request.bucket_location.bucket_type == BucketType::Sequel {
669 let original_client_id = original_post.header.client_id()?;
670 if original_client_id != client_id {
671 anyhow::bail!("Sequel post author does not match original post author");
672 }
673 }
674 }
675
676 {
678 let delta = (time_millis - decoded_post.header.time_millis).abs();
679 if delta > config::CLIENT_POST_TIMESTAMP_DELTA_THRESHOLD {
680 anyhow::bail!("The post timestamp delta is too large ({} > {})", delta, config::CLIENT_POST_TIMESTAMP_DELTA_THRESHOLD);
681 }
682 }
683
684 let (peers_nearer, among_peers_nearer) = self.kademlia.read().get_peers_for_key(&request.bucket_location.location_id, config::REDUNDANT_SERVERS_PER_POST);
686
687 let submit_post_claim_token = match among_peers_nearer {
688 true => {
689 let _post_bundle_lock = self.environment.get_write_lock_for_location_id(&request.bucket_location.location_id);
691 let post_bundle_metadata = self.environment.get_post_bundle_metadata(time_millis, &request.bucket_location.location_id)?;
692 let mut post_bundle_metadata = post_bundle_metadata.unwrap_or_else(PostBundleMetadata::zero);
693
694 if !post_bundle_metadata.sealed {
696 post_bundle_metadata.num_posts_granted += 1;
697 post_bundle_metadata.overflowed = post_bundle_metadata.num_posts > config::ENCODED_POST_BUNDLE_V1_OVERFLOWED_NUM_POSTS || post_bundle_metadata.num_posts_granted > config::ENCODED_POST_BUNDLE_V1_OVERFLOWED_NUM_POSTS_GRANTED;
698 post_bundle_metadata.sealed = request.bucket_location.is_sealed_at(time_millis, post_bundle_metadata.overflowed);
699
700 self.environment.put_post_bundle_metadata(time_millis, &request.bucket_location.location_id, &post_bundle_metadata, 0)?;
701 }
702
703 match post_bundle_metadata.sealed {
705 false => {
706 info!("Granted for {}: num_posts={} num_posts_granted={}", request.bucket_location, post_bundle_metadata.num_posts, post_bundle_metadata.num_posts_granted);
707 Some(SubmitPostClaimTokenV1::new(self.peer_self.read().clone(), request.bucket_location.clone(), decoded_post.post_id, &self.server_id.keys.signature_key))
708 }
709 true => {
710 info!(
711 "Not granting SubmitPostClaimTokenV1 to {} as we have num_posts={} num_posts_granted={}",
712 request.bucket_location, post_bundle_metadata.num_posts, post_bundle_metadata.num_posts_granted
713 );
714 None
715 }
716 }
717 }
718
719 false => None,
720 };
721
722 if submit_post_claim_token.is_some() {
724 if request.bucket_location.bucket_type == BucketType::User && !request.referenced_hashtags.is_empty() {
726 let author_verification_key_bytes = &decoded_post.header.verification_key_bytes;
727 for referenced_hashtag in &request.referenced_hashtags {
728 let hashtag_id = match Id::from_hashtag_str(referenced_hashtag) {
729 Ok(id) => id,
730 Err(_) => continue, };
732 if !decoded_post.header.linked_base_ids.contains(&hashtag_id) {
733 continue; }
735 let mut hll = self.trending_hashtags.get(referenced_hashtag).unwrap_or_default();
736 hll.insert(author_verification_key_bytes.as_ref());
737 self.trending_hashtags.insert(referenced_hashtag.clone(), hll);
738 }
739 }
740 }
741
742 let response = SubmitPostClaimResponseV1 { peers_nearer, submit_post_claim_token };
743 Ok((PayloadResponseKind::SubmitPostClaimResponseV1, BytesGatherer::from_bytes(json::struct_to_bytes(&response)?)))
744 }
745
746 #[allow(non_snake_case)]
747 async fn dispatch_network_payload_x_SubmitPostCommitV1(&self, _cancellation_token: CancellationToken, payload_request_kind: PayloadRequestKind, mut bytes: Bytes) -> anyhow::Result<(PayloadResponseKind, BytesGatherer)> {
748 anyhow_assert_eq!(&PayloadRequestKind::SubmitPostCommitV1, &payload_request_kind);
749
750 let time_millis = self.runtime_services.time_provider.as_ref().current_time_millis();
751
752 let request = SubmitPostCommitV1::from_bytes(&mut bytes)?;
753 trace!("received SubmitPostCommitV1");
754
755 let peer_self = self.peer_self.read(); if request.submit_post_claim_token.peer.id != peer_self.id {
759 anyhow::bail!("The submit_post_claim_token is not from us");
760 }
761
762 request.bucket_location.validate()?;
764 if request.bucket_location != request.submit_post_claim_token.bucket_location {
765 anyhow::bail!("The location_id in the SubmitPostCommit does not match the bucket_location in the SubmitPostClaimToken");
766 }
767
768 let decoded_post = EncodedPostV1::decode_from_bytes(request.encoded_post_bytes.clone(), &request.bucket_location.base_id, true, false)?;
770
771 if decoded_post.post_id != request.submit_post_claim_token.post_id {
773 anyhow::bail!("The post_id of the committed post does not match the post_id in the SubmitPostClaimToken");
774 }
775
776 let _post_bundle_lock = self.environment.get_write_lock_for_location_id(&request.bucket_location.location_id);
778 let post_bundle_metadata = self.environment.get_post_bundle_metadata(time_millis, &request.bucket_location.location_id)?;
779 let post_bundle_bytes = self.environment.get_post_bundle_bytes(time_millis, &request.bucket_location.location_id)?;
780
781 let mut post_bundle_metadata = post_bundle_metadata.unwrap_or_else(PostBundleMetadata::zero);
783
784 let mut post_bundle = match post_bundle_bytes {
786 Some(bytes) => {
787 let bytes = Bytes::from_owner(bytes);
788
789 EncodedPostBundleV1::from_bytes(bytes, true)?
790 }
791 None => {
792 let header = EncodedPostBundleHeaderV1 {
793 time_millis: TimeMillis::zero(),
794 location_id: request.bucket_location.location_id,
795 overflowed: false,
796 sealed: false,
797 num_posts: 0,
798 encoded_post_ids: vec![],
799 encoded_post_lengths: vec![],
800 encoded_post_healed: HashSet::new(),
801 peer: self.peer_self.read().clone(),
802 signature: Signature::zero(),
803 };
804
805 EncodedPostBundleV1 { header, encoded_posts_bytes: Bytes::new() }
806 }
807 };
808
809 if post_bundle.header.encoded_post_ids.contains(&decoded_post.post_id) {
811 anyhow::bail!("Post {} is already in the bundle", decoded_post.post_id);
812 }
813
814 post_bundle.header.time_millis = time_millis;
816 post_bundle.header.num_posts += 1;
817 post_bundle.header.overflowed = post_bundle.header.num_posts > config::ENCODED_POST_BUNDLE_V1_OVERFLOWED_NUM_POSTS || post_bundle_metadata.num_posts_granted > config::ENCODED_POST_BUNDLE_V1_OVERFLOWED_NUM_POSTS_GRANTED;
818 post_bundle.header.sealed = request.bucket_location.is_sealed_at(time_millis, post_bundle.header.overflowed);
819 post_bundle.header.encoded_post_ids.push(decoded_post.post_id);
820 post_bundle.header.encoded_post_lengths.push(request.encoded_post_bytes.len());
821 post_bundle.header.signature_generate(&self.server_id.keys.signature_key)?;
822 let mut posts_mut = BytesMut::from(post_bundle.encoded_posts_bytes.as_ref());
823 posts_mut.extend_from_slice(request.encoded_post_bytes.as_ref());
824 post_bundle.encoded_posts_bytes = posts_mut.freeze();
825 let post_bundle_bytes_new = post_bundle.to_bytes()?;
826
827 post_bundle_metadata.num_posts = post_bundle.header.num_posts;
829 post_bundle_metadata.overflowed = post_bundle.header.overflowed;
830 post_bundle_metadata.sealed = post_bundle.header.sealed;
831 post_bundle_metadata.size = post_bundle.header.encoded_post_lengths.iter().sum();
832
833 {
834 self.environment.put_post_bundle_bytes(time_millis, &request.bucket_location.location_id, &post_bundle_bytes_new)?;
835 self.environment
836 .put_post_bundle_metadata(time_millis, &request.bucket_location.location_id, &post_bundle_metadata, request.encoded_post_bytes.len())?;
837 }
838
839 info!("Persisted for {}: num_posts={} num_posts_granted={}", request.bucket_location, post_bundle_metadata.num_posts, post_bundle_metadata.num_posts_granted);
840
841 let submit_post_commit_token = SubmitPostCommitTokenV1::new(peer_self.clone(), request.bucket_location, decoded_post.post_id, &self.server_id.keys.signature_key);
842
843 let response = SubmitPostCommitResponseV1 { submit_post_commit_token };
844 Ok((PayloadResponseKind::SubmitPostCommitResponseV1, BytesGatherer::from_bytes(json::struct_to_bytes(&response)?)))
845 }
846
847 #[allow(non_snake_case)]
848 async fn dispatch_network_payload_x_SubmitPostFeedbackV1(&self, _cancellation_token: CancellationToken, pow: Option<PeerPow>, payload_request_kind: PayloadRequestKind, mut bytes: Bytes) -> anyhow::Result<(PayloadResponseKind, BytesGatherer)> {
849 anyhow_assert_eq!(&PayloadRequestKind::SubmitPostFeedbackV1, &payload_request_kind);
850
851 let time_millis = self.runtime_services.time_provider.as_ref().current_time_millis();
852
853 let request = SubmitPostFeedbackV1::from_bytes(&mut bytes)?;
854 trace!("received SubmitPostFeedbackV1");
855
856 let pow = pow.ok_or_else(|| anyhow::anyhow!("pow required for SubmitPostFeedbackV1"))?;
858 if pow.pow < config::POW_MINIMUM_PER_FEEDBACK {
859 anyhow::bail!("Insufficient pow for feedback: {} < {}", pow.pow, config::POW_MINIMUM_PER_FEEDBACK);
860 }
861
862 request.encoded_post_feedback.pow_verify()?;
864
865 let location_id = request.bucket_location.location_id;
866
867 let (peers_nearer, among_peers_nearer) = self.kademlia.read().get_peers_for_key(&location_id, config::REDUNDANT_SERVERS_PER_POST);
869
870 let accepted = (|| -> anyhow::Result<bool> {
871 if !among_peers_nearer {
872 return Ok(false);
873 }
874
875 let _post_bundle_lock = self.environment.get_read_lock_for_location_id(&location_id);
877 let Some(post_bundle_bytes) = self.environment.get_post_bundle_bytes(time_millis, &location_id)?
878 else {
879 return Ok(false);
880 };
881
882 let post_bundle = EncodedPostBundleV1::from_bytes(Bytes::from_owner(post_bundle_bytes), false)?;
883 if !post_bundle.header.encoded_post_ids.contains(&request.encoded_post_feedback.post_id) {
884 return Ok(false);
885 }
886
887 Ok(true)
888 })()?;
889
890 if accepted {
892 trace!("Accepted post feedback for location_id={} encoded_post_feedback={:?}", location_id, request.encoded_post_feedback);
893 self.environment.put_post_feedback_if_more_powerful(time_millis, &location_id, &request.encoded_post_feedback)?;
894 }
895
896 let response = SubmitPostFeedbackResponseV1 { peers_nearer, accepted };
897 Ok((PayloadResponseKind::SubmitPostFeedbackResponseV1, BytesGatherer::from_bytes(json::struct_to_bytes(&response)?)))
898 }
899
900 fn verify_heal_donor_header(&self, donor_header: &EncodedPostBundleHeaderV1, time_millis: TimeMillis) -> anyhow::Result<()> {
904 donor_header.verify()?;
905 donor_header.peer.verify().map_err(|e| anyhow::anyhow!("heal donor peer {} failed verification: {}", donor_header.peer.id, e))?;
906
907 if donor_header.time_millis > time_millis + config::CLIENT_POST_TIMESTAMP_DELTA_THRESHOLD {
909 anyhow::bail!("heal donor header time_millis {} is in the future (now {})", donor_header.time_millis, time_millis);
910 }
911
912 let (nearest_peers, _) = self.kademlia.read().get_peers_for_key(&donor_header.location_id, config::REDUNDANT_SERVERS_PER_POST);
913 let nearest_peer_ids: Vec<Id> = nearest_peers.iter().map(|peer| peer.id).collect();
914 if !is_plausible_heal_donor(&donor_header.peer.id, &donor_header.location_id, &nearest_peer_ids, config::REDUNDANT_SERVERS_PER_POST) {
915 anyhow::bail!("heal donor peer {} is too far from location_id {}", donor_header.peer.id, donor_header.location_id);
916 }
917
918 Ok(())
919 }
920
921 #[allow(non_snake_case)]
922 async fn dispatch_network_payload_x_HealPostBundleClaimV1(&self, _cancellation_token: CancellationToken, payload_request_kind: PayloadRequestKind, mut bytes: Bytes) -> anyhow::Result<(PayloadResponseKind, BytesGatherer)> {
923 anyhow_assert_eq!(&PayloadRequestKind::HealPostBundleClaimV1, &payload_request_kind);
924
925 fn generate_negatory_response() -> anyhow::Result<(PayloadResponseKind, BytesGatherer)> {
926 let response = HealPostBundleClaimResponseV1 { needed_post_ids: vec![], token: None };
927 Ok((PayloadResponseKind::HealPostBundleClaimResponseV1, BytesGatherer::from_bytes(json::struct_to_bytes(&response)?)))
928 }
929
930 let time_millis = self.runtime_services.time_provider.as_ref().current_time_millis();
931 let request = HealPostBundleClaimV1::from_bytes(&mut bytes)?;
932 trace!("received HealPostBundleClaimV1");
933
934 request.bucket_location.validate()?;
936 if request.bucket_location.location_id != request.donor_header.location_id {
937 anyhow::bail!("HealPostBundleClaimV1: bucket_location.location_id does not match donor_header.location_id");
938 }
939
940 self.verify_heal_donor_header(&request.donor_header, time_millis)?;
942
943 let (_, among_peers_nearer) = self.kademlia.read().get_peers_for_key(&request.donor_header.location_id, config::REDUNDANT_SERVERS_PER_POST);
945 if !among_peers_nearer {
946 return generate_negatory_response();
947 }
948
949 if self.heal_in_progress.contains_key(&request.donor_header.location_id) {
951 return generate_negatory_response();
952 }
953
954 let _lock = self.environment.get_read_lock_for_location_id(&request.donor_header.location_id);
956 let post_bundle_bytes = self.environment.get_post_bundle_bytes(time_millis, &request.donor_header.location_id)?;
957
958 let our_post_ids: HashSet<Id> = match post_bundle_bytes {
959 Some(bytes) => {
960 let bundle = EncodedPostBundleV1::from_bytes(Bytes::from_owner(bytes), false)?;
961 bundle.header.encoded_post_ids.into_iter().collect()
962 }
963 None => HashSet::new(),
964 };
965
966 let needed_post_ids: Vec<Id> = request.donor_header.encoded_post_ids.iter().filter(|id| !our_post_ids.contains(*id)).copied().collect();
968
969 if needed_post_ids.is_empty() {
970 return generate_negatory_response();
971 }
972
973 self.heal_in_progress.insert(request.donor_header.location_id, ());
974
975 let token = Some(HealPostBundleClaimTokenV1::new(
976 self.peer_self.read().clone(),
977 request.bucket_location,
978 needed_post_ids.clone(),
979 request.donor_header.signature,
980 &self.server_id.keys.signature_key,
981 ));
982 let response = HealPostBundleClaimResponseV1 { needed_post_ids, token };
983 Ok((PayloadResponseKind::HealPostBundleClaimResponseV1, BytesGatherer::from_bytes(json::struct_to_bytes(&response)?)))
984 }
985
986 #[allow(non_snake_case)]
987 async fn dispatch_network_payload_x_HealPostBundleCommitV1(&self, _cancellation_token: CancellationToken, payload_request_kind: PayloadRequestKind, mut bytes: Bytes) -> anyhow::Result<(PayloadResponseKind, BytesGatherer)> {
988 anyhow_assert_eq!(&PayloadRequestKind::HealPostBundleCommitV1, &payload_request_kind);
989
990 let time_millis = self.runtime_services.time_provider.as_ref().current_time_millis();
991 let request = HealPostBundleCommitV1::from_bytes(&mut bytes)?;
992 trace!("received HealPostBundleCommitV1");
993
994 let peer_self = self.peer_self.read().clone();
996 if request.token.peer.id != peer_self.id {
997 anyhow::bail!("HealPostBundleCommitV1: token was not issued by this server");
998 }
999 request.token.verify()?;
1000
1001 let token_location_id = request.token.bucket_location.location_id;
1006 let commit_result = self.commit_heal_post_bundle(&request, &peer_self, time_millis);
1007 self.heal_in_progress.invalidate(&token_location_id);
1008 let added_any = commit_result?;
1009
1010 let response = HealPostBundleCommitResponseV1 { accepted: added_any };
1011 Ok((PayloadResponseKind::HealPostBundleCommitResponseV1, BytesGatherer::from_bytes(json::struct_to_bytes(&response)?)))
1012 }
1013
1014 fn commit_heal_post_bundle(&self, request: &HealPostBundleCommitV1, peer_self: &Peer, time_millis: TimeMillis) -> anyhow::Result<bool> {
1016 if request.donor_header.signature != request.token.donor_header_signature {
1018 anyhow::bail!("HealPostBundleCommitV1: donor_header signature does not match token");
1019 }
1020 self.verify_heal_donor_header(&request.donor_header, time_millis)?;
1021
1022 if request.token.bucket_location.location_id != request.donor_header.location_id {
1023 anyhow::bail!("HealPostBundleCommitV1: token location_id does not match donor_header");
1024 }
1025
1026 let location_id = request.donor_header.location_id;
1027
1028 let mut remaining_bytes = request.encoded_posts_bytes.clone();
1030 let mut posts_to_add: Vec<(Id, Bytes)> = Vec::new();
1031 for post_id in &request.token.needed_post_ids {
1032 let len = request
1033 .donor_header
1034 .encoded_post_ids
1035 .iter()
1036 .zip(request.donor_header.encoded_post_lengths.iter())
1037 .find(|(id, _)| *id == post_id)
1038 .map(|(_, len)| *len)
1039 .ok_or_else(|| anyhow::anyhow!("needed_post_id {} not found in donor_header", post_id))?;
1040 if remaining_bytes.len() < len {
1041 anyhow::bail!("HealPostBundleCommitV1: not enough bytes for post {}", post_id);
1042 }
1043 let post_bytes = remaining_bytes.split_to(len);
1044 posts_to_add.push((*post_id, post_bytes));
1045 }
1046 if !remaining_bytes.is_empty() {
1047 anyhow::bail!("HealPostBundleCommitV1: {} excess bytes", remaining_bytes.len());
1048 }
1049
1050 let admission_reference_time_millis = std::cmp::min(request.donor_header.time_millis, time_millis);
1055 for (post_id, post_bytes) in &posts_to_add {
1056 let decoded_post = EncodedPostV1::decode_from_bytes(post_bytes.clone(), &request.token.bucket_location.base_id, true, true)
1057 .map_err(|e| anyhow::anyhow!("HealPostBundleCommitV1: post {} failed decryption: {}", post_id, e))?;
1058 if decoded_post.post_id != *post_id {
1059 anyhow::bail!("HealPostBundleCommitV1: post bytes are for {} not {}", decoded_post.post_id, post_id);
1060 }
1061 decoded_post
1062 .check_admissible_to_bucket(&request.token.bucket_location, admission_reference_time_millis)
1063 .map_err(|e| anyhow::anyhow!("HealPostBundleCommitV1: {}", e))?;
1064 }
1065
1066 let _lock = self.environment.get_write_lock_for_location_id(&location_id);
1068 let post_bundle_bytes = self.environment.get_post_bundle_bytes(time_millis, &location_id)?;
1069 let post_bundle_metadata = self.environment.get_post_bundle_metadata(time_millis, &location_id)?;
1070 let mut post_bundle_metadata = post_bundle_metadata.unwrap_or_else(PostBundleMetadata::zero);
1071
1072 let mut post_bundle = match post_bundle_bytes {
1073 Some(b) => EncodedPostBundleV1::from_bytes(Bytes::from_owner(b), true)?,
1074 None => EncodedPostBundleV1 {
1075 header: EncodedPostBundleHeaderV1 {
1076 time_millis: TimeMillis::zero(),
1077 location_id,
1078 overflowed: false, sealed: false,
1080 num_posts: 0,
1081 encoded_post_ids: vec![],
1082 encoded_post_lengths: vec![],
1083 encoded_post_healed: HashSet::new(),
1084 peer: peer_self.clone(),
1085 signature: Signature::zero(),
1086 },
1087 encoded_posts_bytes: Bytes::new(),
1088 },
1089 };
1090
1091 let our_post_ids: HashSet<Id> = post_bundle.header.encoded_post_ids.iter().copied().collect();
1092
1093 let mut posts_mut = BytesMut::from(post_bundle.encoded_posts_bytes.as_ref());
1094 let mut added_any = false;
1095 for (post_id, post_bytes) in posts_to_add {
1096 if post_bundle.header.encoded_post_ids.len() >= u8::MAX as usize {
1097 warn!("HealPostBundleCommitV1: bundle for location_id={} is full so not healing any more posts into it", location_id);
1098 break;
1099 }
1100 if !our_post_ids.contains(&post_id) {
1101 let len = post_bytes.len();
1102 posts_mut.extend_from_slice(&post_bytes);
1103 post_bundle.header.encoded_post_ids.push(post_id);
1104 post_bundle.header.encoded_post_lengths.push(len);
1105 post_bundle.header.encoded_post_healed.insert(post_id);
1106 added_any = true;
1107 }
1108 }
1109 post_bundle.encoded_posts_bytes = posts_mut.freeze();
1110
1111 if added_any {
1112 post_bundle.header.time_millis = time_millis;
1113 post_bundle.header.num_posts = post_bundle.header.encoded_post_ids.len() as u8;
1114 post_bundle.header.overflowed = post_bundle.header.num_posts > config::ENCODED_POST_BUNDLE_V1_OVERFLOWED_NUM_POSTS || post_bundle_metadata.num_posts_granted > config::ENCODED_POST_BUNDLE_V1_OVERFLOWED_NUM_POSTS_GRANTED;
1115 post_bundle.header.sealed = request.token.bucket_location.is_sealed_at(time_millis, post_bundle.header.overflowed);
1116 post_bundle.header.signature_generate(&self.server_id.keys.signature_key)?;
1117
1118 let new_bytes = post_bundle.to_bytes()?;
1119 post_bundle_metadata.num_posts = post_bundle.header.num_posts;
1120 post_bundle_metadata.size = post_bundle.header.encoded_post_lengths.iter().sum();
1121 post_bundle_metadata.overflowed = post_bundle.header.overflowed;
1122 post_bundle_metadata.sealed = post_bundle.header.sealed;
1123
1124 self.environment.put_post_bundle_bytes(time_millis, &location_id, &new_bytes)?;
1125 self.environment.put_post_bundle_metadata(time_millis, &location_id, &post_bundle_metadata, 0)?;
1126
1127 info!("Healed {} post(s) for location_id={}", post_bundle.header.encoded_post_healed.len(), location_id);
1128 }
1129
1130 Ok(added_any)
1131 }
1132
1133 #[allow(non_snake_case)]
1134 async fn dispatch_network_payload_x_HealPostBundleFeedbackV1(&self, _cancellation_token: CancellationToken, payload_request_kind: PayloadRequestKind, mut bytes: Bytes) -> anyhow::Result<(PayloadResponseKind, BytesGatherer)> {
1135 anyhow_assert_eq!(&PayloadRequestKind::HealPostBundleFeedbackV1, &payload_request_kind);
1136
1137 let time_millis = self.runtime_services.time_provider.as_ref().current_time_millis();
1138 let request = HealPostBundleFeedbackV1::from_bytes(&mut bytes)?;
1139 trace!("received HealPostBundleFeedbackV1 for location_id={}", request.location_id);
1140
1141 let (_, among_peers_nearer) = self.kademlia.read().get_peers_for_key(&request.location_id, config::REDUNDANT_SERVERS_PER_POST);
1143 if !among_peers_nearer {
1144 let response = HealPostBundleFeedbackResponseV1 { accepted_count: 0 };
1145 return Ok((PayloadResponseKind::HealPostBundleFeedbackResponseV1, BytesGatherer::from_bytes(json::struct_to_bytes(&response)?)));
1146 }
1147
1148 let _post_bundle_lock = self.environment.get_read_lock_for_location_id(&request.location_id);
1150 let post_bundle_bytes = self.environment.get_post_bundle_bytes(time_millis, &request.location_id)?;
1151 let Some(post_bundle_bytes) = post_bundle_bytes
1152 else {
1153 let response = HealPostBundleFeedbackResponseV1 { accepted_count: 0 };
1154 return Ok((PayloadResponseKind::HealPostBundleFeedbackResponseV1, BytesGatherer::from_bytes(json::struct_to_bytes(&response)?)));
1155 };
1156 let post_bundle = EncodedPostBundleV1::from_bytes(Bytes::from_owner(post_bundle_bytes), false)?;
1157
1158 let mut accepted_count: u32 = 0;
1159 for feedback in &request.encoded_post_feedbacks {
1160 if !post_bundle.header.encoded_post_ids.contains(&feedback.post_id) {
1162 continue;
1163 }
1164 if let Err(e) = feedback.pow_verify() {
1167 warn!("Rejecting healed feedback with invalid pow for post_id={} at location_id={}: {}", feedback.post_id, request.location_id, e);
1168 continue;
1169 }
1170 self.environment.put_post_feedback_if_more_powerful(time_millis, &request.location_id, feedback)?;
1171 accepted_count += 1;
1172 }
1173
1174 if accepted_count > 0 {
1175 trace!("Accepted {} healed feedback(s) for location_id={}", accepted_count, request.location_id);
1176 }
1177
1178 let response = HealPostBundleFeedbackResponseV1 { accepted_count };
1179 Ok((PayloadResponseKind::HealPostBundleFeedbackResponseV1, BytesGatherer::from_bytes(json::struct_to_bytes(&response)?)))
1180 }
1181
1182 #[allow(non_snake_case)]
1183 async fn dispatch_network_payload_x_CachePostBundleV1(&self, _cancellation_token: CancellationToken, payload_request_kind: PayloadRequestKind, mut bytes: Bytes) -> anyhow::Result<(PayloadResponseKind, BytesGatherer)> {
1184 anyhow_assert_eq!(&PayloadRequestKind::CachePostBundleV1, &payload_request_kind);
1185
1186 let time_millis = self.runtime_services.time_provider.as_ref().current_time_millis();
1187 let request = CachePostBundleV1::from_bytes(&mut bytes)?;
1188 trace!("received CachePostBundleV1 for bucket_location={}", request.token.bucket_location);
1189
1190 let peer_self = self.peer_self.read().clone();
1192 if request.token.peer.id != peer_self.id {
1193 anyhow::bail!("CachePostBundleV1: token was not issued by this server");
1194 }
1195 request.token.verify()?;
1196 if request.token.is_expired(time_millis) {
1197 anyhow::bail!("CachePostBundleV1: token has expired");
1198 }
1199
1200 let mut any_accepted = false;
1201 for bundle_bytes in request.encoded_post_bundles {
1202 let parse_result: anyhow::Result<()> = try {
1203 let mut encoded_post_bundle = EncodedPostBundleV1::from_bytes(bundle_bytes.clone(), true)?;
1204
1205 encoded_post_bundle.verify_for_bucket(&request.token.bucket_location, time_millis)?;
1208 if encoded_post_bundle.header.num_posts == 0 {
1209 Err(anyhow::anyhow!("empty bundle is not worth caching"))?; }
1211
1212 encoded_post_bundle.normalise_flags_for_bucket(&request.token.bucket_location, time_millis);
1215 let originator_peer_id = encoded_post_bundle.header.peer.id;
1216 let is_sealed = encoded_post_bundle.header.sealed;
1217 let bundle_time_millis = encoded_post_bundle.header.time_millis;
1218 if self.post_bundle_cache.on_upload(request.token.bucket_location.location_id, originator_peer_id, bundle_bytes, bundle_time_millis, time_millis, is_sealed) {
1219 any_accepted = true;
1220 }
1221 };
1222 if let Err(e) = &parse_result {
1223 warn!("CachePostBundleV1: failed to parse bundle: {}", e);
1224 }
1225 }
1226 let response = CachePostBundleResponseV1 { accepted: any_accepted };
1227 Ok((PayloadResponseKind::CachePostBundleResponseV1, BytesGatherer::from_bytes(json::struct_to_bytes(&response)?)))
1228 }
1229
1230 #[allow(non_snake_case)]
1231 async fn dispatch_network_payload_x_CachePostBundleFeedbackV1(&self, _cancellation_token: CancellationToken, payload_request_kind: PayloadRequestKind, mut bytes: Bytes) -> anyhow::Result<(PayloadResponseKind, BytesGatherer)> {
1232 anyhow_assert_eq!(&PayloadRequestKind::CachePostBundleFeedbackV1, &payload_request_kind);
1233
1234 let time_millis = self.runtime_services.time_provider.as_ref().current_time_millis();
1235 let request = CachePostBundleFeedbackV1::from_bytes(&mut bytes)?;
1236 trace!("received CachePostBundleFeedbackV1 for bucket_location={}", request.token.bucket_location);
1237
1238 let peer_self = self.peer_self.read().clone();
1240 if request.token.peer.id != peer_self.id {
1241 anyhow::bail!("CachePostBundleFeedbackV1: token was not issued by this server");
1242 }
1243 request.token.verify()?;
1244 if request.token.is_expired(time_millis) {
1245 anyhow::bail!("CachePostBundleFeedbackV1: token has expired");
1246 }
1247
1248 let result: anyhow::Result<bool> = try {
1249 let encoded_post_bundle_feedback = EncodedPostBundleFeedbackV1::from_bytes(request.encoded_post_bundle_feedback_bytes.clone())?;
1250
1251 encoded_post_bundle_feedback.verify_for_location(&request.token.bucket_location.location_id)?;
1252
1253 let originator_peer_id = encoded_post_bundle_feedback.header.peer.id;
1254 self.post_bundle_feedback_cache
1256 .on_upload(request.token.bucket_location.location_id, originator_peer_id, request.encoded_post_bundle_feedback_bytes, time_millis, false)
1257 };
1258 let accepted = result.unwrap_or_else(|e| {
1259 warn!("CachePostBundleFeedbackV1: parse error: {}", e);
1260 false
1261 });
1262
1263 let response = CachePostBundleFeedbackResponseV1 { accepted };
1264 Ok((PayloadResponseKind::CachePostBundleFeedbackResponseV1, BytesGatherer::from_bytes(json::struct_to_bytes(&response)?)))
1265 }
1266
1267 #[allow(non_snake_case)]
1268 async fn dispatch_network_payload_x_FetchUrlPreviewV1(&self, _cancellation_token: CancellationToken, pow: Option<PeerPow>, payload_request_kind: PayloadRequestKind, mut bytes: Bytes) -> anyhow::Result<(PayloadResponseKind, BytesGatherer)> {
1269 anyhow_assert_eq!(&PayloadRequestKind::FetchUrlPreviewV1, &payload_request_kind);
1270
1271 let pow = pow.ok_or_else(|| anyhow::anyhow!("pow required for FetchUrlPreviewV1"))?;
1273 if pow.pow < config::POW_MINIMUM_PER_URL_FETCH {
1274 anyhow::bail!("Insufficient pow for FetchUrlPreviewV1: {} < {}", pow.pow, config::POW_MINIMUM_PER_URL_FETCH);
1275 }
1276
1277 let request = FetchUrlPreviewV1::from_bytes(&mut bytes)?;
1278 trace!("received FetchUrlPreviewV1 for url={}", request.url);
1279
1280 if !request.url.starts_with("https://") {
1283 anyhow::bail!("FetchUrlPreviewV1 SSRF: only https:// URLs are allowed");
1284 }
1285
1286 let host_and_port = request.url["https://".len()..].split(&['/', '?', '#'][..]).next().unwrap_or("");
1288 let host = if host_and_port.starts_with('[') {
1289 host_and_port.trim_start_matches('[').split(']').next().unwrap_or("")
1291 } else {
1292 host_and_port.split(':').next().unwrap_or(host_and_port)
1294 };
1295 if host.is_empty() {
1296 anyhow::bail!("FetchUrlPreviewV1 SSRF: could not extract host from URL");
1297 }
1298 if host.parse::<std::net::IpAddr>().is_ok() {
1299 anyhow::bail!("FetchUrlPreviewV1 SSRF: bare IP addresses are not allowed");
1300 }
1301
1302 let resolved_socket_addrs: Vec<std::net::SocketAddr> = resolve_url_preview_host(host, config::URL_FETCH_DNS_TIMEOUT_MILLIS.into(), tokio::net::lookup_host((host, 443u16))).await?;
1306 if resolved_socket_addrs.is_empty() {
1307 anyhow::bail!("FetchUrlPreviewV1 SSRF: DNS returned no addresses for {}", host);
1308 }
1309 for socket_addr in &resolved_socket_addrs {
1310 let ip = socket_addr.ip();
1311 if is_ssrf_protected_ip(ip) {
1312 anyhow::bail!("FetchUrlPreviewV1 SSRF: {} resolved to protected address {}", host, ip);
1313 }
1314 }
1315
1316 let http_client = reqwest::Client::builder()
1323 .connect_timeout(config::URL_FETCH_CONNECT_TIMEOUT_MILLIS.into())
1324 .timeout(config::URL_FETCH_TOTAL_TIMEOUT_MILLIS.into())
1325 .user_agent("hashiverse-preview/1.0")
1326 .resolve_to_addrs(host, &resolved_socket_addrs)
1327 .redirect(reqwest::redirect::Policy::none())
1328 .no_proxy()
1329 .build()?;
1330
1331 const URL_FETCH_MAX_BODY_BYTES: usize = 2 * 1024 * 1024;
1332 let mut http_response = http_client.get(&request.url).send().await?;
1333
1334 if let Some(content_length) = http_response.content_length() {
1336 if content_length > URL_FETCH_MAX_BODY_BYTES as u64 {
1337 anyhow::bail!("FetchUrlPreviewV1: Content-Length {} exceeds {} byte limit", content_length, URL_FETCH_MAX_BODY_BYTES);
1338 }
1339 }
1340 let mut body_bytes = BytesMut::new();
1341 while let Some(chunk) = http_response.chunk().await? {
1342 let remaining = URL_FETCH_MAX_BODY_BYTES - body_bytes.len();
1347 body_bytes.extend_from_slice(&chunk[..chunk.len().min(remaining)]);
1348 if body_bytes.len() >= URL_FETCH_MAX_BODY_BYTES {
1349 break;
1350 }
1351 }
1352 let html = String::from_utf8_lossy(&body_bytes).into_owned();
1353
1354 let preview_data = url_preview::extract_url_preview(&html);
1355
1356 let response = FetchUrlPreviewResponseV1 {
1357 url: if preview_data.canonical_url.is_empty() { request.url } else { preview_data.canonical_url },
1358 title: preview_data.title,
1359 description: preview_data.description,
1360 image_url: preview_data.image_url,
1361 };
1362
1363 Ok((PayloadResponseKind::FetchUrlPreviewResponseV1, BytesGatherer::from_bytes(response.to_bytes()?)))
1364 }
1365
1366 #[allow(non_snake_case)]
1367 async fn dispatch_network_payload_x_TrendingHashtagsFetchV1(&self, _cancellation_token: CancellationToken, payload_request_kind: PayloadRequestKind, mut bytes: Bytes) -> anyhow::Result<(PayloadResponseKind, BytesGatherer)> {
1368 anyhow_assert_eq!(&PayloadRequestKind::TrendingHashtagsFetchV1, &payload_request_kind);
1369
1370 let request = TrendingHashtagsFetchV1::from_bytes(&mut bytes)?;
1371 trace!("received TrendingHashtagsFetchV1 with limit={}", request.limit);
1372
1373 let time_millis = self.runtime_services.time_provider.current_time_millis();
1374
1375 let cached_response = {
1377 let cache = self.trending_hashtags_response_cache.lock();
1378 match cache.as_ref() {
1379 Some((cached_time, cached_response)) if (time_millis - *cached_time) < MILLIS_IN_SECOND.const_mul(30) => {
1380 Some(cached_response.clone())
1381 }
1382 _ => None,
1383 }
1384 };
1385
1386 let mut response = match cached_response {
1387 Some(mut cached) => {
1388 cached.trending_hashtags.truncate(request.limit as usize);
1389 cached
1390 }
1391 None => {
1392 let mut trending_hashtags: Vec<TrendingHashtagV1> = self.trending_hashtags.iter()
1394 .map(|(hashtag, hll)| TrendingHashtagV1 {
1395 hashtag: hashtag.as_ref().clone(),
1396 count: hll.count(),
1397 })
1398 .filter(|entry| entry.count > 0)
1399 .collect();
1400
1401 trending_hashtags.sort_by_key(|entry| std::cmp::Reverse(entry.count));
1402
1403 let full_response = TrendingHashtagsFetchResponseV1 { trending_hashtags };
1404
1405 {
1407 let mut cache = self.trending_hashtags_response_cache.lock();
1408 *cache = Some((time_millis, full_response.clone()));
1409 }
1410
1411 let mut truncated_response = full_response;
1412 truncated_response.trending_hashtags.truncate(request.limit as usize);
1413 truncated_response
1414 }
1415 };
1416
1417 top_up_trending_hashtags_with_fallback(&mut response.trending_hashtags, request.limit, TRENDING_HASHTAGS_FALLBACK);
1418
1419 Ok((PayloadResponseKind::TrendingHashtagsFetchResponseV1, BytesGatherer::from_bytes(response.to_bytes()?)))
1420 }
1421
1422 #[allow(non_snake_case)]
1423 async fn dispatch_network_payload_x_PeerStatsRequestV1(&self, _cancellation_token: CancellationToken, pow: Option<PeerPow>, payload_request_kind: PayloadRequestKind, bytes: Bytes) -> anyhow::Result<(PayloadResponseKind, BytesGatherer)> {
1424 anyhow_assert_eq!(&PayloadRequestKind::PeerStatsRequestV1, &payload_request_kind);
1425
1426 let _request = PeerStatsRequestV1::from_bytes(&bytes)?;
1427
1428 let pow = pow.ok_or_else(|| anyhow::anyhow!("pow required for PeerStatsRequestV1"))?;
1429 if pow.pow < config::POW_MINIMUM_PER_PEER_STATS {
1430 anyhow::bail!("Insufficient pow for PeerStatsRequestV1: {} < {}", pow.pow, config::POW_MINIMUM_PER_PEER_STATS);
1431 }
1432
1433 let time_millis = self.runtime_services.time_provider.current_time_millis();
1434
1435 let cached_response = {
1438 let cache = self.peer_stats_response_cache.lock();
1439 match cache.as_ref() {
1440 Some((cached_time, cached_response)) if (time_millis - *cached_time) < MILLIS_IN_SECOND.const_mul(60) => Some(cached_response.clone()),
1441 _ => None,
1442 }
1443 };
1444
1445 let response = match cached_response {
1446 Some(cached) => cached,
1447 None => {
1448 let doc = serde_json::json!({
1449 "version": env!("CARGO_PKG_VERSION"),
1450 "requests": request_counts_subtree(&self.request_counters, &self.request_rate_windows, time_millis),
1451 "system": system_stats_subtree(),
1452 "kademlia": kademlia_stats_subtree(&self.kademlia.read()),
1453 "environment": environment_stats_subtree(&self.environment),
1454 });
1455
1456 let json_bytes = serde_json::to_vec(&doc)?;
1457 let json_compressed = compression::compress_for_speed(&json_bytes)?.to_bytes();
1458
1459 let signing_input = PeerStatsResponseV1::signing_input(time_millis, &json_compressed);
1460 let signature = signing::sign(&self.server_id.keys.signature_key, &signing_input);
1461
1462 let response = PeerStatsResponseV1 {
1463 peer: self.peer_self.read().clone(),
1464 timestamp: time_millis,
1465 json_compressed,
1466 signature,
1467 };
1468
1469 *self.peer_stats_response_cache.lock() = Some((time_millis, response.clone()));
1470 response
1471 }
1472 };
1473
1474 Ok((PayloadResponseKind::PeerStatsResponseV1, BytesGatherer::from_bytes(response.to_bytes()?)))
1475 }
1476}
1477
1478#[cfg(test)]
1479mod tests {
1480 use super::*;
1481
1482 mod complete_incoming_request_outcomes {
1483 use super::*;
1484 use hashiverse_lib::transport::ddos::ddos::{DdosConnectionGuard, DdosProtection};
1485 use hashiverse_lib::transport::ddos::mem_ddos::MemDdosProtection;
1486 use hashiverse_lib::tools::time_provider::manual_time_provider::ManualTimeProvider;
1487 use std::sync::Arc;
1488 use tokio::sync::oneshot;
1489
1490 const CALLER_IP: &str = "10.1.2.3";
1491
1492 fn make_incoming_request() -> (Arc<MemDdosProtection>, IncomingRequest, oneshot::Receiver<BytesGatherer>) {
1494 let ddos = Arc::new(MemDdosProtection::new(1.0, 0.0, 1.0, 8, Arc::new(ManualTimeProvider::default())));
1495 let ddos_connection_guard = DdosConnectionGuard::try_new(ddos.clone(), CALLER_IP).expect("fresh ip must get a connection slot");
1496 let (reply_tx, reply_rx) = oneshot::channel();
1497 let incoming = IncomingRequest::new(CALLER_IP.to_string(), Bytes::new(), reply_tx, Arc::new(ddos_connection_guard));
1498 (ddos, incoming, reply_rx)
1499 }
1500
1501 fn is_blocked(ddos: &MemDdosProtection) -> bool {
1503 !ddos.try_acquire_connection(CALLER_IP)
1504 }
1505
1506 #[tokio::test]
1507 async fn panic_outcome_reports_bad_request() {
1508 let (ddos, incoming, reply_rx) = make_incoming_request();
1509 assert!(!is_blocked(&ddos));
1510 complete_incoming_request(incoming, Err(anyhow::anyhow!("panic: boom")));
1511 assert!(is_blocked(&ddos), "a handler panic must count against the caller");
1512 assert!(reply_rx.await.is_err(), "no reply must be sent for a panic");
1513 }
1514
1515 #[tokio::test]
1516 async fn error_outcome_reports_bad_request() {
1517 let (ddos, incoming, reply_rx) = make_incoming_request();
1518 complete_incoming_request(incoming, Ok(Err(anyhow::anyhow!("bad packet"))));
1519 assert!(is_blocked(&ddos));
1520 assert!(reply_rx.await.is_err());
1521 }
1522
1523 #[tokio::test]
1524 async fn success_outcome_does_not_report_and_replies() {
1525 let (ddos, incoming, reply_rx) = make_incoming_request();
1526 complete_incoming_request(incoming, Ok(Ok(BytesGatherer::from_bytes(Bytes::from_static(b"ok")))));
1527 assert!(!is_blocked(&ddos));
1528 assert_eq!(reply_rx.await.expect("reply must be sent").to_bytes(), Bytes::from_static(b"ok"));
1529 }
1530 }
1531
1532 #[tokio::test]
1533 async fn catch_panic_as_error_passes_through_value() {
1534 let result = catch_panic_as_error(async { 42 }).await;
1535 assert_eq!(result.unwrap(), 42);
1536 }
1537
1538 #[tokio::test]
1539 async fn catch_panic_as_error_converts_str_panic() {
1540 let result: anyhow::Result<()> = catch_panic_as_error(async { panic!("boom") }).await;
1541 let error_message = result.unwrap_err().to_string();
1542 assert!(error_message.contains("boom"), "error_message={}", error_message);
1543 }
1544
1545 #[tokio::test]
1546 async fn catch_panic_as_error_converts_formatted_panic() {
1547 let duration = 60000;
1548 let result: anyhow::Result<()> = catch_panic_as_error(async move { panic!("bad duration {}", duration) }).await;
1549 let error_message = result.unwrap_err().to_string();
1550 assert!(error_message.contains("bad duration 60000"), "error_message={}", error_message);
1551 }
1552
1553 #[tokio::test]
1554 async fn catch_panic_as_error_converts_assert_panic() {
1555 let result: anyhow::Result<()> = catch_panic_as_error(async { assert!(1 > 2) }).await;
1556 assert!(result.is_err());
1557 }
1558
1559 fn make_trending_hashtag(hashtag: &str, count: u64) -> TrendingHashtagV1 {
1560 TrendingHashtagV1 { hashtag: hashtag.to_string(), count }
1561 }
1562
1563 #[test]
1564 fn top_up_adds_fallback_when_empty() {
1565 let mut trending_hashtags: Vec<TrendingHashtagV1> = vec![];
1566 top_up_trending_hashtags_with_fallback(&mut trending_hashtags, 5, &["#hashiverse", "#news"]);
1567 assert_eq!(trending_hashtags.len(), 2);
1568 assert_eq!(trending_hashtags[0].hashtag, "#hashiverse");
1569 assert_eq!(trending_hashtags[0].count, 0);
1570 assert_eq!(trending_hashtags[1].hashtag, "#news");
1571 assert_eq!(trending_hashtags[1].count, 0);
1572 }
1573
1574 #[test]
1575 fn top_up_respects_limit_smaller_than_fallback_list() {
1576 let mut trending_hashtags: Vec<TrendingHashtagV1> = vec![];
1577 top_up_trending_hashtags_with_fallback(&mut trending_hashtags, 1, &["#hashiverse", "#news"]);
1578 assert_eq!(trending_hashtags.len(), 1);
1579 assert_eq!(trending_hashtags[0].hashtag, "#hashiverse");
1580 }
1581
1582 #[test]
1583 fn top_up_preserves_fallback_order() {
1584 let mut trending_hashtags: Vec<TrendingHashtagV1> = vec![];
1585 top_up_trending_hashtags_with_fallback(&mut trending_hashtags, 3, &["#first", "#second", "#third"]);
1586 assert_eq!(trending_hashtags[0].hashtag, "#first");
1587 assert_eq!(trending_hashtags[1].hashtag, "#second");
1588 assert_eq!(trending_hashtags[2].hashtag, "#third");
1589 }
1590
1591 #[test]
1592 fn top_up_is_noop_when_already_at_limit() {
1593 let mut trending_hashtags = vec![make_trending_hashtag("#rust", 10), make_trending_hashtag("#golang", 5)];
1594 top_up_trending_hashtags_with_fallback(&mut trending_hashtags, 2, &["#hashiverse", "#news"]);
1595 assert_eq!(trending_hashtags.len(), 2);
1596 assert_eq!(trending_hashtags[0].hashtag, "#rust");
1597 assert_eq!(trending_hashtags[1].hashtag, "#golang");
1598 }
1599
1600 #[test]
1601 fn top_up_partially_fills_when_real_trending_exists() {
1602 let mut trending_hashtags = vec![make_trending_hashtag("#rust", 10)];
1603 top_up_trending_hashtags_with_fallback(&mut trending_hashtags, 3, &["#hashiverse", "#news"]);
1604 assert_eq!(trending_hashtags.len(), 3);
1605 assert_eq!(trending_hashtags[0].hashtag, "#rust");
1606 assert_eq!(trending_hashtags[0].count, 10);
1607 assert_eq!(trending_hashtags[1].hashtag, "#hashiverse");
1608 assert_eq!(trending_hashtags[1].count, 0);
1609 assert_eq!(trending_hashtags[2].hashtag, "#news");
1610 assert_eq!(trending_hashtags[2].count, 0);
1611 }
1612
1613 #[test]
1614 fn top_up_skips_fallback_already_present_exact_match() {
1615 let mut trending_hashtags = vec![make_trending_hashtag("#hashiverse", 42)];
1616 top_up_trending_hashtags_with_fallback(&mut trending_hashtags, 3, &["#hashiverse", "#news"]);
1617 assert_eq!(trending_hashtags.len(), 2);
1618 assert_eq!(trending_hashtags[0].hashtag, "#hashiverse");
1619 assert_eq!(trending_hashtags[0].count, 42, "real trending entry must not be overwritten by filler");
1620 assert_eq!(trending_hashtags[1].hashtag, "#news");
1621 assert_eq!(trending_hashtags[1].count, 0);
1622 }
1623
1624 #[test]
1625 fn top_up_dedup_is_case_insensitive_and_prefix_agnostic() {
1626 let mut trending_hashtags = vec![make_trending_hashtag("HashiVerse", 7)];
1628 top_up_trending_hashtags_with_fallback(&mut trending_hashtags, 3, &["#hashiverse", "#news"]);
1629 assert_eq!(trending_hashtags.len(), 2);
1630 assert_eq!(trending_hashtags[0].hashtag, "HashiVerse");
1631 assert_eq!(trending_hashtags[1].hashtag, "#news");
1632 }
1633
1634 #[test]
1635 fn top_up_with_empty_fallback_is_noop() {
1636 let mut trending_hashtags: Vec<TrendingHashtagV1> = vec![];
1637 top_up_trending_hashtags_with_fallback(&mut trending_hashtags, 5, &[]);
1638 assert_eq!(trending_hashtags.len(), 0);
1639 }
1640
1641 #[test]
1642 fn top_up_handles_zero_limit() {
1643 let mut trending_hashtags: Vec<TrendingHashtagV1> = vec![];
1644 top_up_trending_hashtags_with_fallback(&mut trending_hashtags, 0, &["#hashiverse", "#news"]);
1645 assert_eq!(trending_hashtags.len(), 0);
1646 }
1647
1648 #[test]
1649 fn top_up_exhausts_fallback_without_reaching_limit() {
1650 let mut trending_hashtags: Vec<TrendingHashtagV1> = vec![];
1652 top_up_trending_hashtags_with_fallback(&mut trending_hashtags, 10, &["#hashiverse", "#news"]);
1653 assert_eq!(trending_hashtags.len(), 2, "should stop at the end of the fallback list, not pad further");
1654 }
1655
1656 mod fetch_url_preview {
1657 use super::*;
1658 use crate::environment::mem_environment_store::MemEnvironmentFactory;
1659 use crate::environment::environment::EnvironmentFactory;
1660 use crate::server::args::Args;
1661 use crate::server::hashiverse_server::HashiverseServer;
1662 use hashiverse_lib::protocol::peer::PeerPow;
1663 use hashiverse_lib::tools::pow_generator::single_threaded_pow_generator::SingleThreadedPowGenerator;
1664 use hashiverse_lib::tools::runtime_services::RuntimeServices;
1665 use hashiverse_lib::tools::time_provider::time_provider::RealTimeProvider;
1666 use hashiverse_lib::tools::types::Pow;
1667 use hashiverse_lib::transport::mem_transport::MemTransportFactory;
1668 use std::sync::Arc;
1669 use std::time::Duration;
1670
1671 async fn make_server() -> anyhow::Result<Arc<HashiverseServer>> {
1672 let time_provider = Arc::new(RealTimeProvider);
1673 let transport_factory = MemTransportFactory::default();
1674 let pow_generator = Arc::new(SingleThreadedPowGenerator::new());
1675 let runtime_services = Arc::new(RuntimeServices { time_provider, transport_factory, pow_generator });
1676 let environment_factory = Arc::new(MemEnvironmentFactory::new(""));
1677 let args = Args::default_for_testing();
1678 HashiverseServer::new(runtime_services, environment_factory, args).await
1679 }
1680
1681 fn synthetic_pow(pow: Pow) -> PeerPow {
1682 let mut peer_pow = PeerPow::zero();
1683 peer_pow.pow = pow;
1684 peer_pow
1685 }
1686
1687 fn request_bytes() -> Bytes {
1689 FetchUrlPreviewV1::new_to_bytes("http://example.com/").expect("FetchUrlPreviewV1 must serialise")
1690 }
1691
1692 async fn fetch_url_preview_error(pow: Option<PeerPow>) -> String {
1693 let server = make_server().await.expect("server must start");
1694 let result = server.dispatch_network_payload_x_FetchUrlPreviewV1(CancellationToken::new(), pow, PayloadRequestKind::FetchUrlPreviewV1, request_bytes()).await;
1695 result.err().expect("handler must reject this request").to_string()
1696 }
1697
1698 #[tokio::test]
1699 async fn rejects_missing_pow() {
1700 let error = fetch_url_preview_error(None).await;
1701 assert!(error.contains("pow required for FetchUrlPreviewV1"), "unexpected error: {}", error);
1702 }
1703
1704 #[tokio::test]
1705 async fn rejects_insufficient_pow() {
1706 let error = fetch_url_preview_error(Some(synthetic_pow(Pow(config::POW_MINIMUM_PER_URL_FETCH.0 - 1)))).await;
1707 assert!(error.contains("Insufficient pow for FetchUrlPreviewV1"), "unexpected error: {}", error);
1708 }
1709
1710 #[tokio::test]
1711 async fn accepts_threshold_pow() {
1712 let error = fetch_url_preview_error(Some(synthetic_pow(config::POW_MINIMUM_PER_URL_FETCH))).await;
1713 assert!(error.contains("only https:// URLs are allowed"), "pow check should pass and fall through to the SSRF check, got: {}", error);
1714 }
1715
1716 #[tokio::test]
1717 async fn dns_lookup_times_out() {
1718 let never_resolving_lookup = std::future::pending::<std::io::Result<std::vec::IntoIter<std::net::SocketAddr>>>();
1719 let error = resolve_url_preview_host("example.com", Duration::from_millis(50), never_resolving_lookup).await.err().expect("lookup must time out");
1720 assert!(error.to_string().contains("DNS resolution timed out for example.com"), "unexpected error: {}", error);
1721 }
1722
1723 #[tokio::test]
1724 async fn dns_lookup_passes_through_results_and_errors() {
1725 let socket_addr: std::net::SocketAddr = "93.184.216.34:443".parse().unwrap();
1726 let resolved = resolve_url_preview_host("example.com", Duration::from_secs(1), async move { Ok(vec![socket_addr].into_iter()) }).await.expect("lookup must succeed");
1727 assert_eq!(resolved, vec![socket_addr]);
1728
1729 let failed_lookup = async { Err::<std::vec::IntoIter<std::net::SocketAddr>, _>(std::io::Error::other("nxdomain")) };
1730 let error = resolve_url_preview_host("example.com", Duration::from_secs(1), failed_lookup).await.err().expect("lookup must fail");
1731 assert!(error.to_string().contains("DNS resolution failed for example.com"), "unexpected error: {}", error);
1732 }
1733 }
1734
1735 mod peer_stats {
1736 use super::*;
1737 use crate::environment::mem_environment_store::MemEnvironmentFactory;
1738 use crate::environment::environment::EnvironmentFactory;
1739 use crate::server::args::Args;
1740 use crate::server::hashiverse_server::HashiverseServer;
1741 use hashiverse_lib::protocol::payload::payload::{PAYLOAD_REQUEST_KIND_COUNT, PeerStatsRequestV1, PeerStatsResponseV1};
1742 use hashiverse_lib::protocol::peer::PeerPow;
1743 use hashiverse_lib::tools::compression;
1744 use hashiverse_lib::tools::pow_generator::single_threaded_pow_generator::SingleThreadedPowGenerator;
1745 use hashiverse_lib::tools::runtime_services::RuntimeServices;
1746 use hashiverse_lib::tools::time::TimeMillis;
1747 use hashiverse_lib::tools::time_provider::time_provider::RealTimeProvider;
1748 use hashiverse_lib::tools::types::{Pow, VerificationKey};
1749 use hashiverse_lib::transport::mem_transport::MemTransportFactory;
1750 use std::sync::Arc;
1751 use std::sync::atomic::Ordering;
1752
1753 async fn make_server() -> anyhow::Result<Arc<HashiverseServer>> {
1754 let time_provider = Arc::new(RealTimeProvider);
1755 let transport_factory = MemTransportFactory::default();
1756 let pow_generator = Arc::new(SingleThreadedPowGenerator::new());
1757 let runtime_services = Arc::new(RuntimeServices { time_provider, transport_factory, pow_generator });
1758 let environment_factory = Arc::new(MemEnvironmentFactory::new(""));
1759 let args = Args::default_for_testing();
1760 HashiverseServer::new(runtime_services, environment_factory, args).await
1761 }
1762
1763 fn synthetic_pow(pow: Pow) -> PeerPow {
1767 let mut peer_pow = PeerPow::zero();
1768 peer_pow.pow = pow;
1769 peer_pow
1770 }
1771
1772 fn empty_request_bytes() -> Bytes {
1773 PeerStatsRequestV1 {}.to_bytes().expect("PeerStatsRequestV1 must serialise")
1774 }
1775
1776 fn decode_doc(response: &PeerStatsResponseV1) -> serde_json::Value {
1777 let bytes = compression::decompress(&response.json_compressed).expect("decompress doc").to_bytes();
1778 serde_json::from_slice(&bytes).expect("doc must be valid JSON")
1779 }
1780
1781 #[tokio::test]
1782 async fn rejects_insufficient_pow() {
1783 let server = make_server().await.expect("server must start");
1784 let result = server
1785 .dispatch_network_payload_x_PeerStatsRequestV1(
1786 CancellationToken::new(),
1787 Some(synthetic_pow(Pow(config::POW_MINIMUM_PER_PEER_STATS.0.saturating_sub(1)))),
1788 PayloadRequestKind::PeerStatsRequestV1,
1789 empty_request_bytes(),
1790 )
1791 .await;
1792 assert!(result.is_err(), "expected insufficient PoW to be rejected");
1793 }
1794
1795 #[tokio::test]
1796 async fn returns_response_with_expected_top_level_keys() {
1797 let server = make_server().await.expect("server must start");
1798 let (response_kind, gatherer) = server
1799 .dispatch_network_payload_x_PeerStatsRequestV1(
1800 CancellationToken::new(),
1801 Some(synthetic_pow(config::POW_MINIMUM_PER_PEER_STATS)),
1802 PayloadRequestKind::PeerStatsRequestV1,
1803 empty_request_bytes(),
1804 )
1805 .await
1806 .expect("handler must succeed at threshold pow");
1807 assert_eq!(response_kind, PayloadResponseKind::PeerStatsResponseV1);
1808
1809 let response_bytes = gatherer.to_bytes();
1810 let response = PeerStatsResponseV1::from_bytes(&response_bytes).expect("response must decode");
1811 let doc = decode_doc(&response);
1812
1813 assert!(doc.get("version").and_then(|v| v.as_str()).map(|s| !s.is_empty()).unwrap_or(false), "version must be a non-empty string");
1814 assert_eq!(doc["version"].as_str().unwrap(), env!("CARGO_PKG_VERSION"));
1815 assert!(doc.get("requests").is_some(), "requests subtree missing");
1816 assert!(doc.get("system").is_some(), "system subtree missing");
1817 assert!(doc.get("kademlia").is_some(), "kademlia subtree missing");
1818 assert!(doc.get("environment").is_some(), "environment subtree missing");
1819
1820 for key in ["memory_total_bytes", "memory_free_bytes", "disk_total_bytes", "disk_free_bytes", "load_1m", "load_5m", "load_15m"] {
1821 assert!(doc["system"].get(key).map(|v| v.is_number()).unwrap_or(false), "system.{key} must be a number");
1822 }
1823 for key in ["post_bundle_count", "post_bundle_feedback_count", "post_bundle_total_bytes"] {
1824 assert!(doc["environment"].get(key).map(|v| v.is_number()).unwrap_or(false), "environment.{key} must be a number");
1825 }
1826 }
1827
1828 #[tokio::test]
1829 async fn counters_reflect_recorded_dispatches() {
1830 let server = make_server().await.expect("server must start");
1831
1832 for _ in 0..7 {
1836 server.request_counters[PayloadRequestKind::PingV1 as usize].fetch_add(1, Ordering::Relaxed);
1837 }
1838
1839 let (_, gatherer) = server
1840 .dispatch_network_payload_x_PeerStatsRequestV1(
1841 CancellationToken::new(),
1842 Some(synthetic_pow(config::POW_MINIMUM_PER_PEER_STATS)),
1843 PayloadRequestKind::PeerStatsRequestV1,
1844 empty_request_bytes(),
1845 )
1846 .await
1847 .expect("handler must succeed");
1848 let response = PeerStatsResponseV1::from_bytes(&gatherer.to_bytes()).expect("response must decode");
1849 let doc = decode_doc(&response);
1850 assert_eq!(doc["requests"]["PingV1"]["total"].as_u64(), Some(7));
1851 }
1852
1853 #[tokio::test]
1854 async fn cache_returns_byte_identical_response_within_ttl() {
1855 let server = make_server().await.expect("server must start");
1856 let pow = synthetic_pow(config::POW_MINIMUM_PER_PEER_STATS);
1857
1858 let (_, gatherer_a) = server
1859 .dispatch_network_payload_x_PeerStatsRequestV1(CancellationToken::new(), Some(pow.clone()), PayloadRequestKind::PeerStatsRequestV1, empty_request_bytes())
1860 .await
1861 .expect("first call must succeed");
1862 let bytes_a = gatherer_a.to_bytes();
1863
1864 server.request_counters[PayloadRequestKind::PingV1 as usize].fetch_add(99, Ordering::Relaxed);
1867
1868 let (_, gatherer_b) = server
1869 .dispatch_network_payload_x_PeerStatsRequestV1(CancellationToken::new(), Some(pow), PayloadRequestKind::PeerStatsRequestV1, empty_request_bytes())
1870 .await
1871 .expect("second call must succeed");
1872 let bytes_b = gatherer_b.to_bytes();
1873
1874 assert_eq!(bytes_a, bytes_b, "cached response should be byte-identical across the TTL");
1875 }
1876
1877 #[tokio::test]
1878 async fn signature_verifies_and_fails_on_tamper() {
1879 let server = make_server().await.expect("server must start");
1880 let (_, gatherer) = server
1881 .dispatch_network_payload_x_PeerStatsRequestV1(
1882 CancellationToken::new(),
1883 Some(synthetic_pow(config::POW_MINIMUM_PER_PEER_STATS)),
1884 PayloadRequestKind::PeerStatsRequestV1,
1885 empty_request_bytes(),
1886 )
1887 .await
1888 .expect("handler must succeed");
1889 let response = PeerStatsResponseV1::from_bytes(&gatherer.to_bytes()).expect("response must decode");
1890
1891 let verification_key = VerificationKey::from_bytes(&response.peer.verification_key_bytes).expect("verification key must decode");
1892 let signing_input = PeerStatsResponseV1::signing_input(response.timestamp, &response.json_compressed);
1893 signing::verify(&verification_key, &response.signature, &signing_input).expect("signature must verify against transmitted bytes");
1894
1895 let mut tampered = response.json_compressed.to_vec();
1897 let tamper_index = tampered.len() / 2;
1898 tampered[tamper_index] ^= 0xff;
1899 let tampered_signing_input = PeerStatsResponseV1::signing_input(response.timestamp, &tampered);
1900 assert!(signing::verify(&verification_key, &response.signature, &tampered_signing_input).is_err(), "verification must fail when json_compressed is tampered");
1901
1902 let bumped_signing_input = PeerStatsResponseV1::signing_input(TimeMillis(response.timestamp.0 + 1), &response.json_compressed);
1904 assert!(signing::verify(&verification_key, &response.signature, &bumped_signing_input).is_err(), "verification must fail when timestamp is mutated");
1905 }
1906
1907 #[test]
1908 fn request_counts_subtree_covers_every_variant() {
1909 let counters: [std::sync::atomic::AtomicU64; PAYLOAD_REQUEST_KIND_COUNT] = std::array::from_fn(|_| std::sync::atomic::AtomicU64::new(0));
1912 let windows: [parking_lot::Mutex<crate::server::stats::RequestRateWindows>; PAYLOAD_REQUEST_KIND_COUNT] =
1913 std::array::from_fn(|_| parking_lot::Mutex::new(crate::server::stats::RequestRateWindows::new()));
1914 let subtree = request_counts_subtree(&counters, &windows, TimeMillis::zero());
1915 let map = subtree.as_object().expect("request_counts subtree must be an object");
1916 assert_eq!(map.len(), PAYLOAD_REQUEST_KIND_COUNT);
1917 }
1918 }
1919
1920 mod announce_v2 {
1921 use super::*;
1922 use crate::environment::mem_environment_store::MemEnvironmentFactory;
1923 use crate::environment::environment::EnvironmentFactory;
1924 use crate::server::args::Args;
1925 use crate::server::hashiverse_server::HashiverseServer;
1926 use hashiverse_lib::protocol::payload::payload::AnnounceV2;
1927 use hashiverse_lib::protocol::peer::Peer;
1928 use hashiverse_lib::tools::pow_generator::pow_generator::PowGenerator;
1929 use hashiverse_lib::tools::pow_generator::single_threaded_pow_generator::SingleThreadedPowGenerator;
1930 use hashiverse_lib::tools::runtime_services::RuntimeServices;
1931 use hashiverse_lib::tools::server_id::ServerId;
1932 use hashiverse_lib::tools::time_provider::time_provider::{RealTimeProvider, TimeProvider};
1933 use hashiverse_lib::transport::mem_transport::MemTransportFactory;
1934 use std::sync::Arc;
1935
1936 async fn make_test_server() -> Arc<HashiverseServer> {
1937 let time_provider = Arc::new(RealTimeProvider);
1938 let transport_factory = MemTransportFactory::default();
1939 let pow_generator = Arc::new(SingleThreadedPowGenerator::new());
1940 let runtime_services = Arc::new(RuntimeServices { time_provider, transport_factory, pow_generator });
1941 let environment_factory = Arc::new(MemEnvironmentFactory::new(""));
1942 let args = Args::default_for_testing();
1943 HashiverseServer::new(runtime_services, environment_factory, args).await.expect("server must start")
1944 }
1945
1946 async fn make_announcing_peer(time_provider: &dyn TimeProvider, pow_generator: &dyn PowGenerator, address: &str) -> Peer {
1950 let server_id = ServerId::new("own_pow", time_provider, config::SERVER_KEY_POW_MIN, true, pow_generator).await.expect("ServerId::new must succeed in tests (PoW reduced under cfg(test))");
1951 let mut peer = server_id.to_peer(time_provider).expect("to_peer must succeed");
1952 peer.address = address.to_string();
1953 peer.sign(time_provider, &server_id.keys.signature_key).expect("sign must succeed");
1954 peer
1955 }
1956
1957 #[tokio::test]
1958 async fn v2_admits_peer_when_proof_is_valid_for_mem_transport() {
1959 let server = make_test_server().await;
1960 let pow_generator = SingleThreadedPowGenerator::new();
1961 let announcing_peer = make_announcing_peer(server.runtime_services.time_provider.as_ref(), &pow_generator, "9999").await;
1962 let announcing_peer_id = announcing_peer.id;
1963
1964 let baseline_len = server.kademlia.read().len();
1965
1966 let request_bytes = json::struct_to_bytes(&AnnounceV2 { peer_self: announcing_peer, proof_payload: vec![] }).expect("encode V2");
1968
1969 let (response_kind, _gatherer) = server
1970 .dispatch_network_payload_x_AnnounceV2(CancellationToken::new(), PayloadRequestKind::AnnounceV2, request_bytes)
1971 .await
1972 .expect("valid V2 must succeed on mem-transport");
1973
1974 assert_eq!(response_kind, PayloadResponseKind::AnnounceResponseV2);
1975 assert_eq!(server.kademlia.read().len(), baseline_len + 1, "announcer should now be in Kademlia");
1976
1977 let (peers, _) = server.kademlia.read().get_peers_for_key(&announcing_peer_id, 8);
1979 assert!(peers.iter().any(|p| p.id == announcing_peer_id), "announcing peer must be reachable through get_peers_for_key");
1980 }
1981
1982 #[tokio::test]
1983 async fn v2_bails_and_does_not_admit_when_proof_fails() {
1984 let server = make_test_server().await;
1985 let pow_generator = SingleThreadedPowGenerator::new();
1986 let announcing_peer = make_announcing_peer(server.runtime_services.time_provider.as_ref(), &pow_generator, "8888").await;
1987 let announcing_peer_id = announcing_peer.id;
1988
1989 let baseline_len = server.kademlia.read().len();
1990
1991 let request_bytes = json::struct_to_bytes(&AnnounceV2 { peer_self: announcing_peer, proof_payload: vec![1, 2, 3, 4] }).expect("encode V2");
1995
1996 let result = server
1997 .dispatch_network_payload_x_AnnounceV2(CancellationToken::new(), PayloadRequestKind::AnnounceV2, request_bytes)
1998 .await;
1999
2000 assert!(result.is_err(), "failed proof must bail so the dispatcher wraps it as ErrorResponseV1");
2001 assert_eq!(server.kademlia.read().len(), baseline_len, "no Kademlia mutation on failed proof");
2002
2003 let (peers, _) = server.kademlia.read().get_peers_for_key(&announcing_peer_id, 8);
2005 assert!(!peers.iter().any(|p| p.id == announcing_peer_id), "rejected peer must not be reachable through get_peers_for_key");
2006 }
2007
2008 #[tokio::test]
2011 async fn retired_announce_v1_is_rejected() -> anyhow::Result<()> {
2012 use hashiverse_lib::protocol::rpc::rpc_request::{RpcRequestPacketTx, RpcRequestPacketTxFlags};
2013
2014 let server = make_test_server().await;
2015 let time_provider = server.runtime_services.time_provider.clone();
2016 let pow_generator = SingleThreadedPowGenerator::new();
2017 let announcing_peer = make_announcing_peer(time_provider.as_ref(), &pow_generator, "7777").await;
2018 let announcing_peer_id = announcing_peer.id;
2019 let baseline_len = server.kademlia.read().len();
2020
2021 #[derive(serde::Serialize)]
2023 struct LegacyAnnounceV1 {
2024 peer_self: Peer,
2025 }
2026 let legacy_payload = json::struct_to_bytes(&LegacyAnnounceV1 { peer_self: announcing_peer })?;
2027
2028 let rpc_request_packet_tx = RpcRequestPacketTx::encode(
2029 time_provider.as_ref(),
2030 config::POW_MINIMUM_PER_RPC_SERVER_KNOWN,
2031 RpcRequestPacketTxFlags::SERVER_KNOWN,
2032 PayloadRequestKind::RetiredAnnounceV1,
2033 &server.server_id.id,
2034 &server.server_id.keys.verification_key_bytes,
2035 &server.server_id.keys.pq_commitment_bytes,
2036 legacy_payload,
2037 &pow_generator,
2038 )
2039 .await?;
2040 let rpc_request_packet_rx = RpcRequestPacketRx::decode(&time_provider.current_time_millis(), &server.server_id.keys.verification_key_bytes, &server.server_id.keys.pq_commitment_bytes, rpc_request_packet_tx.bytes)?;
2041 assert_eq!(rpc_request_packet_rx.payload_request_kind, PayloadRequestKind::RetiredAnnounceV1);
2042
2043 let error = server.dispatch_network_envelope(CancellationToken::new(), None, rpc_request_packet_rx).await.err().expect("retired AnnounceV1 must be rejected");
2044 assert!(error.to_string().contains("retired"), "error={}", error);
2045
2046 assert_eq!(server.kademlia.read().len(), baseline_len, "no Kademlia mutation from a retired announce");
2047 let (peers, _) = server.kademlia.read().get_peers_for_key(&announcing_peer_id, 8);
2048 assert!(!peers.iter().any(|p| p.id == announcing_peer_id));
2049 Ok(())
2050 }
2051 }
2052
2053 mod heal_donor_plausibility {
2054 use super::*;
2055
2056 fn id_with_leading_agreement_bits(location_id: &Id, leading_agreement_bits: usize) -> Id {
2058 let mut bytes = location_id.as_ref().to_vec();
2059 bytes[leading_agreement_bits / 8] ^= 0x80 >> (leading_agreement_bits % 8);
2060 Id::from_slice(&bytes).expect("32 bytes")
2061 }
2062
2063 #[test]
2064 fn id_helper_produces_requested_agreement() {
2065 let location_id = Id::random();
2066 for leading_agreement_bits in [0usize, 1, 7, 8, 13, 40] {
2067 let id = id_with_leading_agreement_bits(&location_id, leading_agreement_bits);
2068 assert_eq!(tools::leading_agreement_bits_xor(location_id.as_ref(), id.as_ref()), leading_agreement_bits as LeadingAgreementBits);
2069 }
2070 }
2071
2072 #[test]
2073 fn accepts_anyone_when_we_know_too_few_servers() {
2074 let location_id = Id::random();
2075 let nearest_peer_ids = vec![id_with_leading_agreement_bits(&location_id, 20), id_with_leading_agreement_bits(&location_id, 21)];
2076 let far_donor_id = id_with_leading_agreement_bits(&location_id, 0);
2077 assert!(is_plausible_heal_donor(&far_donor_id, &location_id, &nearest_peer_ids, 3));
2078 }
2079
2080 #[test]
2081 fn accepts_donors_within_slack_of_furthest_nearest_and_refuses_beyond() {
2082 let location_id = Id::random();
2083 let nearest_peer_ids = vec![
2085 id_with_leading_agreement_bits(&location_id, 12),
2086 id_with_leading_agreement_bits(&location_id, 14),
2087 id_with_leading_agreement_bits(&location_id, 10),
2088 ];
2089 let slack = config::HEAL_DONOR_LEADING_AGREEMENT_BITS_SLACK as usize;
2090
2091 assert!(is_plausible_heal_donor(&id_with_leading_agreement_bits(&location_id, 10), &location_id, &nearest_peer_ids, 3));
2093 assert!(is_plausible_heal_donor(&id_with_leading_agreement_bits(&location_id, 30), &location_id, &nearest_peer_ids, 3));
2094
2095 assert!(is_plausible_heal_donor(&id_with_leading_agreement_bits(&location_id, 10 - slack), &location_id, &nearest_peer_ids, 3));
2097
2098 assert!(!is_plausible_heal_donor(&id_with_leading_agreement_bits(&location_id, 10 - slack - 1), &location_id, &nearest_peer_ids, 3));
2100 assert!(!is_plausible_heal_donor(&id_with_leading_agreement_bits(&location_id, 0), &location_id, &nearest_peer_ids, 3));
2101 }
2102 }
2103
2104 mod submit_post_claim {
2105 use super::*;
2106 use crate::environment::environment::EnvironmentFactory;
2107 use crate::environment::mem_environment_store::MemEnvironmentFactory;
2108 use crate::server::args::Args;
2109 use hashiverse_lib::client::key_locker::key_locker::{KeyLocker, KeyLockerManager};
2110 use hashiverse_lib::client::key_locker::mem_key_locker::MemKeyLockerManager;
2111 use hashiverse_lib::tools::pow_generator::single_threaded_pow_generator::SingleThreadedPowGenerator;
2112 use hashiverse_lib::tools::runtime_services::RuntimeServices;
2113 use hashiverse_lib::tools::time::MILLIS_IN_MINUTE;
2114 use hashiverse_lib::tools::time_provider::time_provider::{RealTimeProvider, TimeProvider};
2115 use hashiverse_lib::transport::mem_transport::MemTransportFactory;
2116 use std::sync::Arc;
2117
2118 #[tokio::test]
2122 async fn one_minute_bucket_is_rejected_for_insufficient_pow_not_panic() -> anyhow::Result<()> {
2123 let runtime_services = Arc::new(RuntimeServices { time_provider: Arc::new(RealTimeProvider), transport_factory: MemTransportFactory::default(), pow_generator: Arc::new(SingleThreadedPowGenerator::new()) });
2124 let server = HashiverseServer::new(runtime_services, Arc::new(MemEnvironmentFactory::new("")), Args::default_for_testing()).await?;
2125
2126 let key_locker_manager = MemKeyLockerManager::new().await?;
2127 let key_locker: Arc<dyn KeyLocker> = key_locker_manager.create("one minute keyphrase".to_string()).await?;
2128 let time_millis_now = RealTimeProvider.current_time_millis();
2129 let bucket_location = BucketLocation::new(BucketType::User, key_locker.client_id().id, MILLIS_IN_MINUTE, time_millis_now)?;
2130
2131 let mut encoded_post = EncodedPostV1::new(key_locker.client_id(), time_millis_now, vec![bucket_location.base_id], "one minute post");
2132 let encoded_post_bytes = encoded_post.encode_to_bytes_direct(&key_locker).await?;
2133 let request_bytes = SubmitPostClaimV1::new_to_bytes(&bucket_location, None, &[], encoded_post_bytes.bytes_without_body())?;
2134
2135 let result = catch_panic_as_error(server.dispatch_network_payload_x_SubmitPostClaimV1(CancellationToken::new(), Some(PeerPow::zero()), PayloadRequestKind::SubmitPostClaimV1, request_bytes)).await?;
2136 let error_message = result.err().expect("zero PoW must be rejected").to_string();
2137 assert!(error_message.contains("Insufficient proof of work"), "error_message={}", error_message);
2138 Ok(())
2139 }
2140 }
2141
2142 mod heal_post_bundle {
2143 use super::*;
2144 use crate::environment::environment::EnvironmentFactory;
2145 use crate::environment::mem_environment_store::MemEnvironmentFactory;
2146 use crate::server::args::Args;
2147 use crate::server::hashiverse_server::HashiverseServer;
2148 use hashiverse_lib::client::key_locker::key_locker::{KeyLocker, KeyLockerManager};
2149 use hashiverse_lib::client::key_locker::mem_key_locker::MemKeyLockerManager;
2150 use hashiverse_lib::protocol::peer::Peer;
2151 use hashiverse_lib::protocol::posting::encoded_post_feedback::{self, EncodedPostFeedbackV1};
2152 use hashiverse_lib::tools::pow;
2153 use hashiverse_lib::tools::types::Salt;
2154 use hashiverse_lib::tools::pow_generator::single_threaded_pow_generator::SingleThreadedPowGenerator;
2155 use hashiverse_lib::tools::runtime_services::RuntimeServices;
2156 use hashiverse_lib::tools::server_id::ServerId;
2157 use hashiverse_lib::tools::time::MILLIS_IN_MONTH;
2158 use hashiverse_lib::tools::time_provider::time_provider::{RealTimeProvider, TimeProvider};
2159 use hashiverse_lib::tools::types::Pow;
2160 use hashiverse_lib::transport::mem_transport::MemTransportFactory;
2161 use std::sync::Arc;
2162
2163 pub(super) struct HealFixture {
2164 pub(super) server: Arc<HashiverseServer>,
2165 pub(super) key_locker: Arc<dyn KeyLocker>,
2166 pub(super) bucket_location: BucketLocation,
2167 pub(super) time_millis_now: TimeMillis,
2168 }
2169
2170 impl HealFixture {
2171 pub(super) async fn new() -> anyhow::Result<Self> {
2172 let time_provider = Arc::new(RealTimeProvider);
2173 let transport_factory = MemTransportFactory::default();
2174 let pow_generator = Arc::new(SingleThreadedPowGenerator::new());
2175 let runtime_services = Arc::new(RuntimeServices { time_provider, transport_factory, pow_generator });
2176 let environment_factory = Arc::new(MemEnvironmentFactory::new(""));
2177 let server = HashiverseServer::new(runtime_services, environment_factory, Args::default_for_testing()).await?;
2178
2179 let key_locker_manager = MemKeyLockerManager::new().await?;
2180 let key_locker: Arc<dyn KeyLocker> = key_locker_manager.create("heal keyphrase".to_string()).await?;
2181 let time_millis_now = RealTimeProvider.current_time_millis();
2182 let bucket_location = BucketLocation::new(BucketType::User, key_locker.client_id().id, MILLIS_IN_MONTH, time_millis_now)?;
2183 Ok(Self { server, key_locker, bucket_location, time_millis_now })
2184 }
2185
2186 pub(super) async fn make_server_id(pow_min: Pow) -> anyhow::Result<ServerId> {
2187 ServerId::new("heal_donor", &RealTimeProvider, pow_min, true, &SingleThreadedPowGenerator::new()).await
2188 }
2189
2190 async fn make_post_by(key_locker: &Arc<dyn KeyLocker>, timestamp: TimeMillis, linked_base_ids: Vec<Id>) -> anyhow::Result<(Id, Bytes)> {
2191 let mut encoded_post = EncodedPostV1::new(key_locker.client_id(), timestamp, linked_base_ids, "healed post");
2192 let encoded_post_bytes = encoded_post.encode_to_bytes_direct(key_locker).await?;
2193 Ok((encoded_post.post_id, Bytes::copy_from_slice(encoded_post_bytes.bytes())))
2194 }
2195
2196 pub(super) async fn make_own_post(&self) -> anyhow::Result<(Id, Bytes)> {
2197 Self::make_post_by(&self.key_locker, self.time_millis_now, vec![self.bucket_location.base_id]).await
2198 }
2199
2200 pub(super) fn make_donor_header(&self, donor_server_id: &ServerId, posts: &[(Id, Bytes)], customise: impl FnOnce(&mut EncodedPostBundleHeaderV1)) -> anyhow::Result<EncodedPostBundleHeaderV1> {
2201 let mut donor_header = EncodedPostBundleHeaderV1 {
2202 time_millis: self.time_millis_now,
2203 location_id: self.bucket_location.location_id,
2204 overflowed: false,
2205 sealed: false,
2206 num_posts: posts.len() as u8,
2207 encoded_post_ids: posts.iter().map(|(post_id, _)| *post_id).collect(),
2208 encoded_post_lengths: posts.iter().map(|(_, post_bytes)| post_bytes.len()).collect(),
2209 encoded_post_healed: HashSet::new(),
2210 peer: donor_server_id.to_peer(&RealTimeProvider)?,
2211 signature: Signature::zero(),
2212 };
2213 customise(&mut donor_header);
2214 donor_header.signature_generate(&donor_server_id.keys.signature_key)?;
2215 Ok(donor_header)
2216 }
2217
2218 async fn claim(&self, donor_header: &EncodedPostBundleHeaderV1) -> anyhow::Result<HealPostBundleClaimResponseV1> {
2219 let request_bytes = HealPostBundleClaimV1::new_to_bytes(donor_header, &self.bucket_location)?;
2220 let (_, bytes_gatherer) = self.server.dispatch_network_payload_x_HealPostBundleClaimV1(CancellationToken::new(), PayloadRequestKind::HealPostBundleClaimV1, request_bytes).await?;
2221 json::bytes_to_struct::<HealPostBundleClaimResponseV1>(&bytes_gatherer.to_bytes())
2222 }
2223
2224 async fn heal(&self, donor_header: &EncodedPostBundleHeaderV1, posts: &[(Id, Bytes)]) -> anyhow::Result<bool> {
2226 let claim_response = self.claim(donor_header).await?;
2227 let token = claim_response.token.ok_or_else(|| anyhow::anyhow!("no claim token"))?;
2228 let encoded_posts_bytes = Self::encoded_posts_bytes_for_token(&token, posts)?;
2229 self.commit(&token, donor_header, &encoded_posts_bytes).await
2230 }
2231
2232 fn encoded_posts_bytes_for_token(token: &HealPostBundleClaimTokenV1, posts: &[(Id, Bytes)]) -> anyhow::Result<Vec<u8>> {
2234 let mut encoded_posts_bytes = Vec::new();
2235 for needed_post_id in &token.needed_post_ids {
2236 let (_, post_bytes) = posts.iter().find(|(post_id, _)| post_id == needed_post_id).ok_or_else(|| anyhow::anyhow!("missing post"))?;
2237 encoded_posts_bytes.extend_from_slice(post_bytes);
2238 }
2239 Ok(encoded_posts_bytes)
2240 }
2241
2242 async fn commit(&self, token: &HealPostBundleClaimTokenV1, donor_header: &EncodedPostBundleHeaderV1, encoded_posts_bytes: &[u8]) -> anyhow::Result<bool> {
2243 let request_bytes = HealPostBundleCommitV1::new_to_bytes(token, donor_header, encoded_posts_bytes)?;
2244 let (_, bytes_gatherer) = self.server.dispatch_network_payload_x_HealPostBundleCommitV1(CancellationToken::new(), PayloadRequestKind::HealPostBundleCommitV1, request_bytes).await?;
2245 Ok(json::bytes_to_struct::<HealPostBundleCommitResponseV1>(&bytes_gatherer.to_bytes())?.accepted)
2246 }
2247
2248 fn stored_bundle(&self) -> anyhow::Result<EncodedPostBundleV1> {
2249 let post_bundle_bytes = self.server.environment.get_post_bundle_bytes(self.time_millis_now, &self.bucket_location.location_id)?.ok_or_else(|| anyhow::anyhow!("no stored bundle"))?;
2250 EncodedPostBundleV1::from_bytes(post_bundle_bytes, true)
2251 }
2252
2253 async fn make_server_id_at_distance(&self, predicate: impl Fn(LeadingAgreementBits) -> bool) -> anyhow::Result<ServerId> {
2254 loop {
2255 let server_id = Self::make_server_id(config::SERVER_KEY_POW_MIN).await?;
2256 if predicate(tools::leading_agreement_bits_xor(self.bucket_location.location_id.as_ref(), server_id.id.as_ref())) {
2257 return Ok(server_id);
2258 }
2259 }
2260 }
2261
2262 async fn add_near_servers_to_kademlia(&self, count: usize, minimum_leading_agreement_bits: LeadingAgreementBits) -> anyhow::Result<()> {
2264 for _ in 0..count {
2265 let server_id = self.make_server_id_at_distance(|leading_agreement_bits| leading_agreement_bits >= minimum_leading_agreement_bits).await?;
2266 let peer: Peer = server_id.to_peer(&RealTimeProvider)?;
2267 self.server.kademlia.write().add_peer(peer, self.time_millis_now)?;
2268 }
2269 Ok(())
2270 }
2271 }
2272
2273 async fn heal_feedbacks(fixture: &HealFixture, encoded_post_feedbacks: &[EncodedPostFeedbackV1]) -> anyhow::Result<u32> {
2274 let request_bytes = HealPostBundleFeedbackV1::new_to_bytes(&fixture.bucket_location.location_id, encoded_post_feedbacks)?;
2275 let (_, bytes_gatherer) = fixture.server.dispatch_network_payload_x_HealPostBundleFeedbackV1(CancellationToken::new(), PayloadRequestKind::HealPostBundleFeedbackV1, request_bytes).await?;
2276 Ok(json::bytes_to_struct::<HealPostBundleFeedbackResponseV1>(&bytes_gatherer.to_bytes())?.accepted_count)
2277 }
2278
2279 fn stored_feedbacks(fixture: &HealFixture) -> anyhow::Result<Vec<EncodedPostFeedbackV1>> {
2280 let feedbacks_bytes = fixture.server.environment.get_post_bundle_encoded_post_feedbacks_bytes(fixture.time_millis_now, &fixture.bucket_location.location_id)?;
2281 feedbacks_bytes.chunks_exact(encoded_post_feedback::ENTRY_SIZE).map(EncodedPostFeedbackV1::decode_from_bytes).collect()
2282 }
2283
2284 fn make_valid_feedback(post_id: Id, feedback_type: u8) -> anyhow::Result<EncodedPostFeedbackV1> {
2285 let salt = Salt::random();
2286 let (measured_pow, _) = pow::pow_measure(&[post_id.as_bytes(), &[feedback_type]], &salt)?;
2287 Ok(EncodedPostFeedbackV1::new(post_id, feedback_type, salt, measured_pow))
2288 }
2289
2290 async fn fixture_with_stored_post() -> anyhow::Result<(HealFixture, Id)> {
2292 let fixture = HealFixture::new().await?;
2293 let donor_server_id = HealFixture::make_server_id(config::SERVER_KEY_POW_MIN).await?;
2294 let posts = vec![fixture.make_own_post().await?];
2295 let donor_header = fixture.make_donor_header(&donor_server_id, &posts, |_| {})?;
2296 assert!(fixture.heal(&donor_header, &posts).await?);
2297 let post_id = posts[0].0;
2298 Ok((fixture, post_id))
2299 }
2300
2301 #[tokio::test]
2303 async fn heal_feedback_rejects_forged_pow() -> anyhow::Result<()> {
2304 let (fixture, post_id) = fixture_with_stored_post().await?;
2305 let forged_feedback = EncodedPostFeedbackV1::new(post_id, 3, Salt::random(), Pow(255));
2306 assert!(forged_feedback.pow_verify().is_err());
2307
2308 assert_eq!(heal_feedbacks(&fixture, &[forged_feedback]).await?, 0);
2309 assert!(stored_feedbacks(&fixture)?.is_empty(), "forged feedback must not be stored");
2310 Ok(())
2311 }
2312
2313 #[tokio::test]
2314 async fn heal_feedback_accepts_valid_pow_and_skips_forged_in_same_request() -> anyhow::Result<()> {
2315 let (fixture, post_id) = fixture_with_stored_post().await?;
2316 let valid_feedback = make_valid_feedback(post_id, 1)?;
2317 let forged_feedback = EncodedPostFeedbackV1::new(post_id, 2, Salt::random(), Pow(255));
2318
2319 assert_eq!(heal_feedbacks(&fixture, &[forged_feedback, valid_feedback.clone()]).await?, 1);
2320 assert_eq!(stored_feedbacks(&fixture)?, vec![valid_feedback]);
2321 Ok(())
2322 }
2323
2324 #[tokio::test]
2327 async fn failed_commit_releases_heal_claim() -> anyhow::Result<()> {
2328 let fixture = HealFixture::new().await?;
2329 let donor_server_id = HealFixture::make_server_id(config::SERVER_KEY_POW_MIN).await?;
2330 let posts = vec![fixture.make_own_post().await?];
2331 let donor_header = fixture.make_donor_header(&donor_server_id, &posts, |_| {})?;
2332
2333 let token = fixture.claim(&donor_header).await?.token.expect("first claim must get a token");
2334 assert!(fixture.claim(&donor_header).await?.token.is_none(), "claim must be held while a heal is in progress");
2335
2336 let mut encoded_posts_bytes = HealFixture::encoded_posts_bytes_for_token(&token, &posts)?;
2337 encoded_posts_bytes.push(0u8);
2338 let commit_error = fixture.commit(&token, &donor_header, &encoded_posts_bytes).await.expect_err("commit with excess bytes must fail");
2339 assert!(commit_error.to_string().contains("excess bytes"), "commit_error={}", commit_error);
2340
2341 assert!(fixture.claim(&donor_header).await?.token.is_some(), "a failed commit must release the claim");
2342 Ok(())
2343 }
2344
2345 #[tokio::test]
2347 async fn commit_with_foreign_token_does_not_release_heal_claim() -> anyhow::Result<()> {
2348 let fixture = HealFixture::new().await?;
2349 let donor_server_id = HealFixture::make_server_id(config::SERVER_KEY_POW_MIN).await?;
2350 let posts = vec![fixture.make_own_post().await?];
2351 let donor_header = fixture.make_donor_header(&donor_server_id, &posts, |_| {})?;
2352
2353 let genuine_token = fixture.claim(&donor_header).await?.token.expect("first claim must get a token");
2354
2355 let foreign_server_id = HealFixture::make_server_id(config::SERVER_KEY_POW_MIN).await?;
2356 let foreign_token = HealPostBundleClaimTokenV1::new(foreign_server_id.to_peer(&RealTimeProvider)?, fixture.bucket_location.clone(), genuine_token.needed_post_ids.clone(), donor_header.signature, &foreign_server_id.keys.signature_key);
2357 let encoded_posts_bytes = HealFixture::encoded_posts_bytes_for_token(&foreign_token, &posts)?;
2358 let commit_error = fixture.commit(&foreign_token, &donor_header, &encoded_posts_bytes).await.expect_err("commit with a foreign token must fail");
2359 assert!(commit_error.to_string().contains("not issued by this server"), "commit_error={}", commit_error);
2360
2361 assert!(fixture.claim(&donor_header).await?.token.is_none(), "a foreign token must not release the claim");
2362 Ok(())
2363 }
2364
2365 #[tokio::test]
2366 async fn honest_heal_succeeds_and_does_not_adopt_donor_flags() -> anyhow::Result<()> {
2367 let fixture = HealFixture::new().await?;
2368 let donor_server_id = HealFixture::make_server_id(config::SERVER_KEY_POW_MIN).await?;
2369 let posts = vec![fixture.make_own_post().await?];
2370 let donor_header = fixture.make_donor_header(&donor_server_id, &posts, |donor_header| {
2372 donor_header.sealed = true;
2373 donor_header.overflowed = true;
2374 })?;
2375
2376 assert!(fixture.heal(&donor_header, &posts).await?);
2377
2378 let stored_bundle = fixture.stored_bundle()?;
2379 assert_eq!(stored_bundle.header.encoded_post_ids, vec![posts[0].0]);
2380 assert!(stored_bundle.header.encoded_post_healed.contains(&posts[0].0));
2381 assert!(!stored_bundle.header.sealed, "donor's sealed flag must not be adopted");
2382 assert!(!stored_bundle.header.overflowed, "donor's overflowed flag must not be adopted");
2383 stored_bundle.verify_for_bucket(&fixture.bucket_location, fixture.time_millis_now)?;
2384 Ok(())
2385 }
2386
2387 #[tokio::test]
2388 async fn stand_in_donor_whose_posts_are_all_healed_is_accepted() -> anyhow::Result<()> {
2389 let fixture = HealFixture::new().await?;
2390 let donor_server_id = HealFixture::make_server_id(config::SERVER_KEY_POW_MIN).await?;
2391 let posts = vec![fixture.make_own_post().await?, fixture.make_own_post().await?];
2392 let donor_header = fixture.make_donor_header(&donor_server_id, &posts, |donor_header| {
2393 donor_header.encoded_post_healed = donor_header.encoded_post_ids.iter().copied().collect();
2394 })?;
2395
2396 assert!(fixture.heal(&donor_header, &posts).await?);
2397 assert_eq!(fixture.stored_bundle()?.header.num_posts, 2);
2398 Ok(())
2399 }
2400
2401 #[tokio::test]
2402 async fn donor_without_server_pow_is_refused() -> anyhow::Result<()> {
2403 let fixture = HealFixture::new().await?;
2404 let unworked_server_id = loop {
2405 let server_id = HealFixture::make_server_id(Pow(0)).await?;
2406 if server_id.pow < config::SERVER_KEY_POW_MIN {
2407 break server_id;
2408 }
2409 };
2410 let posts = vec![fixture.make_own_post().await?];
2411 let donor_header = fixture.make_donor_header(&unworked_server_id, &posts, |_| {})?;
2412 donor_header.verify()?; assert!(fixture.claim(&donor_header).await.is_err());
2415 assert!(fixture.stored_bundle().is_err(), "nothing must have been stored");
2416 Ok(())
2417 }
2418
2419 #[tokio::test]
2420 async fn donor_with_future_timestamp_is_refused() -> anyhow::Result<()> {
2421 let fixture = HealFixture::new().await?;
2422 let donor_server_id = HealFixture::make_server_id(config::SERVER_KEY_POW_MIN).await?;
2423 let posts = vec![fixture.make_own_post().await?];
2424 let donor_header = fixture.make_donor_header(&donor_server_id, &posts, |donor_header| {
2425 donor_header.time_millis = donor_header.time_millis + config::CLIENT_POST_TIMESTAMP_DELTA_THRESHOLD + MILLIS_IN_SECOND;
2426 })?;
2427
2428 assert!(fixture.claim(&donor_header).await.is_err());
2429 Ok(())
2430 }
2431
2432 #[tokio::test]
2433 async fn future_post_is_judged_against_our_clock_not_a_future_donor_clock() -> anyhow::Result<()> {
2434 let fixture = HealFixture::new().await?;
2435 let donor_server_id = HealFixture::make_server_id(config::SERVER_KEY_POW_MIN).await?;
2436
2437 let donor_time_millis = fixture.time_millis_now + config::CLIENT_POST_TIMESTAMP_DELTA_THRESHOLD - MILLIS_IN_SECOND;
2440 let future_post_time_millis = fixture.time_millis_now + config::CLIENT_POST_TIMESTAMP_DELTA_THRESHOLD + MILLIS_IN_SECOND.const_mul(60);
2441 if BucketLocation::round_down_to_bucket_start(future_post_time_millis, fixture.bucket_location.duration) != fixture.bucket_location.bucket_time_millis {
2442 return Ok(()); }
2444 let future_post = HealFixture::make_post_by(&fixture.key_locker, future_post_time_millis, vec![fixture.bucket_location.base_id]).await?;
2445 let posts = vec![future_post];
2446 let donor_header = fixture.make_donor_header(&donor_server_id, &posts, |donor_header| {
2447 donor_header.time_millis = donor_time_millis;
2448 })?;
2449
2450 assert!(fixture.heal(&donor_header, &posts).await.is_err());
2451 assert!(fixture.stored_bundle().is_err(), "nothing must have been stored");
2452 Ok(())
2453 }
2454
2455 #[tokio::test]
2456 async fn far_donor_is_refused_but_dead_near_donor_is_accepted() -> anyhow::Result<()> {
2457 let fixture = HealFixture::new().await?;
2458 let slack = config::HEAL_DONOR_LEADING_AGREEMENT_BITS_SLACK;
2459 let near_leading_agreement_bits = slack + 2;
2460
2461 fixture.add_near_servers_to_kademlia(config::REDUNDANT_SERVERS_PER_POST, near_leading_agreement_bits).await?;
2463 let posts = vec![fixture.make_own_post().await?];
2464
2465 let far_donor_server_id = fixture.make_server_id_at_distance(|leading_agreement_bits| leading_agreement_bits < near_leading_agreement_bits - slack).await?;
2467 let far_donor_header = fixture.make_donor_header(&far_donor_server_id, &posts, |_| {})?;
2468 assert!(fixture.claim(&far_donor_header).await.is_err());
2469
2470 let dead_near_donor_server_id = fixture.make_server_id_at_distance(|leading_agreement_bits| leading_agreement_bits >= near_leading_agreement_bits).await?;
2472 assert!(!fixture.server.kademlia.read().get_all_peers().iter().any(|peer| peer.id == dead_near_donor_server_id.id));
2473 let dead_near_donor_header = fixture.make_donor_header(&dead_near_donor_server_id, &posts, |_| {})?;
2474 fixture.claim(&dead_near_donor_header).await?;
2476 Ok(())
2477 }
2478
2479 #[tokio::test]
2480 async fn out_of_bucket_post_is_refused() -> anyhow::Result<()> {
2481 let fixture = HealFixture::new().await?;
2482 let donor_server_id = HealFixture::make_server_id(config::SERVER_KEY_POW_MIN).await?;
2483 let old_post = HealFixture::make_post_by(&fixture.key_locker, fixture.bucket_location.bucket_time_millis - MILLIS_IN_SECOND, vec![fixture.bucket_location.base_id]).await?;
2485 let posts = vec![old_post];
2486 let donor_header = fixture.make_donor_header(&donor_server_id, &posts, |_| {})?;
2487
2488 assert!(fixture.heal(&donor_header, &posts).await.is_err());
2489 assert!(fixture.stored_bundle().is_err(), "nothing must have been stored");
2490 Ok(())
2491 }
2492
2493 #[tokio::test]
2494 async fn foreign_author_in_user_bucket_is_refused() -> anyhow::Result<()> {
2495 let fixture = HealFixture::new().await?;
2496 let donor_server_id = HealFixture::make_server_id(config::SERVER_KEY_POW_MIN).await?;
2497
2498 let attacker_key_locker_manager = MemKeyLockerManager::new().await?;
2500 let attacker_key_locker: Arc<dyn KeyLocker> = attacker_key_locker_manager.create("attacker keyphrase".to_string()).await?;
2501 let foreign_post = HealFixture::make_post_by(&attacker_key_locker, fixture.time_millis_now, vec![fixture.bucket_location.base_id]).await?;
2502 let posts = vec![foreign_post];
2503 let donor_header = fixture.make_donor_header(&donor_server_id, &posts, |_| {})?;
2504
2505 assert!(fixture.heal(&donor_header, &posts).await.is_err());
2506 assert!(fixture.stored_bundle().is_err(), "nothing must have been stored");
2507 Ok(())
2508 }
2509 }
2510
2511 mod cache_upload {
2513 use super::*;
2514 use super::heal_post_bundle::HealFixture;
2515 use crate::server::post_bundle_caching_shared::CACHE_HIT_THRESHOLD;
2516 use hashiverse_lib::protocol::posting::encoded_post_feedback::EncodedPostFeedbackV1;
2517 use hashiverse_lib::tools::pow;
2518 use hashiverse_lib::tools::server_id::ServerId;
2519 use hashiverse_lib::tools::time::MILLIS_IN_MINUTE;
2520 use hashiverse_lib::tools::time_provider::time_provider::RealTimeProvider;
2521 use hashiverse_lib::tools::types::{Pow, Salt};
2522
2523 async fn make_unworked_server_id() -> anyhow::Result<ServerId> {
2524 loop {
2525 let server_id = HealFixture::make_server_id(Pow(0)).await?;
2526 if server_id.pow < config::SERVER_KEY_POW_MIN {
2527 return Ok(server_id);
2528 }
2529 }
2530 }
2531
2532 fn make_post_bundle_bytes(fixture: &HealFixture, server_id: &ServerId, posts: &[(Id, Bytes)], customise: impl FnOnce(&mut EncodedPostBundleHeaderV1)) -> anyhow::Result<Bytes> {
2534 let header = fixture.make_donor_header(server_id, posts, customise)?;
2535 let encoded_posts_bytes: Vec<u8> = posts.iter().flat_map(|(_, post_bytes)| post_bytes.iter().copied()).collect();
2536 EncodedPostBundleV1 { header, encoded_posts_bytes: Bytes::from(encoded_posts_bytes) }.to_bytes()
2537 }
2538
2539 async fn upload_post_bundle(fixture: &HealFixture, encoded_post_bundle_bytes: &Bytes) -> anyhow::Result<bool> {
2541 let peer_self = fixture.server.peer_self.read().clone();
2542 let mut cache_request_token = None;
2543 for _ in 0..CACHE_HIT_THRESHOLD {
2544 cache_request_token = cache_request_token.or(fixture.server.post_bundle_cache.on_get(&fixture.bucket_location, &[], &peer_self, &fixture.server.server_id, fixture.time_millis_now).cache_request_token);
2545 }
2546 let cache_request_token = cache_request_token.ok_or_else(|| anyhow::anyhow!("no cache request token issued"))?;
2547 let request_bytes = CachePostBundleV1::new_to_bytes(&cache_request_token, &[encoded_post_bundle_bytes.as_ref()])?;
2548 let (_, bytes_gatherer) = fixture.server.dispatch_network_payload_x_CachePostBundleV1(CancellationToken::new(), PayloadRequestKind::CachePostBundleV1, request_bytes).await?;
2549 Ok(json::bytes_to_struct::<CachePostBundleResponseV1>(&bytes_gatherer.to_bytes())?.accepted)
2550 }
2551
2552 fn cached_post_bundle_count_at(fixture: &HealFixture, time_millis: TimeMillis) -> usize {
2553 let peer_self = fixture.server.peer_self.read().clone();
2554 fixture.server.post_bundle_cache.on_get(&fixture.bucket_location, &[], &peer_self, &fixture.server.server_id, time_millis).cached_items.len()
2555 }
2556
2557 #[tokio::test]
2558 async fn genuine_bundle_is_cached() -> anyhow::Result<()> {
2559 let fixture = HealFixture::new().await?;
2560 let server_id = HealFixture::make_server_id(config::SERVER_KEY_POW_MIN).await?;
2561 let posts = vec![fixture.make_own_post().await?];
2562 assert!(upload_post_bundle(&fixture, &make_post_bundle_bytes(&fixture, &server_id, &posts, |_| {})?).await?);
2563 assert_eq!(cached_post_bundle_count_at(&fixture, fixture.time_millis_now), 1);
2564 Ok(())
2565 }
2566
2567 #[tokio::test]
2568 async fn empty_bundle_is_rejected() -> anyhow::Result<()> {
2569 let fixture = HealFixture::new().await?;
2570 let server_id = HealFixture::make_server_id(config::SERVER_KEY_POW_MIN).await?;
2571 assert!(!upload_post_bundle(&fixture, &make_post_bundle_bytes(&fixture, &server_id, &[], |_| {})?).await?);
2572 assert_eq!(cached_post_bundle_count_at(&fixture, fixture.time_millis_now), 0);
2573 Ok(())
2574 }
2575
2576 #[tokio::test]
2577 async fn unworked_publisher_is_rejected() -> anyhow::Result<()> {
2578 let fixture = HealFixture::new().await?;
2579 let unworked_server_id = make_unworked_server_id().await?;
2580 let posts = vec![fixture.make_own_post().await?];
2581 assert!(!upload_post_bundle(&fixture, &make_post_bundle_bytes(&fixture, &unworked_server_id, &posts, |_| {})?).await?);
2582 assert_eq!(cached_post_bundle_count_at(&fixture, fixture.time_millis_now), 0);
2583 Ok(())
2584 }
2585
2586 #[tokio::test]
2587 async fn bundle_for_other_location_is_rejected() -> anyhow::Result<()> {
2588 let fixture = HealFixture::new().await?;
2589 let server_id = HealFixture::make_server_id(config::SERVER_KEY_POW_MIN).await?;
2590 let posts = vec![fixture.make_own_post().await?];
2591 let encoded_post_bundle_bytes = make_post_bundle_bytes(&fixture, &server_id, &posts, |header| header.location_id = Id::random())?;
2592 assert!(!upload_post_bundle(&fixture, &encoded_post_bundle_bytes).await?);
2593 assert_eq!(cached_post_bundle_count_at(&fixture, fixture.time_millis_now), 0);
2594 Ok(())
2595 }
2596
2597 #[tokio::test]
2598 async fn self_asserted_sealed_is_not_pinned() -> anyhow::Result<()> {
2599 let fixture = HealFixture::new().await?;
2600 let server_id = HealFixture::make_server_id(config::SERVER_KEY_POW_MIN).await?;
2601 let posts = vec![fixture.make_own_post().await?];
2602 let encoded_post_bundle_bytes = make_post_bundle_bytes(&fixture, &server_id, &posts, |header| header.sealed = true)?;
2604 assert!(upload_post_bundle(&fixture, &encoded_post_bundle_bytes).await?);
2605 assert_eq!(cached_post_bundle_count_at(&fixture, fixture.time_millis_now), 1);
2606 assert_eq!(cached_post_bundle_count_at(&fixture, fixture.time_millis_now + MILLIS_IN_MINUTE.const_mul(6)), 0, "a self-asserted seal must not pin the bundle in the cache");
2607 Ok(())
2608 }
2609
2610 fn make_feedback_bundle_bytes(server_id: &ServerId, location_id: Id, time_millis: TimeMillis) -> anyhow::Result<Bytes> {
2611 let post_id = Id::random();
2612 let salt = Salt::random();
2613 let (measured_pow, _) = pow::pow_measure(&[post_id.as_bytes(), &[1u8]], &salt)?;
2614 let mut feedbacks_bytes = Vec::new();
2615 EncodedPostFeedbackV1::new(post_id, 1, salt, measured_pow).append_encode_to_bytes(&mut feedbacks_bytes)?;
2616 let feedbacks_bytes = Bytes::from(feedbacks_bytes);
2617 let mut header = EncodedPostBundleFeedbackHeaderV1 {
2618 time_millis,
2619 location_id,
2620 feedbacks_bytes_hash: hashing::hash(feedbacks_bytes.as_ref()),
2621 peer: server_id.to_peer(&RealTimeProvider)?,
2622 signature: Signature::zero(),
2623 };
2624 header.signature_generate(&server_id.keys.signature_key);
2625 EncodedPostBundleFeedbackV1 { header, feedbacks_bytes }.to_bytes()
2626 }
2627
2628 async fn upload_post_bundle_feedback(fixture: &HealFixture, encoded_post_bundle_feedback_bytes: &Bytes) -> anyhow::Result<bool> {
2629 let peer_self = fixture.server.peer_self.read().clone();
2630 let mut cache_request_token = None;
2631 for _ in 0..CACHE_HIT_THRESHOLD {
2632 cache_request_token = cache_request_token.or(fixture.server.post_bundle_feedback_cache.on_get(&fixture.bucket_location, &[], &peer_self, &fixture.server.server_id, fixture.time_millis_now).cache_request_token);
2633 }
2634 let cache_request_token = cache_request_token.ok_or_else(|| anyhow::anyhow!("no cache request token issued"))?;
2635 let request_bytes = CachePostBundleFeedbackV1::new_to_bytes(&cache_request_token, encoded_post_bundle_feedback_bytes)?;
2636 let (_, bytes_gatherer) = fixture.server.dispatch_network_payload_x_CachePostBundleFeedbackV1(CancellationToken::new(), PayloadRequestKind::CachePostBundleFeedbackV1, request_bytes).await?;
2637 Ok(json::bytes_to_struct::<CachePostBundleFeedbackResponseV1>(&bytes_gatherer.to_bytes())?.accepted)
2638 }
2639
2640 #[tokio::test]
2641 async fn genuine_feedback_is_cached() -> anyhow::Result<()> {
2642 let fixture = HealFixture::new().await?;
2643 let server_id = HealFixture::make_server_id(config::SERVER_KEY_POW_MIN).await?;
2644 assert!(upload_post_bundle_feedback(&fixture, &make_feedback_bundle_bytes(&server_id, fixture.bucket_location.location_id, fixture.time_millis_now)?).await?);
2645 Ok(())
2646 }
2647
2648 #[tokio::test]
2649 async fn feedback_from_unworked_publisher_is_rejected() -> anyhow::Result<()> {
2650 let fixture = HealFixture::new().await?;
2651 let unworked_server_id = make_unworked_server_id().await?;
2652 assert!(!upload_post_bundle_feedback(&fixture, &make_feedback_bundle_bytes(&unworked_server_id, fixture.bucket_location.location_id, fixture.time_millis_now)?).await?);
2653 Ok(())
2654 }
2655
2656 #[tokio::test]
2657 async fn feedback_for_other_location_is_rejected() -> anyhow::Result<()> {
2658 let fixture = HealFixture::new().await?;
2659 let server_id = HealFixture::make_server_id(config::SERVER_KEY_POW_MIN).await?;
2660 assert!(!upload_post_bundle_feedback(&fixture, &make_feedback_bundle_bytes(&server_id, Id::random(), fixture.time_millis_now)?).await?);
2661 Ok(())
2662 }
2663 }
2664}