referrerpolicy=no-referrer-when-downgrade

sc_hop/
cli.rs

1// Copyright (C) Parity Technologies (UK) Ltd.
2// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0
3
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13
14// You should have received a copy of the GNU General Public License
15// along with this program. If not, see <https://www.gnu.org/licenses/>.
16
17//! HOP CLI parameters.
18//!
19//! ## Usage
20//!
21//! To integrate HOP into your Substrate node CLI, flatten these parameters:
22//!
23//! ```rust,ignore
24//! use sc_hop::HopParams;
25//!
26//! #[derive(Debug, clap::Parser)]
27//! pub struct Cli {
28//!     // ... your other CLI fields ...
29//!
30//!     #[clap(flatten)]
31//!     pub hop: HopParams,
32//! }
33//! ```
34
35use crate::{
36	metrics::HopMetrics,
37	pool::HopDataPool,
38	rate_limit::RateLimitConfig,
39	types::{
40		HopError, DEFAULT_BANDWIDTH_BURST_MIB, DEFAULT_BANDWIDTH_PER_MIN_MIB,
41		DEFAULT_CHECK_INTERVAL_SECS, DEFAULT_MAX_POOL_SIZE_MIB, DEFAULT_MAX_USER_SIZE_MIB,
42		DEFAULT_PROMOTION_BUFFER_SECS, DEFAULT_RETENTION_SECS, DEFAULT_SUBMIT_BURST,
43		DEFAULT_SUBMIT_RATE_PER_MIN,
44	},
45};
46use clap::Parser;
47use prometheus_endpoint::Registry;
48use std::{path::PathBuf, sync::Arc};
49
50/// HOP (Hand-Off Protocol) configuration parameters
51#[derive(Debug, Clone, Parser)]
52pub struct HopParams {
53	/// Enable HOP
54	#[arg(id = "enable-hop", long = "enable-hop", default_value_t = false)]
55	pub enabled: bool,
56
57	/// HOP maximum data pool size in MiB. Must be at least 1.
58	#[arg(
59		long = "hop-max-pool-size",
60		default_value_t = DEFAULT_MAX_POOL_SIZE_MIB,
61		value_parser = clap::value_parser!(u64).range(1..),
62	)]
63	pub max_pool_size: u64,
64
65	/// HOP maximum per-user pool size in MiB (hard cap, not scaled by active users). Must be at
66	/// least 1.
67	#[arg(
68		long = "hop-max-user-size",
69		default_value_t = DEFAULT_MAX_USER_SIZE_MIB,
70		value_parser = clap::value_parser!(u64).range(1..),
71	)]
72	pub max_user_size: u64,
73
74	/// HOP data retention period in seconds (24h = 86400s). Must be at least 1.
75	#[arg(
76		long = "hop-retention-secs",
77		default_value_t = DEFAULT_RETENTION_SECS,
78		value_parser = clap::value_parser!(u64).range(1..),
79	)]
80	pub retention_secs: u64,
81
82	/// HOP expiry cleanup interval in seconds. Must be at least 1 (a value of 0
83	/// would turn the maintenance loop into a CPU-burning busy loop).
84	#[arg(
85		long = "hop-check-interval",
86		default_value_t = DEFAULT_CHECK_INTERVAL_SECS,
87		value_parser = clap::value_parser!(u64).range(1..),
88	)]
89	pub check_interval: u64,
90
91	/// Seconds before expiry at which to start promoting entries on-chain. Must be at least 1.
92	#[arg(
93		long = "hop-promotion-buffer-secs",
94		default_value_t = DEFAULT_PROMOTION_BUFFER_SECS,
95		value_parser = clap::value_parser!(u64).range(1..),
96	)]
97	pub promotion_buffer_secs: u64,
98
99	/// Sustained per-account submit rate (requests per minute). Must be at least 1
100	/// when rate limiting is enabled — use `--hop-disable-rate-limit` to turn it off.
101	#[arg(
102		long = "hop-submit-rate-per-min",
103		default_value_t = DEFAULT_SUBMIT_RATE_PER_MIN,
104		value_parser = clap::value_parser!(u32).range(1..),
105	)]
106	pub submit_rate_per_min: u32,
107
108	/// Per-account submit burst size (requests). Must be at least 1.
109	#[arg(
110		long = "hop-submit-burst",
111		default_value_t = DEFAULT_SUBMIT_BURST,
112		value_parser = clap::value_parser!(u32).range(1..),
113	)]
114	pub submit_burst: u32,
115
116	/// Sustained per-account bandwidth (MiB per minute). Must be at least 1
117	/// when rate limiting is enabled — use `--hop-disable-rate-limit` to turn it off.
118	#[arg(
119		long = "hop-bandwidth-per-min-mib",
120		default_value_t = DEFAULT_BANDWIDTH_PER_MIN_MIB,
121		value_parser = clap::value_parser!(u64).range(1..),
122	)]
123	pub bandwidth_per_min_mib: u64,
124
125	/// Per-account bandwidth burst size (MiB). Must be at least 1.
126	#[arg(
127		long = "hop-bandwidth-burst-mib",
128		default_value_t = DEFAULT_BANDWIDTH_BURST_MIB,
129		value_parser = clap::value_parser!(u64).range(1..),
130	)]
131	pub bandwidth_burst_mib: u64,
132
133	/// Disable per-account submit rate limiting (intended for tests and dev nodes).
134	#[arg(long = "hop-disable-rate-limit")]
135	pub disable_rate_limit: bool,
136
137	/// Directory for HOP persistent data storage.
138	///
139	/// If not specified, defaults to `<chain-data-dir>/hop`.
140	#[arg(long = "hop-data-dir")]
141	pub data_dir: Option<std::path::PathBuf>,
142}
143
144impl Default for HopParams {
145	fn default() -> Self {
146		Self {
147			enabled: false,
148			max_pool_size: DEFAULT_MAX_POOL_SIZE_MIB,
149			max_user_size: DEFAULT_MAX_USER_SIZE_MIB,
150			retention_secs: DEFAULT_RETENTION_SECS,
151			check_interval: DEFAULT_CHECK_INTERVAL_SECS,
152			promotion_buffer_secs: DEFAULT_PROMOTION_BUFFER_SECS,
153			submit_rate_per_min: DEFAULT_SUBMIT_RATE_PER_MIN,
154			submit_burst: DEFAULT_SUBMIT_BURST,
155			bandwidth_per_min_mib: DEFAULT_BANDWIDTH_PER_MIN_MIB,
156			bandwidth_burst_mib: DEFAULT_BANDWIDTH_BURST_MIB,
157			disable_rate_limit: false,
158			data_dir: None,
159		}
160	}
161}
162
163impl HopParams {
164	/// Derive a [`RateLimitConfig`] from these CLI parameters.
165	pub fn rate_limit_config(&self) -> RateLimitConfig {
166		if self.disable_rate_limit {
167			return RateLimitConfig::disabled();
168		}
169		RateLimitConfig {
170			enabled: true,
171			submit_rate_per_min: self.submit_rate_per_min,
172			submit_burst: self.submit_burst,
173			bandwidth_per_min: self.bandwidth_per_min_mib.saturating_mul(1024 * 1024),
174			bandwidth_burst: self.bandwidth_burst_mib.saturating_mul(1024 * 1024),
175		}
176	}
177
178	/// Build a HOP data pool from these CLI parameters, resolving the data directory.
179	///
180	/// The resolved data directory is [`Self::data_dir`] if set, otherwise
181	/// `<database_path>/hop`; if neither is available, returns [`HopError::MissingDataDir`].
182	/// Callers gate on whether HOP is enabled (e.g. via `--enable-hop`) before calling this.
183	///
184	/// Metrics are registered with `registry` when given; a registration failure
185	/// only disables metrics, it never fails pool construction.
186	pub fn build_pool(
187		&self,
188		database_path: Option<PathBuf>,
189		registry: Option<&Registry>,
190	) -> Result<Arc<HopDataPool>, HopError> {
191		let data_dir = match &self.data_dir {
192			Some(dir) => dir.clone(),
193			None => database_path.ok_or(HopError::MissingDataDir)?.join("hop"),
194		};
195
196		tracing::info!(
197			target: "hop",
198			params = ?self,
199			data_dir = %data_dir.display(),
200			"Initializing HOP data pool",
201		);
202
203		let metrics = HopMetrics::new(registry).unwrap_or_else(|e| {
204			tracing::warn!(
205				target: "hop",
206				error = %e,
207				"Failed to register HOP metrics; continuing without metrics"
208			);
209			HopMetrics::disabled()
210		});
211
212		let pool = HopDataPool::new(
213			self.max_pool_size.saturating_mul(1024 * 1024),
214			self.max_user_size.saturating_mul(1024 * 1024),
215			self.retention_secs,
216			data_dir,
217			self.rate_limit_config(),
218			metrics,
219		)?;
220
221		tracing::info!(
222			target: "hop",
223			status = ?pool.status(),
224			"HOP data pool initialized, RPC methods will be registered",
225		);
226
227		Ok(Arc::new(pool))
228	}
229}
230
231#[cfg(test)]
232mod tests {
233	use super::*;
234	use clap::Parser;
235
236	/// Wrap `HopParams` so we can drive `clap`'s parser with a synthetic argv.
237	#[derive(Parser)]
238	struct TestCli {
239		#[clap(flatten)]
240		hop: HopParams,
241	}
242
243	#[test]
244	fn build_pool_without_any_dir_returns_missing_data_dir() {
245		match HopParams::default().build_pool(None, None) {
246			Err(HopError::MissingDataDir) => (),
247			Err(other) => panic!("expected MissingDataDir, got: {other:?}"),
248			Ok(_) => panic!("expected MissingDataDir, got Ok"),
249		}
250	}
251
252	#[test]
253	fn cli_rejects_zero_for_critical_numeric_parameters() {
254		// Each of these parameters would, at zero, either lock the maintenance
255		// loop into a busy spin, expire entries the same block they're created,
256		// or break rate-limit math. clap must reject them at parse time.
257		let zero_flags = [
258			"--hop-max-pool-size",
259			"--hop-max-user-size",
260			"--hop-retention-secs",
261			"--hop-check-interval",
262			"--hop-promotion-buffer-secs",
263			"--hop-submit-rate-per-min",
264			"--hop-submit-burst",
265			"--hop-bandwidth-per-min-mib",
266			"--hop-bandwidth-burst-mib",
267		];
268		for flag in zero_flags {
269			let argv = ["test-bin", flag, "0"];
270			let result = TestCli::try_parse_from(argv);
271			assert!(
272				result.is_err(),
273				"clap accepted zero for {flag} but it should have been rejected",
274			);
275		}
276	}
277
278	#[test]
279	fn cli_accepts_one_for_critical_numeric_parameters() {
280		let one_flags = ["--hop-max-pool-size", "--hop-retention-secs", "--hop-check-interval"];
281		for flag in one_flags {
282			let argv = ["test-bin", flag, "1"];
283			TestCli::try_parse_from(argv).expect("parse should succeed");
284		}
285	}
286}