Skip to main content

quantus_miner_api/
lib.rs

1use serde::{Deserialize, Serialize};
2use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
3
4/// Maximum message size (1 KB) to prevent memory exhaustion attacks.
5///
6/// Real MinerMessage payloads are only a few hundred bytes (Ready, NewJob, JobResult).
7/// 1 KB provides sufficient headroom while minimizing the amplification attack surface.
8pub const MAX_MESSAGE_SIZE: u32 = 1024;
9
10/// Status codes returned in API responses.
11#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
12#[serde(rename_all = "snake_case")]
13pub enum ApiResponseStatus {
14	Accepted,
15	Running,
16	Completed,
17	Failed,
18	Cancelled,
19	NotFound,
20	Error,
21}
22
23/// QUIC protocol messages exchanged between node and miner.
24///
25/// The protocol is:
26/// - Miner sends `Ready` immediately after connecting to establish the stream
27/// - Node sends `NewJob` to submit a mining job (implicitly cancels any previous job)
28/// - Miner sends `JobResult` when mining completes
29#[derive(Serialize, Deserialize, Debug, Clone)]
30pub enum MinerMessage {
31	/// Miner → Node: Sent immediately after connecting to establish the stream.
32	/// This is required because QUIC streams are lazily initialized.
33	Ready,
34
35	/// Node → Miner: Submit a new mining job.
36	/// If a job is already running, it will be cancelled and replaced.
37	NewJob(MiningRequest),
38
39	/// Miner → Node: Mining result (completed, failed, or cancelled).
40	JobResult(MiningResult),
41}
42
43/// Write a length-prefixed JSON message to an async writer.
44///
45/// Wire format: 4-byte big-endian length prefix followed by JSON payload.
46pub async fn write_message<W: AsyncWrite + Unpin>(
47	writer: &mut W,
48	msg: &MinerMessage,
49) -> std::io::Result<()> {
50	let json = serde_json::to_vec(msg)
51		.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
52	let len = json.len() as u32;
53	writer.write_all(&len.to_be_bytes()).await?;
54	writer.write_all(&json).await?;
55	Ok(())
56}
57
58/// Read a length-prefixed JSON message from an async reader.
59///
60/// Wire format: 4-byte big-endian length prefix followed by JSON payload.
61/// Returns an error if the message exceeds MAX_MESSAGE_SIZE.
62pub async fn read_message<R: AsyncRead + Unpin>(reader: &mut R) -> std::io::Result<MinerMessage> {
63	let mut len_buf = [0u8; 4];
64	reader.read_exact(&mut len_buf).await?;
65	let len = u32::from_be_bytes(len_buf);
66
67	if len > MAX_MESSAGE_SIZE {
68		return Err(std::io::Error::new(
69			std::io::ErrorKind::InvalidData,
70			format!("Message size {} exceeds maximum {}", len, MAX_MESSAGE_SIZE),
71		));
72	}
73
74	let mut buf = vec![0u8; len as usize];
75	reader.read_exact(&mut buf).await?;
76	serde_json::from_slice(&buf)
77		.map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))
78}
79
80/// Request payload sent from Node to Miner.
81///
82/// The miner will choose its own random starting nonce, enabling multiple
83/// miners to work on the same job without coordination.
84#[derive(Serialize, Deserialize, Debug, Clone)]
85pub struct MiningRequest {
86	pub job_id: String,
87	/// Hex encoded header hash (32 bytes -> 64 chars, no 0x prefix)
88	pub mining_hash: String,
89	/// Difficulty (U512 as decimal string). Must be non-zero.
90	pub difficulty: String,
91}
92
93/// Response payload for job submission (`/mine`) and cancellation (`/cancel`).
94#[derive(Serialize, Deserialize, Debug, Clone)]
95pub struct MiningResponse {
96	pub status: ApiResponseStatus,
97	pub job_id: String,
98	#[serde(skip_serializing_if = "Option::is_none")]
99	pub message: Option<String>,
100}
101
102/// Response payload for checking job results (`/result/{job_id}`).
103#[derive(Serialize, Deserialize, Debug, Clone)]
104pub struct MiningResult {
105	pub status: ApiResponseStatus,
106	pub job_id: String,
107	/// Hex encoded U512 representation of the final/winning nonce (no 0x prefix).
108	pub nonce: Option<String>,
109	/// Hex encoded [u8; 64] representation of the winning nonce (128 chars, no 0x prefix).
110	/// This is the primary field the Node uses for verification.
111	pub work: Option<String>,
112	pub hash_count: u64,
113	pub elapsed_time: f64,
114	/// Miner ID assigned by the node (set server-side, not by the miner).
115	#[serde(default, skip_serializing_if = "Option::is_none")]
116	pub miner_id: Option<u64>,
117}