@@ -43,6 +43,8 @@ pub fn router(state: Arc<AppState>) -> Router {
4343 . route ( "/batch/{id}/tasks" , get ( get_batch_tasks) )
4444 . route ( "/batch/{id}/task/{task_id}" , get ( get_task) )
4545 . route ( "/batches" , get ( list_batches) )
46+ . route ( "/verify/{batch_id}" , get ( verify_batch) )
47+ . route ( "/instance" , get ( instance_info) )
4648 . route ( "/ws" , get ( ws:: ws_handler) )
4749 . with_state ( state)
4850}
@@ -400,3 +402,119 @@ async fn list_batches(State(state): State<Arc<AppState>>) -> Json<Vec<BatchListE
400402 . collect ( ) ,
401403 )
402404}
405+
406+ /// Execution proof for a completed batch.
407+ /// Returns a SHA256 hash of the batch results that validators can verify.
408+ #[ derive( Serialize ) ]
409+ struct ExecutionProof {
410+ batch_id : String ,
411+ status : crate :: session:: BatchStatus ,
412+ total_tasks : usize ,
413+ passed_tasks : usize ,
414+ failed_tasks : usize ,
415+ aggregate_reward : f64 ,
416+ /// SHA256 hash of: batch_id + task results (task_id, passed, reward) sorted
417+ results_hash : String ,
418+ /// Per-task summary
419+ task_summaries : Vec < TaskSummary > ,
420+ /// Executor version
421+ executor_version : String ,
422+ /// Instance uptime in seconds
423+ uptime_secs : i64 ,
424+ }
425+
426+ #[ derive( Serialize ) ]
427+ struct TaskSummary {
428+ task_id : String ,
429+ passed : bool ,
430+ reward : f64 ,
431+ duration_ms : Option < u64 > ,
432+ }
433+
434+ async fn verify_batch (
435+ State ( state) : State < Arc < AppState > > ,
436+ axum:: extract:: Path ( batch_id) : axum:: extract:: Path < String > ,
437+ ) -> Result < Json < ExecutionProof > , StatusCode > {
438+ let batch = state. sessions . get ( & batch_id) . ok_or ( StatusCode :: NOT_FOUND ) ?;
439+ let result = batch. result . lock ( ) . await ;
440+
441+ // Only return proof for completed batches
442+ if result. status != crate :: session:: BatchStatus :: Completed
443+ && result. status != crate :: session:: BatchStatus :: Failed
444+ {
445+ return Err ( StatusCode :: CONFLICT ) ;
446+ }
447+
448+ // Build deterministic hash of results
449+ let mut hasher = Sha256 :: new ( ) ;
450+ hasher. update ( result. batch_id . as_bytes ( ) ) ;
451+ let mut sorted_tasks: Vec < _ > = result. tasks . iter ( ) . collect ( ) ;
452+ sorted_tasks. sort_by ( |a, b| a. task_id . cmp ( & b. task_id ) ) ;
453+ for task in & sorted_tasks {
454+ hasher. update ( task. task_id . as_bytes ( ) ) ;
455+ hasher. update ( if task. passed == Some ( true ) { b"1" } else { b"0" } ) ;
456+ hasher. update ( task. reward . to_bits ( ) . to_le_bytes ( ) ) ;
457+ }
458+ let results_hash = hex:: encode ( hasher. finalize ( ) ) ;
459+
460+ let task_summaries: Vec < TaskSummary > = sorted_tasks
461+ . iter ( )
462+ . map ( |t| TaskSummary {
463+ task_id : t. task_id . clone ( ) ,
464+ passed : t. passed == Some ( true ) ,
465+ reward : t. reward ,
466+ duration_ms : t. duration_ms ,
467+ } )
468+ . collect ( ) ;
469+
470+ let uptime = ( Utc :: now ( ) - state. started_at ) . num_seconds ( ) ;
471+
472+ Ok ( Json ( ExecutionProof {
473+ batch_id : result. batch_id . clone ( ) ,
474+ status : result. status . clone ( ) ,
475+ total_tasks : result. total_tasks ,
476+ passed_tasks : result. passed_tasks ,
477+ failed_tasks : result. failed_tasks ,
478+ aggregate_reward : result. aggregate_reward ,
479+ results_hash,
480+ task_summaries,
481+ executor_version : env ! ( "CARGO_PKG_VERSION" ) . to_string ( ) ,
482+ uptime_secs : uptime,
483+ } ) )
484+ }
485+
486+ /// Instance metadata — returns info about this executor instance.
487+ /// Validators use this to verify the executor is running the expected image.
488+ #[ derive( Serialize ) ]
489+ struct InstanceInfo {
490+ /// Executor version from Cargo.toml
491+ version : String ,
492+ /// Image name (from IMAGE_NAME env var, set in Dockerfile)
493+ image : String ,
494+ /// Image digest (from IMAGE_DIGEST env var, set at build/deploy time)
495+ image_digest : String ,
496+ /// Uptime in seconds
497+ uptime_secs : i64 ,
498+ /// Node hostname
499+ hostname : String ,
500+ /// Max concurrent tasks
501+ max_concurrent_tasks : usize ,
502+ /// Bittensor netuid
503+ netuid : u16 ,
504+ }
505+
506+ async fn instance_info ( State ( state) : State < Arc < AppState > > ) -> Json < InstanceInfo > {
507+ let uptime = ( Utc :: now ( ) - state. started_at ) . num_seconds ( ) ;
508+ Json ( InstanceInfo {
509+ version : env ! ( "CARGO_PKG_VERSION" ) . to_string ( ) ,
510+ image : std:: env:: var ( "IMAGE_NAME" )
511+ . unwrap_or_else ( |_| "platformnetwork/term-executor" . to_string ( ) ) ,
512+ image_digest : std:: env:: var ( "IMAGE_DIGEST" ) . unwrap_or_default ( ) ,
513+ uptime_secs : uptime,
514+ hostname : hostname:: get ( )
515+ . map ( |h| h. to_string_lossy ( ) . to_string ( ) )
516+ . unwrap_or_default ( ) ,
517+ max_concurrent_tasks : state. config . max_concurrent_tasks ,
518+ netuid : state. config . bittensor_netuid ,
519+ } )
520+ }
0 commit comments