1use crate::peer_auth::NodeIdentity;
4use serde_json;
5use sha2::{Digest, Sha256};
6use std::sync::Arc;
7use std::time::Duration;
8use tokio::sync::RwLock;
9
10pub struct GossipClient {
12 client: reqwest::Client,
14 peers: Arc<RwLock<Vec<String>>>,
16 timeout: Duration,
18 identity: Option<Arc<NodeIdentity>>,
20}
21
22impl GossipClient {
23 pub fn new(peer_urls: Vec<String>, timeout_secs: u64) -> Self {
29 Self {
30 client: reqwest::Client::new(),
31 peers: Arc::new(RwLock::new(peer_urls)),
32 timeout: Duration::from_secs(timeout_secs),
33 identity: None,
34 }
35 }
36
37 pub fn with_identity(
44 peer_urls: Vec<String>,
45 timeout_secs: u64,
46 identity: Arc<NodeIdentity>,
47 ) -> Self {
48 Self {
49 client: reqwest::Client::new(),
50 peers: Arc::new(RwLock::new(peer_urls)),
51 timeout: Duration::from_secs(timeout_secs),
52 identity: Some(identity),
53 }
54 }
55
56 fn sign_request(&self, method: &str, path: &str, body: &[u8]) -> Vec<(String, String)> {
63 let Some(identity) = &self.identity else {
64 return vec![];
65 };
66
67 let timestamp = chrono::Utc::now().timestamp();
68 let body_hash = hex::encode(Sha256::digest(body));
69 let message = format!("{}:{}:{}:{}", timestamp, method, path, body_hash);
70 let signature = identity.sign_message(message.as_bytes());
71
72 vec![
73 ("X-Node-Id".to_string(), identity.node_id().to_string()),
74 (
75 "X-Node-Pubkey".to_string(),
76 hex::encode(identity.public_key_bytes()),
77 ),
78 ("X-Node-Signature".to_string(), hex::encode(signature)),
79 ("X-Node-Timestamp".to_string(), timestamp.to_string()),
80 ]
81 }
82
83 pub async fn broadcast_challenge(
92 &self,
93 challenge_id: &str,
94 challenge_json: &serde_json::Value,
95 ) -> Result<usize, Box<dyn std::error::Error>> {
96 let peers = self.peers.read().await.clone();
97 let mut success_count = 0;
98
99 for peer_url in peers {
100 let payload = serde_json::json!({
101 "challenge_id": challenge_id,
102 "challenge": challenge_json,
103 });
104 let body_bytes = serde_json::to_vec(&payload)?;
105 let headers = self.sign_request("POST", "/challenge", &body_bytes);
106
107 let mut request = self
108 .client
109 .post(format!("{}/challenge", peer_url))
110 .timeout(self.timeout)
111 .body(body_bytes)
112 .header("content-type", "application/json");
113
114 for (key, value) in headers {
115 request = request.header(key, value);
116 }
117
118 let result = request.send().await;
119
120 if let Ok(response) = result {
121 if response.status().is_success() {
122 success_count += 1;
123 }
124 }
125 }
126
127 Ok(success_count)
128 }
129
130 pub async fn pull_challenge(
135 &self,
136 peer_url: &str,
137 ) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
138 let response = self
139 .client
140 .get(format!("{}/challenge", peer_url))
141 .timeout(self.timeout)
142 .send()
143 .await?;
144
145 let challenge = response.json::<serde_json::Value>().await?;
146 Ok(challenge)
147 }
148
149 pub async fn register_peer(
156 &self,
157 peer_url: &str,
158 our_url: &str,
159 node_id: &str,
160 ) -> Result<(), Box<dyn std::error::Error>> {
161 self.client
162 .post(format!("{}/register", peer_url))
163 .timeout(self.timeout)
164 .json(&serde_json::json!({
165 "node_id": node_id,
166 "url": our_url,
167 }))
168 .send()
169 .await?;
170
171 Ok(())
172 }
173
174 pub async fn health_check(&self, peer_url: &str) -> bool {
179 let result = self
180 .client
181 .get(format!("{}/health", peer_url))
182 .timeout(self.timeout)
183 .send()
184 .await;
185
186 result.is_ok()
187 }
188
189 pub async fn update_peers(&self, new_peers: Vec<String>) {
194 let mut peers = self.peers.write().await;
195 *peers = new_peers;
196 }
197
198 pub async fn broadcast_lattice(
207 &self,
208 node_id: &str,
209 snapshot: &serde_json::Value,
210 ) -> Result<usize, Box<dyn std::error::Error>> {
211 let peers = self.peers.read().await.clone();
212 let mut success_count = 0;
213
214 for peer_url in peers {
215 let payload = serde_json::json!({
216 "node_id": node_id,
217 "snapshot": snapshot,
218 });
219 let body_bytes = serde_json::to_vec(&payload)?;
220 let headers = self.sign_request("POST", "/hexchain/lattice/sync", &body_bytes);
221
222 let mut request = self
223 .client
224 .post(format!("{}/hexchain/lattice/sync", peer_url))
225 .timeout(self.timeout)
226 .body(body_bytes)
227 .header("content-type", "application/json");
228
229 for (key, value) in headers {
230 request = request.header(key, value);
231 }
232
233 let result = request.send().await;
234
235 if let Ok(response) = result {
236 if response.status().is_success() {
237 success_count += 1;
238 }
239 }
240 }
241
242 Ok(success_count)
243 }
244
245 pub async fn broadcast_block(
246 &self,
247 proof: &serde_json::Value,
248 ) -> Result<usize, Box<dyn std::error::Error>> {
249 let peers = self.peers.read().await.clone();
250 let mut success_count = 0;
251
252 for peer_url in peers {
253 let body_bytes = serde_json::to_vec(proof)?;
254 let headers = self.sign_request("POST", "/hexchain/submit", &body_bytes);
255
256 let mut request = self
257 .client
258 .post(format!("{}/hexchain/submit", peer_url))
259 .timeout(self.timeout)
260 .body(body_bytes)
261 .header("content-type", "application/json");
262
263 for (key, value) in headers {
264 request = request.header(key, value);
265 }
266
267 let result = request.send().await;
268
269 if let Ok(response) = result {
270 if response.status().is_success() {
271 success_count += 1;
272 }
273 }
274 }
275
276 Ok(success_count)
277 }
278}
279
280#[cfg(test)]
281mod tests {
282 use super::*;
283 use serde_json::json;
284
285 #[test]
286 fn test_new_creates_client_with_correct_timeout() {
287 let peer_urls = vec!["http://peer1.local".to_string()];
288 let gossip = GossipClient::new(peer_urls.clone(), 30);
289
290 assert_eq!(gossip.timeout, Duration::from_secs(30));
292 }
293
294 #[test]
295 fn test_new_creates_client_with_peer_urls() {
296 let peer_urls = vec![
297 "http://peer1.local".to_string(),
298 "http://peer2.local".to_string(),
299 ];
300 let _gossip = GossipClient::new(peer_urls.clone(), 30);
301
302 assert_eq!(peer_urls.len(), 2);
304 }
305
306 #[tokio::test]
307 async fn test_broadcast_challenge_counts_successful_peers() {
308 let mut server = mockito::Server::new_async().await;
309 let peer1_url = server.url();
310
311 let _m = server
313 .mock("POST", "/challenge")
314 .with_status(200)
315 .with_body(r#"{"status": "ok"}"#)
316 .expect(1)
317 .create_async()
318 .await;
319
320 let gossip = GossipClient::new(vec![peer1_url], 5);
321 let challenge_id = "test-challenge-123";
322 let challenge_json = json!({"difficulty": 100, "nonce": 42});
323
324 let result = gossip
325 .broadcast_challenge(challenge_id, &challenge_json)
326 .await;
327
328 assert!(result.is_ok());
329 assert_eq!(result.unwrap(), 1);
330 }
331
332 #[tokio::test]
333 async fn test_broadcast_challenge_handles_partial_failures() {
334 let mut server1 = mockito::Server::new_async().await;
335 let mut server2 = mockito::Server::new_async().await;
336
337 let peer1_url = server1.url();
338 let peer2_url = server2.url();
339
340 let _m1 = server1
342 .mock("POST", "/challenge")
343 .with_status(200)
344 .expect(1)
345 .create_async()
346 .await;
347
348 let _m2 = server2
350 .mock("POST", "/challenge")
351 .with_status(500)
352 .expect(1)
353 .create_async()
354 .await;
355
356 let gossip = GossipClient::new(vec![peer1_url, peer2_url], 5);
357 let challenge_id = "test-challenge";
358 let challenge_json = json!({"difficulty": 100});
359
360 let result = gossip
361 .broadcast_challenge(challenge_id, &challenge_json)
362 .await;
363
364 assert!(result.is_ok());
366 assert_eq!(result.unwrap(), 1);
368 }
369
370 #[tokio::test]
371 async fn test_pull_challenge_returns_challenge_json() {
372 let mut server = mockito::Server::new_async().await;
373 let peer_url = server.url();
374
375 let expected_challenge = json!({
376 "challenge_id": "ch-123",
377 "difficulty": 100,
378 "nonce": 42
379 });
380
381 let _m = server
382 .mock("GET", "/challenge")
383 .with_status(200)
384 .with_body(expected_challenge.to_string())
385 .expect(1)
386 .create_async()
387 .await;
388
389 let gossip = GossipClient::new(vec![], 5);
390 let result = gossip.pull_challenge(&peer_url).await;
391
392 assert!(result.is_ok());
393 let received = result.unwrap();
394 assert_eq!(received["challenge_id"], "ch-123");
395 assert_eq!(received["difficulty"], 100);
396 }
397
398 #[tokio::test]
399 async fn test_register_peer_sends_correct_payload() {
400 let mut server = mockito::Server::new_async().await;
401 let peer_url = server.url();
402
403 let _m = server
404 .mock("POST", "/register")
405 .match_body(mockito::Matcher::Json(json!({
406 "node_id": "node-123",
407 "url": "http://our-node"
408 })))
409 .with_status(200)
410 .expect(1)
411 .create_async()
412 .await;
413
414 let gossip = GossipClient::new(vec![], 5);
415 let result = gossip
416 .register_peer(&peer_url, "http://our-node", "node-123")
417 .await;
418
419 assert!(result.is_ok());
420 }
421
422 #[tokio::test]
423 async fn test_health_check_returns_true_for_healthy_peer() {
424 let mut server = mockito::Server::new_async().await;
425 let peer_url = server.url();
426
427 let _m = server
428 .mock("GET", "/health")
429 .with_status(200)
430 .with_body(r#"{"status": "healthy"}"#)
431 .expect(1)
432 .create_async()
433 .await;
434
435 let gossip = GossipClient::new(vec![], 5);
436 let result = gossip.health_check(&peer_url).await;
437
438 assert!(result);
439 }
440
441 #[tokio::test]
442 async fn test_health_check_returns_false_for_unreachable_peer() {
443 let gossip = GossipClient::new(vec![], 1); let result = gossip.health_check("http://127.0.0.1:1").await;
447
448 assert!(!result);
449 }
450
451 #[tokio::test]
452 async fn test_update_peers_modifies_peer_list() {
453 let initial_peers = vec!["http://peer1.local".to_string()];
454 let gossip = GossipClient::new(initial_peers, 30);
455
456 let new_peers = vec![
457 "http://peer2.local".to_string(),
458 "http://peer3.local".to_string(),
459 ];
460 gossip.update_peers(new_peers.clone()).await;
461
462 let peers = gossip.peers.read().await;
464 assert_eq!(peers.len(), 2);
465 assert_eq!(*peers, new_peers);
466 }
467
468 #[tokio::test]
469 async fn test_broadcast_challenge_to_multiple_peers() {
470 let mut server1 = mockito::Server::new_async().await;
471 let mut server2 = mockito::Server::new_async().await;
472 let mut server3 = mockito::Server::new_async().await;
473
474 let peer1_url = server1.url();
475 let peer2_url = server2.url();
476 let peer3_url = server3.url();
477
478 let _m1 = server1
480 .mock("POST", "/challenge")
481 .with_status(200)
482 .expect(1)
483 .create_async()
484 .await;
485
486 let _m2 = server2
487 .mock("POST", "/challenge")
488 .with_status(200)
489 .expect(1)
490 .create_async()
491 .await;
492
493 let _m3 = server3
494 .mock("POST", "/challenge")
495 .with_status(200)
496 .expect(1)
497 .create_async()
498 .await;
499
500 let gossip = GossipClient::new(vec![peer1_url, peer2_url, peer3_url], 5);
501
502 let challenge_id = "test-ch";
503 let challenge_json = json!({"difficulty": 100});
504
505 let result = gossip
506 .broadcast_challenge(challenge_id, &challenge_json)
507 .await;
508
509 assert!(result.is_ok());
510 assert_eq!(result.unwrap(), 3);
511 }
512}