Skip to main content

solana_cli/
validator_info.rs

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
37// Return an error if a validator details are longer than the max length.
38pub 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
61// Return an error if url field is too long or cannot be parsed.
62pub 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
73// Return an error if a validator field is longer than the max length.
74pub 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()) // Being phased out
206                                .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()) // Don't document this argument to discourage its use
222                                .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    // Prepare validator info
253    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    // Validate keybase username
283    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    // Check for existing validator-info account
308    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    // Create validator-info keypair to use if info_pubkey not provided or does not exist
325    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    // Check existence of validator-info account
335    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    // Submit transaction
406    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        // 70-character string
652        let max_short_string =
653            "Max Length String KWpP299aFCBWvWg1MHpSuaoTsud7cv8zMJsh99aAtP8X1s26yrR1".to_string();
654        // 300-character string
655        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}