solana_cli/
validator_info.rs

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
34// Return an error if a validator details are longer than the max length.
35pub 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
58// Return an error if url field is too long or cannot be parsed.
59pub 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
70// Return an error if a validator field is longer than the max length.
71pub 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()) // Being phased out
203                                .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()) // Don't document this argument to discourage its use
219                                .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    // Prepare validator info
250    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    // Validate keybase username
280    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    // Check for existing validator-info account
305    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    // Create validator-info keypair to use if info_pubkey not provided or does not exist
320    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    // Check existence of validator-info account
330    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    // Submit transaction
397    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        // 70-character string
634        let max_short_string =
635            "Max Length String KWpP299aFCBWvWg1MHpSuaoTsud7cv8zMJsh99aAtP8X1s26yrR1".to_string();
636        // 300-character string
637        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}