Skip to main content

sv1_api/
lib.rs

1#![allow(clippy::result_unit_err)]
2//! Stratum V1 application protocol:
3//!
4//! json-rpc has two types of messages: **request** and **response**.
5//! A request message can be either a **notification** or a **standard message**.
6//! Standard messages expect a response, notifications do not. A typical example of a notification
7//! is the broadcasting of a new block.
8//!
9//! Every RPC request contains three parts:
10//! * message ID: integer or string
11//! * remote method: unicode string
12//! * parameters: list of parameters
13//!
14//! ## Standard requests
15//! Message ID must be an unique identifier of request during current transport session. It may be
16//! integer or some unique string, like UUID. ID must be unique only from one side (it means, both
17//! server and clients can initiate request with id “1”). Client or server can choose string/UUID
18//! identifier for example in the case when standard “atomic” counter isn’t available.
19//!
20//! ## Notifications
21//! Notifications are like Request, but it does not expect any response and message ID is always
22//! null:
23//! * message ID: null
24//! * remote method: unicode string
25//! * parameters: list of parameters
26//!
27//! ## Responses
28//! Every response contains the following parts
29//! * message ID: same ID as in request, for pairing request-response together
30//! * result: any json-encoded result object (number, string, list, array, …)
31//! * error: null or list (error code, error message)
32//!
33//! References:
34//! [https://docs.google.com/document/d/17zHy1SUlhgtCMbypO8cHgpWH73V5iUQKk_0rWvMqSNs/edit?hl=en_US#]
35//! [https://braiins.com/stratum-v1/docs]
36//! [https://en.bitcoin.it/wiki/Stratum_mining_protocol]
37//! [https://en.bitcoin.it/wiki/BIP_0310]
38//! [https://docs.google.com/spreadsheets/d/1z8a3S9gFkS8NGhBCxOMUDqs7h9SQltz8-VX3KPHk7Jw/edit#gid=0]
39
40pub mod error;
41pub mod json_rpc;
42pub mod methods;
43pub mod utils;
44
45use bitcoin_hashes::hex::FromHex;
46use std::convert::{TryFrom, TryInto};
47use tracing::debug;
48
49// use error::Result;
50use error::Error;
51pub use json_rpc::Message;
52pub use methods::{client_to_server, server_to_client, Method, MethodError, ParsingMethodError};
53use utils::{Extranonce, HexU32Be};
54
55/// json_rpc Response are not handled because stratum v1 does not have any request from a server to
56/// a client
57/// TODO: Should update to accommodate miner requesting a difficulty change
58///
59/// A stratum v1 server represent a single connection with a client
60pub trait IsServer<'a> {
61    /// handle the received message and return a response if the message is a request or
62    /// notification.
63    fn handle_message(
64        &mut self,
65        client_id: Option<usize>,
66        msg: json_rpc::Message,
67    ) -> Result<Option<json_rpc::Response>, Error<'a>>
68    where
69        Self: std::marker::Sized,
70    {
71        match msg {
72            Message::StandardRequest(_) => {
73                // handle valid standard request
74                self.handle_request(client_id, msg)
75            }
76            Message::Notification(_) => {
77                // handle valid server notification
78                self.handle_request(client_id, msg)
79            }
80            _ => {
81                // Server shouldn't receive json_rpc responses
82                Err(Error::InvalidJsonRpcMessageKind)
83            }
84        }
85    }
86
87    /// Call the right handler according with the called method
88    fn handle_request(
89        &mut self,
90        client_id: Option<usize>,
91        msg: json_rpc::Message,
92    ) -> Result<Option<json_rpc::Response>, Error<'a>>
93    where
94        Self: std::marker::Sized,
95    {
96        let request = msg.try_into()?;
97
98        match request {
99            // TODO: Handle suggested difficulty
100            methods::Client2Server::SuggestDifficulty() => Ok(None),
101            methods::Client2Server::Authorize(authorize) => {
102                let authorized = self.handle_authorize(client_id, &authorize);
103                if authorized {
104                    self.authorize(client_id, &authorize.name);
105                }
106                Ok(Some(authorize.respond(authorized)))
107            }
108            methods::Client2Server::Configure(configure) => {
109                debug!("{:?}", configure);
110                self.set_version_rolling_mask(client_id, configure.version_rolling_mask());
111                self.set_version_rolling_min_bit(
112                    client_id,
113                    configure.version_rolling_min_bit_count(),
114                );
115                let (version_rolling, min_diff) = self.handle_configure(client_id, &configure);
116                Ok(Some(configure.respond(version_rolling, min_diff)))
117            }
118            methods::Client2Server::ExtranonceSubscribe(_) => {
119                self.handle_extranonce_subscribe();
120                Ok(None)
121            }
122            methods::Client2Server::Submit(submit) => {
123                let has_valid_version_bits = match &submit.version_bits {
124                    Some(a) => {
125                        if let Some(version_rolling_mask) = self.version_rolling_mask(client_id) {
126                            version_rolling_mask.check_mask(a)
127                        } else {
128                            false
129                        }
130                    }
131                    None => self.version_rolling_mask(client_id).is_none(),
132                };
133
134                let is_valid_submission = self.is_authorized(client_id, &submit.user_name)
135                    && self.extranonce2_size(client_id) == submit.extra_nonce2.len()
136                    && has_valid_version_bits;
137
138                if is_valid_submission {
139                    let accepted = self.handle_submit(client_id, &submit);
140                    Ok(Some(submit.respond(accepted)))
141                } else {
142                    Err(Error::InvalidSubmission)
143                }
144            }
145            methods::Client2Server::Subscribe(subscribe) => {
146                let subscriptions = self.handle_subscribe(client_id, &subscribe);
147                let extra_n1 = self.set_extranonce1(client_id, None);
148                let extra_n2_size = self.set_extranonce2_size(client_id, None);
149                Ok(Some(subscribe.respond(
150                    subscriptions,
151                    extra_n1,
152                    extra_n2_size,
153                )))
154            }
155        }
156    }
157
158    /// This message (JSON RPC Request) SHOULD be the first message sent by the miner after the
159    /// connection with the server is established.
160    fn handle_configure(
161        &mut self,
162        client_id: Option<usize>,
163        request: &client_to_server::Configure,
164    ) -> (Option<server_to_client::VersionRollingParams>, Option<bool>);
165
166    /// On the beginning of the session, client subscribes current connection for receiving mining
167    /// jobs.
168    ///
169    /// The client can specify [mining.notify][a] job_id the client wishes to resume working with
170    ///
171    /// The result contains three items:
172    /// * Subscriptions details: 2-tuple with name of subscribed notification and subscription ID.
173    ///   Teoretically it may be used for unsubscribing, but obviously miners won't use it.
174    /// * Extranonce1 - Hex-encoded, per-connection unique string which will be used for coinbase
175    ///   serialization later. Keep it safe!
176    /// * Extranonce2_size - Represents expected length of extranonce2 which will be generated by
177    ///   the miner.
178    ///
179    /// Almost instantly after the subscription server start to send [jobs][a]
180    ///
181    /// This function return the first item of the result (2 tuple with name of subscibed ...)
182    ///
183    /// [a]: crate::methods::server_to_client::Notify
184    fn handle_subscribe(
185        &self,
186        client_id: Option<usize>,
187        request: &client_to_server::Subscribe,
188    ) -> Vec<(String, String)>;
189
190    /// You can authorize as many workers as you wish and at any
191    /// time during the session. In this way, you can handle big basement of independent mining rigs
192    /// just by one Stratum connection.
193    ///
194    /// https://bitcoin.stackexchange.com/questions/29416/how-do-pool-servers-handle-multiple-workers-sharing-one-connection-with-stratum
195    fn handle_authorize(
196        &self,
197        client_id: Option<usize>,
198        request: &client_to_server::Authorize,
199    ) -> bool;
200
201    /// When miner find the job which meets requested difficulty, it can submit share to the server.
202    /// Only [Submit](client_to_server::Submit) requests for authorized user names can be submitted.
203    fn handle_submit(
204        &self,
205        client_id: Option<usize>,
206        request: &client_to_server::Submit<'a>,
207    ) -> bool;
208
209    /// Indicates to the server that the client supports the mining.set_extranonce method.
210    fn handle_extranonce_subscribe(&self);
211
212    fn is_authorized(&self, client_id: Option<usize>, name: &str) -> bool;
213
214    fn authorize(&mut self, client_id: Option<usize>, name: &str);
215
216    /// Set extranonce1 to extranonce1 if provided. If not create a new one and set it.
217    fn set_extranonce1(
218        &mut self,
219        client_id: Option<usize>,
220        extranonce1: Option<Extranonce<'a>>,
221    ) -> Extranonce<'a>;
222
223    fn extranonce1(&self, client_id: Option<usize>) -> Extranonce<'a>;
224
225    /// Set extranonce2_size to extranonce2_size if provided. If not create a new one and set it.
226    fn set_extranonce2_size(
227        &mut self,
228        client_id: Option<usize>,
229        extra_nonce2_size: Option<usize>,
230    ) -> usize;
231
232    fn extranonce2_size(&self, client_id: Option<usize>) -> usize;
233
234    fn version_rolling_mask(&self, client_id: Option<usize>) -> Option<HexU32Be>;
235
236    fn set_version_rolling_mask(&mut self, client_id: Option<usize>, mask: Option<HexU32Be>);
237
238    fn set_version_rolling_min_bit(&mut self, client_id: Option<usize>, mask: Option<HexU32Be>);
239
240    fn update_extranonce(
241        &mut self,
242        client_id: Option<usize>,
243        extra_nonce1: Extranonce<'a>,
244        extra_nonce2_size: usize,
245    ) -> Result<json_rpc::Message, Error<'a>> {
246        self.set_extranonce1(client_id, Some(extra_nonce1.clone()));
247        self.set_extranonce2_size(client_id, Some(extra_nonce2_size));
248
249        Ok(server_to_client::SetExtranonce {
250            extra_nonce1,
251            extra_nonce2_size,
252        }
253        .into())
254    }
255    // {"params":["00003000"], "id":null, "method": "mining.set_version_mask"}
256    // fn update_version_rolling_mask
257
258    fn notify(&mut self, client_id: Option<usize>) -> Result<json_rpc::Message, Error<'_>>;
259
260    fn handle_set_difficulty(
261        &mut self,
262        _client_id: Option<usize>,
263        value: f64,
264    ) -> Result<json_rpc::Message, Error<'_>> {
265        let set_difficulty = server_to_client::SetDifficulty { value };
266        Ok(set_difficulty.into())
267    }
268}
269
270pub trait IsClient<'a> {
271    /// Deserialize a [raw json_rpc message][a] into a [stratum v1 message][b] and handle the
272    /// result.
273    ///
274    /// [a]: crate::...
275    /// [b]:
276    fn handle_message(
277        &mut self,
278        server_id: Option<usize>,
279        msg: json_rpc::Message,
280    ) -> Result<Option<json_rpc::Message>, Error<'a>>
281    where
282        Self: std::marker::Sized,
283    {
284        let method: Result<Method<'a>, MethodError<'a>> = msg.try_into();
285
286        match method {
287            Ok(m) => match m {
288                Method::Server2ClientResponse(response) => {
289                    let response = self.update_response(server_id, response)?;
290                    self.handle_response(server_id, response)
291                }
292                Method::Server2Client(request) => self.handle_request(server_id, request),
293                Method::Client2Server(_) => Err(Error::InvalidReceiver(m.into())),
294                Method::ErrorMessage(msg) => self.handle_error_message(server_id, msg),
295            },
296            Err(e) => Err(e.into()),
297        }
298    }
299
300    fn update_response(
301        &mut self,
302        server_id: Option<usize>,
303        response: methods::Server2ClientResponse<'a>,
304    ) -> Result<methods::Server2ClientResponse<'a>, Error<'a>> {
305        match &response {
306            methods::Server2ClientResponse::GeneralResponse(general) => {
307                let is_authorize = self.id_is_authorize(server_id, &general.id);
308                let is_submit = self.id_is_submit(server_id, &general.id);
309                match (is_authorize, is_submit) {
310                    (Some(prev_name), false) => {
311                        let authorize = general.clone().into_authorize(prev_name);
312                        Ok(methods::Server2ClientResponse::Authorize(authorize))
313                    }
314                    (None, false) => Ok(methods::Server2ClientResponse::Submit(
315                        general.clone().into_submit(),
316                    )),
317                    _ => Err(Error::UnknownID(general.id)),
318                }
319            }
320            _ => Ok(response),
321        }
322    }
323
324    /// Call the right handler according with the called method
325    fn handle_request(
326        &mut self,
327        server_id: Option<usize>,
328        request: methods::Server2Client<'a>,
329    ) -> Result<Option<json_rpc::Message>, Error<'a>>
330    where
331        Self: std::marker::Sized,
332    {
333        match request {
334            methods::Server2Client::Notify(notify) => {
335                self.handle_notify(server_id, notify)?;
336                Ok(None)
337            }
338            methods::Server2Client::SetDifficulty(mut set_diff) => {
339                self.handle_set_difficulty(server_id, &mut set_diff)?;
340                Ok(None)
341            }
342            methods::Server2Client::SetExtranonce(mut set_extra_nonce) => {
343                self.handle_set_extranonce(server_id, &mut set_extra_nonce)?;
344                Ok(None)
345            }
346            methods::Server2Client::SetVersionMask(mut set_version_mask) => {
347                self.handle_set_version_mask(server_id, &mut set_version_mask)?;
348                Ok(None)
349            }
350        }
351    }
352
353    fn handle_response(
354        &mut self,
355        server_id: Option<usize>,
356        response: methods::Server2ClientResponse<'a>,
357    ) -> Result<Option<json_rpc::Message>, Error<'a>>
358    where
359        Self: std::marker::Sized,
360    {
361        match response {
362            methods::Server2ClientResponse::Configure(mut configure) => {
363                self.handle_configure(server_id, &mut configure)?;
364                self.set_version_rolling_mask(server_id, configure.version_rolling_mask());
365                self.set_version_rolling_min_bit(server_id, configure.version_rolling_min_bit());
366                self.set_status(server_id, ClientStatus::Configured);
367
368                //in sv1 the mining.configure message should be the first message to come in before
369                // the subscribe - the subscribe response is where the server hands out the
370                // extranonce so it doesnt really matter what the server sets the
371                // extranonce to in the mining.configure handler
372                debug!("NOTICE: Subscribe extranonce is hardcoded by server");
373                let subscribe = self
374                    .subscribe(
375                        server_id,
376                        configure.id,
377                        Some(Extranonce::try_from(
378                            Vec::<u8>::from_hex("08000002").map_err(Error::HexError)?,
379                        )?),
380                    )
381                    .ok();
382                Ok(subscribe)
383            }
384            methods::Server2ClientResponse::Subscribe(subscribe) => {
385                self.handle_subscribe(server_id, &subscribe)?;
386                self.set_extranonce1(server_id, subscribe.extra_nonce1);
387                self.set_extranonce2_size(server_id, subscribe.extra_nonce2_size);
388                self.set_status(server_id, ClientStatus::Subscribed);
389                Ok(None)
390            }
391            methods::Server2ClientResponse::Authorize(authorize) => {
392                if authorize.is_ok() {
393                    self.authorize_user_name(server_id, authorize.user_name());
394                };
395                Ok(None)
396            }
397            methods::Server2ClientResponse::Submit(_) => Ok(None),
398            // impossible state
399            methods::Server2ClientResponse::GeneralResponse(_) => panic!(),
400            methods::Server2ClientResponse::SetDifficulty(_) => Ok(None),
401        }
402    }
403
404    fn handle_error_message(
405        &mut self,
406        server_id: Option<usize>,
407        message: Message,
408    ) -> Result<Option<json_rpc::Message>, Error<'a>>;
409
410    /// Check if the client sent an Authorize request with the given id, if so it return the
411    /// authorized name
412    fn id_is_authorize(&mut self, server_id: Option<usize>, id: &u64) -> Option<String>;
413
414    /// Check if the client sent a Submit request with the given id
415    fn id_is_submit(&mut self, server_id: Option<usize>, id: &u64) -> bool;
416
417    fn handle_notify(
418        &mut self,
419        server_id: Option<usize>,
420        notify: server_to_client::Notify<'a>,
421    ) -> Result<(), Error<'a>>;
422
423    fn handle_configure(
424        &mut self,
425        server_id: Option<usize>,
426        conf: &mut server_to_client::Configure,
427    ) -> Result<(), Error<'a>>;
428
429    fn handle_set_difficulty(
430        &mut self,
431        server_id: Option<usize>,
432        m: &mut server_to_client::SetDifficulty,
433    ) -> Result<(), Error<'a>>;
434
435    fn handle_set_extranonce(
436        &mut self,
437        server_id: Option<usize>,
438        m: &mut server_to_client::SetExtranonce,
439    ) -> Result<(), Error<'a>>;
440
441    fn handle_set_version_mask(
442        &mut self,
443        server_id: Option<usize>,
444        m: &mut server_to_client::SetVersionMask,
445    ) -> Result<(), Error<'a>>;
446
447    fn handle_subscribe(
448        &mut self,
449        server_id: Option<usize>,
450        subscribe: &server_to_client::Subscribe<'a>,
451    ) -> Result<(), Error<'a>>;
452
453    fn set_extranonce1(&mut self, server_id: Option<usize>, extranonce1: Extranonce<'a>);
454
455    fn extranonce1(&self, server_id: Option<usize>) -> Extranonce<'a>;
456
457    fn set_extranonce2_size(&mut self, server_id: Option<usize>, extra_nonce2_size: usize);
458
459    fn extranonce2_size(&self, server_id: Option<usize>) -> usize;
460
461    fn version_rolling_mask(&self, server_id: Option<usize>) -> Option<HexU32Be>;
462
463    fn set_version_rolling_mask(&mut self, server_id: Option<usize>, mask: Option<HexU32Be>);
464
465    fn set_version_rolling_min_bit(&mut self, server_id: Option<usize>, min: Option<HexU32Be>);
466
467    fn version_rolling_min_bit(&mut self, server_id: Option<usize>) -> Option<HexU32Be>;
468
469    fn set_status(&mut self, server_id: Option<usize>, status: ClientStatus);
470
471    fn signature(&self, server_id: Option<usize>) -> String;
472
473    fn status(&self, server_id: Option<usize>) -> ClientStatus;
474
475    fn last_notify(&self, server_id: Option<usize>) -> Option<server_to_client::Notify<'_>>;
476
477    /// Check if the given user_name has been authorized by the server
478    #[allow(clippy::ptr_arg)]
479    fn is_authorized(&self, server_id: Option<usize>, name: &String) -> bool;
480
481    /// Register the given user_name has authorized by the server
482    fn authorize_user_name(&mut self, server_id: Option<usize>, name: String);
483
484    fn configure(&mut self, server_id: Option<usize>, id: u64) -> json_rpc::Message {
485        if self.version_rolling_min_bit(server_id).is_none()
486            && self.version_rolling_mask(server_id).is_none()
487        {
488            client_to_server::Configure::void(id).into()
489        } else {
490            client_to_server::Configure::new(
491                id,
492                self.version_rolling_mask(server_id),
493                self.version_rolling_min_bit(server_id),
494            )
495            .into()
496        }
497    }
498
499    fn subscribe(
500        &mut self,
501        server_id: Option<usize>,
502        id: u64,
503        extranonce1: Option<Extranonce<'a>>,
504    ) -> Result<json_rpc::Message, Error<'a>> {
505        match self.status(server_id) {
506            ClientStatus::Init => Err(Error::IncorrectClientStatus("mining.subscribe".to_string())),
507            _ => Ok(client_to_server::Subscribe {
508                id,
509                agent_signature: self.signature(server_id),
510                extranonce1,
511            }
512            .try_into()?),
513        }
514    }
515
516    fn authorize(
517        &mut self,
518        server_id: Option<usize>,
519        id: u64,
520        name: String,
521        password: String,
522    ) -> Result<json_rpc::Message, Error<'_>> {
523        match self.status(server_id) {
524            ClientStatus::Init => Err(Error::IncorrectClientStatus("mining.authorize".to_string())),
525            _ => Ok(client_to_server::Authorize { id, name, password }.into()),
526        }
527    }
528
529    #[allow(clippy::too_many_arguments)]
530    fn submit(
531        &mut self,
532        server_id: Option<usize>,
533        id: u64,
534        user_name: String,
535        extra_nonce2: Extranonce<'a>,
536        time: i64,
537        nonce: i64,
538        version_bits: Option<HexU32Be>,
539    ) -> Result<json_rpc::Message, Error<'a>> {
540        match self.status(server_id) {
541            ClientStatus::Init => Err(Error::IncorrectClientStatus("mining.submit".to_string())),
542            _ => {
543                if let Some(notify) = self.last_notify(server_id) {
544                    if !self.is_authorized(server_id, &user_name) {
545                        return Err(Error::UnauthorizedClient(user_name));
546                    }
547                    Ok(client_to_server::Submit {
548                        job_id: notify.job_id,
549                        user_name,
550                        extra_nonce2,
551                        time: HexU32Be(time as u32),
552                        nonce: HexU32Be(nonce as u32),
553                        version_bits,
554                        id,
555                    }
556                    .into())
557                } else {
558                    Err(Error::IncorrectClientStatus(
559                        "No Notify instance found".to_string(),
560                    ))
561                }
562            }
563        }
564    }
565}
566
567#[derive(Debug, Copy, Clone, PartialEq, Eq)]
568pub enum ClientStatus {
569    Init,
570    Configured,
571    Subscribed,
572}
573
574#[cfg(test)]
575mod tests {
576    use super::*;
577    use std::collections::HashSet;
578
579    // A minimal implementation of IsServer trait for testing
580    struct TestServer<'a> {
581        authorized_users: HashSet<String>,
582        extranonce1: Extranonce<'a>,
583        extranonce2_size: usize,
584        version_rolling_mask: Option<HexU32Be>,
585        version_rolling_min_bit: Option<HexU32Be>,
586    }
587
588    impl<'a> TestServer<'a> {
589        fn new(extranonce1: Extranonce<'a>, extranonce2_size: usize) -> Self {
590            Self {
591                authorized_users: HashSet::new(),
592                extranonce1,
593                extranonce2_size,
594                version_rolling_mask: None,
595                version_rolling_min_bit: None,
596            }
597        }
598    }
599
600    impl<'a> IsServer<'a> for TestServer<'a> {
601        fn handle_configure(
602            &mut self,
603            _client_id: Option<usize>,
604            _request: &client_to_server::Configure,
605        ) -> (Option<server_to_client::VersionRollingParams>, Option<bool>) {
606            (None, None)
607        }
608
609        fn handle_subscribe(
610            &self,
611            _client_id: Option<usize>,
612            _request: &client_to_server::Subscribe,
613        ) -> Vec<(String, String)> {
614            vec![("mining.notify".to_string(), "1".to_string())]
615        }
616
617        fn handle_authorize(
618            &self,
619            _client_id: Option<usize>,
620            _request: &client_to_server::Authorize,
621        ) -> bool {
622            true
623        }
624
625        fn notify(&mut self, _client_id: Option<usize>) -> Result<json_rpc::Message, Error<'_>> {
626            Ok(json_rpc::Message::StandardRequest(
627                json_rpc::StandardRequest {
628                    id: 1,
629                    method: "mining.notify".to_string(),
630                    params: serde_json::json!([]),
631                },
632            ))
633        }
634
635        fn handle_submit(
636            &self,
637            _client_id: Option<usize>,
638            _request: &client_to_server::Submit<'a>,
639        ) -> bool {
640            true
641        }
642
643        fn handle_extranonce_subscribe(&self) {}
644
645        fn is_authorized(&self, _client_id: Option<usize>, name: &str) -> bool {
646            self.authorized_users.contains(name)
647        }
648
649        fn authorize(&mut self, _client_id: Option<usize>, name: &str) {
650            self.authorized_users.insert(name.to_string());
651        }
652
653        fn set_extranonce1(
654            &mut self,
655            _client_id: Option<usize>,
656            extranonce1: Option<Extranonce<'a>>,
657        ) -> Extranonce<'a> {
658            if let Some(extranonce1) = extranonce1 {
659                self.extranonce1 = extranonce1;
660            }
661            self.extranonce1.clone()
662        }
663
664        fn extranonce1(&self, _client_id: Option<usize>) -> Extranonce<'a> {
665            self.extranonce1.clone()
666        }
667
668        fn set_extranonce2_size(
669            &mut self,
670            _client_id: Option<usize>,
671            extra_nonce2_size: Option<usize>,
672        ) -> usize {
673            if let Some(extra_nonce2_size) = extra_nonce2_size {
674                self.extranonce2_size = extra_nonce2_size;
675            }
676            self.extranonce2_size
677        }
678
679        fn extranonce2_size(&self, _client_id: Option<usize>) -> usize {
680            self.extranonce2_size
681        }
682
683        fn version_rolling_mask(&self, _client_id: Option<usize>) -> Option<HexU32Be> {
684            None
685        }
686
687        fn set_version_rolling_mask(&mut self, _client_id: Option<usize>, mask: Option<HexU32Be>) {
688            self.version_rolling_mask = mask;
689        }
690
691        fn set_version_rolling_min_bit(
692            &mut self,
693            _client_id: Option<usize>,
694            mask: Option<HexU32Be>,
695        ) {
696            self.version_rolling_min_bit = mask;
697        }
698    }
699
700    #[test]
701    fn test_server_handle_invalid_message() {
702        let extranonce1 = Extranonce::try_from(Vec::<u8>::from_hex("08000002").unwrap()).unwrap();
703        let mut server = TestServer::new(extranonce1, 4);
704
705        // Create an invalid message (response)
706        let request_message = json_rpc::Message::StandardRequest(json_rpc::StandardRequest {
707            id: 42,
708            method: "mining.subscribe_bad".to_string(),
709            params: serde_json::json!([]),
710        });
711
712        let result = server.handle_message(None, request_message);
713
714        assert!(result.is_err());
715        match result.unwrap_err() {
716            Error::Method(inner) => match *inner {
717                MethodError::MethodNotFound(_) => {}
718                other => panic!("Expected MethodNotFound error, got {:?}", other),
719            },
720            other => panic!("Expected Error::Method, got {:?}", other),
721        }
722    }
723
724    #[test]
725    fn version_mask_invalid_len() {
726        let raw = serde_json::json!([
727            "mining.set_version_mask",
728            ["123456789"] // len > 8 bytes
729        ]);
730
731        let msg: Result<Message, _> = serde_json::from_value(raw);
732
733        if let Ok(msg) = msg {
734            let result = Method::try_from(msg);
735            assert!(result.is_err(), "Expected error for invalid hex length");
736
737            match result.unwrap_err() {
738                MethodError::ParsingMethodError((ParsingMethodError::InvalidHexLen(_), _)) => {}
739                other => panic!("Expected InvalidHexLen, got {:?}", other),
740            }
741        } else {
742            panic!("Message parsing failed unexpectedly");
743        }
744    }
745}