Skip to main content

sv1_api/methods/
server_to_client.rs

1use bitcoin_hashes::hex::FromHex;
2use serde_json::{
3    Value,
4    Value::{Array as JArrary, Bool as JBool, Number as JNumber, String as JString},
5};
6use std::convert::{TryFrom, TryInto};
7use std::fmt;
8
9use crate::{
10    error::Error,
11    json_rpc::{Message, Notification, Response},
12    methods::ParsingMethodError,
13    utils::{Extranonce, HexBytes, HexU32Be, MerkleNode, PrevHash},
14};
15
16// client.get_version()
17
18// client.reconnect
19
20// client.show_message
21
22/// Fields in order:
23///
24/// * Job ID: This is included when miners submit a results so work can be matched with proper
25///   transactions.
26/// * Hash of previous block: Used to build the header.
27/// * Generation transaction (part 1): The miner inserts ExtraNonce1 and ExtraNonce2 after this
28///   section of the transaction data.
29/// * Generation transaction (part 2): The miner appends this after the first part of the
30///   transaction data and the two ExtraNonce values.
31/// * List of merkle branches: The generation transaction is hashed against the merkle branches to
32///   build the final merkle root.
33/// * Bitcoin block version: Used in the block header.
34///     * nBits: The encoded network difficulty. Used in the block header.
35///     * nTime: The current time. nTime rolling should be supported, but should not increase faster
36///       than actual time.
37/// * Clean Jobs: If true, miners should abort their current work and immediately use the new job.
38///   If false, they can still use the current job, but should move to the new one after exhausting
39///   the current nonce range.
40#[derive(Debug, Clone)]
41pub struct Notify<'a> {
42    pub job_id: String,
43    pub prev_hash: PrevHash<'a>,
44    pub coin_base1: HexBytes,
45    pub coin_base2: HexBytes,
46    pub merkle_branch: Vec<MerkleNode<'a>>,
47    pub version: HexU32Be,
48    pub bits: HexU32Be,
49    pub time: HexU32Be,
50    pub clean_jobs: bool,
51}
52
53impl<'a> fmt::Display for Notify<'a> {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        write!(
56            f,
57            "Notify {{ job_id: {}, prev_hash: {}, coin_base1: {}, coin_base2: {}, merkle_branch: ",
58            self.job_id, self.prev_hash, self.coin_base1, self.coin_base2,
59        )?;
60
61        match self.merkle_branch.len() {
62            0 => write!(f, "[]")?,
63            1 => write!(f, "[{}]", self.merkle_branch[0])?,
64            2 => write!(f, "[{}, {}]", self.merkle_branch[0], self.merkle_branch[1])?,
65            _ => write!(
66                f,
67                "[{}, ..., {}]",
68                self.merkle_branch.first().unwrap(),
69                self.merkle_branch.last().unwrap()
70            )?,
71        }
72
73        write!(
74            f,
75            " ({} nodes), version: {}, bits: {}, time: {}, clean_jobs: {} }}",
76            self.merkle_branch.len(),
77            self.version,
78            self.bits,
79            self.time,
80            self.clean_jobs,
81        )
82    }
83}
84
85impl From<Notify<'_>> for Message {
86    fn from(notify: Notify) -> Self {
87        let prev_hash: Value = notify.prev_hash.into();
88        let coin_base1: Value = notify.coin_base1.into();
89        let coin_base2: Value = notify.coin_base2.into();
90        let mut merkle_branch: Vec<Value> = vec![];
91        for mb in notify.merkle_branch {
92            let mb: Value = mb.into();
93            merkle_branch.push(mb);
94        }
95        let merkle_branch = JArrary(merkle_branch);
96        let version: Value = notify.version.into();
97        let bits: Value = notify.bits.into();
98        let time: Value = notify.time.into();
99        Message::Notification(Notification {
100            method: "mining.notify".to_string(),
101            params: (&[
102                notify.job_id.into(),
103                prev_hash,
104                coin_base1,
105                coin_base2,
106                merkle_branch,
107                version,
108                bits,
109                time,
110                notify.clean_jobs.into(),
111            ][..])
112                .into(),
113        })
114    }
115}
116
117impl TryFrom<Notification> for Notify<'_> {
118    type Error = ParsingMethodError;
119
120    #[allow(clippy::many_single_char_names)]
121    fn try_from(msg: Notification) -> Result<Self, Self::Error> {
122        let params = msg
123            .params
124            .as_array()
125            .ok_or_else(|| ParsingMethodError::not_array_from_value(msg.params.clone()))?;
126        let (
127            job_id,
128            prev_hash,
129            coin_base1,
130            coin_base2,
131            merkle_branch_,
132            version,
133            bits,
134            time,
135            clean_jobs,
136        ) = match &params[..] {
137            [JString(a), JString(b), JString(c), JString(d), JArrary(e), JString(f), JString(g), JString(h), JBool(i)] => {
138                (
139                    a.into(),
140                    b.as_str().try_into()?,
141                    c.as_str().try_into()?,
142                    d.as_str().try_into()?,
143                    e,
144                    f.as_str().try_into()?,
145                    g.as_str().try_into()?,
146                    h.as_str().try_into()?,
147                    *i,
148                )
149            }
150            _ => {
151                return Err(ParsingMethodError::wrong_args_from_value(
152                    msg.params.clone(),
153                ))
154            }
155        };
156        let mut merkle_branch = vec![];
157        for h in merkle_branch_ {
158            let h: MerkleNode = h
159                .as_str()
160                .ok_or_else(|| ParsingMethodError::not_string_from_value(h.clone()))?
161                .try_into()?;
162
163            merkle_branch.push(h);
164        }
165        let notify = Notify {
166            job_id,
167            prev_hash,
168            coin_base1,
169            coin_base2,
170            merkle_branch,
171            version,
172            bits,
173            time,
174            clean_jobs,
175        };
176        Ok(notify.clone())
177    }
178}
179
180/// mining.set_difficulty(difficulty)
181///
182/// The server can adjust the difficulty required for miner shares with the "mining.set_difficulty"
183/// method. The miner should begin enforcing the new difficulty on the next job received. Some pools
184/// may force a new job out when set_difficulty is sent, using clean_jobs to force the miner to
185/// begin using the new difficulty immediately.
186#[derive(Debug, Clone)]
187pub struct SetDifficulty {
188    pub value: f64,
189}
190
191impl fmt::Display for SetDifficulty {
192    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193        write!(f, "{}", self.value)
194    }
195}
196
197impl From<SetDifficulty> for Message {
198    fn from(sd: SetDifficulty) -> Self {
199        let value: Value = sd.value.into();
200        Message::Notification(Notification {
201            method: "mining.set_difficulty".to_string(),
202            params: (&[value][..]).into(),
203        })
204    }
205}
206
207impl TryFrom<Notification> for SetDifficulty {
208    type Error = ParsingMethodError;
209
210    fn try_from(msg: Notification) -> Result<Self, Self::Error> {
211        let params = msg
212            .params
213            .as_array()
214            .ok_or_else(|| ParsingMethodError::not_array_from_value(msg.params.clone()))?;
215        let (value,) = match &params[..] {
216            [a] => (a
217                .as_f64()
218                .ok_or_else(|| ParsingMethodError::not_float_from_value(a.clone()))?,),
219            _ => return Err(ParsingMethodError::wrong_args_from_value(msg.params)),
220        };
221        Ok(SetDifficulty { value })
222    }
223}
224
225/// SetExtranonce message (sent if we subscribed with `ExtranonceSubscribe`)
226///
227/// mining.set_extranonce("extranonce1", extranonce2_size)
228///
229/// These values, when provided, replace the initial subscription values beginning with the next
230/// mining.notify job.
231///
232/// check if it is a Notification or a StandardRequest this implementation assume that it is a
233/// Notification
234#[derive(Debug, Clone)]
235pub struct SetExtranonce<'a> {
236    pub extra_nonce1: Extranonce<'a>,
237    pub extra_nonce2_size: usize,
238}
239
240impl fmt::Display for SetExtranonce<'_> {
241    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242        write!(
243            f,
244            "SetExtranonce {{ extra_nonce1: {}, extra_nonce2_size: {} }}",
245            self.extra_nonce1, self.extra_nonce2_size
246        )
247    }
248}
249
250impl From<SetExtranonce<'_>> for Message {
251    fn from(se: SetExtranonce) -> Self {
252        let extra_nonce1: Value = se.extra_nonce1.into();
253        let extra_nonce2_size: Value = se.extra_nonce2_size.into();
254        Message::Notification(Notification {
255            method: "mining.set_extranonce".to_string(),
256            params: (&[extra_nonce1, extra_nonce2_size][..]).into(),
257        })
258    }
259}
260
261impl TryFrom<Notification> for SetExtranonce<'_> {
262    type Error = ParsingMethodError;
263
264    fn try_from(msg: Notification) -> Result<Self, Self::Error> {
265        let params = msg
266            .params
267            .as_array()
268            .ok_or_else(|| ParsingMethodError::not_array_from_value(msg.params.clone()))?;
269        let (extra_nonce1, extra_nonce2_size) = match &params[..] {
270            [JString(a), JNumber(b)] => (
271                Extranonce::try_from(Vec::<u8>::from_hex(a)?)?,
272                b.as_u64()
273                    .ok_or_else(|| ParsingMethodError::not_unsigned_from_value(b.clone()))?
274                    as usize,
275            ),
276            _ => return Err(ParsingMethodError::wrong_args_from_value(msg.params)),
277        };
278        Ok(SetExtranonce {
279            extra_nonce1,
280            extra_nonce2_size,
281        })
282    }
283}
284
285#[derive(Debug, Clone)]
286/// Server may arbitrarily adjust version mask
287pub struct SetVersionMask {
288    version_mask: HexU32Be,
289}
290
291impl fmt::Display for SetVersionMask {
292    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
293        write!(
294            f,
295            "SetVersionMask {{ version_mask: {} }}",
296            self.version_mask
297        )
298    }
299}
300
301impl From<SetVersionMask> for Message {
302    fn from(sv: SetVersionMask) -> Self {
303        let version_mask: Value = sv.version_mask.into();
304        Message::Notification(Notification {
305            method: "mining.set_version".to_string(),
306            params: (&[version_mask][..]).into(),
307        })
308    }
309}
310
311impl TryFrom<Notification> for SetVersionMask {
312    type Error = ParsingMethodError;
313
314    fn try_from(msg: Notification) -> Result<Self, Self::Error> {
315        let params = msg
316            .params
317            .as_array()
318            .ok_or_else(|| ParsingMethodError::not_array_from_value(msg.params.clone()))?;
319        let version_mask = match &params[..] {
320            [JString(a)] => a.as_str().try_into()?,
321            _ => return Err(ParsingMethodError::wrong_args_from_value(msg.params)),
322        };
323        Ok(SetVersionMask { version_mask })
324    }
325}
326
327//pub struct Authorize(pub crate::json_rpc::Response, pub String);
328
329/// Authorize and Submit responsed are identical
330#[derive(Debug, Clone)]
331pub struct GeneralResponse {
332    pub id: u64,
333    result: bool,
334}
335
336impl fmt::Display for GeneralResponse {
337    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
338        write!(
339            f,
340            "GeneralResponse {{ id: {}, result: {} }}",
341            self.id, self.result
342        )
343    }
344}
345
346impl GeneralResponse {
347    pub fn into_authorize(self, prev_request_name: String) -> Authorize {
348        Authorize {
349            id: self.id,
350            authorized: self.result,
351            prev_request_name,
352        }
353    }
354    pub fn into_submit(self) -> Submit {
355        Submit {
356            id: self.id,
357            is_ok: self.result,
358        }
359    }
360}
361
362impl TryFrom<&Response> for GeneralResponse {
363    type Error = ParsingMethodError;
364
365    fn try_from(msg: &Response) -> Result<Self, Self::Error> {
366        let id = msg.id;
367        let result = msg.result.as_bool().ok_or_else(|| {
368            ParsingMethodError::ImpossibleToParseResultField(Box::new(msg.clone()))
369        })?;
370        Ok(GeneralResponse { id, result })
371    }
372}
373
374#[derive(Debug, Clone)]
375pub struct Authorize {
376    pub id: u64,
377    authorized: bool,
378    pub prev_request_name: String,
379}
380
381impl fmt::Display for Authorize {
382    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
383        write!(
384            f,
385            "Authorize {{ id: {}, authorized: {}, prev_request_name: {} }}",
386            self.id, self.authorized, self.prev_request_name
387        )
388    }
389}
390
391impl Authorize {
392    pub fn is_ok(&self) -> bool {
393        self.authorized
394    }
395
396    pub fn user_name(self) -> String {
397        self.prev_request_name
398    }
399}
400
401#[derive(Debug, Clone)]
402pub struct Submit {
403    pub id: u64,
404    is_ok: bool,
405}
406
407impl fmt::Display for Submit {
408    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
409        write!(f, "Submit {{ id: {}, is_ok: {} }}", self.id, self.is_ok)
410    }
411}
412
413impl Submit {
414    pub fn is_ok(&self) -> bool {
415        self.is_ok
416    }
417}
418
419/// mining.subscribe
420/// mining.subscribe("user agent/version", "extranonce1")
421/// The optional second parameter specifies a mining.notify subscription id the client wishes to
422/// resume working with (possibly due to a dropped connection). If provided, a server MAY (at its
423/// option) issue the connection the same extranonce1. Note that the extranonce1 may be the same
424/// (allowing a resumed connection) even if the subscription id is changed!
425///
426/// The client receives a result:
427///
428///
429/// The result contains three items:
430///
431///    Subscriptions. - An array of 2-item tuples, each with a subscription type and id.
432///
433///    ExtraNonce1. - Hex-encoded, per-connection unique string which will be used for creating
434///    generation transactions later.
435///
436///    ExtraNonce2_size. - The number of bytes that the miner users for its ExtraNonce2 counter.
437#[derive(Debug, Clone)]
438pub struct Subscribe<'a> {
439    pub id: u64,
440    pub extra_nonce1: Extranonce<'a>,
441    pub extra_nonce2_size: usize,
442    pub subscriptions: Vec<(String, String)>,
443}
444
445impl fmt::Display for Subscribe<'_> {
446    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
447        write!(
448            f,
449            "Subscribe {{ id: {}, extra_nonce1: {}, extra_nonce2_size: {}, subscriptions: {:?} }}",
450            self.id, self.extra_nonce1, self.extra_nonce2_size, self.subscriptions
451        )
452    }
453}
454
455impl From<Subscribe<'_>> for Message {
456    fn from(su: Subscribe) -> Self {
457        let extra_nonce1: Value = su.extra_nonce1.into();
458        let extra_nonce2_size: Value = su.extra_nonce2_size.into();
459        let subscriptions: Vec<Value> = su
460            .subscriptions
461            .iter()
462            .map(|x| JArrary(vec![JString(x.0.clone()), JString(x.1.clone())]))
463            .collect();
464        let subscriptions: Value = subscriptions.into();
465        Message::OkResponse(Response {
466            id: su.id,
467            error: None,
468            result: (&[subscriptions, extra_nonce1, extra_nonce2_size][..]).into(),
469        })
470    }
471}
472
473impl TryFrom<&Response> for Subscribe<'_> {
474    type Error = ParsingMethodError;
475
476    fn try_from(msg: &Response) -> Result<Self, Self::Error> {
477        let id = msg.id;
478        let params = msg.result.as_array().ok_or_else(|| {
479            ParsingMethodError::ImpossibleToParseResultField(Box::new(msg.clone()))
480        })?;
481        let (subscriptions_, extra_nonce1, extra_nonce2_size) = match &params[..] {
482            [JArrary(a), JString(b), JNumber(c)] => (
483                a,
484                // infallible
485                b.as_str().try_into()?,
486                c.as_u64().ok_or_else(|| {
487                    ParsingMethodError::ImpossibleToParseAsU64(Box::new(c.clone()))
488                })? as usize,
489            ),
490            _ => return Err(ParsingMethodError::UnexpectedArrayParams(params.clone())),
491        };
492        let mut subscriptions: Vec<(String, String)> = vec![];
493        for s in subscriptions_ {
494            // we already checked that subscriptions_ is an array
495            let s = s.as_array().unwrap();
496            if s.len() != 2 {
497                return Err(ParsingMethodError::UnexpectedArrayParams(params.clone()));
498            };
499            let s = (
500                s[0].as_str()
501                    .ok_or_else(|| ParsingMethodError::UnexpectedArrayParams(params.clone()))?
502                    .to_string(),
503                s[1].as_str()
504                    .ok_or_else(|| ParsingMethodError::UnexpectedArrayParams(params.clone()))?
505                    .to_string(),
506            );
507            subscriptions.push(s);
508        }
509        Ok(Subscribe {
510            id,
511            extra_nonce1,
512            extra_nonce2_size,
513            subscriptions,
514        })
515    }
516}
517
518#[derive(Debug, Clone)]
519pub struct Configure {
520    pub id: u64,
521    pub version_rolling: Option<VersionRollingParams>,
522    pub minimum_difficulty: Option<bool>,
523}
524
525impl fmt::Display for Configure {
526    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
527        write!(
528            f,
529            "Configure {{ id: {}, version_rolling: {:?}, minimum_difficulty: {:?} }}",
530            self.id, self.version_rolling, self.minimum_difficulty
531        )
532    }
533}
534
535impl Configure {
536    pub fn version_rolling_mask(&self) -> Option<HexU32Be> {
537        match &self.version_rolling {
538            None => None,
539            Some(a) => {
540                if a.version_rolling {
541                    Some(a.version_rolling_mask.clone())
542                } else {
543                    None
544                }
545            }
546        }
547    }
548    pub fn version_rolling_min_bit(&self) -> Option<HexU32Be> {
549        match &self.version_rolling {
550            None => None,
551            Some(a) => {
552                if a.version_rolling {
553                    Some(a.version_rolling_min_bit_count.clone())
554                } else {
555                    None
556                }
557            }
558        }
559    }
560}
561
562impl From<Configure> for Message {
563    fn from(co: Configure) -> Self {
564        let mut params = serde_json::Map::new();
565        if let Some(version_rolling_) = co.version_rolling {
566            let mut version_rolling: serde_json::Map<String, Value> = version_rolling_.into();
567            params.append(&mut version_rolling);
568        };
569        if let Some(min_diff) = co.minimum_difficulty {
570            let minimum_difficulty: Value = min_diff.into();
571            params.insert("minimum-difficulty".to_string(), minimum_difficulty);
572        };
573        Message::OkResponse(Response {
574            id: co.id,
575            error: None,
576            result: serde_json::Value::Object(params),
577        })
578    }
579}
580
581impl TryFrom<&Response> for Configure {
582    type Error = ParsingMethodError;
583
584    fn try_from(msg: &Response) -> Result<Self, ParsingMethodError> {
585        let id = msg.id;
586        let params = msg.result.as_object().ok_or_else(|| {
587            ParsingMethodError::ImpossibleToParseResultField(Box::new(msg.clone()))
588        })?;
589
590        let version_rolling_ = params.get("version-rolling");
591        let version_rolling_mask = params.get("version-rolling.mask");
592        let version_rolling_min_bit_count = params.get("version-rolling.min-bit-count");
593        let minimum_difficulty = params.get("minimum-difficulty");
594
595        // Deserialize version-rolling response.
596        // Composed by 3 fields:
597        //   version-rolling (required),
598        //   version-rolling.mask (required)
599        //   version-rolling.min-bit-count (optional)
600        let version_rolling: Option<VersionRollingParams>;
601        if version_rolling_.is_some() && version_rolling_mask.is_some() {
602            let vr: bool = version_rolling_
603                .unwrap()
604                .as_bool()
605                .ok_or_else(|| ParsingMethodError::UnexpectedObjectParams(params.clone()))?;
606
607            let version_rolling_mask: HexU32Be = version_rolling_mask
608                .unwrap()
609                .as_str()
610                .ok_or_else(|| ParsingMethodError::UnexpectedObjectParams(params.clone()))?
611                .try_into()?;
612
613            // version-rolling.min-bit-count is often not returned by stratum servers,
614            // but min-bit-count should be taken into consideration in the returned mask
615            let version_rolling_min_bit_count: HexU32Be = match version_rolling_min_bit_count {
616                Some(version_rolling_min_bit_count) => version_rolling_min_bit_count
617                    .as_str()
618                    .ok_or_else(|| ParsingMethodError::UnexpectedObjectParams(params.clone()))?
619                    .try_into()?,
620                None => HexU32Be(0),
621            };
622
623            version_rolling = Some(VersionRollingParams {
624                version_rolling: vr,
625                version_rolling_mask,
626                version_rolling_min_bit_count,
627            });
628        } else if version_rolling_.is_none()
629            && version_rolling_mask.is_none()
630            && version_rolling_min_bit_count.is_none()
631        {
632            version_rolling = None;
633        } else {
634            return Err(ParsingMethodError::UnexpectedObjectParams(params.clone()));
635        };
636
637        let minimum_difficulty = match minimum_difficulty {
638            Some(a) => Some(
639                a.as_bool()
640                    .ok_or_else(|| ParsingMethodError::UnexpectedObjectParams(params.clone()))?,
641            ),
642            None => None,
643        };
644
645        Ok(Configure {
646            id,
647            version_rolling,
648            minimum_difficulty,
649        })
650    }
651}
652
653#[derive(Debug, Clone)]
654pub struct VersionRollingParams {
655    pub version_rolling: bool,
656    pub version_rolling_mask: HexU32Be,
657    pub version_rolling_min_bit_count: HexU32Be,
658}
659
660impl fmt::Display for VersionRollingParams {
661    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
662        write!(
663            f,
664            "VersionRollingParams {{ version_rolling: {}, version_rolling_mask: {}, version_rolling_min_bit_count: {} }}",
665            self.version_rolling,
666            self.version_rolling_mask,
667            self.version_rolling_min_bit_count
668        )
669    }
670}
671
672#[test]
673fn configure_response_parsing_all_fields() {
674    let client_response_str = r#"{"id":0,
675            "result":{
676                "version-rolling":true,
677                "version-rolling.mask":"1fffe000",
678                "version-rolling.min-bit-count":"00000005",
679                "minimum-difficulty":false
680            }
681        }"#;
682    let client_response = serde_json::from_str(client_response_str).unwrap();
683    let server_configure = Configure::try_from(&client_response).unwrap();
684    println!("{server_configure:?}");
685
686    let version_rolling = server_configure.version_rolling.unwrap();
687    assert!(version_rolling.version_rolling);
688    assert_eq!(version_rolling.version_rolling_mask, HexU32Be(0x1fffe000));
689    assert_eq!(version_rolling.version_rolling_min_bit_count, HexU32Be(5));
690
691    assert_eq!(server_configure.minimum_difficulty, Some(false));
692}
693
694#[test]
695fn configure_response_parsing_no_vr_min_bit_count() {
696    let client_response_str = r#"{"id":0,
697            "result":{
698                "version-rolling":true,
699                "version-rolling.mask":"1fffe000",
700                "minimum-difficulty":false
701            }
702        }"#;
703    let client_response = serde_json::from_str(client_response_str).unwrap();
704    let server_configure = Configure::try_from(&client_response).unwrap();
705    println!("{server_configure:?}");
706
707    let version_rolling = server_configure.version_rolling.unwrap();
708    assert!(version_rolling.version_rolling);
709    assert_eq!(version_rolling.version_rolling_mask, HexU32Be(0x1fffe000));
710    assert_eq!(version_rolling.version_rolling_min_bit_count, HexU32Be(0));
711
712    assert_eq!(server_configure.minimum_difficulty, Some(false));
713}
714
715impl VersionRollingParams {
716    pub fn new(
717        version_rolling_mask: HexU32Be,
718        version_rolling_min_bit_count: HexU32Be,
719    ) -> Result<Self, Error<'static>> {
720        // 0x1FFFE000 should be configured
721        let negotiated_mask = HexU32Be(version_rolling_mask.clone() & 0x1FFFE000);
722
723        let version_head_ok = negotiated_mask.0 >> 29 == 0;
724        let version_tail_ok = negotiated_mask.0 << 19 == 0;
725        if version_head_ok && version_tail_ok {
726            Ok(VersionRollingParams {
727                version_rolling: true,
728                version_rolling_mask: negotiated_mask,
729                version_rolling_min_bit_count,
730            })
731        } else {
732            Err(Error::InvalidVersionMask(version_rolling_mask))
733        }
734    }
735}
736
737impl From<VersionRollingParams> for serde_json::Map<String, Value> {
738    fn from(vp: VersionRollingParams) -> Self {
739        let version_rolling: Value = vp.version_rolling.into();
740        let version_rolling_mask: Value = vp.version_rolling_mask.into();
741        let version_rolling_min_bit_count: Value = vp.version_rolling_min_bit_count.into();
742        let mut params = serde_json::Map::new();
743        params.insert("version-rolling".to_string(), version_rolling);
744        params.insert("version-rolling.mask".to_string(), version_rolling_mask);
745        params.insert(
746            "version-rolling.min-bit-count".to_string(),
747            version_rolling_min_bit_count,
748        );
749        params
750    }
751}