1use {
2 crate::{
3 cli::{CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult},
4 compute_budget::{ComputeUnitConfig, WithComputeUnitConfig},
5 spend_utils::{SpendAmount, resolve_spend_tx_and_check_account_balance},
6 },
7 bincode::{deserialize, serialized_size},
8 clap::{App, AppSettings, Arg, ArgMatches, SubCommand},
9 reqwest::blocking::Client,
10 serde_json::{Map, Value},
11 solana_account::Account,
12 solana_account_decoder::validator_info::{
13 self, MAX_LONG_FIELD_LENGTH, MAX_SHORT_FIELD_LENGTH, MAX_VALIDATOR_INFO, ValidatorInfo,
14 },
15 solana_clap_utils::{
16 compute_budget::{COMPUTE_UNIT_PRICE_ARG, ComputeUnitLimit, compute_unit_price_arg},
17 hidden_unless_forced,
18 input_parsers::{pubkey_of, value_of},
19 input_validators::{is_pubkey, is_url},
20 keypair::DefaultSigner,
21 },
22 solana_cli_output::{CliValidatorInfo, CliValidatorInfoVec},
23 solana_config_interface::{
24 instruction::{self as config_instruction},
25 state::{ConfigKeys, get_config_data},
26 },
27 solana_keypair::Keypair,
28 solana_message::Message,
29 solana_pubkey::Pubkey,
30 solana_remote_wallet::remote_wallet::RemoteWalletManager,
31 solana_rpc_client::nonblocking::rpc_client::RpcClient,
32 solana_signer::Signer,
33 solana_transaction::Transaction,
34 std::{error, rc::Rc},
35};
36
37pub fn check_details_length(string: String) -> Result<(), String> {
39 if string.len() > MAX_LONG_FIELD_LENGTH {
40 Err(format!(
41 "validator details longer than {MAX_LONG_FIELD_LENGTH:?}-byte limit"
42 ))
43 } else {
44 Ok(())
45 }
46}
47
48pub fn check_total_length(info: &ValidatorInfo) -> Result<(), String> {
49 let size = serialized_size(&info).unwrap();
50 let limit = MAX_VALIDATOR_INFO;
51
52 if size > limit {
53 Err(format!(
54 "Total size {size:?} exceeds limit of {limit:?} bytes"
55 ))
56 } else {
57 Ok(())
58 }
59}
60
61pub fn check_url(string: String) -> Result<(), String> {
63 is_url(string.clone())?;
64 if string.len() > MAX_SHORT_FIELD_LENGTH {
65 Err(format!(
66 "url longer than {MAX_SHORT_FIELD_LENGTH:?}-byte limit"
67 ))
68 } else {
69 Ok(())
70 }
71}
72
73pub fn is_short_field(string: String) -> Result<(), String> {
75 if string.len() > MAX_SHORT_FIELD_LENGTH {
76 Err(format!(
77 "validator field longer than {MAX_SHORT_FIELD_LENGTH:?}-byte limit"
78 ))
79 } else {
80 Ok(())
81 }
82}
83
84fn verify_keybase(
85 validator_pubkey: &Pubkey,
86 keybase_username: &Value,
87) -> Result<(), Box<dyn error::Error>> {
88 if let Some(keybase_username) = keybase_username.as_str() {
89 let url =
90 format!("https://keybase.pub/{keybase_username}/solana/validator-{validator_pubkey:?}");
91 let client = Client::new();
92 if client.head(&url).send()?.status().is_success() {
93 Ok(())
94 } else {
95 Err(format!(
96 "keybase_username could not be confirmed at: {url}. Please add this pubkey file \
97 to your keybase profile to connect"
98 )
99 .into())
100 }
101 } else {
102 Err(format!("keybase_username could not be parsed as String: {keybase_username}").into())
103 }
104}
105
106fn parse_args(matches: &ArgMatches<'_>) -> Value {
107 let mut map = Map::new();
108 map.insert(
109 "name".to_string(),
110 Value::String(matches.value_of("name").unwrap().to_string()),
111 );
112 if let Some(url) = matches.value_of("website") {
113 map.insert("website".to_string(), Value::String(url.to_string()));
114 }
115
116 if let Some(icon_url) = matches.value_of("icon_url") {
117 map.insert("iconUrl".to_string(), Value::String(icon_url.to_string()));
118 }
119 if let Some(details) = matches.value_of("details") {
120 map.insert("details".to_string(), Value::String(details.to_string()));
121 }
122 if let Some(keybase_username) = matches.value_of("keybase_username") {
123 map.insert(
124 "keybaseUsername".to_string(),
125 Value::String(keybase_username.to_string()),
126 );
127 }
128 Value::Object(map)
129}
130
131fn parse_validator_info(
132 pubkey: &Pubkey,
133 account: &Account,
134) -> Result<(Pubkey, Map<String, serde_json::value::Value>), Box<dyn error::Error>> {
135 if account.owner != solana_config_interface::id() {
136 return Err(format!("{pubkey} is not a validator info account").into());
137 }
138 let key_list: ConfigKeys = deserialize(&account.data)?;
139 if !key_list.keys.is_empty() {
140 let (validator_pubkey, _) = key_list.keys[1];
141 let validator_info_string: String = deserialize(get_config_data(&account.data)?)?;
142 let validator_info: Map<_, _> = serde_json::from_str(&validator_info_string)?;
143 Ok((validator_pubkey, validator_info))
144 } else {
145 Err(format!("{pubkey} could not be parsed as a validator info account").into())
146 }
147}
148
149pub trait ValidatorInfoSubCommands {
150 fn validator_info_subcommands(self) -> Self;
151}
152
153impl ValidatorInfoSubCommands for App<'_, '_> {
154 fn validator_info_subcommands(self) -> Self {
155 self.subcommand(
156 SubCommand::with_name("validator-info")
157 .about("Publish/get Validator info on Solana")
158 .setting(AppSettings::SubcommandRequiredElseHelp)
159 .subcommand(
160 SubCommand::with_name("publish")
161 .about("Publish Validator info on Solana")
162 .arg(
163 Arg::with_name("info_pubkey")
164 .short("p")
165 .long("info-pubkey")
166 .value_name("PUBKEY")
167 .takes_value(true)
168 .validator(is_pubkey)
169 .help("The pubkey of the Validator info account to update"),
170 )
171 .arg(
172 Arg::with_name("name")
173 .index(1)
174 .value_name("NAME")
175 .takes_value(true)
176 .required(true)
177 .validator(is_short_field)
178 .help("Validator name"),
179 )
180 .arg(
181 Arg::with_name("website")
182 .short("w")
183 .long("website")
184 .value_name("URL")
185 .takes_value(true)
186 .validator(check_url)
187 .help("Validator website url"),
188 )
189 .arg(
190 Arg::with_name("icon_url")
191 .short("i")
192 .long("icon-url")
193 .value_name("URL")
194 .takes_value(true)
195 .validator(check_url)
196 .help("Validator icon URL"),
197 )
198 .arg(
199 Arg::with_name("keybase_username")
200 .short("n")
201 .long("keybase")
202 .value_name("USERNAME")
203 .takes_value(true)
204 .validator(is_short_field)
205 .hidden(hidden_unless_forced()) .help("Validator Keybase username"),
207 )
208 .arg(
209 Arg::with_name("details")
210 .short("d")
211 .long("details")
212 .value_name("DETAILS")
213 .takes_value(true)
214 .validator(check_details_length)
215 .help("Validator description"),
216 )
217 .arg(
218 Arg::with_name("force")
219 .long("force")
220 .takes_value(false)
221 .hidden(hidden_unless_forced()) .help("Override keybase username validity check"),
223 )
224 .arg(compute_unit_price_arg()),
225 )
226 .subcommand(
227 SubCommand::with_name("get")
228 .about("Get and parse Solana Validator info")
229 .arg(
230 Arg::with_name("info_pubkey")
231 .index(1)
232 .value_name("PUBKEY")
233 .takes_value(true)
234 .validator(is_pubkey)
235 .help(
236 "The pubkey of the Validator info account; without this \
237 argument, returns all Validator info accounts",
238 ),
239 ),
240 ),
241 )
242 }
243}
244
245pub fn parse_validator_info_command(
246 matches: &ArgMatches<'_>,
247 default_signer: &DefaultSigner,
248 wallet_manager: &mut Option<Rc<RemoteWalletManager>>,
249) -> Result<CliCommandInfo, CliError> {
250 let info_pubkey = pubkey_of(matches, "info_pubkey");
251 let compute_unit_price = value_of(matches, COMPUTE_UNIT_PRICE_ARG.name);
252 let validator_info = parse_args(matches);
254 Ok(CliCommandInfo {
255 command: CliCommand::SetValidatorInfo {
256 validator_info,
257 force_keybase: matches.is_present("force"),
258 info_pubkey,
259 compute_unit_price,
260 },
261 signers: vec![default_signer.signer_from_path(matches, wallet_manager)?],
262 })
263}
264
265pub fn parse_get_validator_info_command(
266 matches: &ArgMatches<'_>,
267) -> Result<CliCommandInfo, CliError> {
268 let info_pubkey = pubkey_of(matches, "info_pubkey");
269 Ok(CliCommandInfo::without_signers(
270 CliCommand::GetValidatorInfo(info_pubkey),
271 ))
272}
273
274pub async fn process_set_validator_info(
275 rpc_client: &RpcClient,
276 config: &CliConfig<'_>,
277 validator_info: &Value,
278 force_keybase: bool,
279 info_pubkey: Option<Pubkey>,
280 compute_unit_price: Option<u64>,
281) -> ProcessResult {
282 if let Some(string) = validator_info.get("keybaseUsername") {
284 if force_keybase {
285 println!("--force supplied, skipping Keybase verification");
286 } else {
287 let result = verify_keybase(&config.signers[0].pubkey(), string);
288 if result.is_err() {
289 result.map_err(|err| {
290 CliError::BadParameter(format!("Invalid validator keybase username: {err}"))
291 })?;
292 }
293 }
294 }
295 let validator_string = serde_json::to_string(&validator_info).unwrap();
296 let validator_info = ValidatorInfo {
297 info: validator_string,
298 };
299
300 let result = check_total_length(&validator_info);
301 if result.is_err() {
302 result.map_err(|err| {
303 CliError::BadParameter(format!("Maximum size for validator info: {err}"))
304 })?;
305 }
306
307 let all_config = rpc_client
309 .get_program_accounts(&solana_config_interface::id())
310 .await?;
311 let existing_account = all_config
312 .iter()
313 .filter(
314 |(_, account)| match deserialize::<ConfigKeys>(&account.data) {
315 Ok(key_list) => key_list.keys.contains(&(validator_info::id(), false)),
316 Err(_) => false,
317 },
318 )
319 .find(|(pubkey, account)| {
320 let (validator_pubkey, _) = parse_validator_info(pubkey, account).unwrap();
321 validator_pubkey == config.signers[0].pubkey()
322 });
323
324 let info_keypair = Keypair::new();
326 let mut info_pubkey = if let Some(pubkey) = info_pubkey {
327 pubkey
328 } else if let Some(validator_info) = existing_account {
329 validator_info.0
330 } else {
331 info_keypair.pubkey()
332 };
333
334 let balance = rpc_client.get_balance(&info_pubkey).await.unwrap_or(0);
336
337 let keys = vec![
338 (validator_info::id(), false),
339 (config.signers[0].pubkey(), true),
340 ];
341 let data_len = MAX_VALIDATOR_INFO
342 .checked_add(serialized_size(&ConfigKeys { keys: keys.clone() }).unwrap())
343 .expect("ValidatorInfo and two keys fit into a u64");
344 let lamports = rpc_client
345 .get_minimum_balance_for_rent_exemption(data_len as usize)
346 .await?;
347
348 let signers = if balance == 0 {
349 if info_pubkey != info_keypair.pubkey() {
350 println!("Account {info_pubkey:?} does not exist. Generating new keypair...");
351 info_pubkey = info_keypair.pubkey();
352 }
353 vec![config.signers[0], &info_keypair]
354 } else {
355 vec![config.signers[0]]
356 };
357
358 let compute_unit_limit = ComputeUnitLimit::Simulated;
359 let build_message = |lamports| {
360 let keys = keys.clone();
361 if balance == 0 {
362 println!(
363 "Publishing info for Validator {:?}",
364 config.signers[0].pubkey()
365 );
366 let mut instructions =
367 config_instruction::create_account_with_max_config_space::<ValidatorInfo>(
368 &config.signers[0].pubkey(),
369 &info_pubkey,
370 lamports,
371 MAX_VALIDATOR_INFO,
372 keys.clone(),
373 )
374 .with_compute_unit_config(&ComputeUnitConfig {
375 compute_unit_price,
376 compute_unit_limit,
377 });
378 instructions.extend_from_slice(&[config_instruction::store(
379 &info_pubkey,
380 true,
381 keys,
382 &validator_info,
383 )]);
384 Message::new(&instructions, Some(&config.signers[0].pubkey()))
385 } else {
386 println!(
387 "Updating Validator {:?} info at: {:?}",
388 config.signers[0].pubkey(),
389 info_pubkey
390 );
391 let instructions = vec![config_instruction::store(
392 &info_pubkey,
393 false,
394 keys,
395 &validator_info,
396 )]
397 .with_compute_unit_config(&ComputeUnitConfig {
398 compute_unit_price,
399 compute_unit_limit,
400 });
401 Message::new(&instructions, Some(&config.signers[0].pubkey()))
402 }
403 };
404
405 let latest_blockhash = rpc_client.get_latest_blockhash().await?;
407 let (message, _) = resolve_spend_tx_and_check_account_balance(
408 rpc_client,
409 false,
410 SpendAmount::Some(lamports),
411 &latest_blockhash,
412 &config.signers[0].pubkey(),
413 compute_unit_limit,
414 build_message,
415 config.commitment,
416 )
417 .await?;
418 let mut tx = Transaction::new_unsigned(message);
419 tx.try_sign(&signers, latest_blockhash)?;
420 let signature_str = rpc_client
421 .send_and_confirm_transaction_with_spinner_and_config(
422 &tx,
423 config.commitment,
424 config.send_transaction_config,
425 )
426 .await?;
427
428 println!("Success! Validator info published at: {info_pubkey:?}");
429 println!("{signature_str}");
430 Ok("".to_string())
431}
432
433pub async fn process_get_validator_info(
434 rpc_client: &RpcClient,
435 config: &CliConfig<'_>,
436 pubkey: Option<Pubkey>,
437) -> ProcessResult {
438 let validator_info: Vec<(Pubkey, Account)> = if let Some(validator_info_pubkey) = pubkey {
439 vec![(
440 validator_info_pubkey,
441 rpc_client.get_account(&validator_info_pubkey).await?,
442 )]
443 } else {
444 let all_config = rpc_client
445 .get_program_accounts(&solana_config_interface::id())
446 .await?;
447 all_config
448 .into_iter()
449 .filter(|(_, validator_info_account)| {
450 match deserialize::<ConfigKeys>(&validator_info_account.data) {
451 Ok(key_list) => key_list.keys.contains(&(validator_info::id(), false)),
452 Err(_) => false,
453 }
454 })
455 .collect()
456 };
457
458 let mut validator_info_list: Vec<CliValidatorInfo> = vec![];
459 if validator_info.is_empty() {
460 println!("No validator info accounts found");
461 }
462 for (validator_info_pubkey, validator_info_account) in validator_info.iter() {
463 let (validator_pubkey, validator_info) =
464 parse_validator_info(validator_info_pubkey, validator_info_account)?;
465 validator_info_list.push(CliValidatorInfo {
466 identity_pubkey: validator_pubkey.to_string(),
467 info_pubkey: validator_info_pubkey.to_string(),
468 info: validator_info,
469 });
470 }
471 Ok(config
472 .output_format
473 .formatted_string(&CliValidatorInfoVec::new(validator_info_list)))
474}
475
476#[cfg(test)]
477mod tests {
478 use {
479 super::*,
480 crate::clap_app::get_clap_app,
481 bincode::{serialize, serialized_size},
482 serde_json::json,
483 };
484
485 #[test]
486 fn test_check_details_length() {
487 let short_details = (0..MAX_LONG_FIELD_LENGTH).map(|_| "X").collect::<String>();
488 assert_eq!(check_details_length(short_details), Ok(()));
489
490 let long_details = (0..MAX_LONG_FIELD_LENGTH + 1)
491 .map(|_| "X")
492 .collect::<String>();
493 assert_eq!(
494 check_details_length(long_details),
495 Err(format!(
496 "validator details longer than {MAX_LONG_FIELD_LENGTH:?}-byte limit"
497 ))
498 );
499 }
500
501 #[test]
502 fn test_check_url() {
503 let url = "http://test.com";
504 assert_eq!(check_url(url.to_string()), Ok(()));
505 let long_url = "http://7cLvFwLCbyHuXQ1RGzhCMobAWYPMSZ3VbUml1CMobAWYPMSZ3VbUml1qWi1nkc3FD7zj9hzTZzMvYJ.com";
506 assert!(check_url(long_url.to_string()).is_err());
507 let non_url = "not parseable";
508 assert!(check_url(non_url.to_string()).is_err());
509 }
510
511 #[test]
512 fn test_is_short_field() {
513 let name = "Alice Validator";
514 assert_eq!(is_short_field(name.to_string()), Ok(()));
515 let long_name = "Alice 7cLvFwLCbyHuXQ1RGzhCMobAWYPMSZ3VbUml1qWi1nkc3FD7zj9hzTZzMvYJt6rY9j9hzTZzMvYJt6rY9";
516 assert!(is_short_field(long_name.to_string()).is_err());
517 }
518
519 #[test]
520 fn test_verify_keybase_username_not_string() {
521 let pubkey = solana_pubkey::new_rand();
522 let value = Value::Bool(true);
523
524 assert_eq!(
525 verify_keybase(&pubkey, &value).unwrap_err().to_string(),
526 "keybase_username could not be parsed as String: true".to_string()
527 )
528 }
529
530 #[test]
531 fn test_parse_args() {
532 let matches = get_clap_app("test", "desc", "version").get_matches_from(vec![
533 "test",
534 "validator-info",
535 "publish",
536 "Alice",
537 "-n",
538 "alice_keybase",
539 "-i",
540 "https://test.com/icon.png",
541 ]);
542 let subcommand_matches = matches.subcommand();
543 assert_eq!(subcommand_matches.0, "validator-info");
544 assert!(subcommand_matches.1.is_some());
545 let subcommand_matches = subcommand_matches.1.unwrap().subcommand();
546 assert_eq!(subcommand_matches.0, "publish");
547 assert!(subcommand_matches.1.is_some());
548 let matches = subcommand_matches.1.unwrap();
549 let expected = json!({
550 "name": "Alice",
551 "keybaseUsername": "alice_keybase",
552 "iconUrl": "https://test.com/icon.png",
553 });
554 assert_eq!(parse_args(matches), expected);
555 }
556
557 #[test]
558 fn test_validator_info_serde() {
559 let mut info = Map::new();
560 info.insert("name".to_string(), Value::String("Alice".to_string()));
561 let info_string = serde_json::to_string(&Value::Object(info)).unwrap();
562
563 let validator_info = ValidatorInfo {
564 info: info_string.clone(),
565 };
566
567 assert_eq!(serialized_size(&validator_info).unwrap(), 24);
568 assert_eq!(
569 serialize(&validator_info).unwrap(),
570 vec![
571 16, 0, 0, 0, 0, 0, 0, 0, 123, 34, 110, 97, 109, 101, 34, 58, 34, 65, 108, 105, 99,
572 101, 34, 125
573 ]
574 );
575
576 let deserialized: ValidatorInfo = deserialize(&[
577 16, 0, 0, 0, 0, 0, 0, 0, 123, 34, 110, 97, 109, 101, 34, 58, 34, 65, 108, 105, 99, 101,
578 34, 125,
579 ])
580 .unwrap();
581 assert_eq!(deserialized.info, info_string);
582 }
583
584 #[test]
585 fn test_parse_validator_info() {
586 let pubkey = solana_pubkey::new_rand();
587 let keys = vec![(validator_info::id(), false), (pubkey, true)];
588 let config = ConfigKeys { keys };
589
590 let mut info = Map::new();
591 info.insert("name".to_string(), Value::String("Alice".to_string()));
592 let info_string = serde_json::to_string(&Value::Object(info.clone())).unwrap();
593 let validator_info = ValidatorInfo { info: info_string };
594 let data = serialize(&(config, validator_info)).unwrap();
595
596 assert_eq!(
597 parse_validator_info(
598 &Pubkey::default(),
599 &Account {
600 owner: solana_config_interface::id(),
601 data,
602 ..Account::default()
603 }
604 )
605 .unwrap(),
606 (pubkey, info)
607 );
608 }
609
610 #[test]
611 fn test_parse_validator_info_not_validator_info_account() {
612 assert!(
613 parse_validator_info(
614 &Pubkey::default(),
615 &Account {
616 owner: solana_pubkey::new_rand(),
617 ..Account::default()
618 }
619 )
620 .unwrap_err()
621 .to_string()
622 .contains("is not a validator info account")
623 );
624 }
625
626 #[test]
627 fn test_parse_validator_info_empty_key_list() {
628 let config = ConfigKeys { keys: vec![] };
629 let validator_info = ValidatorInfo {
630 info: String::new(),
631 };
632 let data = serialize(&(config, validator_info)).unwrap();
633
634 assert!(
635 parse_validator_info(
636 &Pubkey::default(),
637 &Account {
638 owner: solana_config_interface::id(),
639 data,
640 ..Account::default()
641 },
642 )
643 .unwrap_err()
644 .to_string()
645 .contains("could not be parsed as a validator info account")
646 );
647 }
648
649 #[test]
650 fn test_validator_info_max_space() {
651 let max_short_string =
653 "Max Length String KWpP299aFCBWvWg1MHpSuaoTsud7cv8zMJsh99aAtP8X1s26yrR1".to_string();
654 let max_long_string = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut libero \
656 quam, volutpat et aliquet eu, varius in mi. Aenean vestibulum ex \
657 in tristique faucibus. Maecenas in imperdiet turpis. Nullam \
658 feugiat aliquet erat. Morbi malesuada turpis sed dui pulvinar \
659 lobortis. Pellentesque a lectus eu leo nullam."
660 .to_string();
661 let mut info = Map::new();
662 info.insert("name".to_string(), Value::String(max_short_string.clone()));
663 info.insert(
664 "website".to_string(),
665 Value::String(max_short_string.clone()),
666 );
667 info.insert(
668 "keybaseUsername".to_string(),
669 Value::String(max_short_string),
670 );
671 info.insert("details".to_string(), Value::String(max_long_string));
672 let info_string = serde_json::to_string(&Value::Object(info)).unwrap();
673
674 let validator_info = ValidatorInfo { info: info_string };
675
676 assert_eq!(
677 serialized_size(&validator_info).unwrap(),
678 MAX_VALIDATOR_INFO
679 );
680 }
681}