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