hashiverse_lib/client/post_bundle/
post_bundle_healing.rs1use crate::anyhow_assert_eq;
20use crate::protocol::payload::payload::{HealPostBundleClaimResponseV1, HealPostBundleClaimV1, HealPostBundleCommitResponseV1, HealPostBundleCommitV1, PayloadRequestKind, PayloadResponseKind};
21use crate::protocol::peer::Peer;
22use crate::protocol::posting::encoded_post_bundle::{EncodedPostBundleHeaderV1, EncodedPostBundleV1};
23use crate::protocol::rpc;
24use crate::tools::buckets::BucketLocation;
25use crate::tools::json;
26use crate::tools::runtime_services::RuntimeServices;
27use crate::tools::types::Id;
28use bytes::Bytes;
29use log::{info, warn};
30use std::collections::HashMap;
31use std::sync::Arc;
32
33struct HealWork {
34 target_peer: Peer,
35 bucket_location: BucketLocation,
36 donor_header: EncodedPostBundleHeaderV1,
37 donor_posts_bytes: Bytes,
38 post_byte_map: HashMap<Id, (usize, usize)>,
39}
40
41pub fn heal_post_bundles(runtime_services: Arc<RuntimeServices>, sponsor_id: Id, bucket_location: BucketLocation, donor_bundles: &[EncodedPostBundleV1], target_bundles: &[(Peer, EncodedPostBundleV1)]) {
49 let work = build_heal_work(&bucket_location, donor_bundles, target_bundles);
50
51 if !work.is_empty() {
52 crate::tools::tools::spawn_background_task(async move {
53 for task in work {
54 let result = try_heal_single_bundle(&runtime_services, &sponsor_id, &task.target_peer, task.bucket_location, task.donor_header, &task.donor_posts_bytes, &task.post_byte_map).await;
55 if let Err(e) = result {
56 warn!("Healing failed for target_peer={}: {}", task.target_peer, e);
57 }
58 }
59 });
60 }
61}
62
63fn build_heal_work(bucket_location: &BucketLocation, donor_bundles: &[EncodedPostBundleV1], target_bundles: &[(Peer, EncodedPostBundleV1)]) -> Vec<HealWork> {
66 let donor_byte_maps: Vec<HashMap<Id, (usize, usize)>> = donor_bundles
68 .iter()
69 .map(|donor_bundle| {
70 let mut map = HashMap::new();
71 let mut offset = 0usize;
72 for (post_id, &len) in donor_bundle.header.encoded_post_ids.iter().zip(donor_bundle.header.encoded_post_lengths.iter()) {
73 map.insert(*post_id, (offset, len));
74 offset += len;
75 }
76 map
77 })
78 .collect();
79
80 let mut work: Vec<HealWork> = Vec::new();
81 for (donor_bundle, donor_byte_map) in donor_bundles.iter().zip(donor_byte_maps.iter()) {
82 for (target_peer, target_bundle) in target_bundles {
83 if donor_bundle.header.peer.id == target_peer.id {
84 continue;
85 }
86 if donor_bundle.header.encoded_post_ids.iter().any(|id| !target_bundle.header.encoded_post_ids.contains(id)) {
87 info!("Queueing healing for target_peer={} bucket_location={}", target_peer, bucket_location);
88 work.push(HealWork {
89 target_peer: target_peer.clone(),
90 bucket_location: bucket_location.clone(),
91 donor_header: donor_bundle.header.clone(),
92 donor_posts_bytes: donor_bundle.encoded_posts_bytes.clone(),
93 post_byte_map: donor_byte_map.clone(),
94 });
95 }
96 }
97 }
98 work
99}
100
101async fn try_heal_single_bundle(
102 runtime_services: &RuntimeServices,
103 sponsor_id: &Id,
104 target_peer: &Peer,
105 bucket_location: BucketLocation,
106 donor_header: EncodedPostBundleHeaderV1,
107 donor_posts_bytes: &[u8],
108 post_byte_map: &HashMap<Id, (usize, usize)>,
109) -> anyhow::Result<()> {
110 info!("About to heal {}", target_peer);
111
112 let request_bytes = HealPostBundleClaimV1::new_to_bytes(&donor_header, &bucket_location)?;
114 let response = rpc::rpc::rpc_server_known(runtime_services, sponsor_id, target_peer, PayloadRequestKind::HealPostBundleClaimV1, request_bytes).await?;
115 anyhow_assert_eq!(&PayloadResponseKind::HealPostBundleClaimResponseV1, &response.response_request_kind);
116 let response = json::bytes_to_struct::<HealPostBundleClaimResponseV1>(&response.bytes)?;
117
118 let Some(token) = response.token
119 else {
120 info!("Healing skipped because of no HealPostBundleClaimTokenV1 for {}", target_peer);
121 return Ok(()); };
123
124 let mut encoded_posts_bytes = Vec::new();
126 for post_id in &token.needed_post_ids {
127 let &(offset, len) = post_byte_map.get(post_id).ok_or_else(|| anyhow::anyhow!("needed post {} not in donor bundle", post_id))?;
128 encoded_posts_bytes.extend_from_slice(&donor_posts_bytes[offset..offset + len]);
129 }
130
131 let request_bytes = HealPostBundleCommitV1::new_to_bytes(&token, &donor_header, &encoded_posts_bytes)?;
133 let response = rpc::rpc::rpc_server_known_with_no_compression(runtime_services, sponsor_id, target_peer, PayloadRequestKind::HealPostBundleCommitV1, request_bytes).await?;
134 anyhow_assert_eq!(&PayloadResponseKind::HealPostBundleCommitResponseV1, &response.response_request_kind);
135 let response = json::bytes_to_struct::<HealPostBundleCommitResponseV1>(&response.bytes)?;
136
137 if response.accepted {
138 info!("Healed {} post(s) on {} from {}", token.needed_post_ids.len(), target_peer, donor_header.peer);
139 }
140 else {
141 info!("Healing not accepted for {} post(s) on {} from {}", token.needed_post_ids.len(), target_peer, donor_header.peer);
142 }
143
144 Ok(())
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150 use crate::protocol::posting::encoded_post_bundle::test_fixtures::BucketBundleFixture;
151 use crate::tools::time_provider::time_provider::RealTimeProvider;
152
153 #[tokio::test]
154 async fn heals_targets_missing_donor_posts_addressed_via_the_rpc_peer() -> anyhow::Result<()> {
155 let fixture = BucketBundleFixture::new().await?;
156 let posts = fixture.make_own_posts(2).await?;
157
158 let donor_server_id = BucketBundleFixture::new_server_id().await?;
159 let donor_bundle = fixture.make_bundle_from(&donor_server_id, &posts)?;
160
161 let target_server_id = BucketBundleFixture::new_server_id().await?;
163 let mut header_peer = target_server_id.to_peer(&RealTimeProvider)?;
164 header_peer.address = "attacker-chosen-address".to_string();
165 header_peer.sign(&RealTimeProvider, &target_server_id.keys.signature_key)?;
166 let target_bundle = fixture.make_bundle_signed_by(&target_server_id, header_peer, fixture.time_millis_now, &posts[..1])?;
167 let mut rpc_peer = target_server_id.to_peer(&RealTimeProvider)?;
168 rpc_peer.address = "the-address-we-actually-used".to_string();
169
170 let work = build_heal_work(&fixture.bucket_location, std::slice::from_ref(&donor_bundle), &[(rpc_peer, target_bundle)]);
171 assert_eq!(work.len(), 1);
172 assert_eq!(work[0].target_peer.address, "the-address-we-actually-used");
173 assert_eq!(work[0].donor_header, donor_bundle.header);
174 Ok(())
175 }
176
177 #[tokio::test]
178 async fn does_not_heal_up_to_date_targets_or_donors_themselves() -> anyhow::Result<()> {
179 let fixture = BucketBundleFixture::new().await?;
180 let posts = fixture.make_own_posts(2).await?;
181
182 let donor_server_id = BucketBundleFixture::new_server_id().await?;
183 let donor_bundle = fixture.make_bundle_from(&donor_server_id, &posts)?;
184 let up_to_date_server_id = BucketBundleFixture::new_server_id().await?;
185 let up_to_date_bundle = fixture.make_bundle_from(&up_to_date_server_id, &posts)?;
186
187 let target_bundles = vec![
188 (donor_server_id.to_peer(&RealTimeProvider)?, donor_bundle.clone()),
189 (up_to_date_server_id.to_peer(&RealTimeProvider)?, up_to_date_bundle),
190 ];
191 assert!(build_heal_work(&fixture.bucket_location, std::slice::from_ref(&donor_bundle), &target_bundles).is_empty());
192 Ok(())
193 }
194
195 #[tokio::test]
196 async fn empty_direct_responder_is_a_target() -> anyhow::Result<()> {
197 let fixture = BucketBundleFixture::new().await?;
198 let posts = fixture.make_own_posts(1).await?;
199
200 let donor_server_id = BucketBundleFixture::new_server_id().await?;
201 let donor_bundle = fixture.make_bundle_from(&donor_server_id, &posts)?;
202 let empty_server_id = BucketBundleFixture::new_server_id().await?;
203 let empty_bundle = fixture.make_bundle_from(&empty_server_id, &[])?;
204
205 let work = build_heal_work(&fixture.bucket_location, &[donor_bundle], &[(empty_server_id.to_peer(&RealTimeProvider)?, empty_bundle)]);
206 assert_eq!(work.len(), 1);
207 assert_eq!(work[0].target_peer.id, empty_server_id.id);
208 Ok(())
209 }
210}