Skip to content

Commit eb2262d

Browse files
authored
refactor(staking-cli): move skip-metadata-validation to command args (#3937)
* refactor(staking-cli): move skip-metadata-validation to command args Move --skip-metadata-validation from global flag to MetadataUriArgs, making it a command-specific flag that appears after the subcommand. Changes: - Move skip_metadata_validation field to MetadataUriArgs - Refactor metadata_uri to Option<Url> with custom value parser - Add Clap constraints: requires, conflicts_with, required_unless_present - Move MetadataUriArgs to metadata.rs for better organization - Add tests for expected error messaged. Breaking change: --skip-metadata-validation must now appear after the subcommand instead of before it. Before: staking-cli --skip-metadata-validation register-validator ... After: staking-cli register-validator ... --skip-metadata-validation * refactor(staking-cli): extract test helper for metadata command setup
1 parent a391b8e commit eb2262d

File tree

7 files changed

+318
-122
lines changed

7 files changed

+318
-122
lines changed

staking-cli/README.md

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -169,11 +169,6 @@ Options:
169169
170170
[env: SKIP_SIMULATION=]
171171
172-
--skip-metadata-validation
173-
Skip metadata URI validation (fetch and schema check)
174-
175-
[env: SKIP_METADATA_VALIDATION=]
176-
177172
--output <OUTPUT>
178173
Output file path. If not specified, outputs to stdout
179174
@@ -468,7 +463,19 @@ Options for `--metadata-uri`:
468463

469464
3. **No metadata:** `--no-metadata-uri`
470465

471-
Use `--skip-metadata-validation` if your endpoint isn't ready yet. URL cannot exceed 2048 bytes.
466+
**Skipping validation:** If your metadata endpoint isn't ready yet, use `--skip-metadata-validation` after the
467+
`--metadata-uri` argument:
468+
469+
```bash
470+
staking-cli register-validator \
471+
--consensus-private-key BLS_SIGNING_KEY~... \
472+
--state-private-key SCHNORR_SIGNING_KEY~... \
473+
--commission 4.99 \
474+
--metadata-uri https://my-validator.example.com/status/metrics \
475+
--skip-metadata-validation
476+
```
477+
478+
URL cannot exceed 2048 bytes.
472479
473480
The CLI automatically detects the format (JSON or OpenMetrics) by examining the content. This works with any hosting
474481
service, including GitHub raw URLs.
@@ -530,6 +537,13 @@ staking-cli update-metadata-uri --metadata-uri https://my-validator.example.com/
530537
--consensus-public-key BLS_VER_KEY~...
531538
```
532539
540+
To skip validation (if endpoint isn't ready):
541+
542+
```bash
543+
staking-cli update-metadata-uri --metadata-uri https://my-validator.example.com/status/metrics \
544+
--skip-metadata-validation
545+
```
546+
533547
See [Validator Metadata](#validator-metadata) for format options. Use `--no-metadata-uri` to clear.
534548

535549
#### Metadata JSON Schema (for custom hosting)

staking-cli/src/cli.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,7 @@ pub async fn run() -> Result<()> {
480480

481481
// Validate metadata URI if present and validation not skipped
482482
if let Some(url) = metadata_uri.url() {
483-
if !config.skip_metadata_validation {
483+
if !metadata_uri_args.skip_metadata_validation {
484484
validate_metadata_uri(url, &payload.bls_vk)
485485
.await
486486
.context("use --skip-metadata-validation to skip")?;
@@ -534,7 +534,7 @@ pub async fn run() -> Result<()> {
534534

535535
// Validate metadata URI if present and validation not skipped
536536
if let Some(url) = metadata_uri.url() {
537-
if !config.skip_metadata_validation {
537+
if !metadata_uri_args.skip_metadata_validation {
538538
let bls_vk = consensus_public_key.ok_or_else(|| {
539539
anyhow::anyhow!(
540540
"--consensus-public-key is required for metadata validation (use \

staking-cli/src/lib.rs

Lines changed: 2 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use alloy::{
55
signers::local::{coins_bip39::English, MnemonicBuilder, PrivateKeySigner},
66
};
77
use anyhow::{bail, Result};
8-
use clap::{ArgAction, Args as ClapArgs, Parser, Subcommand};
8+
use clap::{ArgAction, Parser, Subcommand};
99
use clap_serde_derive::ClapSerde;
1010
use demo::DelegationConfig;
1111
use espresso_contract_deployer::provider::connect_ledger;
@@ -14,7 +14,7 @@ pub(crate) use hotshot_types::{
1414
signature_key::{BLSPrivKey, BLSPubKey},
1515
};
1616
pub(crate) use jf_signature::bls_over_bn254::KeyPair as BLSKeyPair;
17-
use metadata::MetadataUri;
17+
use metadata::MetadataUriArgs;
1818
use parse::Commission;
1919
use sequencer_utils::logging;
2020
use serde::{Deserialize, Serialize};
@@ -115,11 +115,6 @@ pub struct Config {
115115
#[serde(skip)]
116116
pub skip_simulation: bool,
117117

118-
/// Skip metadata URI validation (fetch and schema check).
119-
#[clap(long, env = "SKIP_METADATA_VALIDATION", action = ArgAction::SetTrue)]
120-
#[serde(skip)]
121-
pub skip_metadata_validation: bool,
122-
123118
#[clap(flatten)]
124119
#[serde(skip)]
125120
pub output: OutputArgs,
@@ -219,93 +214,6 @@ impl ValidSignerConfig {
219214
}
220215
}
221216

222-
#[derive(ClapArgs, Debug, Clone)]
223-
#[group(required = true, multiple = false)]
224-
pub struct MetadataUriArgs {
225-
/// URL where validator metadata is hosted (JSON or OpenMetrics format).
226-
#[clap(long, env = "METADATA_URI")]
227-
pub metadata_uri: Option<String>,
228-
229-
/// Register without a metadata URI.
230-
#[clap(long, env = "NO_METADATA_URI")]
231-
pub no_metadata_uri: bool,
232-
}
233-
234-
impl MetadataUriArgs {
235-
/// Parse the metadata URI arguments into an optional URL.
236-
fn to_url(&self) -> Result<Option<Url>> {
237-
if self.no_metadata_uri {
238-
Ok(None)
239-
} else if let Some(uri_str) = &self.metadata_uri {
240-
Ok(Some(Url::parse(uri_str)?))
241-
} else {
242-
bail!("Either --metadata-uri or --no-metadata-uri must be provided")
243-
}
244-
}
245-
}
246-
247-
impl TryFrom<MetadataUriArgs> for MetadataUri {
248-
type Error = anyhow::Error;
249-
250-
fn try_from(args: MetadataUriArgs) -> Result<Self> {
251-
match args.to_url()? {
252-
Some(url) => MetadataUri::try_from(url),
253-
None => Ok(MetadataUri::empty()),
254-
}
255-
}
256-
}
257-
258-
#[cfg(test)]
259-
mod metadata_uri_args_tests {
260-
use super::*;
261-
262-
#[test]
263-
fn test_to_url_with_uri() {
264-
let args = MetadataUriArgs {
265-
metadata_uri: Some("https://example.com/metadata.json".to_string()),
266-
no_metadata_uri: false,
267-
};
268-
let url = args.to_url().unwrap();
269-
assert_eq!(
270-
url.map(|u| u.as_str().to_string()),
271-
Some("https://example.com/metadata.json".to_string())
272-
);
273-
}
274-
275-
#[test]
276-
fn test_to_url_with_no_metadata() {
277-
let args = MetadataUriArgs {
278-
metadata_uri: None,
279-
no_metadata_uri: true,
280-
};
281-
let url = args.to_url().unwrap();
282-
assert!(url.is_none());
283-
}
284-
285-
#[test]
286-
fn test_metadata_uri_args_to_metadata_uri() {
287-
let args = MetadataUriArgs {
288-
metadata_uri: Some("https://example.com/metadata.json".to_string()),
289-
no_metadata_uri: false,
290-
};
291-
let metadata_uri = MetadataUri::try_from(args).unwrap();
292-
assert_eq!(
293-
metadata_uri.url().map(|u| u.as_str()),
294-
Some("https://example.com/metadata.json")
295-
);
296-
}
297-
298-
#[test]
299-
fn test_metadata_uri_args_to_empty_metadata_uri() {
300-
let args = MetadataUriArgs {
301-
metadata_uri: None,
302-
no_metadata_uri: true,
303-
};
304-
let metadata_uri = MetadataUri::try_from(args).unwrap();
305-
assert!(metadata_uri.url().is_none());
306-
}
307-
}
308-
309217
impl Default for Commands {
310218
fn default() -> Self {
311219
Commands::StakeTable {

staking-cli/src/metadata.rs

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
use std::{fmt, str::FromStr, time::Duration};
44

55
use anyhow::{bail, Context, Result};
6+
use clap::{ArgAction, Args as ClapArgs};
67
use hotshot_types::signature_key::BLSPubKey;
78
use thiserror::Error;
89
use url::Url;
@@ -148,13 +149,141 @@ impl fmt::Display for MetadataUri {
148149
}
149150
}
150151

152+
/// Custom value parser for metadata URIs that enforces 2048 byte limit.
153+
///
154+
/// While HTTP specs don't mandate a URL length limit, most browsers and servers
155+
/// support at least 2048 characters. This practical limit ensures compatibility
156+
/// across different HTTP clients and servers.
157+
fn parse_metadata_url(s: &str) -> Result<Url, String> {
158+
let url = Url::parse(s).map_err(|e| e.to_string())?;
159+
if url.as_str().len() > 2048 {
160+
return Err("metadata URI cannot exceed 2048 bytes".to_string());
161+
}
162+
Ok(url)
163+
}
164+
165+
/// Command-line arguments for specifying validator metadata.
166+
#[derive(ClapArgs, Debug, Clone)]
167+
pub struct MetadataUriArgs {
168+
/// URL where validator metadata is hosted (JSON or OpenMetrics format).
169+
#[clap(
170+
long,
171+
env = "METADATA_URI",
172+
required_unless_present = "no_metadata_uri",
173+
conflicts_with = "no_metadata_uri",
174+
value_parser = parse_metadata_url
175+
)]
176+
pub metadata_uri: Option<Url>,
177+
178+
/// Register without a metadata URI.
179+
// Provided as separate flag to prevent accidental omission of metadata URI.
180+
#[clap(long, env = "NO_METADATA_URI", conflicts_with = "metadata_uri")]
181+
pub no_metadata_uri: bool,
182+
183+
/// Skip metadata URI validation (fetch and schema check).
184+
#[clap(
185+
long,
186+
env = "SKIP_METADATA_VALIDATION",
187+
action = ArgAction::SetTrue,
188+
conflicts_with = "no_metadata_uri",
189+
requires = "metadata_uri"
190+
)]
191+
pub skip_metadata_validation: bool,
192+
}
193+
194+
impl TryFrom<MetadataUriArgs> for MetadataUri {
195+
type Error = anyhow::Error;
196+
197+
fn try_from(args: MetadataUriArgs) -> Result<Self> {
198+
match args.metadata_uri {
199+
Some(url) => Self::try_from(url),
200+
None => Ok(Self::empty()),
201+
}
202+
}
203+
}
204+
151205
#[cfg(test)]
152206
fn generate_bls_pub_key() -> BLSPubKey {
153207
use jf_signature::bls_over_bn254::KeyPair;
154208
let keypair = KeyPair::generate(&mut rand::thread_rng());
155209
BLSPubKey::from(keypair.ver_key())
156210
}
157211

212+
#[cfg(test)]
213+
mod metadata_uri_args_tests {
214+
use super::*;
215+
216+
#[test]
217+
fn test_metadata_uri_args_with_url() {
218+
let args = MetadataUriArgs {
219+
metadata_uri: Some(Url::parse("https://example.com/metadata.json").unwrap()),
220+
no_metadata_uri: false,
221+
skip_metadata_validation: false,
222+
};
223+
assert_eq!(
224+
args.metadata_uri.as_ref().map(|u| u.as_str()),
225+
Some("https://example.com/metadata.json")
226+
);
227+
}
228+
229+
#[test]
230+
fn test_metadata_uri_args_with_no_metadata() {
231+
let args = MetadataUriArgs {
232+
metadata_uri: None,
233+
no_metadata_uri: true,
234+
skip_metadata_validation: false,
235+
};
236+
assert!(args.metadata_uri.is_none());
237+
assert!(args.no_metadata_uri);
238+
}
239+
240+
#[test]
241+
fn test_metadata_uri_args_to_metadata_uri() {
242+
let args = MetadataUriArgs {
243+
metadata_uri: Some(Url::parse("https://example.com/metadata.json").unwrap()),
244+
no_metadata_uri: false,
245+
skip_metadata_validation: false,
246+
};
247+
let metadata_uri = MetadataUri::try_from(args).unwrap();
248+
assert_eq!(
249+
metadata_uri.url().map(|u| u.as_str()),
250+
Some("https://example.com/metadata.json")
251+
);
252+
}
253+
254+
#[test]
255+
fn test_metadata_uri_args_to_empty_metadata_uri() {
256+
let args = MetadataUriArgs {
257+
metadata_uri: None,
258+
no_metadata_uri: true,
259+
skip_metadata_validation: false,
260+
};
261+
let metadata_uri = MetadataUri::try_from(args).unwrap();
262+
assert!(metadata_uri.url().is_none());
263+
}
264+
265+
#[test]
266+
fn test_parse_metadata_url_valid() {
267+
let url = parse_metadata_url("https://example.com/metadata").unwrap();
268+
assert_eq!(url.as_str(), "https://example.com/metadata");
269+
}
270+
271+
#[test]
272+
fn test_parse_metadata_url_too_long() {
273+
let long_path = "a".repeat(2100);
274+
let url_str = format!("https://example.com/{}", long_path);
275+
let result = parse_metadata_url(&url_str);
276+
assert!(result.is_err());
277+
assert!(result.unwrap_err().contains("cannot exceed 2048 bytes"));
278+
}
279+
280+
#[test]
281+
fn test_parse_metadata_url_invalid() {
282+
let result = parse_metadata_url("not a url");
283+
assert!(result.is_err());
284+
}
285+
}
286+
158287
#[cfg(test)]
159288
mod test {
160289
use super::*;

0 commit comments

Comments
 (0)