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	pool::HopDataPool,
37	rate_limit::RateLimitConfig,
38	types::{
39		HopError, DEFAULT_BANDWIDTH_BURST_MIB, DEFAULT_BANDWIDTH_PER_MIN_MIB,
40		DEFAULT_CHECK_INTERVAL_SECS, DEFAULT_MAX_POOL_SIZE_MIB, DEFAULT_MAX_USER_SIZE_MIB,
41		DEFAULT_PROMOTION_BUFFER_SECS, DEFAULT_RETENTION_SECS, DEFAULT_SUBMIT_BURST,
42		DEFAULT_SUBMIT_RATE_PER_MIN,
43	},
44};
45use clap::Parser;
46use std::{path::PathBuf, sync::Arc};
47
48/// HOP (Hand-Off Protocol) configuration parameters
49#[derive(Debug, Clone, Parser)]
50pub struct HopParams {
51	/// Enable HOP
52	#[arg(id = "enable-hop", long = "enable-hop", default_value_t = false)]
53	pub enabled: bool,
54
55	/// HOP maximum data pool size in MiB. Must be at least 1.
56	#[arg(
57		long = "hop-max-pool-size",
58		default_value_t = DEFAULT_MAX_POOL_SIZE_MIB,
59		value_parser = clap::value_parser!(u64).range(1..),
60	)]
61	pub max_pool_size: u64,
62
63	/// HOP maximum per-user pool size in MiB (hard cap, not scaled by active users). Must be at
64	/// least 1.
65	#[arg(
66		long = "hop-max-user-size",
67		default_value_t = DEFAULT_MAX_USER_SIZE_MIB,
68		value_parser = clap::value_parser!(u64).range(1..),
69	)]
70	pub max_user_size: u64,
71
72	/// HOP data retention period in seconds (24h = 86400s). Must be at least 1.
73	#[arg(
74		long = "hop-retention-secs",
75		default_value_t = DEFAULT_RETENTION_SECS,
76		value_parser = clap::value_parser!(u64).range(1..),
77	)]
78	pub retention_secs: u64,
79
80	/// HOP expiry cleanup interval in seconds. Must be at least 1 (a value of 0
81	/// would turn the maintenance loop into a CPU-burning busy loop).
82	#[arg(
83		long = "hop-check-interval",
84		default_value_t = DEFAULT_CHECK_INTERVAL_SECS,
85		value_parser = clap::value_parser!(u64).range(1..),
86	)]
87	pub check_interval: u64,
88
89	/// Seconds before expiry at which to start promoting entries on-chain. Must be at least 1.
90	#[arg(
91		long = "hop-promotion-buffer-secs",
92		default_value_t = DEFAULT_PROMOTION_BUFFER_SECS,
93		value_parser = clap::value_parser!(u64).range(1..),
94	)]
95	pub promotion_buffer_secs: u64,
96
97	/// Sustained per-account submit rate (requests per minute). Must be at least 1
98	/// when rate limiting is enabled — use `--hop-disable-rate-limit` to turn it off.
99	#[arg(
100		long = "hop-submit-rate-per-min",
101		default_value_t = DEFAULT_SUBMIT_RATE_PER_MIN,
102		value_parser = clap::value_parser!(u32).range(1..),
103	)]
104	pub submit_rate_per_min: u32,
105
106	/// Per-account submit burst size (requests). Must be at least 1.
107	#[arg(
108		long = "hop-submit-burst",
109		default_value_t = DEFAULT_SUBMIT_BURST,
110		value_parser = clap::value_parser!(u32).range(1..),
111	)]
112	pub submit_burst: u32,
113
114	/// Sustained per-account bandwidth (MiB per minute). Must be at least 1
115	/// when rate limiting is enabled — use `--hop-disable-rate-limit` to turn it off.
116	#[arg(
117		long = "hop-bandwidth-per-min-mib",
118		default_value_t = DEFAULT_BANDWIDTH_PER_MIN_MIB,
119		value_parser = clap::value_parser!(u64).range(1..),
120	)]
121	pub bandwidth_per_min_mib: u64,
122
123	/// Per-account bandwidth burst size (MiB). Must be at least 1.
124	#[arg(
125		long = "hop-bandwidth-burst-mib",
126		default_value_t = DEFAULT_BANDWIDTH_BURST_MIB,
127		value_parser = clap::value_parser!(u64).range(1..),
128	)]
129	pub bandwidth_burst_mib: u64,
130
131	/// Disable per-account submit rate limiting (intended for tests and dev nodes).
132	#[arg(long = "hop-disable-rate-limit")]
133	pub disable_rate_limit: bool,
134
135	/// Directory for HOP persistent data storage.
136	///
137	/// If not specified, defaults to `<chain-data-dir>/hop`.
138	#[arg(long = "hop-data-dir")]
139	pub data_dir: Option<std::path::PathBuf>,
140}
141
142impl Default for HopParams {
143	fn default() -> Self {
144		Self {
145			enabled: false,
146			max_pool_size: DEFAULT_MAX_POOL_SIZE_MIB,
147			max_user_size: DEFAULT_MAX_USER_SIZE_MIB,
148			retention_secs: DEFAULT_RETENTION_SECS,
149			check_interval: DEFAULT_CHECK_INTERVAL_SECS,
150			promotion_buffer_secs: DEFAULT_PROMOTION_BUFFER_SECS,
151			submit_rate_per_min: DEFAULT_SUBMIT_RATE_PER_MIN,
152			submit_burst: DEFAULT_SUBMIT_BURST,
153			bandwidth_per_min_mib: DEFAULT_BANDWIDTH_PER_MIN_MIB,
154			bandwidth_burst_mib: DEFAULT_BANDWIDTH_BURST_MIB,
155			disable_rate_limit: false,
156			data_dir: None,
157		}
158	}
159}
160
161impl HopParams {
162	/// Derive a [`RateLimitConfig`] from these CLI parameters.
163	pub fn rate_limit_config(&self) -> RateLimitConfig {
164		if self.disable_rate_limit {
165			return RateLimitConfig::disabled();
166		}
167		RateLimitConfig {
168			enabled: true,
169			submit_rate_per_min: self.submit_rate_per_min,
170			submit_burst: self.submit_burst,
171			bandwidth_per_min: self.bandwidth_per_min_mib.saturating_mul(1024 * 1024),
172			bandwidth_burst: self.bandwidth_burst_mib.saturating_mul(1024 * 1024),
173		}
174	}
175
176	/// Build a HOP data pool from these CLI parameters, resolving the data directory.
177	///
178	/// The resolved data directory is [`Self::data_dir`] if set, otherwise
179	/// `<database_path>/hop`; if neither is available, returns [`HopError::MissingDataDir`].
180	/// Callers gate on whether HOP is enabled (e.g. via `--enable-hop`) before calling this.
181	pub fn build_pool(&self, database_path: Option<PathBuf>) -> Result<Arc<HopDataPool>, HopError> {
182		let data_dir = match &self.data_dir {
183			Some(dir) => dir.clone(),
184			None => database_path.ok_or(HopError::MissingDataDir)?.join("hop"),
185		};
186
187		tracing::info!(
188			target: "hop",
189			params = ?self,
190			data_dir = %data_dir.display(),
191			"Initializing HOP data pool",
192		);
193
194		let pool = HopDataPool::new(
195			self.max_pool_size.saturating_mul(1024 * 1024),
196			self.max_user_size.saturating_mul(1024 * 1024),
197			self.retention_secs,
198			data_dir,
199			self.rate_limit_config(),
200		)?;
201
202		tracing::info!(
203			target: "hop",
204			status = ?pool.status(),
205			"HOP data pool initialized, RPC methods will be registered",
206		);
207
208		Ok(Arc::new(pool))
209	}
210}
211
212#[cfg(test)]
213mod tests {
214	use super::*;
215	use clap::Parser;
216
217	/// Wrap `HopParams` so we can drive `clap`'s parser with a synthetic argv.
218	#[derive(Parser)]
219	struct TestCli {
220		#[clap(flatten)]
221		hop: HopParams,
222	}
223
224	#[test]
225	fn build_pool_without_any_dir_returns_missing_data_dir() {
226		match HopParams::default().build_pool(None) {
227			Err(HopError::MissingDataDir) => (),
228			Err(other) => panic!("expected MissingDataDir, got: {other:?}"),
229			Ok(_) => panic!("expected MissingDataDir, got Ok"),
230		}
231	}
232
233	#[test]
234	fn cli_rejects_zero_for_critical_numeric_parameters() {
235		// Each of these parameters would, at zero, either lock the maintenance
236		// loop into a busy spin, expire entries the same block they're created,
237		// or break rate-limit math. clap must reject them at parse time.
238		let zero_flags = [
239			"--hop-max-pool-size",
240			"--hop-max-user-size",
241			"--hop-retention-secs",
242			"--hop-check-interval",
243			"--hop-promotion-buffer-secs",
244			"--hop-submit-rate-per-min",
245			"--hop-submit-burst",
246			"--hop-bandwidth-per-min-mib",
247			"--hop-bandwidth-burst-mib",
248		];
249		for flag in zero_flags {
250			let argv = ["test-bin", flag, "0"];
251			let result = TestCli::try_parse_from(argv);
252			assert!(
253				result.is_err(),
254				"clap accepted zero for {flag} but it should have been rejected",
255			);
256		}
257	}
258
259	#[test]
260	fn cli_accepts_one_for_critical_numeric_parameters() {
261		let one_flags = ["--hop-max-pool-size", "--hop-retention-secs", "--hop-check-interval"];
262		for flag in one_flags {
263			let argv = ["test-bin", flag, "1"];
264			TestCli::try_parse_from(argv).expect("parse should succeed");
265		}
266	}
267}