miraland_cli/
validator_info.rs

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