Skip to main content

pot_o_validator/
internal_api.rs

1//! Internal API for peer-to-peer communication and challenge broadcasting.
2//!
3//! This module provides HTTP endpoints for validators to communicate with each other,
4//! including peer registration, listing, and challenge broadcast.
5//!
6//! Endpoints:
7//! - POST /internal/peer/register - Register a peer validator
8//! - GET /internal/peers - List known peers
9//! - POST /internal/challenge/broadcast - Receive challenge from peer
10
11use std::sync::Arc;
12
13use axum::{
14    body::Body,
15    extract::{Path, State},
16    http::{Request, StatusCode},
17    middleware::{self, Next},
18    response::IntoResponse,
19    routing::{get, post},
20    Json, Router,
21};
22use chrono::{DateTime, Utc};
23use pot_o_core::TokenType;
24use pot_o_extensions::pool_strategy::ProofRecord;
25use pot_o_extensions::{tx::TransferTransaction, Mempool};
26use serde::{Deserialize, Serialize};
27use serde_json::json;
28use sha2::{Digest, Sha256};
29use tokio::sync::RwLock;
30
31use crate::peer_store::PeerStore;
32
33/// State for the internal API, shared across peer communication handlers.
34#[derive(Clone)]
35pub struct InternalApiState {
36    /// Node ID of this validator (e.g., "validator-1")
37    #[allow(dead_code)]
38    pub node_id: String,
39    /// List of known peers in the network
40    pub peers: Arc<RwLock<Vec<PeerInfo>>>,
41    /// Current challenge being broadcast (if any)
42    pub current_challenge: Arc<RwLock<Option<serde_json::Value>>>,
43    /// Mempool for tribechain transaction gossip
44    pub mempool: Option<Arc<Mempool>>,
45    /// Ledger for tribechain state queries
46    pub ledger: Arc<RwLock<pot_o_extensions::Ledger>>,
47    /// Whether tribechain is enabled
48    pub tribechain_enabled: bool,
49    /// Optional shared secret for mint authorization
50    pub internal_mint_secret: Option<String>,
51    /// Optional peer store for persisting peers to disk
52    pub peer_store: Option<Arc<PeerStore>>,
53}
54
55/// Information about a peer validator in the network.
56#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
57pub struct PeerInfo {
58    /// Unique identifier for this peer
59    pub node_id: String,
60    /// HTTP URL to reach this peer
61    pub url: String,
62    /// Last time we heard from this peer
63    pub last_seen: DateTime<Utc>,
64}
65
66/// Request payload for peer registration
67#[derive(Debug, Serialize, Deserialize)]
68pub struct RegisterPeerRequest {
69    pub node_id: String,
70    pub url: String,
71}
72
73/// Request payload for challenge broadcast
74#[derive(Debug, Serialize, Deserialize)]
75pub struct BroadcastChallengeRequest {
76    #[serde(flatten)]
77    pub challenge: serde_json::Value,
78}
79
80/// Generic response for API operations
81#[derive(Debug, Serialize)]
82pub struct ApiResponse {
83    pub message: String,
84}
85
86/// Builds the Axum router for internal API endpoints.
87pub fn internal_router(state: InternalApiState) -> Router {
88    Router::new()
89        .route("/internal/peer/register", post(handle_peer_register))
90        .route("/internal/peers", get(handle_list_peers))
91        .route(
92            "/internal/challenge/broadcast",
93            post(handle_challenge_broadcast),
94        )
95        .route("/internal/tx/broadcast", post(handle_tx_broadcast))
96        .route("/internal/mempool/hashes", get(handle_mempool_hashes))
97        .route("/internal/tx/:hash", get(handle_get_tx_by_hash))
98        .route("/internal/mint", post(handle_internal_mint))
99        .route("/api/pool/submit-batch", post(handle_submit_batch))
100        .layer(middleware::from_fn(verify_peer_signature_middleware))
101        .with_state(state)
102}
103
104/// Middleware that verifies peer signatures on incoming requests.
105///
106/// If the request contains `X-Node-Pubkey` and `X-Node-Signature` headers,
107/// the signature is verified against the reconstructed message:
108/// `timestamp:method:path:body_hash`
109async fn verify_peer_signature_middleware(req: Request<Body>, next: Next) -> impl IntoResponse {
110    let pubkey_hex = req
111        .headers()
112        .get("x-node-pubkey")
113        .and_then(|v| v.to_str().ok())
114        .map(|s| s.to_string());
115    let signature_hex = req
116        .headers()
117        .get("x-node-signature")
118        .and_then(|v| v.to_str().ok())
119        .map(|s| s.to_string());
120    let timestamp = req
121        .headers()
122        .get("x-node-timestamp")
123        .and_then(|v| v.to_str().ok())
124        .map(|s| s.to_string());
125
126    let has_auth_headers = pubkey_hex.is_some() && signature_hex.is_some() && timestamp.is_some();
127
128    if !has_auth_headers {
129        return next.run(req).await.into_response();
130    }
131
132    let pubkey_hex = pubkey_hex.unwrap();
133    let signature_hex = signature_hex.unwrap();
134    let timestamp = timestamp.unwrap();
135
136    let pubkey_bytes = match hex::decode(&pubkey_hex) {
137        Ok(b) if b.len() == 32 => {
138            let mut arr = [0u8; 32];
139            arr.copy_from_slice(&b);
140            arr
141        }
142        _ => {
143            return (
144                StatusCode::UNAUTHORIZED,
145                Json(json!({"error": "invalid pubkey format"})),
146            )
147                .into_response();
148        }
149    };
150
151    let signature_bytes = match hex::decode(&signature_hex) {
152        Ok(b) if b.len() == 64 => b,
153        _ => {
154            return (
155                StatusCode::UNAUTHORIZED,
156                Json(json!({"error": "invalid signature format"})),
157            )
158                .into_response();
159        }
160    };
161
162    let method = req.method().as_str().to_string();
163    let path = req.uri().path().to_string();
164
165    let body_bytes = match axum::body::to_bytes(req.into_body(), 1024 * 1024).await {
166        Ok(b) => b,
167        Err(_) => {
168            return (
169                StatusCode::BAD_REQUEST,
170                Json(json!({"error": "failed to read body"})),
171            )
172                .into_response();
173        }
174    };
175
176    let body_hash = hex::encode(Sha256::digest(&body_bytes));
177    let message = format!("{}:{}:{}:{}", timestamp, method, path, body_hash);
178
179    if !pot_o_extensions::peer_auth::verify_peer_signature(
180        &pubkey_bytes,
181        message.as_bytes(),
182        &signature_bytes,
183    ) {
184        return (
185            StatusCode::UNAUTHORIZED,
186            Json(json!({"error": "invalid signature"})),
187        )
188            .into_response();
189    }
190
191    let new_req = Request::builder()
192        .method(method.as_str())
193        .uri(path.as_str())
194        .body(Body::from(body_bytes))
195        .unwrap();
196    next.run(new_req).await.into_response()
197}
198
199/// Handler: POST /internal/peer/register
200/// Registers a peer validator in the network, deduplicating by node_id.
201async fn handle_peer_register(
202    State(state): State<InternalApiState>,
203    Json(req): Json<RegisterPeerRequest>,
204) -> impl IntoResponse {
205    if req.node_id.is_empty() || req.url.is_empty() {
206        return (
207            StatusCode::BAD_REQUEST,
208            Json(json!({"error": "node_id and url are required"})),
209        )
210            .into_response();
211    }
212
213    let peer_info = PeerInfo {
214        node_id: req.node_id.clone(),
215        url: req.url,
216        last_seen: Utc::now(),
217    };
218
219    let mut peers = state.peers.write().await;
220
221    // Check if peer already exists (by node_id) and update or insert
222    if let Some(existing) = peers.iter_mut().find(|p| p.node_id == req.node_id) {
223        existing.last_seen = Utc::now();
224        existing.url = peer_info.url;
225    } else {
226        peers.push(peer_info);
227    }
228
229    drop(peers);
230
231    if let Some(ref store) = state.peer_store {
232        let store = store.clone();
233        tokio::spawn(async move {
234            if let Err(e) = store.save().await {
235                tracing::warn!(error = %e, "Failed to persist peers after registration");
236            }
237        });
238    }
239
240    (
241        StatusCode::OK,
242        Json(ApiResponse {
243            message: "Peer registered".to_string(),
244        }),
245    )
246        .into_response()
247}
248
249/// Handler: GET /internal/peers
250/// Returns a list of all known peers in JSON format.
251async fn handle_list_peers(State(state): State<InternalApiState>) -> impl IntoResponse {
252    let peers = state.peers.read().await.clone();
253    (StatusCode::OK, Json(peers)).into_response()
254}
255
256/// Handler: POST /internal/challenge/broadcast
257/// Receives and stores a challenge broadcast from a peer.
258async fn handle_challenge_broadcast(
259    State(state): State<InternalApiState>,
260    Json(req): Json<BroadcastChallengeRequest>,
261) -> impl IntoResponse {
262    let mut challenge = state.current_challenge.write().await;
263    *challenge = Some(req.challenge);
264
265    (
266        StatusCode::OK,
267        Json(ApiResponse {
268            message: "Challenge broadcast received".to_string(),
269        }),
270    )
271        .into_response()
272}
273
274/// Handler: POST /internal/tx/broadcast
275/// Receives a transaction from a peer and submits it to the local mempool.
276async fn handle_tx_broadcast(
277    State(state): State<InternalApiState>,
278    Json(tx_val): Json<serde_json::Value>,
279) -> impl IntoResponse {
280    if !state.tribechain_enabled {
281        return (
282            StatusCode::BAD_REQUEST,
283            Json(json!({"error": "tribechain not enabled"})),
284        )
285            .into_response();
286    }
287
288    let Some(mempool) = &state.mempool else {
289        return (
290            StatusCode::SERVICE_UNAVAILABLE,
291            Json(json!({"error": "mempool not available"})),
292        )
293            .into_response();
294    };
295
296    let tx: TransferTransaction = match serde_json::from_value(tx_val.clone()) {
297        Ok(tx) => tx,
298        Err(e) => {
299            tracing::warn!(error = %e, "Invalid transaction from peer");
300            return (
301                StatusCode::BAD_REQUEST,
302                Json(json!({"error": "invalid transaction"})),
303            )
304                .into_response();
305        }
306    };
307
308    match mempool.submit(tx, &state.ledger).await {
309        Ok(tx_hash) => {
310            tracing::debug!(tx_hash = %hex::encode(tx_hash), "Received tx from peer, added to mempool");
311            let peers = state.peers.read().await.clone();
312            let tx_val_clone = tx_val.clone();
313            tokio::spawn(async move {
314                for peer in &peers {
315                    let url = format!("{}/internal/tx/broadcast", peer.url.trim_end_matches('/'));
316                    let _ = reqwest::Client::new()
317                        .post(&url)
318                        .timeout(std::time::Duration::from_secs(5))
319                        .json(&tx_val_clone)
320                        .send()
321                        .await;
322                }
323            });
324            (
325                StatusCode::OK,
326                Json(json!({
327                    "accepted": true,
328                    "tx_hash": hex::encode(tx_hash),
329                })),
330            )
331                .into_response()
332        }
333        Err(e) => {
334            tracing::warn!(error = %e, "Rejected tx from peer");
335            (
336                StatusCode::BAD_REQUEST,
337                Json(json!({"error": "transaction rejected"})),
338            )
339                .into_response()
340        }
341    }
342}
343
344/// Handler: GET /internal/mempool/hashes
345/// Returns all tx hashes currently in the mempool for reconciliation.
346async fn handle_mempool_hashes(State(state): State<InternalApiState>) -> impl IntoResponse {
347    if !state.tribechain_enabled {
348        return (
349            StatusCode::BAD_REQUEST,
350            Json(json!({"error": "tribechain not enabled"})),
351        )
352            .into_response();
353    }
354
355    let Some(mempool) = &state.mempool else {
356        return (
357            StatusCode::SERVICE_UNAVAILABLE,
358            Json(json!({"error": "mempool not available"})),
359        )
360            .into_response();
361    };
362
363    let hashes: Vec<String> = mempool.hashes().iter().map(hex::encode).collect();
364    (StatusCode::OK, Json(json!({"hashes": hashes}))).into_response()
365}
366
367/// Handler: GET /internal/tx/:hash
368/// Returns a single transaction by its hex-encoded hash.
369async fn handle_get_tx_by_hash(
370    State(state): State<InternalApiState>,
371    Path(hash_hex): Path<String>,
372) -> impl IntoResponse {
373    if !state.tribechain_enabled {
374        return (
375            StatusCode::BAD_REQUEST,
376            Json(json!({"error": "tribechain not enabled"})),
377        )
378            .into_response();
379    }
380
381    let Some(mempool) = &state.mempool else {
382        return (
383            StatusCode::SERVICE_UNAVAILABLE,
384            Json(json!({"error": "mempool not available"})),
385        )
386            .into_response();
387    };
388
389    let hash_bytes = match hex::decode(&hash_hex) {
390        Ok(b) if b.len() == 32 => {
391            let mut arr = [0u8; 32];
392            arr.copy_from_slice(&b);
393            arr
394        }
395        _ => {
396            return (
397                StatusCode::BAD_REQUEST,
398                Json(json!({"error": "invalid hash format"})),
399            )
400                .into_response();
401        }
402    };
403
404    match mempool.get_tx(&hash_bytes) {
405        Some(tx) => match serde_json::to_value(&tx) {
406            Ok(val) => (StatusCode::OK, Json(val)).into_response(),
407            Err(_) => (
408                StatusCode::INTERNAL_SERVER_ERROR,
409                Json(json!({"error": "failed to serialize transaction"})),
410            )
411                .into_response(),
412        },
413        None => (
414            StatusCode::NOT_FOUND,
415            Json(json!({"error": "transaction not found"})),
416        )
417            .into_response(),
418    }
419}
420
421/// Request payload for batch submission
422#[derive(Debug, Serialize, Deserialize)]
423pub struct SubmitBatchRequest {
424    pub node_id: String,
425    pub pool_strategy: String,
426    pub batch: Vec<ProofRecord>,
427}
428
429/// Handler: POST /api/pool/submit-batch
430/// Receives a batch of proof records from a peer validator for pool accounting.
431/// Returns a deterministic submission ID built from the batch content.
432async fn handle_submit_batch(
433    State(_state): State<InternalApiState>,
434    Json(req): Json<SubmitBatchRequest>,
435) -> impl IntoResponse {
436    let batch_size = req.batch.len();
437
438    if batch_size == 0 {
439        return (
440            StatusCode::BAD_REQUEST,
441            Json(json!({"error": "batch cannot be empty"})),
442        )
443            .into_response();
444    }
445
446    // Deterministic hash of the batch
447    let mut hasher = Sha256::new();
448    hasher.update(b"pot-o-batch:");
449    hasher.update(req.node_id.as_bytes());
450    hasher.update(b":");
451    for record in &req.batch {
452        hasher.update(record.miner_pubkey.as_bytes());
453        hasher.update(b":");
454        hasher.update(record.challenge_id.as_bytes());
455        hasher.update(b":");
456        hasher.update(record.reward.to_le_bytes());
457        hasher.update(b":");
458        hasher.update(record.timestamp.timestamp().to_le_bytes());
459        hasher.update(b"|");
460    }
461    let hash = hex::encode(hasher.finalize());
462    let submission_id = format!("pool-batch-{}", &hash[..40]);
463
464    tracing::info!(
465        from_node = %req.node_id,
466        batch_size,
467        submission_id = %submission_id,
468        "Received batch submission from peer validator"
469    );
470
471    (
472        StatusCode::OK,
473        Json(json!({
474            "submission_id": submission_id,
475            "accepted": true,
476            "batch_size": batch_size,
477        })),
478    )
479        .into_response()
480}
481
482#[derive(Debug, Deserialize)]
483struct InternalMintRequest {
484    to: String,
485    token_type: String,
486    amount: u64,
487}
488
489async fn handle_internal_mint(
490    State(state): State<InternalApiState>,
491    headers: axum::http::HeaderMap,
492    Json(body): Json<InternalMintRequest>,
493) -> impl IntoResponse {
494    if let Some(ref expected) = state.internal_mint_secret {
495        let got = headers
496            .get("x-internal-secret")
497            .and_then(|v| v.to_str().ok());
498        if got != Some(expected.as_str()) {
499            return (
500                StatusCode::UNAUTHORIZED,
501                Json(json!({"error": "invalid internal secret"})),
502            )
503                .into_response();
504        }
505    }
506
507    if !state.tribechain_enabled {
508        return (
509            StatusCode::SERVICE_UNAVAILABLE,
510            Json(json!({"error": "tribechain not enabled"})),
511        )
512            .into_response();
513    }
514
515    let token = match token_type_from_str(&body.token_type) {
516        Some(t) => t,
517        None => {
518            return (
519                StatusCode::BAD_REQUEST,
520                Json(json!({"error": format!("Unknown token: {}", body.token_type)})),
521            )
522                .into_response();
523        }
524    };
525
526    let mut ledger = state.ledger.write().await;
527    if let Err(e) = ledger.try_issue(&body.to, &token, body.amount) {
528        return (StatusCode::BAD_REQUEST, Json(json!({"error": e}))).into_response();
529    }
530
531    let receipt = json!({
532        "status": "ok",
533        "to": body.to,
534        "token": body.token_type,
535        "amount": body.amount,
536        "tx_hash": format!("mint-{}-{}", body.to, body.token_type),
537    });
538    (StatusCode::OK, Json(receipt)).into_response()
539}
540
541fn token_type_from_str(s: &str) -> Option<TokenType> {
542    match s.to_lowercase().as_str() {
543        "tribechain" | "native" | "tribe" => Some(TokenType::TribeChain),
544        "pttc" => Some(TokenType::PTtC),
545        "nmtc" => Some(TokenType::NMTC),
546        "stomp" => Some(TokenType::STOMP),
547        "aum" => Some(TokenType::AUM),
548        "ai3" => Some(TokenType::AI3),
549        "ravecoin" => Some(TokenType::RAVECOIN),
550        _ => None,
551    }
552}
553
554#[cfg(test)]
555mod tests {
556    use super::*;
557
558    #[tokio::test]
559    async fn test_internal_api_state_creation() {
560        let state = InternalApiState {
561            node_id: "validator-1".to_string(),
562            peers: Arc::new(RwLock::new(vec![])),
563            current_challenge: Arc::new(RwLock::new(None)),
564            mempool: None,
565            ledger: Arc::new(RwLock::new(pot_o_extensions::Ledger::new(
566                "test".to_string(),
567            ))),
568            tribechain_enabled: false,
569            internal_mint_secret: None,
570            peer_store: None,
571        };
572
573        assert_eq!(state.node_id, "validator-1");
574        let peers = state.peers.read().await;
575        assert_eq!(peers.len(), 0);
576        drop(peers);
577
578        let challenge = state.current_challenge.read().await;
579        assert!(challenge.is_none());
580    }
581
582    #[tokio::test]
583    async fn test_peer_info_creation() {
584        let now = Utc::now();
585        let peer = PeerInfo {
586            node_id: "validator-2".to_string(),
587            url: "http://validator-2:8900".to_string(),
588            last_seen: now,
589        };
590
591        assert_eq!(peer.node_id, "validator-2");
592        assert_eq!(peer.url, "http://validator-2:8900");
593        assert_eq!(peer.last_seen, now);
594    }
595
596    #[tokio::test]
597    async fn test_peer_registration_creates_new_entry() {
598        let state = InternalApiState {
599            node_id: "validator-1".to_string(),
600            peers: Arc::new(RwLock::new(vec![])),
601            current_challenge: Arc::new(RwLock::new(None)),
602            mempool: None,
603            ledger: Arc::new(RwLock::new(pot_o_extensions::Ledger::new(
604                "test".to_string(),
605            ))),
606            tribechain_enabled: false,
607            internal_mint_secret: None,
608            peer_store: None,
609        };
610
611        let req = RegisterPeerRequest {
612            node_id: "validator-2".to_string(),
613            url: "http://validator-2:8900".to_string(),
614        };
615
616        let mut peers = state.peers.write().await;
617        if let Some(existing) = peers.iter_mut().find(|p| p.node_id == req.node_id) {
618            existing.last_seen = Utc::now();
619            existing.url = req.url;
620        } else {
621            peers.push(PeerInfo {
622                node_id: req.node_id,
623                url: req.url,
624                last_seen: Utc::now(),
625            });
626        }
627        drop(peers);
628
629        let peers = state.peers.read().await;
630        assert_eq!(peers.len(), 1);
631        assert_eq!(peers[0].node_id, "validator-2");
632    }
633
634    #[tokio::test]
635    async fn test_peer_registration_updates_existing_peer() {
636        let initial_peer = PeerInfo {
637            node_id: "validator-2".to_string(),
638            url: "http://validator-2:8900".to_string(),
639            last_seen: Utc::now(),
640        };
641
642        let state = InternalApiState {
643            node_id: "validator-1".to_string(),
644            peers: Arc::new(RwLock::new(vec![initial_peer.clone()])),
645            current_challenge: Arc::new(RwLock::new(None)),
646            mempool: None,
647            ledger: Arc::new(RwLock::new(pot_o_extensions::Ledger::new(
648                "test".to_string(),
649            ))),
650            tribechain_enabled: false,
651            internal_mint_secret: None,
652            peer_store: None,
653        };
654
655        let req = RegisterPeerRequest {
656            node_id: "validator-2".to_string(),
657            url: "http://validator-2:9000".to_string(), // Different URL
658        };
659
660        let mut peers = state.peers.write().await;
661        if let Some(existing) = peers.iter_mut().find(|p| p.node_id == req.node_id) {
662            existing.last_seen = Utc::now();
663            existing.url = req.url;
664        } else {
665            peers.push(PeerInfo {
666                node_id: req.node_id,
667                url: req.url,
668                last_seen: Utc::now(),
669            });
670        }
671        drop(peers);
672
673        let peers = state.peers.read().await;
674        assert_eq!(peers.len(), 1); // Still only one peer
675        assert_eq!(peers[0].url, "http://validator-2:9000"); // URL updated
676    }
677
678    #[tokio::test]
679    async fn test_list_peers_empty_by_default() {
680        let state = InternalApiState {
681            node_id: "validator-1".to_string(),
682            peers: Arc::new(RwLock::new(vec![])),
683            current_challenge: Arc::new(RwLock::new(None)),
684            mempool: None,
685            ledger: Arc::new(RwLock::new(pot_o_extensions::Ledger::new(
686                "test".to_string(),
687            ))),
688            tribechain_enabled: false,
689            internal_mint_secret: None,
690            peer_store: None,
691        };
692
693        let peers = state.peers.read().await;
694        assert_eq!(peers.len(), 0);
695    }
696
697    #[tokio::test]
698    async fn test_list_peers_returns_all_registered_peers() {
699        let peer1 = PeerInfo {
700            node_id: "validator-2".to_string(),
701            url: "http://validator-2:8900".to_string(),
702            last_seen: Utc::now(),
703        };
704
705        let peer2 = PeerInfo {
706            node_id: "validator-3".to_string(),
707            url: "http://validator-3:8900".to_string(),
708            last_seen: Utc::now(),
709        };
710
711        let state = InternalApiState {
712            node_id: "validator-1".to_string(),
713            peers: Arc::new(RwLock::new(vec![peer1.clone(), peer2.clone()])),
714            current_challenge: Arc::new(RwLock::new(None)),
715            mempool: None,
716            ledger: Arc::new(RwLock::new(pot_o_extensions::Ledger::new(
717                "test".to_string(),
718            ))),
719            tribechain_enabled: false,
720            internal_mint_secret: None,
721            peer_store: None,
722        };
723
724        let peers = state.peers.read().await;
725        assert_eq!(peers.len(), 2);
726        assert_eq!(peers[0], peer1);
727        assert_eq!(peers[1], peer2);
728    }
729
730    #[tokio::test]
731    async fn test_challenge_broadcast_stores_challenge() {
732        let state = InternalApiState {
733            node_id: "validator-1".to_string(),
734            peers: Arc::new(RwLock::new(vec![])),
735            current_challenge: Arc::new(RwLock::new(None)),
736            mempool: None,
737            ledger: Arc::new(RwLock::new(pot_o_extensions::Ledger::new(
738                "test".to_string(),
739            ))),
740            tribechain_enabled: false,
741            internal_mint_secret: None,
742            peer_store: None,
743        };
744
745        let challenge_data = json!({"id": "challenge-1", "difficulty": 2});
746        let mut challenge = state.current_challenge.write().await;
747        *challenge = Some(challenge_data.clone());
748        drop(challenge);
749
750        let challenge = state.current_challenge.read().await;
751        assert!(challenge.is_some());
752        assert_eq!(*challenge, Some(challenge_data));
753    }
754
755    #[tokio::test]
756    async fn test_challenge_broadcast_replaces_old_challenge() {
757        let old_challenge = json!({"id": "challenge-1", "difficulty": 1});
758        let new_challenge = json!({"id": "challenge-2", "difficulty": 2});
759
760        let state = InternalApiState {
761            node_id: "validator-1".to_string(),
762            peers: Arc::new(RwLock::new(vec![])),
763            current_challenge: Arc::new(RwLock::new(Some(old_challenge))),
764            mempool: None,
765            ledger: Arc::new(RwLock::new(pot_o_extensions::Ledger::new(
766                "test".to_string(),
767            ))),
768            tribechain_enabled: false,
769            internal_mint_secret: None,
770            peer_store: None,
771        };
772
773        let mut challenge = state.current_challenge.write().await;
774        *challenge = Some(new_challenge.clone());
775        drop(challenge);
776
777        let challenge = state.current_challenge.read().await;
778        assert_eq!(*challenge, Some(new_challenge));
779    }
780
781    #[tokio::test]
782    async fn test_peer_deduplication_by_node_id() {
783        let peer1 = PeerInfo {
784            node_id: "validator-2".to_string(),
785            url: "http://validator-2:8900".to_string(),
786            last_seen: Utc::now(),
787        };
788
789        let peer2 = PeerInfo {
790            node_id: "validator-3".to_string(),
791            url: "http://validator-3:8900".to_string(),
792            last_seen: Utc::now(),
793        };
794
795        let state = InternalApiState {
796            node_id: "validator-1".to_string(),
797            peers: Arc::new(RwLock::new(vec![peer1, peer2])),
798            current_challenge: Arc::new(RwLock::new(None)),
799            mempool: None,
800            ledger: Arc::new(RwLock::new(pot_o_extensions::Ledger::new(
801                "test".to_string(),
802            ))),
803            tribechain_enabled: false,
804            internal_mint_secret: None,
805            peer_store: None,
806        };
807
808        // Register with duplicate node_id
809        let req = RegisterPeerRequest {
810            node_id: "validator-2".to_string(),
811            url: "http://validator-2:9000".to_string(),
812        };
813
814        let mut peers = state.peers.write().await;
815        if let Some(existing) = peers.iter_mut().find(|p| p.node_id == req.node_id) {
816            existing.url = req.url;
817            existing.last_seen = Utc::now();
818        } else {
819            peers.push(PeerInfo {
820                node_id: req.node_id,
821                url: req.url,
822                last_seen: Utc::now(),
823            });
824        }
825        drop(peers);
826
827        let peers = state.peers.read().await;
828        assert_eq!(peers.len(), 2); // Still 2 peers, not 3
829        assert_eq!(
830            peers.iter().filter(|p| p.node_id == "validator-2").count(),
831            1
832        );
833    }
834
835    #[tokio::test]
836    async fn test_last_seen_update_on_re_registration() {
837        let initial_time = Utc::now();
838        let peer = PeerInfo {
839            node_id: "validator-2".to_string(),
840            url: "http://validator-2:8900".to_string(),
841            last_seen: initial_time,
842        };
843
844        let state = InternalApiState {
845            node_id: "validator-1".to_string(),
846            peers: Arc::new(RwLock::new(vec![peer])),
847            current_challenge: Arc::new(RwLock::new(None)),
848            mempool: None,
849            ledger: Arc::new(RwLock::new(pot_o_extensions::Ledger::new(
850                "test".to_string(),
851            ))),
852            tribechain_enabled: false,
853            internal_mint_secret: None,
854            peer_store: None,
855        };
856
857        // Wait a bit and re-register
858        tokio::time::sleep(tokio::time::Duration::from_millis(10)).await;
859
860        let req = RegisterPeerRequest {
861            node_id: "validator-2".to_string(),
862            url: "http://validator-2:8900".to_string(),
863        };
864
865        let mut peers = state.peers.write().await;
866        if let Some(existing) = peers.iter_mut().find(|p| p.node_id == req.node_id) {
867            existing.last_seen = Utc::now();
868            existing.url = req.url;
869        } else {
870            peers.push(PeerInfo {
871                node_id: req.node_id,
872                url: req.url,
873                last_seen: Utc::now(),
874            });
875        }
876        drop(peers);
877
878        let peers = state.peers.read().await;
879        assert!(peers[0].last_seen > initial_time);
880    }
881
882    #[tokio::test]
883    async fn test_handler_peer_registration_success() {
884        let state = InternalApiState {
885            node_id: "validator-1".to_string(),
886            peers: Arc::new(RwLock::new(vec![])),
887            current_challenge: Arc::new(RwLock::new(None)),
888            mempool: None,
889            ledger: Arc::new(RwLock::new(pot_o_extensions::Ledger::new(
890                "test".to_string(),
891            ))),
892            tribechain_enabled: false,
893            internal_mint_secret: None,
894            peer_store: None,
895        };
896
897        // Verify the handler returns correct structure
898        let peers = state.peers.read().await;
899        assert_eq!(peers.len(), 0);
900    }
901
902    #[tokio::test]
903    async fn test_handler_challenge_broadcast_success() {
904        let state = InternalApiState {
905            node_id: "validator-1".to_string(),
906            peers: Arc::new(RwLock::new(vec![])),
907            current_challenge: Arc::new(RwLock::new(None)),
908            mempool: None,
909            ledger: Arc::new(RwLock::new(pot_o_extensions::Ledger::new(
910                "test".to_string(),
911            ))),
912            tribechain_enabled: false,
913            internal_mint_secret: None,
914            peer_store: None,
915        };
916
917        let challenge = json!({"id": "c1", "difficulty": 2});
918        let mut challenge_lock = state.current_challenge.write().await;
919        *challenge_lock = Some(challenge.clone());
920        drop(challenge_lock);
921
922        let stored = state.current_challenge.read().await;
923        assert_eq!(*stored, Some(challenge));
924    }
925
926    #[tokio::test]
927    async fn test_multiple_peers_can_coexist() {
928        let peers_vec = vec![
929            PeerInfo {
930                node_id: "v2".to_string(),
931                url: "http://v2:8900".to_string(),
932                last_seen: Utc::now(),
933            },
934            PeerInfo {
935                node_id: "v3".to_string(),
936                url: "http://v3:8900".to_string(),
937                last_seen: Utc::now(),
938            },
939            PeerInfo {
940                node_id: "v4".to_string(),
941                url: "http://v4:8900".to_string(),
942                last_seen: Utc::now(),
943            },
944        ];
945
946        let state = InternalApiState {
947            node_id: "validator-1".to_string(),
948            peers: Arc::new(RwLock::new(peers_vec)),
949            current_challenge: Arc::new(RwLock::new(None)),
950            mempool: None,
951            ledger: Arc::new(RwLock::new(pot_o_extensions::Ledger::new(
952                "test".to_string(),
953            ))),
954            tribechain_enabled: false,
955            internal_mint_secret: None,
956            peer_store: None,
957        };
958
959        let peers = state.peers.read().await;
960        assert_eq!(peers.len(), 3);
961        assert_eq!(peers[0].node_id, "v2");
962        assert_eq!(peers[1].node_id, "v3");
963        assert_eq!(peers[2].node_id, "v4");
964    }
965}