Skip to main content

stratum_apps/monitoring/
server.rs

1//! Server monitoring types
2//!
3//! These types are for monitoring the **server** (upstream connection).
4//! An app typically has one server connection with one or more channels.
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use utoipa::ToSchema;
9
10/// Information about an extended channel opened with the server
11#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
12pub struct ServerExtendedChannelInfo {
13    pub channel_id: u32,
14    pub user_identity: String,
15    /// None when vardiff is disabled and hashrate cannot be reliably tracked
16    pub nominal_hashrate: Option<f32>,
17    pub target_hex: String,
18    pub extranonce_prefix_hex: String,
19    pub full_extranonce_size: usize,
20    pub rollable_extranonce_size: u16,
21    pub version_rolling: bool,
22    pub shares_acknowledged: u32,
23    pub shares_submitted: u32,
24    pub shares_rejected: u32,
25    pub shares_rejected_by_reason: HashMap<String, u32>,
26    /// Work acknowledged by upstream via `SubmitSharesSuccess.new_shares_sum`.
27    pub acknowledged_work_sum: u64,
28    /// Work locally validated by the client channel.
29    pub validated_work_sum: f64,
30    pub best_diff: f64,
31    pub blocks_found: u32,
32}
33
34/// Information about a standard channel opened with the server
35#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
36pub struct ServerStandardChannelInfo {
37    pub channel_id: u32,
38    pub user_identity: String,
39    /// None when vardiff is disabled and hashrate cannot be reliably tracked
40    pub nominal_hashrate: Option<f32>,
41    pub target_hex: String,
42    pub extranonce_prefix_hex: String,
43    pub shares_acknowledged: u32,
44    pub shares_submitted: u32,
45    pub shares_rejected: u32,
46    pub shares_rejected_by_reason: HashMap<String, u32>,
47    /// Work acknowledged by upstream via `SubmitSharesSuccess.new_shares_sum`.
48    pub acknowledged_work_sum: u64,
49    /// Work locally validated by the client channel.
50    pub validated_work_sum: f64,
51    pub best_diff: f64,
52    pub blocks_found: u32,
53}
54
55/// Information about the server (upstream connection)
56#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
57pub struct ServerInfo {
58    pub extended_channels: Vec<ServerExtendedChannelInfo>,
59    pub standard_channels: Vec<ServerStandardChannelInfo>,
60}
61
62impl ServerInfo {
63    /// Get total number of channels with the server
64    pub fn total_channels(&self) -> usize {
65        self.extended_channels.len() + self.standard_channels.len()
66    }
67
68    /// Get total hashrate across all server channels
69    pub fn total_hashrate(&self) -> f32 {
70        self.extended_channels
71            .iter()
72            .filter_map(|c| c.nominal_hashrate)
73            .sum::<f32>()
74            + self
75                .standard_channels
76                .iter()
77                .filter_map(|c| c.nominal_hashrate)
78                .sum::<f32>()
79    }
80}
81
82/// Aggregate information about the server connection
83#[derive(Debug, Clone, Serialize, Deserialize, ToSchema)]
84pub struct ServerSummary {
85    pub total_channels: usize,
86    pub extended_channels: usize,
87    pub standard_channels: usize,
88    pub total_hashrate: f32,
89}
90
91/// Trait for monitoring the server (upstream connection)
92pub trait ServerMonitoring: Send + Sync {
93    /// Get server connection info with all its channels
94    fn get_server(&self) -> ServerInfo;
95
96    /// Get summary of server connection
97    fn get_server_summary(&self) -> ServerSummary {
98        let server = self.get_server();
99
100        ServerSummary {
101            total_channels: server.total_channels(),
102            extended_channels: server.extended_channels.len(),
103            standard_channels: server.standard_channels.len(),
104            total_hashrate: server.total_hashrate(),
105        }
106    }
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    use stratum_core::mining_sv2::ERROR_CODE_SUBMIT_SHARES_DUPLICATE_SHARE;
113
114    // ── helpers ──────────────────────────────────────────────────────
115
116    fn create_server_extended_channel_info(
117        channel_id: u32,
118        hashrate: Option<f32>,
119    ) -> ServerExtendedChannelInfo {
120        ServerExtendedChannelInfo {
121            channel_id,
122            user_identity: format!("pool-ext-{}", channel_id),
123            nominal_hashrate: hashrate,
124            target_hex: "00ff".into(),
125            extranonce_prefix_hex: "aa".into(),
126            full_extranonce_size: 16,
127            rollable_extranonce_size: 4,
128            version_rolling: true,
129            shares_acknowledged: 10,
130            shares_rejected: 0,
131            shares_rejected_by_reason: HashMap::new(),
132            acknowledged_work_sum: 100,
133            validated_work_sum: 100.0,
134            shares_submitted: 12,
135            best_diff: 50.0,
136            blocks_found: 0,
137        }
138    }
139
140    fn create_server_standard_channel_info(
141        channel_id: u32,
142        hashrate: Option<f32>,
143    ) -> ServerStandardChannelInfo {
144        ServerStandardChannelInfo {
145            channel_id,
146            user_identity: format!("pool-std-{}", channel_id),
147            nominal_hashrate: hashrate,
148            target_hex: "00ff".into(),
149            extranonce_prefix_hex: "bb".into(),
150            shares_acknowledged: 20,
151            shares_submitted: 22,
152            shares_rejected: 1,
153            shares_rejected_by_reason: HashMap::from([(
154                ERROR_CODE_SUBMIT_SHARES_DUPLICATE_SHARE.to_string(),
155                1,
156            )]),
157            acknowledged_work_sum: 200,
158            validated_work_sum: 200.0,
159            best_diff: 80.0,
160            blocks_found: 0,
161        }
162    }
163
164    // ── ServerInfo unit tests ───────────────────────────────────────
165
166    #[test]
167    fn server_info_empty() {
168        let server = ServerInfo {
169            extended_channels: vec![],
170            standard_channels: vec![],
171        };
172        assert_eq!(server.total_channels(), 0);
173        assert_eq!(server.total_hashrate(), 0.0);
174    }
175
176    #[test]
177    fn server_info_aggregates_both_channel_types() {
178        let server = ServerInfo {
179            extended_channels: vec![create_server_extended_channel_info(1, Some(100.0))],
180            standard_channels: vec![
181                create_server_standard_channel_info(2, Some(50.0)),
182                create_server_standard_channel_info(3, Some(75.0)),
183            ],
184        };
185        assert_eq!(server.total_channels(), 3);
186        assert_eq!(server.total_hashrate(), 225.0);
187    }
188
189    #[test]
190    fn server_info_hashrate_skips_none_values() {
191        let server = ServerInfo {
192            extended_channels: vec![
193                create_server_extended_channel_info(1, Some(100.0)),
194                create_server_extended_channel_info(2, None),
195            ],
196            standard_channels: vec![
197                create_server_standard_channel_info(3, Some(50.0)),
198                create_server_standard_channel_info(4, None),
199            ],
200        };
201        assert_eq!(server.total_channels(), 4);
202        assert_eq!(server.total_hashrate(), 150.0);
203    }
204
205    // ── ServerMonitoring trait default implementations ───────────────
206
207    struct MockServer(ServerInfo);
208    impl ServerMonitoring for MockServer {
209        fn get_server(&self) -> ServerInfo {
210            self.0.clone()
211        }
212    }
213
214    #[test]
215    fn server_monitoring_summary_empty() {
216        let monitor = MockServer(ServerInfo {
217            extended_channels: vec![],
218            standard_channels: vec![],
219        });
220        let summary = monitor.get_server_summary();
221
222        assert_eq!(summary.total_channels, 0);
223        assert_eq!(summary.extended_channels, 0);
224        assert_eq!(summary.standard_channels, 0);
225        assert_eq!(summary.total_hashrate, 0.0);
226    }
227
228    #[test]
229    fn server_monitoring_summary_aggregates_correctly() {
230        let monitor = MockServer(ServerInfo {
231            extended_channels: vec![
232                create_server_extended_channel_info(1, Some(100.0)),
233                create_server_extended_channel_info(2, Some(200.0)),
234            ],
235            standard_channels: vec![create_server_standard_channel_info(3, Some(50.0))],
236        });
237        let summary = monitor.get_server_summary();
238
239        assert_eq!(summary.total_channels, 3);
240        assert_eq!(summary.extended_channels, 2);
241        assert_eq!(summary.standard_channels, 1);
242        assert_eq!(summary.total_hashrate, 350.0);
243    }
244}