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