@@ -29,9 +29,43 @@ use platform_subnet_manager::BanList;
2929use std:: collections:: HashMap ;
3030use std:: path:: PathBuf ;
3131use std:: sync:: Arc ;
32+ use std:: time:: { SystemTime , UNIX_EPOCH } ;
3233use tokio:: sync:: mpsc;
3334use tracing:: { debug, error, info, warn} ;
3435
36+ // ==================== Container Authentication ====================
37+
38+ /// Session with a challenge container
39+ #[ derive( Debug , Clone ) ]
40+ #[ allow( dead_code) ]
41+ struct ContainerAuthSession {
42+ /// The authentication token
43+ token : String ,
44+ /// When the session expires
45+ expires_at : u64 ,
46+ /// Challenge container name (for logging)
47+ container_name : String ,
48+ }
49+
50+ /// Request body for authenticating with a challenge container
51+ #[ derive( Debug , Clone , serde:: Serialize ) ]
52+ struct ContainerAuthRequest {
53+ hotkey : String ,
54+ challenge_id : String ,
55+ timestamp : u64 ,
56+ nonce : String ,
57+ signature : String ,
58+ }
59+
60+ /// Response from challenge container authentication
61+ #[ derive( Debug , Clone , serde:: Deserialize ) ]
62+ struct ContainerAuthResponse {
63+ success : bool ,
64+ session_token : Option < String > ,
65+ expires_at : Option < u64 > ,
66+ error : Option < String > ,
67+ }
68+
3569#[ derive( Parser , Debug ) ]
3670#[ command( name = "validator-node" ) ]
3771#[ command( about = "Mini-chain validator node" ) ]
@@ -339,6 +373,10 @@ async fn main() -> Result<()> {
339373 let challenge_endpoints: Arc < RwLock < std:: collections:: HashMap < String , String > > > =
340374 Arc :: new ( RwLock :: new ( std:: collections:: HashMap :: new ( ) ) ) ;
341375
376+ // Store authenticated sessions with challenge containers
377+ let container_auth_sessions: Arc < RwLock < HashMap < String , ContainerAuthSession > > > =
378+ Arc :: new ( RwLock :: new ( HashMap :: new ( ) ) ) ;
379+
342380 // Start RPC server (if enabled)
343381 // Challenge-specific logic is handled by Docker containers
344382 // The validator only proxies requests to challenges via HTTP
@@ -1194,6 +1232,9 @@ async fn main() -> Result<()> {
11941232 info ! ( "Validator node running. Press Ctrl+C to stop." ) ;
11951233
11961234 let chain_state_clone = chain_state. clone ( ) ;
1235+ // Clone keypair and auth sessions for P2P message handling
1236+ let keypair_for_p2p = Some ( keypair. clone ( ) ) ;
1237+ let auth_sessions_for_p2p = Some ( container_auth_sessions. clone ( ) ) ;
11971238 // Get challenge_routes Arc for auto-registration when receiving via P2P
11981239 let challenge_routes_for_p2p = rpc_handler. as_ref ( ) . map ( |h| h. challenge_routes . clone ( ) ) ;
11991240 // Get distributed_db for P2P message handling
@@ -1568,7 +1609,7 @@ async fn main() -> Result<()> {
15681609
15691610 if has_sufficient_stake {
15701611 // Forward all messages to consensus handler
1571- handle_message( & consensus, signed, & chain_state_clone, challenge_orchestrator. as_ref( ) , challenge_routes_for_p2p. as_ref( ) , db_for_p2p. as_ref( ) ) . await ;
1612+ handle_message( & consensus, signed, & chain_state_clone, challenge_orchestrator. as_ref( ) , challenge_routes_for_p2p. as_ref( ) , db_for_p2p. as_ref( ) , keypair_for_p2p . as_ref ( ) , auth_sessions_for_p2p . as_ref ( ) ) . await ;
15721613 } else {
15731614 // Allow Sudo to bypass stake check for bootstrapping and upgrades
15741615 let is_sudo = {
@@ -1578,7 +1619,7 @@ async fn main() -> Result<()> {
15781619
15791620 if is_sudo {
15801621 info!( "Bypassing stake check for Sudo message from {}" , & signer_hex[ ..16 ] ) ;
1581- handle_message( & consensus, signed, & chain_state_clone, challenge_orchestrator. as_ref( ) , challenge_routes_for_p2p. as_ref( ) , db_for_p2p. as_ref( ) ) . await ;
1622+ handle_message( & consensus, signed, & chain_state_clone, challenge_orchestrator. as_ref( ) , challenge_routes_for_p2p. as_ref( ) , db_for_p2p. as_ref( ) , keypair_for_p2p . as_ref ( ) , auth_sessions_for_p2p . as_ref ( ) ) . await ;
15821623 } else {
15831624 warn!(
15841625 "Rejected message from {} - insufficient stake (min {} TAO required)" ,
@@ -1878,6 +1919,8 @@ async fn handle_message(
18781919 & Arc < RwLock < HashMap < String , Vec < platform_challenge_sdk:: ChallengeRoute > > > > ,
18791920 > ,
18801921 distributed_db : Option < & Arc < distributed_db:: DistributedDB > > ,
1922+ keypair : Option < & Keypair > ,
1923+ container_auth_sessions : Option < & Arc < RwLock < HashMap < String , ContainerAuthSession > > > > ,
18811924) {
18821925 let signer = msg. signer ( ) . clone ( ) ;
18831926
@@ -2073,14 +2116,36 @@ async fn handle_message(
20732116 ) ;
20742117
20752118 // Forward challenge message to the appropriate container via HTTP
2119+ // Need keypair for authentication
2120+ if keypair. is_none ( ) || container_auth_sessions. is_none ( ) {
2121+ debug ! ( "Skipping challenge message forward - no auth context available" ) ;
2122+ return ;
2123+ }
2124+
20762125 let challenge_id = challenge_msg. challenge_id . clone ( ) ;
20772126 let container_name = challenge_id. to_lowercase ( ) . replace ( [ ' ' , '_' ] , "-" ) ;
20782127 let from_hotkey = signer. to_hex ( ) ;
20792128 let msg_payload = challenge_msg. payload . clone ( ) ;
2129+ let kp = keypair. unwrap ( ) . clone ( ) ;
2130+ let sessions = container_auth_sessions. unwrap ( ) . clone ( ) ;
20802131
20812132 tokio:: spawn ( async move {
20822133 let client = reqwest:: Client :: new ( ) ;
2083- let p2p_endpoint = format ! ( "http://challenge-{}:8080/p2p/message" , container_name) ;
2134+ let base_url = format ! ( "http://challenge-{}:8080" , container_name) ;
2135+ let p2p_endpoint = format ! ( "{}/p2p/message" , base_url) ;
2136+
2137+ // Get or create authentication token
2138+ let auth_token =
2139+ match get_container_auth ( & sessions, & base_url, & challenge_id, & kp) . await {
2140+ Ok ( token) => token,
2141+ Err ( e) => {
2142+ warn ! (
2143+ "Failed to authenticate with container {}: {}" ,
2144+ container_name, e
2145+ ) ;
2146+ return ;
2147+ }
2148+ } ;
20842149
20852150 // Convert ChallengeMessageType to ChallengeP2PMessage for the container
20862151 let p2p_message = match challenge_msg. message_type {
@@ -2126,13 +2191,24 @@ async fn handle_message(
21262191 "message" : message
21272192 } ) ;
21282193
2129- match client. post ( & p2p_endpoint) . json ( & req_body) . send ( ) . await {
2194+ match client
2195+ . post ( & p2p_endpoint)
2196+ . header ( "X-Auth-Token" , & auth_token)
2197+ . json ( & req_body)
2198+ . send ( )
2199+ . await
2200+ {
21302201 Ok ( resp) if resp. status ( ) . is_success ( ) => {
21312202 debug ! (
2132- "Forwarded challenge message to container {}" ,
2203+ "Forwarded challenge message to container {} (authenticated) " ,
21332204 container_name
21342205 ) ;
21352206 }
2207+ Ok ( resp) if resp. status ( ) == reqwest:: StatusCode :: UNAUTHORIZED => {
2208+ // Token expired, remove from cache
2209+ sessions. write ( ) . remove ( & challenge_id) ;
2210+ warn ! ( "Auth token expired for container {}, will re-authenticate on next message" , container_name) ;
2211+ }
21362212 Ok ( resp) => {
21372213 debug ! ( "Container {} returned {}" , container_name, resp. status( ) ) ;
21382214 }
@@ -2480,3 +2556,128 @@ async fn discover_routes(url: &str) -> anyhow::Result<RoutesManifestResponse> {
24802556 let manifest: RoutesManifestResponse = response. json ( ) . await ?;
24812557 Ok ( manifest)
24822558}
2559+
2560+ /// Authenticate with a challenge container
2561+ /// Returns the session token on success
2562+ async fn authenticate_with_container (
2563+ base_url : & str ,
2564+ challenge_id : & str ,
2565+ keypair : & Keypair ,
2566+ ) -> anyhow:: Result < ContainerAuthSession > {
2567+ let client = reqwest:: Client :: builder ( )
2568+ . timeout ( std:: time:: Duration :: from_secs ( 30 ) )
2569+ . build ( ) ?;
2570+
2571+ let timestamp = SystemTime :: now ( )
2572+ . duration_since ( UNIX_EPOCH )
2573+ . unwrap ( )
2574+ . as_secs ( ) ;
2575+
2576+ // Generate random nonce
2577+ let nonce: [ u8 ; 32 ] = rand:: random ( ) ;
2578+ let nonce_hex = hex:: encode ( nonce) ;
2579+
2580+ // Create message to sign: "auth:{challenge_id}:{timestamp}:{nonce}"
2581+ let message = format ! ( "auth:{}:{}:{}" , challenge_id, timestamp, nonce_hex) ;
2582+
2583+ // Sign the message
2584+ let signed = keypair. sign ( message. as_bytes ( ) ) ;
2585+ let signature_hex = hex:: encode ( & signed. signature ) ;
2586+
2587+ let auth_request = ContainerAuthRequest {
2588+ hotkey : keypair. hotkey ( ) . to_hex ( ) ,
2589+ challenge_id : challenge_id. to_string ( ) ,
2590+ timestamp,
2591+ nonce : nonce_hex,
2592+ signature : signature_hex,
2593+ } ;
2594+
2595+ let auth_url = format ! ( "{}/auth" , base_url. trim_end_matches( '/' ) ) ;
2596+
2597+ info ! (
2598+ "Authenticating with container at {} for challenge {}" ,
2599+ auth_url, challenge_id
2600+ ) ;
2601+
2602+ let response = client. post ( & auth_url) . json ( & auth_request) . send ( ) . await ?;
2603+
2604+ if !response. status ( ) . is_success ( ) {
2605+ let status = response. status ( ) ;
2606+ let body = response. text ( ) . await . unwrap_or_default ( ) ;
2607+ anyhow:: bail!(
2608+ "Container authentication failed with status {}: {}" ,
2609+ status,
2610+ body
2611+ ) ;
2612+ }
2613+
2614+ let auth_response: ContainerAuthResponse = response. json ( ) . await ?;
2615+
2616+ if !auth_response. success {
2617+ anyhow:: bail!(
2618+ "Container authentication rejected: {}" ,
2619+ auth_response
2620+ . error
2621+ . unwrap_or_else( || "Unknown error" . to_string( ) )
2622+ ) ;
2623+ }
2624+
2625+ let token = auth_response
2626+ . session_token
2627+ . ok_or_else ( || anyhow:: anyhow!( "No session token in auth response" ) ) ?;
2628+
2629+ let expires_at = auth_response. expires_at . unwrap_or ( timestamp + 3600 ) ; // Default 1 hour
2630+
2631+ let container_name = challenge_id. to_lowercase ( ) . replace ( [ ' ' , '_' ] , "-" ) ;
2632+
2633+ info ! (
2634+ "Successfully authenticated with container {} (expires at {})" ,
2635+ container_name, expires_at
2636+ ) ;
2637+
2638+ Ok ( ContainerAuthSession {
2639+ token,
2640+ expires_at,
2641+ container_name,
2642+ } )
2643+ }
2644+
2645+ /// Check if a session is still valid (not expired)
2646+ fn is_session_valid ( session : & ContainerAuthSession ) -> bool {
2647+ let now = SystemTime :: now ( )
2648+ . duration_since ( UNIX_EPOCH )
2649+ . unwrap ( )
2650+ . as_secs ( ) ;
2651+
2652+ // Add 60 second buffer before expiry
2653+ now < session. expires_at . saturating_sub ( 60 )
2654+ }
2655+
2656+ /// Get or refresh authentication session for a container
2657+ async fn get_container_auth (
2658+ sessions : & RwLock < HashMap < String , ContainerAuthSession > > ,
2659+ base_url : & str ,
2660+ challenge_id : & str ,
2661+ keypair : & Keypair ,
2662+ ) -> anyhow:: Result < String > {
2663+ // Check if we have a valid session
2664+ {
2665+ let sessions_read = sessions. read ( ) ;
2666+ if let Some ( session) = sessions_read. get ( challenge_id) {
2667+ if is_session_valid ( session) {
2668+ return Ok ( session. token . clone ( ) ) ;
2669+ }
2670+ }
2671+ }
2672+
2673+ // Need to authenticate or re-authenticate
2674+ let session = authenticate_with_container ( base_url, challenge_id, keypair) . await ?;
2675+ let token = session. token . clone ( ) ;
2676+
2677+ {
2678+ let mut sessions_write = sessions. write ( ) ;
2679+ sessions_write. insert ( challenge_id. to_string ( ) , session) ;
2680+ }
2681+
2682+ Ok ( token)
2683+ }
0 commit comments