Skip to content

Commit 067426c

Browse files
committed
Add /verify/{batch_id} and /instance endpoints for proof-of-execution
- /instance: returns executor version, image name, image digest, netuid IMAGE_NAME and IMAGE_DIGEST injected via Dockerfile env vars - /verify/{batch_id}: returns execution proof with deterministic SHA256 hash of sorted task results (task_id, passed, reward) - Dockerfile: add IMAGE_NAME/IMAGE_DIGEST env vars for verification - Used by term-challenge WASM basilica verification module
1 parent dac8c4a commit 067426c

File tree

4 files changed

+137
-2
lines changed

4 files changed

+137
-2
lines changed

Cargo.lock

Lines changed: 14 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ zip = "2"
5353
# Temp files
5454
tempfile = "3"
5555

56+
# System info
57+
hostname = "0.4"
58+
5659
# Concurrency
5760
parking_lot = "0.12"
5861
dashmap = "6"

Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ COPY --from=builder /build/target/release/term-executor /usr/local/bin/
1919
RUN groupadd --system executor && useradd --system --gid executor --create-home executor \
2020
&& mkdir -p /tmp/sessions && chown executor:executor /tmp/sessions
2121
USER executor
22+
ENV IMAGE_NAME=platformnetwork/term-executor
23+
ENV IMAGE_DIGEST=""
2224
EXPOSE 8080
2325
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
2426
CMD curl -f http://localhost:8080/health || exit 1

src/handlers.rs

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)