1use crate::llm::actions::{
6 protocol_trait::{ActionResult, Protocol, Server},
7 ActionDefinition, Parameter, ParameterDefinition, StartupExamples,
8};
9use crate::protocol::log_template::LogTemplate;
10use crate::protocol::{EventType, SpawnContext};
11use crate::state::app_state::AppState;
12use anyhow::{bail, ensure, Context, Result};
13use serde_json::{json, Value};
14use std::sync::LazyLock;
15
16pub const ACCEPT: &str = "stratum_accept";
17pub const REJECT: &str = "stratum_reject";
18pub const SET_DIFFICULTY: &str = "stratum_set_difficulty";
19pub const NEW_JOB: &str = "stratum_new_job";
20pub const SHOW_MESSAGE: &str = "stratum_show_message";
21pub const DEFAULT_DIFFICULTY: f64 = 1.0;
23pub const MAX_DIFFICULTY: f64 = 1e15;
25pub const DEFAULT_COINBASE_MESSAGE: &str = "NetGet";
27
28#[derive(Default)]
29pub struct StratumProtocol;
30impl StratumProtocol {
31 pub fn new() -> Self {
32 Self
33 }
34}
35
36pub fn p(name: &str, kind: &str, description: &str, required: bool) -> Parameter {
37 Parameter {
38 name: name.into(),
39 type_hint: kind.into(),
40 description: description.into(),
41 required,
42 }
43}
44
45pub fn action(
46 name: &str,
47 description: &str,
48 parameters: Vec<Parameter>,
49 example: Value,
50) -> ActionDefinition {
51 ActionDefinition {
52 name: name.into(),
53 description: description.into(),
54 parameters,
55 example,
56 log_template: Some(LogTemplate::new().with_info(format!("-> Stratum {name}"))),
57 }
58}
59
60fn accept() -> ActionDefinition {
61 action(
62 ACCEPT,
63 "Authorize the worker, or credit the share, being answered.",
64 vec![],
65 json!({"type": ACCEPT}),
66 )
67}
68
69fn reject() -> ActionDefinition {
70 action(
71 REJECT,
72 "Refuse the worker or the share being answered; the miner is told why.",
73 vec![p(
74 "message",
75 "string",
76 "The reason the miner is shown, e.g. unknown worker",
77 true,
78 )],
79 json!({"type": REJECT, "message": "unknown worker"}),
80 )
81}
82
83pub fn set_difficulty() -> ActionDefinition {
84 action(
85 SET_DIFFICULTY,
86 "Set this miner's share difficulty (mining.set_difficulty); it applies to every share from now on.",
87 vec![p("difficulty", "number", "Share difficulty, above 0; 1 is the classic minimum, fractions are allowed", true)],
88 json!({"type": SET_DIFFICULTY, "difficulty": 0.5}),
89 )
90}
91
92fn new_job() -> ActionDefinition {
93 action(
94 NEW_JOB,
95 "Hand this miner new work (mining.notify). Rust builds the coinbase and header fields.",
96 vec![
97 p("message", "string", "Text the coinbase carries, at most 64 bytes (default NetGet)", false),
98 p("prev_hash", "string", "Previous block hash as RPC shows it, 64 hex digits (default all zeros)", false),
99 p("clean_jobs", "boolean", "Whether earlier jobs are void (default true): their shares are then refused as stale", false),
100 ],
101 json!({"type": NEW_JOB, "message": "block 2", "clean_jobs": true}),
102 )
103}
104
105fn show_message() -> ActionDefinition {
106 action(
107 SHOW_MESSAGE,
108 "Show the miner a message (client.show_message).",
109 vec![p(
110 "message",
111 "string",
112 "Text for the miner's operator",
113 true,
114 )],
115 json!({"type": SHOW_MESSAGE, "message": "maintenance at noon"}),
116 )
117}
118
119pub fn all_actions() -> Vec<ActionDefinition> {
120 vec![
121 accept(),
122 reject(),
123 set_difficulty(),
124 new_job(),
125 show_message(),
126 ]
127}
128
129pub static AUTHORIZE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
130 EventType::new(
131 "stratum_authorize",
132 "A miner asked to mine as a worker (mining.authorize). Accept or reject it; work and the difficulty follow an accept.",
133 json!({"type": ACCEPT}),
134 )
135 .with_parameters(vec![
136 p("worker", "string", "The worker name, usually account.rig", true),
137 p("password_given", "boolean", "Whether a password other than empty or x was sent (it is not shown)", true),
138 p("user_agent", "string", "What the miner said it is in mining.subscribe", false),
139 p("remote_addr", "string", "The miner's address and port", true),
140 ])
141 .with_actions(all_actions())
142});
143
144pub static SHARE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
145 EventType::new(
146 "stratum_share",
147 "A miner submitted a share that Rust verified meets its difficulty. Accept (credit) or reject it.",
148 json!({"type": ACCEPT}),
149 )
150 .with_parameters(vec![
151 p("worker", "string", "The worker that submitted it", true),
152 p("job_id", "string", "The job it was mined on", true),
153 p("hash", "string", "The block header hash, as RPC shows hashes", true),
154 p("share_difficulty", "number", "The difficulty the hash actually meets", true),
155 p("difficulty", "number", "The miner's assigned difficulty, which the share meets", true),
156 p("remote_addr", "string", "The miner's address and port", true),
157 ])
158 .with_actions(all_actions())
159});
160
161pub fn check(v: &Value) -> Result<()> {
162 match v["type"].as_str().unwrap_or_default() {
163 ACCEPT => {}
164 REJECT | SHOW_MESSAGE => {
165 let m = v["message"].as_str().context("message is required")?;
166 ensure!(
167 !m.is_empty() && m.len() <= 256,
168 "message must be 1-256 bytes"
169 );
170 }
171 SET_DIFFICULTY => {
172 let d = v["difficulty"]
173 .as_f64()
174 .context("difficulty must be a number")?;
175 ensure!(
176 d > 0.0 && d <= MAX_DIFFICULTY,
177 "difficulty must be above 0 and at most {MAX_DIFFICULTY}"
178 );
179 }
180 NEW_JOB => {
181 if let Some(m) = v.get("message").filter(|m| !m.is_null()) {
182 let m = m.as_str().context("message must be text")?;
183 ensure!(
184 m.len() <= super::wire::MAX_COINBASE_MESSAGE,
185 "message must be at most 64 bytes"
186 );
187 }
188 super::wire::parse_prev_display(v["prev_hash"].as_str())?;
189 }
190 other => bail!("Unknown Stratum action {other:?}"),
191 }
192 Ok(())
193}
194
195impl Protocol for StratumProtocol {
196 fn protocol_name(&self) -> &'static str {
197 "Stratum"
198 }
199 fn stack_name(&self) -> &'static str {
200 "ETH>IP>TCP>Stratum"
201 }
202 fn keywords(&self) -> Vec<&'static str> {
203 vec![
204 "stratum",
205 "mining pool",
206 "bitcoin mining",
207 "miner",
208 "stratum+tcp",
209 ]
210 }
211 fn description(&self) -> &'static str {
212 "Stratum V1 mining pool: the model authorizes workers, credits shares Rust has verified, and sets difficulty and work"
213 }
214 fn get_async_actions(&self, _: &AppState) -> Vec<ActionDefinition> {
215 Vec::new()
216 }
217 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
218 all_actions()
219 }
220 fn get_event_types(&self) -> Vec<EventType> {
221 vec![AUTHORIZE_EVENT.clone(), SHARE_EVENT.clone()]
222 }
223 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
224 vec![
225 ParameterDefinition {
226 name: "difficulty".into(),
227 type_hint: "number".into(),
228 description: "Share difficulty every miner starts at (fractions allowed, above 0)"
229 .into(),
230 required: false,
231 example: json!(0.01),
232 default: Some(json!(DEFAULT_DIFFICULTY)),
233 },
234 ParameterDefinition {
235 name: "idle_timeout_secs".into(),
236 type_hint: "number".into(),
237 description:
238 "Seconds a miner may stay silent before it is disconnected (1..=86400)".into(),
239 required: false,
240 example: json!(1800),
241 default: Some(json!(super::IDLE_TIMEOUT.as_secs())),
242 },
243 ]
244 }
245 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
246 use crate::protocol::metadata::*;
247 ProtocolMetadataV2::builder()
248 .state(DevelopmentState::Experimental)
249 .privilege_requirement(PrivilegeRequirement::None)
250 .well_known_port(3333)
251 .implementation("Newline-delimited JSON-RPC over Tokio TCP: mining.subscribe (a 4-byte extranonce1 per connection, 4-byte extranonce2), mining.authorize, mining.submit, mining.configure (no extensions), mining.extranonce.subscribe and mining.suggest_difficulty; jobs are built in Rust (BIP 34 height and a message in the coinbase, no other transactions) and every share is rebuilt into its header, double-SHA-256 hashed and measured before anyone is asked")
252 .llm_control("Which workers may mine, whether each verified share is credited, and the difficulty, work and messages each miner gets")
253 .e2e_testing("tests/server/stratum: cpuminer 2.5.1 (pooler, sha256d) mines against NetGet's jobs and has its shares accepted; raw JSON-RPC for refused shares, bounds and a failed handler")
254 .notes("The pool is not connected to any node: its work is its own, so a share proves hashing and nothing is ever submitted as a block. Shares below the difficulty, duplicates, stale or unknown jobs, bad extranonce2 sizes and ntimes outside the job's window are refused by Rust (codes 20-25) without asking the model. Lines are capped at 16 KiB; a miner may be silent idle_timeout_secs.")
255 .request_only("Every message answers a miner's request, except the work and difficulty that follow an answer")
256 .answers_on_failure()
257 .max_inbound_bytes(super::wire::MAX_LINE)
258 .build()
259 }
260 fn example_prompt(&self) -> &'static str {
261 "Stratum mining pool on port 3333 that accepts any worker and credits every share"
262 }
263 fn get_startup_examples(&self) -> StartupExamples {
264 let llm = json!({"type":"open_server","base_stack":"stratum","port":3333,
265 "instruction":"Authorize workers whose name starts with netget, credit every share, and raise the difficulty to 2 after the first share"});
266 let mut static_example = llm.clone();
267 static_example["event_handlers"] =
268 json!([{"event_pattern":"*","handler":{"type":"static","actions":[{"type":ACCEPT}]}}]);
269 let mut scripted = llm.clone();
270 scripted["event_handlers"] = json!([{"event_pattern":"*","handler":{"type":"script","language":"python",
271 "code":"import json,sys\ni=json.load(sys.stdin); t=i['event_type_id']; e=i['event']\nif t=='stratum_authorize': a=[{'type':'stratum_accept'}] if e['worker'].startswith('netget') else [{'type':'stratum_reject','message':'unknown worker'}]\nelse: a=[{'type':'stratum_accept'}]\nprint(json.dumps({'actions':a}))"}}]);
272 StartupExamples::new(llm, scripted, static_example)
273 }
274 fn group_name(&self) -> &'static str {
275 "Application"
276 }
277}
278
279impl Server for StratumProtocol {
280 fn spawn(
281 &self,
282 ctx: SpawnContext,
283 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
284 {
285 Box::pin(super::spawn(ctx))
286 }
287 fn execute_action(&self, action: Value) -> Result<ActionResult> {
288 check(&action)?;
289 Ok(ActionResult::Custom {
290 name: action["type"].as_str().unwrap_or_default().to_string(),
291 data: action,
292 })
293 }
294}