Skip to main content

mina_node_native/http_server/routes/
snarker.rs

1//! Snarker endpoints.
2//!
3//! - `POST /snarker/job/commit` - Commit to a snark job
4//! - `GET /snarker/job/spec` - Get snark job specification
5//! - `GET /snarker/workers` - Get snarker workers
6//! - `GET /snarker/config` - Get snarker configuration
7
8use std::str::FromStr;
9
10use axum::{
11    body::Body,
12    extract::{Query, State},
13    http::{header, HeaderMap, Response, StatusCode},
14    Json,
15};
16use mina_p2p_messages::binprot::BinProtWrite;
17use serde::Deserialize;
18use utoipa_axum::{router::OpenApiRouter, routes};
19
20use mina_node::{
21    core::snark::SnarkJobId,
22    rpc::{
23        RpcRequest, RpcSnarkerConfigGetResponse, RpcSnarkerJobCommitResponse,
24        RpcSnarkerJobSpecResponse, RpcSnarkerWorkersResponse,
25    },
26};
27
28use crate::http_server::{AppError, AppResult, AppState};
29
30/// Snarker routes
31pub fn routes() -> OpenApiRouter<AppState> {
32    OpenApiRouter::new()
33        .routes(routes!(job_commit))
34        .routes(routes!(job_spec))
35        .routes(routes!(workers))
36        .routes(routes!(config))
37}
38
39/// Commit to a snark job
40#[utoipa::path(
41    post,
42    path = "/snarker/job/commit",
43    tag = "snarker",
44    responses(
45        (status = 201, description = "Job committed"),
46        (status = 400, description = "Invalid input or job error")
47    )
48)]
49async fn job_commit(
50    State(state): State<AppState>,
51    body: String,
52) -> AppResult<(StatusCode, Json<RpcSnarkerJobCommitResponse>)> {
53    // TODO(binier): make endpoint only accessible locally.
54    // TODO(axum-migration): Error returns bare JSON string for warp compatibility.
55    // Migrate to structured error (e.g., `{"error": "...", "details": {...}}`).
56    let job_id = SnarkJobId::from_str(&body)
57        .map_err(|_| AppError::Json(StatusCode::BAD_REQUEST, serde_json::json!("invalid_input")))?;
58
59    let resp: RpcSnarkerJobCommitResponse =
60        rpc_request!(state, RpcRequest::SnarkerJobCommit { job_id })?;
61
62    let status = match &resp {
63        RpcSnarkerJobCommitResponse::Ok => StatusCode::CREATED,
64        _ => StatusCode::BAD_REQUEST,
65    };
66
67    Ok((status, Json(resp)))
68}
69
70#[derive(Deserialize)]
71struct JobSpecQuery {
72    id: SnarkJobId,
73}
74
75/// Snark job specification
76///
77/// Supports JSON and binary (binprot) output based on Accept header.
78#[utoipa::path(
79    get,
80    path = "/snarker/job/spec",
81    tag = "snarker",
82    params(
83        ("id" = String, Query, description = "Snark job ID")
84    ),
85    responses(
86        (status = 200, description = "JSON job spec", content_type = "application/json"),
87        (status = 200, description = "Binprot job spec", content_type = "application/octet-stream"),
88        (status = 400, description = "Job not found")
89    )
90)]
91async fn job_spec(
92    State(state): State<AppState>,
93    headers: HeaderMap,
94    Query(JobSpecQuery { id: job_id }): Query<JobSpecQuery>,
95) -> AppResult<Response<Body>> {
96    let resp: RpcSnarkerJobSpecResponse =
97        rpc_request!(state, RpcRequest::SnarkerJobSpec { job_id })?;
98
99    let accept = headers
100        .get(header::ACCEPT)
101        .and_then(|v| v.to_str().ok())
102        .unwrap_or("");
103
104    match resp {
105        RpcSnarkerJobSpecResponse::Ok(spec) if accept == "application/octet-stream" => {
106            // Binary output (binprot format with length prefix)
107            let mut vec = Vec::new();
108            spec.binprot_write(&mut vec)
109                .map_err(|e| AppError::Internal(format!("binprot serialization failed: {e}")))?;
110
111            let mut result = Vec::with_capacity(vec.len() + std::mem::size_of::<u64>());
112            result.extend((vec.len() as u64).to_le_bytes());
113            result.extend(vec);
114
115            Response::builder()
116                .status(StatusCode::OK)
117                .header(header::CONTENT_TYPE, "application/octet-stream")
118                .body(Body::from(result))
119                .map_err(|e| AppError::Internal(e.to_string()))
120        }
121        RpcSnarkerJobSpecResponse::Ok(spec) => {
122            // JSON output
123            let body = serde_json::to_vec(&spec)
124                .map_err(|e| AppError::Internal(format!("JSON serialization failed: {e}")))?;
125
126            Response::builder()
127                .status(StatusCode::OK)
128                .header(header::CONTENT_TYPE, "application/json")
129                .body(Body::from(body))
130                .map_err(|e| AppError::Internal(e.to_string()))
131        }
132        _ => {
133            // Error response
134            let body = serde_json::to_vec(&"error")
135                .map_err(|e| AppError::Internal(format!("JSON serialization failed: {e}")))?;
136
137            Response::builder()
138                .status(StatusCode::BAD_REQUEST)
139                .header(header::CONTENT_TYPE, "application/json")
140                .body(Body::from(body))
141                .map_err(|e| AppError::Internal(e.to_string()))
142        }
143    }
144}
145
146/// Snarker workers
147#[utoipa::path(
148    get,
149    path = "/snarker/workers",
150    tag = "snarker",
151    responses(
152        (status = 200, description = "Snarker workers")
153    )
154)]
155async fn workers(State(state): State<AppState>) -> AppResult<Json<RpcSnarkerWorkersResponse>> {
156    jsonify_rpc!(state, RpcRequest::SnarkerWorkers)
157}
158
159/// Snarker configuration
160#[utoipa::path(
161    get,
162    path = "/snarker/config",
163    tag = "snarker",
164    responses(
165        (status = 200, description = "Snarker configuration")
166    )
167)]
168async fn config(State(state): State<AppState>) -> AppResult<Json<RpcSnarkerConfigGetResponse>> {
169    jsonify_rpc!(state, RpcRequest::SnarkerConfig)
170}