Skip to main content

hashiverse_lib/client/post_bundle/
live_post_bundle_manager.rs

1//! # Production [`PostBundleManager`] — cache, fetch, heal
2//!
3//! The real implementation of
4//! [`crate::client::post_bundle::post_bundle_manager::PostBundleManager`] used in
5//! production. Lookup proceeds in three stages:
6//!
7//! 1. **Cache** — check `BUCKET_POST_BUNDLE` in
8//!    [`crate::client::client_storage::client_storage::ClientStorage`]. Serve hot entries
9//!    immediately; serve stale entries only if the bundle is already sealed.
10//! 2. **Network** — otherwise a background walk visits peers closest to the bucket's location id via a
11//!    [`crate::client::peer_tracker::peer_iterator::PeerIterator`], issuing
12//!    `GetPostBundleV1` RPCs, gated by the
13//!    [`crate::client::caching::cache_radius_tracker`] to avoid re-hammering
14//!    already-cached peers. Concurrent lookups for the same location share one walk.
15//!    [`PostBundleManager::get_post_bundle`] answers as soon as the walk accepts its first
16//!    authoritative bundle (a *provisional* answer); [`PostBundleManager::get_post_bundle_settled`]
17//!    waits for the walk's best-of-[`config::REDUNDANT_SERVERS_PER_POST`] answer.
18//! 3. **Settle** — once the walk has its quorum it caches the best bundle locally, then spawns
19//!    [`crate::client::post_bundle::post_bundle_healing`] to reconcile divergent servers and
20//!    uploads the original signed bundles to the servers that asked to cache them.
21//!
22//! A provisional bundle is never stored or forwarded.  Its posts and `overflowed` flag reflect only the
23//! bundles seen so far, so a timeline pass built on it may miss posts (or child buckets) that only later
24//! responders hold; anything fetched after the bundle settles sees the full picture.
25
26use crate::anyhow_assert_eq;
27use crate::client::caching::cache_radius_tracker::CacheRadiusTracker;
28use crate::client::caching::post_bundle_cache_uploader;
29use crate::client::client_storage::client_storage::{ClientStorage, BUCKET_POST_BUNDLE};
30use crate::client::peer_tracker::peer_tracker::{PeerTracker, SharedPeerTracker};
31use crate::client::post_bundle::post_bundle_healing;
32use crate::client::post_bundle::post_bundle_manager::PostBundleManager;
33use crate::protocol::payload::payload::{CacheRequestTokenV1, GetPostBundleResponseV1, GetPostBundleV1, PayloadRequestKind, PayloadResponseKind};
34use crate::protocol::peer::Peer;
35use crate::protocol::posting::encoded_post_bundle::EncodedPostBundleV1;
36use crate::protocol::rpc;
37use crate::tools::buckets::BucketLocation;
38use crate::tools::config::CLIENT_POST_BUNDLE_CACHE_DURATION;
39use crate::tools::runtime_services::RuntimeServices;
40use crate::tools::time::TimeMillis;
41use crate::tools::tools::{spawn_background_task, LeadingAgreementBits};
42use crate::tools::types::Id;
43use crate::tools::{config, json};
44use bytes::Bytes;
45use log::{info, trace, warn};
46use std::collections::HashMap;
47use std::sync::Arc;
48use tokio::sync::watch;
49
50/// The production [`PostBundleManager`] implementation — the one that actually talks to the
51/// network.
52///
53/// `LivePostBundleManager` is the glue between the pure-logic timeline code and the live
54/// network. Each lookup:
55///
56/// 1. Checks [`ClientStorage`] under `BUCKET_POST_BUNDLE` for a sufficiently fresh cached
57///    copy (freshness is governed by `CLIENT_POST_BUNDLE_CACHE_DURATION`).
58/// 2. Otherwise joins (or starts) a background walk of `GetPostBundleV1` RPCs against the closest
59///    peers in the [`PeerTracker`], attributing PoW to the locally-held `sponsor_id`.
60/// 3. Returns the walk's first authoritative bundle (`get_post_bundle`) or its settled best
61///    bundle (`get_post_bundle_settled`).  The walk itself carries on to cache, heal and upload —
62///    see [`crate::client::post_bundle::post_bundle_healing`].
63///
64/// Concurrent requests for the same bundle share one walk via `InflightPostBundleFetches`, so
65/// a burst of timeline scrolls cannot fan out into redundant RPCs. The
66/// [`crate::client::caching::cache_radius_tracker::CacheRadiusTracker`] helps the caching
67/// role of a server decide how widely to mirror bundles.
68pub struct LivePostBundleManager {
69    inner: Arc<LivePostBundleManagerInner>,
70}
71
72/// Everything a walk needs, shared so that the walk can outlive the call that started it.
73struct LivePostBundleManagerInner {
74    runtime_services: Arc<RuntimeServices>,
75    client_storage: Arc<dyn ClientStorage>,
76    peer_tracker: SharedPeerTracker,
77    sponsor_id: Id,
78    inflight_post_bundle_fetches: InflightPostBundleFetches,
79    post_bundle_cache_radius_tracker: CacheRadiusTracker,
80}
81
82impl LivePostBundleManager {
83    pub fn new(
84        runtime_services: Arc<RuntimeServices>,
85        sponsor_id: Id,
86        client_storage: Arc<dyn ClientStorage>,
87        peer_tracker: SharedPeerTracker,
88    ) -> Self {
89        Self {
90            inner: Arc::new(LivePostBundleManagerInner {
91                runtime_services,
92                client_storage,
93                peer_tracker,
94                sponsor_id,
95                inflight_post_bundle_fetches: InflightPostBundleFetches::default(),
96                post_bundle_cache_radius_tracker: CacheRadiusTracker::new(CLIENT_POST_BUNDLE_CACHE_DURATION.const_mul(5)), // We let these cache radius caches last longer than our own local cache of the bundles...
97            }),
98        }
99    }
100}
101
102#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))]
103#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)]
104impl PostBundleManager
105    for LivePostBundleManager
106{
107    async fn get_post_bundle(&self, bucket_location: &BucketLocation, time_millis: TimeMillis) -> anyhow::Result<EncodedPostBundleV1> {
108        self.inner.fetch(bucket_location, time_millis, false).await
109    }
110
111    async fn get_post_bundle_settled(&self, bucket_location: &BucketLocation, time_millis: TimeMillis) -> anyhow::Result<EncodedPostBundleV1> {
112        self.inner.fetch(bucket_location, time_millis, true).await
113    }
114}
115
116impl LivePostBundleManagerInner {
117    async fn get_from_cache(&self, location_id: Id, time_millis: TimeMillis, reason: &str) -> Option<EncodedPostBundleV1> {
118        let result: anyhow::Result<Option<EncodedPostBundleV1>> = try {
119            let raw = self.client_storage.get(BUCKET_POST_BUNDLE, &location_id.to_hex_str(), time_millis).await?;
120            if let Some(raw) = raw {
121                // NB: deliberately not re-verified.  We only ever cache bundles we verified on receipt, but their overflowed/sealed flags
122                // were then merged/normalised locally (see AcceptedPostBundles::select_best) so the header signature may not cover them.
123                let bundle = EncodedPostBundleV1::from_bytes(Bytes::from(raw), true)?;
124                if bundle.header.sealed {
125                    trace!("Using cached sealed PostBundle for {} at {}", location_id, reason);
126                    Some(bundle)
127                }
128                else {
129                    let duration = time_millis - bundle.header.time_millis;
130                    if duration < CLIENT_POST_BUNDLE_CACHE_DURATION {
131                        trace!("Using cached PostBundle for {} (age {}) at {}", location_id, duration, reason);
132                        Some(bundle)
133                    }
134                    else {
135                        trace!("Cached PostBundle for {} expired (age {}) at {}", location_id, duration, reason);
136                        None
137                    }
138                }
139            }
140            else {
141                None
142            }
143        };
144
145        result.unwrap_or_else(|e| {
146            warn!("discarding problematic cached PostBundle: {}", e);
147            None
148        })
149    }
150
151    /// Serves from the cache, else joins (or starts) the walk for this location and waits for a provisional or settled answer.
152    async fn fetch(self: &Arc<Self>, bucket_location: &BucketLocation, time_millis: TimeMillis, wait_for_settled: bool) -> anyhow::Result<EncodedPostBundleV1> {
153        let location_id = bucket_location.location_id;
154
155        // Check the cache
156        if let Some(cached) = self.get_from_cache(location_id, time_millis, "preflight").await {
157            return Ok(cached);
158        }
159
160        let (mut fetch_state_receiver, fetch_state_sender) = self.inflight_post_bundle_fetches.join_or_start(location_id);
161        if let Some(fetch_state_sender) = fetch_state_sender {
162            // Re-check the cache - a walk may have settled and stored its result between our preflight check and joining
163            if let Some(cached) = self.get_from_cache(location_id, time_millis, "postflight").await {
164                fetch_state_sender.send_replace(PostBundleFetchState::Settled(Ok(cached)));
165                self.inflight_post_bundle_fetches.finish(location_id);
166            }
167            else {
168                spawn_background_task(self.clone().walk_and_settle(bucket_location.clone(), time_millis, fetch_state_sender));
169            }
170        }
171
172        await_fetch_answer(location_id, &mut fetch_state_receiver, wait_for_settled).await
173    }
174
175    /// Runs the walk to its quorum, publishing the first authoritative bundle as soon as it is accepted, then settles:
176    /// caches the best bundle, releases waiters, and kicks off healing and cache uploads.
177    async fn walk_and_settle(self: Arc<Self>, bucket_location: BucketLocation, time_millis: TimeMillis, fetch_state_sender: watch::Sender<PostBundleFetchState>) {
178        let location_id = bucket_location.location_id;
179
180        match self.walk(&bucket_location, time_millis, &fetch_state_sender).await {
181            Ok((best_encoded_post_bundle, accepted_post_bundles, cache_request_tokens)) => {
182                fetch_state_sender.send_replace(PostBundleFetchState::Settled(Ok(best_encoded_post_bundle)));
183                self.inflight_post_bundle_fetches.finish(location_id);
184
185                // Kick off healing
186                post_bundle_healing::heal_post_bundles(self.runtime_services.clone(), self.sponsor_id, bucket_location.clone(), &accepted_post_bundles.donor_bundles(), accepted_post_bundles.direct_bundles());
187
188                // Kick off remote cache populating - with the original signed bytes of authoritative bundles only
189                post_bundle_cache_uploader::upload_post_bundle_caches(self.runtime_services.clone(), self.sponsor_id, cache_request_tokens, accepted_post_bundles.bundle_bytes_for_upload());
190            }
191            Err(error) => {
192                warn!("PostBundle walk for {} failed: {:#}", location_id, error);
193                fetch_state_sender.send_replace(PostBundleFetchState::Settled(Err(Arc::new(error))));
194                self.inflight_post_bundle_fetches.finish(location_id);
195            }
196        }
197    }
198
199    /// The walk proper.  Returns the best bundle (already written to `ClientStorage`) plus what healing and uploading need.
200    async fn walk(&self, bucket_location: &BucketLocation, time_millis: TimeMillis, fetch_state_sender: &watch::Sender<PostBundleFetchState>) -> anyhow::Result<(EncodedPostBundleV1, AcceptedPostBundles, Vec<CacheRequestTokenV1>)> {
201        let cache_radius = self.post_bundle_cache_radius_tracker.get(bucket_location.location_id, time_millis);
202
203        // We need to refresh...
204        let mut peers_visited = Vec::new();
205        let mut accepted_post_bundles = AcceptedPostBundles::new(bucket_location.clone(), time_millis);
206        let mut cache_request_tokens: Vec<CacheRequestTokenV1> = Vec::new();
207        let mut positive_responder_leading_agreement_bits: Vec<LeadingAgreementBits> = Vec::new();
208        let mut provisional_published = false;
209        {
210            let mut peer_iter = PeerTracker::iterate_to_location(&self.peer_tracker, bucket_location.location_id, 2 * config::REDUNDANT_SERVERS_PER_POST, cache_radius).await?;
211            while let Some((peer, leading_agreement_bits)) = peer_iter.next_peer() {
212                if accepted_post_bundles.accepted_originator_ids().contains(&peer.id) {
213                    continue;
214                }
215
216                let result: anyhow::Result<()> = try {
217                    info!("Requesting PostBundle bucket_location={} with leading_agreement_bits={} from peer {}", bucket_location, leading_agreement_bits, peer);
218
219                    let request = GetPostBundleV1 {
220                        bucket_location: bucket_location.clone(),
221                        peers_visited: peers_visited.clone(),
222                        already_retrieved_peer_ids: accepted_post_bundles.accepted_originator_ids().to_vec(),
223                    };
224                    let request = json::struct_to_bytes(&request)?;
225                    let response = rpc::rpc::rpc_server_known(&self.runtime_services, &self.sponsor_id, &peer, PayloadRequestKind::GetPostBundleV1, request).await?;
226                    anyhow_assert_eq!(&PayloadResponseKind::GetPostBundleResponseV1, &response.response_request_kind);
227                    let response = GetPostBundleResponseV1::from_bytes(response.bytes)?;
228
229                    peers_visited.push(peer.clone());
230                    peer_iter.add_peers(response.peers_nearer);
231
232                    if let Some(token) = response.cache_request_token {
233                        cache_request_tokens.push(token);
234                    }
235
236                    let mut found_bundle_from_this_peer = false;
237
238                    for cached_bytes in response.post_bundles_cached {
239                        match accepted_post_bundles.offer_cached(cached_bytes) {
240                            Ok(accepted) => found_bundle_from_this_peer |= accepted,
241                            Err(e) => warn!("Rejecting cached PostBundle from peer {}: {}", peer, e),
242                        }
243                    }
244
245                    // A peer that hands us an unacceptable bundle of its own is misbehaving, so this one propagates and drops the peer
246                    if let Some(post_bundle_raw) = response.post_bundle {
247                        found_bundle_from_this_peer |= accepted_post_bundles.offer_direct(post_bundle_raw, &peer)?;
248                    }
249
250                    if found_bundle_from_this_peer {
251                        positive_responder_leading_agreement_bits.push(leading_agreement_bits);
252                    }
253
254                    // The first genuinely signed posts are good enough to show - waiters on get_post_bundle can go now
255                    if !provisional_published && accepted_post_bundles.has_authoritative() {
256                        if let Some(provisional_encoded_post_bundle) = accepted_post_bundles.select_best() {
257                            trace!("Publishing provisional PostBundle for {} with {} posts", bucket_location.location_id, provisional_encoded_post_bundle.header.num_posts);
258                            fetch_state_sender.send_replace(PostBundleFetchState::Provisional(provisional_encoded_post_bundle));
259                            provisional_published = true;
260                        }
261                    }
262
263                    if accepted_post_bundles.quorum_count() >= config::REDUNDANT_SERVERS_PER_POST {
264                        break;
265                    }
266                };
267
268                if let Err(e) = result {
269                    warn!("Error retrieving PostBundle from peer {}: {}", peer, e);
270                    peer_iter.remove_peer(&peer);
271                }
272            }
273
274            let result = PeerTracker::flush(&self.peer_tracker).await;
275            if let Err(e) = result {
276                warn!("Error flushing peer tracker: {}", e);
277            }
278        }
279
280        info!("Accepted post bundles from {} originators after visiting {} peers", accepted_post_bundles.quorum_count(), peers_visited.len());
281
282        // Store our new cache radius - the peer "furthest out" from our bundle's location_id
283        if let Some(new_radius) = positive_responder_leading_agreement_bits.iter().copied().min() {
284            self.post_bundle_cache_radius_tracker.update(bucket_location.location_id, new_radius, time_millis);
285        }
286
287        let best_encoded_post_bundle = accepted_post_bundles.select_best().ok_or_else(|| anyhow::anyhow!("No post bundles discovered for {}", bucket_location.location_id))?;
288
289        // Cache it.  NB: its flags have been merged/normalised locally, so its header signature may no longer cover them - fine for our own cache, never forward it.
290        trace!("Caching PostBundle for {} with {} posts", bucket_location.location_id, best_encoded_post_bundle.header.num_posts);
291        let encoded_post_bundle_bytes = best_encoded_post_bundle.to_bytes()?;
292        self.client_storage.put(BUCKET_POST_BUNDLE, &bucket_location.location_id.to_hex_str(), encoded_post_bundle_bytes.to_vec(), time_millis).await?;
293
294        Ok((best_encoded_post_bundle, accepted_post_bundles, cache_request_tokens))
295    }
296}
297
298/// Waits until the walk has an answer for this caller - a provisional bundle is enough unless `wait_for_settled` - and returns it.
299async fn await_fetch_answer(location_id: Id, fetch_state_receiver: &mut watch::Receiver<PostBundleFetchState>, wait_for_settled: bool) -> anyhow::Result<EncodedPostBundleV1> {
300    let fetch_state = fetch_state_receiver
301        .wait_for(|fetch_state| match fetch_state {
302            PostBundleFetchState::Walking => false,
303            PostBundleFetchState::Provisional(_) => !wait_for_settled,
304            PostBundleFetchState::Settled(_) => true,
305        })
306        .await
307        .map_err(|_| anyhow::anyhow!("PostBundle walk for {} ended without settling", location_id))?
308        .clone();
309
310    match fetch_state {
311        PostBundleFetchState::Provisional(encoded_post_bundle) => Ok(encoded_post_bundle),
312        PostBundleFetchState::Settled(Ok(encoded_post_bundle)) => Ok(encoded_post_bundle),
313        PostBundleFetchState::Settled(Err(error)) => Err(anyhow::Error::new(PostBundleFetchError { location_id, error })),
314        PostBundleFetchState::Walking => anyhow::bail!("PostBundle walk for {} is still walking", location_id),
315    }
316}
317
318/// Where a location's walk has got to, as seen by everyone waiting on it.
319#[derive(Clone)]
320enum PostBundleFetchState {
321    Walking,
322    /// The best of the bundles accepted so far, published once the first authoritative bundle arrives.  Never stored or forwarded.
323    Provisional(EncodedPostBundleV1),
324    /// The best-of-quorum bundle, already written to `ClientStorage` - or why the walk failed.
325    Settled(Result<EncodedPostBundleV1, Arc<anyhow::Error>>),
326}
327
328/// A walk's error, handed to every caller that waited on it (`anyhow::Error` is not `Clone`, so they share it).
329/// Its `source()` is the original error: `{:#}` still prints the whole chain and `chain()`/`downcast_ref` still reach it.
330#[derive(Debug)]
331struct PostBundleFetchError {
332    location_id: Id,
333    error: Arc<anyhow::Error>,
334}
335
336impl std::fmt::Display for PostBundleFetchError {
337    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
338        write!(formatter, "fetching PostBundle {} failed", self.location_id)
339    }
340}
341
342impl std::error::Error for PostBundleFetchError {
343    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
344        let error: &(dyn std::error::Error + Send + Sync + 'static) = self.error.as_ref().as_ref();
345        Some(error)
346    }
347}
348
349/// One shared walk per location: the first caller starts it, later callers watch its progress.
350#[derive(Default)]
351struct InflightPostBundleFetches {
352    fetches: parking_lot::Mutex<HashMap<Id, watch::Receiver<PostBundleFetchState>>>,
353}
354
355impl InflightPostBundleFetches {
356    /// Returns a receiver for this location's walk, plus the sender if the caller must start the walk itself.
357    fn join_or_start(&self, location_id: Id) -> (watch::Receiver<PostBundleFetchState>, Option<watch::Sender<PostBundleFetchState>>) {
358        let mut fetches = self.fetches.lock();
359        if let Some(fetch_state_receiver) = fetches.get(&location_id) {
360            // A walk whose task died without settling (its sender is gone) must not block every later caller
361            if fetch_state_receiver.has_changed().is_ok() {
362                return (fetch_state_receiver.clone(), None);
363            }
364        }
365        let (fetch_state_sender, fetch_state_receiver) = watch::channel(PostBundleFetchState::Walking);
366        fetches.insert(location_id, fetch_state_receiver.clone());
367        (fetch_state_receiver, Some(fetch_state_sender))
368    }
369
370    /// The walk has settled (and stored its result): later callers start afresh from the cache.
371    fn finish(&self, location_id: Id) {
372        self.fetches.lock().remove(&location_id);
373    }
374}
375
376/// The post bundles accepted so far while walking towards a bucket's location.
377///
378/// - An **authoritative** bundle passes [`EncodedPostBundleV1::verify_for_bucket`] and holds at least one post.  Only a genuinely signed,
379///   admissible post proves anything - any server can sign an empty bundle.  Authoritative bundles may come directly from the peer we asked,
380///   or be cached snapshots it relays (possibly signed by servers that have since died).
381/// - A **direct-empty** bundle passes the same checks but holds no posts, and came directly from the peer we asked.  It counts towards
382///   deciding we have asked enough servers (empty buckets are the common case), but carries no content.  Empty cached snapshots are ignored.
383///
384/// Each originating server counts at most once.
385struct AcceptedPostBundles {
386    bucket_location: BucketLocation,
387    time_millis: TimeMillis,
388    accepted_originator_ids: Vec<Id>,
389    authoritative_bundles: Vec<(Bytes, EncodedPostBundleV1)>,
390    direct_bundles: Vec<(Peer, EncodedPostBundleV1)>,
391}
392
393impl AcceptedPostBundles {
394    fn new(bucket_location: BucketLocation, time_millis: TimeMillis) -> Self {
395        Self { bucket_location, time_millis, accepted_originator_ids: Vec::new(), authoritative_bundles: Vec::new(), direct_bundles: Vec::new() }
396    }
397
398    /// Originators whose bundles we have accepted - passed to servers so they don't send us those again.
399    fn accepted_originator_ids(&self) -> &[Id] {
400        &self.accepted_originator_ids
401    }
402
403    /// Whether we hold at least one authoritative bundle (verified, with posts) - enough to show provisionally.
404    fn has_authoritative(&self) -> bool {
405        !self.authoritative_bundles.is_empty()
406    }
407
408    /// How many distinct servers' views we have accepted, authoritative or direct-empty.
409    fn quorum_count(&self) -> usize {
410        self.accepted_originator_ids.len()
411    }
412
413    /// A cached snapshot relayed by some peer.  Returns whether it was accepted; errors if it was unacceptable.
414    fn offer_cached(&mut self, encoded_post_bundle_bytes: Bytes) -> anyhow::Result<bool> {
415        let encoded_post_bundle = EncodedPostBundleV1::from_bytes(encoded_post_bundle_bytes.clone(), true)?;
416        if !self.verify_new_originator(&encoded_post_bundle)? {
417            return Ok(false);
418        }
419        if encoded_post_bundle.header.num_posts == 0 {
420            trace!("Ignoring empty cached PostBundle from peer.id={}", encoded_post_bundle.header.peer.id);
421            return Ok(false);
422        }
423
424        trace!("Accepted cached PostBundle from peer.id={} with {} posts", encoded_post_bundle.header.peer.id, encoded_post_bundle.header.num_posts);
425        self.accepted_originator_ids.push(encoded_post_bundle.header.peer.id);
426        self.authoritative_bundles.push((encoded_post_bundle_bytes, encoded_post_bundle));
427        Ok(true)
428    }
429
430    /// The bundle `responding_peer` holds itself.  Returns whether it was accepted; errors if it was unacceptable.
431    fn offer_direct(&mut self, encoded_post_bundle_bytes: Bytes, responding_peer: &Peer) -> anyhow::Result<bool> {
432        let encoded_post_bundle = EncodedPostBundleV1::from_bytes(encoded_post_bundle_bytes.clone(), true)?;
433        if encoded_post_bundle.header.peer.id != responding_peer.id {
434            anyhow::bail!("PostBundle from peer {} claims to be published by {}", responding_peer.id, encoded_post_bundle.header.peer.id);
435        }
436        if !self.verify_new_originator(&encoded_post_bundle)? {
437            return Ok(false);
438        }
439
440        trace!("Accepted PostBundle from peer.id={} with {} posts", encoded_post_bundle.header.peer.id, encoded_post_bundle.header.num_posts);
441        self.accepted_originator_ids.push(encoded_post_bundle.header.peer.id);
442        if encoded_post_bundle.header.num_posts > 0 {
443            self.authoritative_bundles.push((encoded_post_bundle_bytes, encoded_post_bundle.clone()));
444        }
445        self.direct_bundles.push((responding_peer.clone(), encoded_post_bundle));
446        Ok(true)
447    }
448
449    /// Ok(false) if we have already accepted a bundle from this originator; Err if the bundle is not legitimate for our bucket.
450    fn verify_new_originator(&self, encoded_post_bundle: &EncodedPostBundleV1) -> anyhow::Result<bool> {
451        if self.accepted_originator_ids.contains(&encoded_post_bundle.header.peer.id) {
452            return Ok(false);
453        }
454        encoded_post_bundle.verify_for_bucket(&self.bucket_location, self.time_millis)?;
455        Ok(true)
456    }
457
458    /// The bundle to hand to the caller: the authoritative bundle with the most posts (falling back to a direct-empty one).
459    /// Healing is relied upon to eventually give every responsible server the union of posts.
460    ///
461    /// Its `overflowed` flag is the OR across every accepted bundle: a bundle can win on post count while claiming not to have overflowed,
462    /// which would hide the child buckets, whereas a false "overflowed" only costs some extra fetches.  `sealed` is then recomputed locally.
463    fn select_best(&self) -> Option<EncodedPostBundleV1> {
464        let authoritative = self.authoritative_bundles.iter().map(|(_, encoded_post_bundle)| encoded_post_bundle);
465        let direct = self.direct_bundles.iter().map(|(_, encoded_post_bundle)| encoded_post_bundle);
466
467        let mut best_encoded_post_bundle = authoritative.clone().max_by_key(|encoded_post_bundle| encoded_post_bundle.header.num_posts).or_else(|| direct.clone().next())?.clone();
468        best_encoded_post_bundle.header.overflowed = authoritative
469            .chain(direct)
470            .any(|encoded_post_bundle| encoded_post_bundle.header.overflowed || encoded_post_bundle.header.num_posts > config::ENCODED_POST_BUNDLE_V1_OVERFLOWED_NUM_POSTS);
471        best_encoded_post_bundle.normalise_flags_for_bucket(&self.bucket_location, self.time_millis);
472        Some(best_encoded_post_bundle)
473    }
474
475    fn donor_bundles(&self) -> Vec<EncodedPostBundleV1> {
476        self.authoritative_bundles.iter().map(|(_, encoded_post_bundle)| encoded_post_bundle.clone()).collect()
477    }
478
479    fn direct_bundles(&self) -> &[(Peer, EncodedPostBundleV1)] {
480        &self.direct_bundles
481    }
482
483    fn bundle_bytes_for_upload(&self) -> Vec<(Id, Bytes)> {
484        self.authoritative_bundles.iter().map(|(encoded_post_bundle_bytes, encoded_post_bundle)| (encoded_post_bundle.header.peer.id, encoded_post_bundle_bytes.clone())).collect()
485    }
486}
487
488#[cfg(test)]
489mod tests {
490    use super::*;
491    use crate::protocol::posting::encoded_post_bundle::test_fixtures::BucketBundleFixture;
492    use crate::tools::pow_generator::single_threaded_pow_generator::SingleThreadedPowGenerator;
493    use crate::tools::server_id::ServerId;
494    use crate::tools::time_provider::time_provider::RealTimeProvider;
495    use crate::tools::types::Pow;
496
497    fn new_accepted_post_bundles(fixture: &BucketBundleFixture) -> AcceptedPostBundles {
498        AcceptedPostBundles::new(fixture.bucket_location.clone(), fixture.time_millis_now)
499    }
500
501    #[tokio::test]
502    async fn forged_or_empty_cached_bundles_do_not_count() -> anyhow::Result<()> {
503        let fixture = BucketBundleFixture::new().await?;
504        let posts = fixture.make_own_posts(1).await?;
505        let mut accepted_post_bundles = new_accepted_post_bundles(&fixture);
506
507        // Three empty bundles from genuine servers, relayed as cached snapshots: proves nothing
508        for _ in 0..config::REDUNDANT_SERVERS_PER_POST {
509            let server_id = BucketBundleFixture::new_server_id().await?;
510            assert!(!accepted_post_bundles.offer_cached(fixture.make_bundle_from(&server_id, &[])?.to_bytes()?)?);
511        }
512
513        // A bundle of genuine posts from a keypair with no server PoW behind it
514        let unworked_server_id = loop {
515            let server_id = ServerId::new("unworked", &RealTimeProvider, Pow(0), true, &SingleThreadedPowGenerator::new()).await?;
516            if server_id.pow < config::SERVER_KEY_POW_MIN {
517                break server_id;
518            }
519        };
520        assert!(accepted_post_bundles.offer_cached(fixture.make_bundle_from(&unworked_server_id, &posts)?.to_bytes()?).is_err());
521
522        assert_eq!(accepted_post_bundles.quorum_count(), 0);
523        assert!(accepted_post_bundles.accepted_originator_ids().is_empty());
524        assert!(accepted_post_bundles.select_best().is_none());
525        assert!(accepted_post_bundles.bundle_bytes_for_upload().is_empty());
526        Ok(())
527    }
528
529    #[tokio::test]
530    async fn direct_bundle_naming_another_peer_is_rejected() -> anyhow::Result<()> {
531        let fixture = BucketBundleFixture::new().await?;
532        let posts = fixture.make_own_posts(1).await?;
533        let mut accepted_post_bundles = new_accepted_post_bundles(&fixture);
534
535        let responding_server_id = BucketBundleFixture::new_server_id().await?;
536        let other_server_id = BucketBundleFixture::new_server_id().await?;
537        let other_bundle = fixture.make_bundle_from(&other_server_id, &posts)?;
538        assert!(accepted_post_bundles.offer_direct(other_bundle.to_bytes()?, &responding_server_id.to_peer(&RealTimeProvider)?).is_err());
539        assert_eq!(accepted_post_bundles.quorum_count(), 0);
540        Ok(())
541    }
542
543    #[tokio::test]
544    async fn empty_bucket_reaches_quorum_via_direct_empties() -> anyhow::Result<()> {
545        let fixture = BucketBundleFixture::new().await?;
546        let mut accepted_post_bundles = new_accepted_post_bundles(&fixture);
547
548        for _ in 0..config::REDUNDANT_SERVERS_PER_POST {
549            let server_id = BucketBundleFixture::new_server_id().await?;
550            assert!(accepted_post_bundles.offer_direct(fixture.make_bundle_from(&server_id, &[])?.to_bytes()?, &server_id.to_peer(&RealTimeProvider)?)?);
551        }
552
553        assert_eq!(accepted_post_bundles.quorum_count(), config::REDUNDANT_SERVERS_PER_POST);
554        let best_encoded_post_bundle = accepted_post_bundles.select_best().expect("an empty bundle");
555        assert_eq!(best_encoded_post_bundle.header.num_posts, 0);
556        assert!(accepted_post_bundles.donor_bundles().is_empty(), "empty bundles have nothing to donate");
557        assert!(accepted_post_bundles.bundle_bytes_for_upload().is_empty(), "empty bundles are not worth uploading to caches");
558        assert_eq!(accepted_post_bundles.direct_bundles().len(), config::REDUNDANT_SERVERS_PER_POST, "but they are heal targets");
559        Ok(())
560    }
561
562    #[tokio::test]
563    async fn each_originator_counts_once() -> anyhow::Result<()> {
564        let fixture = BucketBundleFixture::new().await?;
565        let posts = fixture.make_own_posts(1).await?;
566        let mut accepted_post_bundles = new_accepted_post_bundles(&fixture);
567
568        let server_id = BucketBundleFixture::new_server_id().await?;
569        let encoded_post_bundle_bytes = fixture.make_bundle_from(&server_id, &posts)?.to_bytes()?;
570        assert!(accepted_post_bundles.offer_cached(encoded_post_bundle_bytes.clone())?);
571        assert!(!accepted_post_bundles.offer_cached(encoded_post_bundle_bytes.clone())?);
572        assert!(!accepted_post_bundles.offer_direct(encoded_post_bundle_bytes, &server_id.to_peer(&RealTimeProvider)?)?);
573        assert_eq!(accepted_post_bundles.quorum_count(), 1);
574        Ok(())
575    }
576
577    #[tokio::test]
578    async fn selects_largest_bundle_with_overflowed_or_merged() -> anyhow::Result<()> {
579        let fixture = BucketBundleFixture::new().await?;
580        let posts = fixture.make_own_posts(5).await?;
581        let mut accepted_post_bundles = new_accepted_post_bundles(&fixture);
582
583        // A: 3 posts, says overflowed (e.g. it granted many posts that have not all arrived yet)
584        let server_id_a = BucketBundleFixture::new_server_id().await?;
585        let mut encoded_post_bundle_a = fixture.make_bundle_from(&server_id_a, &posts[..3])?;
586        encoded_post_bundle_a.header.overflowed = true;
587        encoded_post_bundle_a.header.sealed = true;
588        encoded_post_bundle_a.header.signature_generate(&server_id_a.keys.signature_key)?;
589        let encoded_post_bundle_bytes_a = encoded_post_bundle_a.to_bytes()?;
590
591        // B: 5 posts, says not overflowed
592        let server_id_b = BucketBundleFixture::new_server_id().await?;
593        let encoded_post_bundle_b = fixture.make_bundle_from(&server_id_b, &posts)?;
594        let encoded_post_bundle_bytes_b = encoded_post_bundle_b.to_bytes()?;
595
596        assert!(accepted_post_bundles.offer_direct(encoded_post_bundle_bytes_a.clone(), &server_id_a.to_peer(&RealTimeProvider)?)?);
597        assert!(accepted_post_bundles.offer_cached(encoded_post_bundle_bytes_b.clone())?);
598
599        let best_encoded_post_bundle = accepted_post_bundles.select_best().expect("a bundle");
600        assert_eq!(best_encoded_post_bundle.header.encoded_post_ids, encoded_post_bundle_b.header.encoded_post_ids, "posts come from the largest bundle");
601        assert!(best_encoded_post_bundle.header.overflowed, "overflowed is OR-ed across all accepted bundles");
602        assert!(best_encoded_post_bundle.header.sealed, "an overflowed bucket is sealed");
603
604        // The merged flag survives the round trip through our local cache format
605        let cached_encoded_post_bundle = EncodedPostBundleV1::from_bytes(best_encoded_post_bundle.to_bytes()?, true)?;
606        assert!(cached_encoded_post_bundle.header.overflowed);
607
608        // Nothing we pass on to the network carries the mutated header
609        let mut bundle_bytes_for_upload = accepted_post_bundles.bundle_bytes_for_upload();
610        bundle_bytes_for_upload.sort_by_key(|(originator_id, _)| *originator_id);
611        let mut expected_bundle_bytes_for_upload = vec![(server_id_a.id, encoded_post_bundle_bytes_a), (server_id_b.id, encoded_post_bundle_bytes_b)];
612        expected_bundle_bytes_for_upload.sort_by_key(|(originator_id, _)| *originator_id);
613        assert_eq!(bundle_bytes_for_upload, expected_bundle_bytes_for_upload);
614        assert!(accepted_post_bundles.donor_bundles().iter().any(|donor_bundle| *donor_bundle == encoded_post_bundle_b));
615        Ok(())
616    }
617
618    #[tokio::test]
619    async fn premature_sealed_claim_is_dropped() -> anyhow::Result<()> {
620        let fixture = BucketBundleFixture::new().await?;
621        let posts = fixture.make_own_posts(1).await?;
622        let mut accepted_post_bundles = new_accepted_post_bundles(&fixture);
623
624        let server_id = BucketBundleFixture::new_server_id().await?;
625        let mut encoded_post_bundle = fixture.make_bundle_from(&server_id, &posts)?;
626        encoded_post_bundle.header.sealed = true; // Would pin this bundle in our cache forever
627        encoded_post_bundle.header.signature_generate(&server_id.keys.signature_key)?;
628        assert!(accepted_post_bundles.offer_cached(encoded_post_bundle.to_bytes()?)?);
629
630        assert!(!accepted_post_bundles.select_best().expect("a bundle").header.sealed);
631        Ok(())
632    }
633
634    #[tokio::test]
635    async fn has_authoritative_only_once_a_bundle_with_posts_is_accepted() -> anyhow::Result<()> {
636        let fixture = BucketBundleFixture::new().await?;
637        let posts = fixture.make_own_posts(1).await?;
638        let mut accepted_post_bundles = new_accepted_post_bundles(&fixture);
639        assert!(!accepted_post_bundles.has_authoritative());
640
641        // A direct empty counts towards the quorum but has nothing to show
642        let empty_server_id = BucketBundleFixture::new_server_id().await?;
643        assert!(accepted_post_bundles.offer_direct(fixture.make_bundle_from(&empty_server_id, &[])?.to_bytes()?, &empty_server_id.to_peer(&RealTimeProvider)?)?);
644        assert!(!accepted_post_bundles.has_authoritative());
645
646        // A cached empty is ignored altogether
647        let cached_empty_server_id = BucketBundleFixture::new_server_id().await?;
648        assert!(!accepted_post_bundles.offer_cached(fixture.make_bundle_from(&cached_empty_server_id, &[])?.to_bytes()?)?);
649        assert!(!accepted_post_bundles.has_authoritative());
650
651        let server_id = BucketBundleFixture::new_server_id().await?;
652        assert!(accepted_post_bundles.offer_cached(fixture.make_bundle_from(&server_id, &posts)?.to_bytes()?)?);
653        assert!(accepted_post_bundles.has_authoritative());
654        Ok(())
655    }
656
657    // ── InflightPostBundleFetches / await_fetch_answer ──
658
659    async fn some_encoded_post_bundle() -> anyhow::Result<EncodedPostBundleV1> {
660        let fixture = BucketBundleFixture::new().await?;
661        let posts = fixture.make_own_posts(1).await?;
662        let server_id = BucketBundleFixture::new_server_id().await?;
663        fixture.make_bundle_from(&server_id, &posts)
664    }
665
666    #[tokio::test]
667    async fn first_caller_starts_the_walk_and_later_callers_join_it() {
668        let inflight_post_bundle_fetches = InflightPostBundleFetches::default();
669        let location_id = Id::random();
670
671        let (_, starter_fetch_state_sender) = inflight_post_bundle_fetches.join_or_start(location_id);
672        assert!(starter_fetch_state_sender.is_some(), "the first caller starts the walk");
673        let (_, joiner_fetch_state_sender) = inflight_post_bundle_fetches.join_or_start(location_id);
674        assert!(joiner_fetch_state_sender.is_none(), "a second caller joins it");
675
676        let (_, other_location_fetch_state_sender) = inflight_post_bundle_fetches.join_or_start(Id::random());
677        assert!(other_location_fetch_state_sender.is_some(), "other locations get their own walk");
678    }
679
680    #[tokio::test]
681    async fn finish_lets_the_next_caller_start_afresh() {
682        let inflight_post_bundle_fetches = InflightPostBundleFetches::default();
683        let location_id = Id::random();
684
685        let (_, fetch_state_sender) = inflight_post_bundle_fetches.join_or_start(location_id);
686        let _fetch_state_sender = fetch_state_sender.expect("starter");
687        inflight_post_bundle_fetches.finish(location_id);
688        assert!(inflight_post_bundle_fetches.join_or_start(location_id).1.is_some());
689    }
690
691    #[tokio::test]
692    async fn a_walk_that_died_without_settling_does_not_block_later_callers() {
693        let inflight_post_bundle_fetches = InflightPostBundleFetches::default();
694        let location_id = Id::random();
695
696        let (mut fetch_state_receiver, fetch_state_sender) = inflight_post_bundle_fetches.join_or_start(location_id);
697        drop(fetch_state_sender); // e.g. the walk task panicked
698        assert!(await_fetch_answer(location_id, &mut fetch_state_receiver, false).await.is_err(), "its own waiters get an error, not a hang");
699        assert!(inflight_post_bundle_fetches.join_or_start(location_id).1.is_some(), "the next caller starts a new walk");
700    }
701
702    #[tokio::test]
703    async fn joiners_see_a_provisional_bundle_immediately() -> anyhow::Result<()> {
704        let inflight_post_bundle_fetches = InflightPostBundleFetches::default();
705        let location_id = Id::random();
706        let provisional_encoded_post_bundle = some_encoded_post_bundle().await?;
707
708        let (_, fetch_state_sender) = inflight_post_bundle_fetches.join_or_start(location_id);
709        let fetch_state_sender = fetch_state_sender.expect("starter");
710        fetch_state_sender.send_replace(PostBundleFetchState::Provisional(provisional_encoded_post_bundle.clone()));
711
712        let (mut joiner_fetch_state_receiver, _) = inflight_post_bundle_fetches.join_or_start(location_id);
713        assert_eq!(provisional_encoded_post_bundle, await_fetch_answer(location_id, &mut joiner_fetch_state_receiver, false).await?);
714        Ok(())
715    }
716
717    #[tokio::test]
718    async fn settled_waiters_ignore_the_provisional_bundle() -> anyhow::Result<()> {
719        let location_id = Id::random();
720        let provisional_encoded_post_bundle = some_encoded_post_bundle().await?;
721        let settled_encoded_post_bundle = some_encoded_post_bundle().await?;
722        let (fetch_state_sender, fetch_state_receiver) = watch::channel(PostBundleFetchState::Walking);
723
724        let mut early_fetch_state_receiver = fetch_state_receiver.clone();
725        let mut settled_fetch_state_receiver = fetch_state_receiver.clone();
726        let early_answer = tokio::spawn(async move { await_fetch_answer(location_id, &mut early_fetch_state_receiver, false).await });
727        let settled_answer = tokio::spawn(async move { await_fetch_answer(location_id, &mut settled_fetch_state_receiver, true).await });
728
729        fetch_state_sender.send_replace(PostBundleFetchState::Provisional(provisional_encoded_post_bundle.clone()));
730        assert_eq!(provisional_encoded_post_bundle, early_answer.await??);
731        tokio::task::yield_now().await;
732        assert!(!settled_answer.is_finished(), "a settled waiter must not wake on the provisional bundle");
733
734        fetch_state_sender.send_replace(PostBundleFetchState::Settled(Ok(settled_encoded_post_bundle.clone())));
735        assert_eq!(settled_encoded_post_bundle, settled_answer.await??);
736        Ok(())
737    }
738
739    #[derive(Debug)]
740    struct WalkRootCause;
741    impl std::fmt::Display for WalkRootCause {
742        fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
743            write!(formatter, "the root cause")
744        }
745    }
746    impl std::error::Error for WalkRootCause {}
747
748    #[tokio::test]
749    async fn walk_errors_reach_every_waiter_with_their_chain_intact() -> anyhow::Result<()> {
750        let location_id = Id::random();
751        let (fetch_state_sender, fetch_state_receiver) = watch::channel(PostBundleFetchState::Walking);
752        let walk_error = anyhow::Error::new(WalkRootCause).context("walking towards the bucket");
753        fetch_state_sender.send_replace(PostBundleFetchState::Settled(Err(Arc::new(walk_error))));
754
755        for wait_for_settled in [false, true] {
756            let mut waiter_fetch_state_receiver = fetch_state_receiver.clone();
757            let error = await_fetch_answer(location_id, &mut waiter_fetch_state_receiver, wait_for_settled).await.expect_err("the walk failed");
758
759            let formatted_error = format!("{:#}", error);
760            assert!(formatted_error.contains(&location_id.to_string()), "names the location: {}", formatted_error);
761            assert!(formatted_error.contains("walking towards the bucket"), "keeps the context: {}", formatted_error);
762            assert!(formatted_error.contains("the root cause"), "keeps the root cause: {}", formatted_error);
763            assert!(error.chain().any(|cause| cause.downcast_ref::<WalkRootCause>().is_some()), "the original error stays reachable");
764            assert!(error.downcast_ref::<PostBundleFetchError>().is_some());
765        }
766        Ok(())
767    }
768}