1use anyhow::{bail, ensure, Context, Result};
5use serde_json::{json, Value};
6use sha2::{Digest, Sha256};
7use std::time::Duration;
8use tokio::io::{AsyncBufReadExt, AsyncRead, BufReader};
9
10pub const MAX_LINE: usize = 16 * 1024;
12pub const IO_TIMEOUT: Duration = Duration::from_secs(30);
13pub const EXTRANONCE2_SIZE: usize = 4;
15pub const EXTRANONCE1_SIZE: usize = 4;
17pub const NTIME_ROLL: u32 = 7200;
19pub const MAX_BRANCHES: usize = 16;
21pub const MAX_COINBASE: usize = 4096;
23pub const MAX_COINBASE_MESSAGE: usize = 64;
25pub const DIFF1: f64 = 26959535291011309493156476344723991336010898738574164086137773096960.0;
27
28pub struct Lines<R> {
30 reader: BufReader<R>,
31 line: Vec<u8>,
32}
33
34impl<R: AsyncRead + Unpin> Lines<R> {
35 pub fn new(reader: R) -> Self {
36 Self {
37 reader: BufReader::new(reader),
38 line: Vec::new(),
39 }
40 }
41
42 pub async fn next(&mut self, idle: Duration) -> Result<Option<Value>> {
45 loop {
46 self.line.clear();
47 loop {
48 let buf = tokio::time::timeout(idle, self.reader.fill_buf())
49 .await
50 .context("Stratum peer idle")??;
51 if buf.is_empty() {
52 if self.line.is_empty() {
53 return Ok(None);
54 }
55 bail!("Stratum stream ended inside a line");
56 }
57 let (take, done) = match buf.iter().position(|b| *b == b'\n') {
58 Some(i) => (i + 1, true),
59 None => (buf.len(), false),
60 };
61 let content = self.line.len() + take - usize::from(done);
62 ensure!(
63 content <= MAX_LINE,
64 "Stratum line longer than {MAX_LINE} bytes"
65 );
66 self.line.extend_from_slice(&buf[..take]);
67 self.reader.consume(take);
68 if done {
69 break;
70 }
71 }
72 let text = std::str::from_utf8(&self.line).context("Stratum line is not UTF-8")?;
73 if text.trim().is_empty() {
74 continue;
75 }
76 return serde_json::from_str(text.trim())
77 .map(Some)
78 .context("Stratum line is not JSON");
79 }
80 }
81}
82
83pub enum Message {
84 Request {
85 id: Value,
86 method: String,
87 params: Value,
88 },
89 Notification {
90 method: String,
91 params: Value,
92 },
93 Response {
94 id: Value,
95 result: Value,
96 error: Value,
97 },
98}
99
100pub fn parse(v: Value) -> Result<Message> {
101 let obj = v
102 .as_object()
103 .context("a Stratum message is a JSON object")?;
104 let id = obj.get("id").cloned().unwrap_or(Value::Null);
105 match obj.get("method") {
106 Some(Value::String(method)) => {
107 let params = obj.get("params").cloned().unwrap_or(json!([]));
108 Ok(if id.is_null() {
109 Message::Notification {
110 method: method.clone(),
111 params,
112 }
113 } else {
114 Message::Request {
115 id,
116 method: method.clone(),
117 params,
118 }
119 })
120 }
121 Some(_) => bail!("method must be a string"),
122 None => Ok(Message::Response {
123 id,
124 result: obj.get("result").cloned().unwrap_or(Value::Null),
125 error: obj.get("error").cloned().unwrap_or(Value::Null),
126 }),
127 }
128}
129
130pub fn line(v: &Value) -> Vec<u8> {
131 let mut out = serde_json::to_vec(v).unwrap_or_default();
132 out.push(b'\n');
133 out
134}
135
136pub fn error(code: u32, message: &str) -> Value {
139 json!([code, message, null])
140}
141
142pub fn sha256d(data: &[u8]) -> [u8; 32] {
143 Sha256::digest(Sha256::digest(data)).into()
144}
145
146pub fn display_hex(hash: &[u8; 32]) -> String {
148 let mut h = *hash;
149 h.reverse();
150 hex::encode(h)
151}
152
153pub fn difficulty(hash: &[u8; 32]) -> f64 {
155 let value = hash
156 .iter()
157 .rev()
158 .fold(0f64, |acc, b| acc * 256.0 + f64::from(*b));
159 if value == 0.0 {
160 f64::INFINITY
161 } else {
162 DIFF1 / value
163 }
164}
165
166pub fn u32_hex(v: u32) -> String {
168 format!("{v:08x}")
169}
170
171pub fn parse_u32_hex(s: &str, what: &str) -> Result<u32> {
172 ensure!(s.len() == 8, "{what} must be 8 hex digits");
173 u32::from_str_radix(s, 16).with_context(|| format!("{what} is not hex"))
174}
175
176#[derive(Clone, Debug)]
178pub struct Job {
179 pub job_id: String,
180 pub prevhash: [u8; 32],
182 pub coinb1: Vec<u8>,
183 pub coinb2: Vec<u8>,
184 pub branches: Vec<[u8; 32]>,
185 pub version: u32,
186 pub nbits: u32,
187 pub ntime: u32,
188 pub clean: bool,
189}
190
191fn swap_words(b: &[u8; 32]) -> [u8; 32] {
193 let mut out = [0u8; 32];
194 for i in 0..8 {
195 for j in 0..4 {
196 out[i * 4 + j] = b[i * 4 + 3 - j];
197 }
198 }
199 out
200}
201
202fn hex32(s: &str, what: &str) -> Result<[u8; 32]> {
203 let v = hex::decode(s).with_context(|| format!("{what} is not hex"))?;
204 v.try_into()
205 .map_err(|_| anyhow::anyhow!("{what} must be 32 bytes"))
206}
207
208impl Job {
209 pub fn from_notify(p: &Value) -> Result<Job> {
210 let a = p
211 .as_array()
212 .context("mining.notify params must be an array")?;
213 ensure!(a.len() >= 9, "mining.notify has {} params, not 9", a.len());
214 let s = |i: usize, what: &str| -> Result<&str> {
215 a[i].as_str()
216 .with_context(|| format!("{what} must be a string"))
217 };
218 let coinb1 = hex::decode(s(2, "coinb1")?).context("coinb1 is not hex")?;
219 let coinb2 = hex::decode(s(3, "coinb2")?).context("coinb2 is not hex")?;
220 ensure!(
221 coinb1.len() <= MAX_COINBASE && coinb2.len() <= MAX_COINBASE,
222 "coinbase longer than {MAX_COINBASE} bytes"
223 );
224 let branches = a[4]
225 .as_array()
226 .context("merkle branches must be an array")?
227 .iter()
228 .map(|b| hex32(b.as_str().unwrap_or_default(), "merkle branch"))
229 .collect::<Result<Vec<_>>>()?;
230 ensure!(
231 branches.len() <= MAX_BRANCHES,
232 "more than {MAX_BRANCHES} merkle branches"
233 );
234 Ok(Job {
235 job_id: s(0, "job_id")?.to_string(),
236 prevhash: swap_words(&hex32(s(1, "prevhash")?, "prevhash")?),
237 coinb1,
238 coinb2,
239 branches,
240 version: parse_u32_hex(s(5, "version")?, "version")?,
241 nbits: parse_u32_hex(s(6, "nbits")?, "nbits")?,
242 ntime: parse_u32_hex(s(7, "ntime")?, "ntime")?,
243 clean: a[8].as_bool().unwrap_or(false),
244 })
245 }
246
247 pub fn notify_params(&self) -> Value {
248 json!([
249 self.job_id,
250 hex::encode(swap_words(&self.prevhash)),
251 hex::encode(&self.coinb1),
252 hex::encode(&self.coinb2),
253 self.branches.iter().map(hex::encode).collect::<Vec<_>>(),
254 u32_hex(self.version),
255 u32_hex(self.nbits),
256 u32_hex(self.ntime),
257 self.clean,
258 ])
259 }
260
261 pub fn prev_display(&self) -> String {
263 display_hex(&self.prevhash)
264 }
265
266 pub fn merkle_root(&self, extranonce1: &[u8], extranonce2: &[u8]) -> [u8; 32] {
267 let mut coinbase = Vec::with_capacity(
268 self.coinb1.len() + extranonce1.len() + extranonce2.len() + self.coinb2.len(),
269 );
270 coinbase.extend_from_slice(&self.coinb1);
271 coinbase.extend_from_slice(extranonce1);
272 coinbase.extend_from_slice(extranonce2);
273 coinbase.extend_from_slice(&self.coinb2);
274 let mut root = sha256d(&coinbase);
275 for b in &self.branches {
276 let mut pair = [0u8; 64];
277 pair[..32].copy_from_slice(&root);
278 pair[32..].copy_from_slice(b);
279 root = sha256d(&pair);
280 }
281 root
282 }
283
284 pub fn header(&self, merkle_root: &[u8; 32], ntime: u32) -> [u8; 80] {
286 let mut h = [0u8; 80];
287 h[0..4].copy_from_slice(&self.version.to_le_bytes());
288 h[4..36].copy_from_slice(&self.prevhash);
289 h[36..68].copy_from_slice(merkle_root);
290 h[68..72].copy_from_slice(&ntime.to_le_bytes());
291 h[72..76].copy_from_slice(&self.nbits.to_le_bytes());
292 h
293 }
294
295 pub fn share_hash(
297 &self,
298 extranonce1: &[u8],
299 extranonce2: &[u8],
300 ntime: u32,
301 nonce: u32,
302 ) -> [u8; 32] {
303 let mut h = self.header(&self.merkle_root(extranonce1, extranonce2), ntime);
304 h[76..80].copy_from_slice(&nonce.to_le_bytes());
305 sha256d(&h)
306 }
307}
308
309fn push(script: &mut Vec<u8>, data: &[u8]) {
311 script.push(data.len() as u8);
312 script.extend_from_slice(data);
313}
314
315pub fn pool_job(
318 job_id: String,
319 prev_display: &[u8; 32],
320 height: u32,
321 message: &str,
322 nbits: u32,
323 ntime: u32,
324 clean: bool,
325) -> Job {
326 let mut prevhash = *prev_display;
327 prevhash.reverse();
328 let mut height_bytes = height.to_le_bytes().to_vec();
329 while height_bytes.len() > 1 && height_bytes.last() == Some(&0) {
330 height_bytes.pop();
331 }
332 if height_bytes.last().is_some_and(|b| b & 0x80 != 0) {
333 height_bytes.push(0);
334 }
335 let message = &message.as_bytes()[..message.len().min(MAX_COINBASE_MESSAGE)];
336 let mut script = Vec::new();
337 push(&mut script, &height_bytes);
338 push(&mut script, message);
339 let script_len = script.len() + 1 + EXTRANONCE1_SIZE + EXTRANONCE2_SIZE;
340 let mut coinb1 = vec![1, 0, 0, 0, 1];
341 coinb1.extend_from_slice(&[0u8; 32]);
342 coinb1.extend_from_slice(&[0xff; 4]);
343 coinb1.push(script_len as u8);
344 coinb1.extend_from_slice(&script);
345 coinb1.push((EXTRANONCE1_SIZE + EXTRANONCE2_SIZE) as u8);
346 let mut coinb2 = vec![0xff; 4];
347 coinb2.push(1);
348 coinb2.extend_from_slice(&0u64.to_le_bytes());
349 let out_script = [&[0x6a, 6][..], b"netget"].concat();
350 coinb2.push(out_script.len() as u8);
351 coinb2.extend_from_slice(&out_script);
352 coinb2.extend_from_slice(&[0u8; 4]);
353 Job {
354 job_id,
355 prevhash,
356 coinb1,
357 coinb2,
358 branches: Vec::new(),
359 version: 0x2000_0000,
360 nbits,
361 ntime,
362 clean,
363 }
364}
365
366pub fn parse_prev_display(s: Option<&str>) -> Result<[u8; 32]> {
368 match s {
369 None | Some("") => Ok([0u8; 32]),
370 Some(s) => hex32(s, "prev_hash"),
371 }
372}