diff --git a/cli/src/cli.rs b/cli/src/cli.rs index f29058e1e7..de34206741 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -41,6 +41,7 @@ use solana_sdk::{ pubkey::Pubkey, signature::{Keypair, Signature, Signer, SignerError}, system_instruction::{self, create_address_with_seed, SystemError, MAX_ADDRESS_SEED_LEN}, + system_program, transaction::{Transaction, TransactionError}, }; use solana_stake_program::{ @@ -1066,9 +1067,10 @@ pub fn parse_create_address_with_seed( }; let program_id = match matches.value_of("program_id").unwrap() { + "NONCE" => system_program::id(), "STAKE" => solana_stake_program::id(), - "VOTE" => solana_vote_program::id(), "STORAGE" => solana_storage_program::id(), + "VOTE" => solana_vote_program::id(), _ => pubkey_of(matches, "program_id").unwrap(), }; @@ -2345,7 +2347,7 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, ' .required(true) .help( "The program_id that the address will ultimately be used for, \n\ - or one of STAKE, VOTE, and STORAGE keywords", + or one of NONCE, STAKE, STORAGE, and VOTE keywords", ), ) .arg( @@ -2767,6 +2769,7 @@ mod tests { ("STAKE", solana_stake_program::id()), ("VOTE", solana_vote_program::id()), ("STORAGE", solana_storage_program::id()), + ("NONCE", system_program::id()), ] { let test_create_address_with_seed = test_commands.clone().get_matches_from(vec![ "test", diff --git a/cli/src/nonce.rs b/cli/src/nonce.rs index 97e1c465df..fe4cb59415 100644 --- a/cli/src/nonce.rs +++ b/cli/src/nonce.rs @@ -110,13 +110,6 @@ impl NonceSubCommands for App<'_, '_> { .validator(is_valid_pubkey) .help("Account to be granted authority of the nonce account"), ) - .arg( - Arg::with_name("seed") - .long("seed") - .value_name("STRING") - .takes_value(true) - .help("Seed for address generation; if specified, the resulting account will be at a derived address of the NONCE_ACCOUNT pubkey") - ) .arg(nonce_authority_arg()), ) .subcommand( @@ -147,6 +140,13 @@ impl NonceSubCommands for App<'_, '_> { .value_name("PUBKEY") .validator(is_valid_pubkey) .help("Assign noncing authority to another entity"), + ) + .arg( + Arg::with_name("seed") + .long("seed") + .value_name("STRING") + .takes_value(true) + .help("Seed for address generation; if specified, the resulting account will be at a derived address of the NONCE_ACCOUNT pubkey") ), ) .subcommand( diff --git a/cli/tests/nonce.rs b/cli/tests/nonce.rs index f2386854b2..75dc090ba7 100644 --- a/cli/tests/nonce.rs +++ b/cli/tests/nonce.rs @@ -1,6 +1,13 @@ -use solana_cli::cli::{process_command, request_and_confirm_airdrop, CliCommand, CliConfig}; +use solana_cli::{ + cli::{process_command, request_and_confirm_airdrop, CliCommand, CliConfig}, + nonce, + offline::{ + blockhash_query::{self, BlockhashQuery}, + parse_sign_only_reply_string, + }, +}; use solana_client::rpc_client::RpcClient; -use solana_core::validator::TestValidator; +use solana_core::validator::{TestValidator, TestValidatorOptions}; use solana_faucet::faucet::run_local_faucet; use solana_sdk::{ hash::Hash, @@ -247,3 +254,124 @@ fn full_battery_tests( check_balance(800, &rpc_client, &nonce_account); check_balance(200, &rpc_client, &payee_pubkey); } + +#[test] +fn test_create_account_with_seed() { + let TestValidator { + server, + leader_data, + alice: mint_keypair, + ledger_path, + .. + } = TestValidator::run_with_options(TestValidatorOptions { + fees: 1, + bootstrap_validator_lamports: 42_000, + }); + + let (sender, receiver) = channel(); + run_local_faucet(mint_keypair, sender, None); + let faucet_addr = receiver.recv().unwrap(); + + let offline_nonce_authority_signer = keypair_from_seed(&[1u8; 32]).unwrap(); + let online_nonce_creator_signer = keypair_from_seed(&[2u8; 32]).unwrap(); + let to_address = Pubkey::new(&[3u8; 32]); + + // Setup accounts + let rpc_client = RpcClient::new_socket(leader_data.rpc); + request_and_confirm_airdrop( + &rpc_client, + &faucet_addr, + &offline_nonce_authority_signer.pubkey(), + 42, + ) + .unwrap(); + request_and_confirm_airdrop( + &rpc_client, + &faucet_addr, + &online_nonce_creator_signer.pubkey(), + 4242, + ) + .unwrap(); + check_balance(42, &rpc_client, &offline_nonce_authority_signer.pubkey()); + check_balance(4242, &rpc_client, &online_nonce_creator_signer.pubkey()); + check_balance(0, &rpc_client, &to_address); + + // Create nonce account + let creator_pubkey = online_nonce_creator_signer.pubkey(); + let authority_pubkey = offline_nonce_authority_signer.pubkey(); + let seed = authority_pubkey.to_string()[0..32].to_string(); + let nonce_address = + create_address_with_seed(&creator_pubkey, &seed, &system_program::id()).unwrap(); + check_balance(0, &rpc_client, &nonce_address); + + let mut creator_config = CliConfig::default(); + creator_config.json_rpc_url = + format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); + creator_config.signers = vec![&online_nonce_creator_signer]; + creator_config.command = CliCommand::CreateNonceAccount { + nonce_account: 0, + seed: Some(seed), + nonce_authority: Some(authority_pubkey), + lamports: 241, + }; + process_command(&creator_config).unwrap(); + check_balance(241, &rpc_client, &nonce_address); + check_balance(42, &rpc_client, &offline_nonce_authority_signer.pubkey()); + check_balance(4000, &rpc_client, &online_nonce_creator_signer.pubkey()); + check_balance(0, &rpc_client, &to_address); + + // Fetch nonce hash + let nonce_hash = nonce::get_account(&rpc_client, &nonce_address) + .and_then(|ref a| nonce::data_from_account(a)) + .unwrap() + .blockhash; + + // Test by creating transfer TX with nonce, fully offline + let mut authority_config = CliConfig::default(); + authority_config.json_rpc_url = String::default(); + authority_config.signers = vec![&offline_nonce_authority_signer]; + // Verify we cannot contact the cluster + authority_config.command = CliCommand::ClusterVersion; + process_command(&authority_config).unwrap_err(); + authority_config.command = CliCommand::Transfer { + lamports: 10, + to: to_address, + from: 0, + sign_only: true, + blockhash_query: BlockhashQuery::None(nonce_hash), + nonce_account: Some(nonce_address), + nonce_authority: 0, + fee_payer: 0, + }; + let sign_only_reply = process_command(&authority_config).unwrap(); + let sign_only = parse_sign_only_reply_string(&sign_only_reply); + let authority_presigner = sign_only.presigner_of(&authority_pubkey).unwrap(); + assert_eq!(sign_only.blockhash, nonce_hash); + + // And submit it + let mut submit_config = CliConfig::default(); + submit_config.json_rpc_url = + format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); + submit_config.signers = vec![&authority_presigner]; + submit_config.command = CliCommand::Transfer { + lamports: 10, + to: to_address, + from: 0, + sign_only: false, + blockhash_query: BlockhashQuery::FeeCalculator( + blockhash_query::Source::NonceAccount(nonce_address), + sign_only.blockhash, + ), + nonce_account: Some(nonce_address), + nonce_authority: 0, + fee_payer: 0, + }; + process_command(&submit_config).unwrap(); + check_balance(241, &rpc_client, &nonce_address); + check_balance(31, &rpc_client, &offline_nonce_authority_signer.pubkey()); + check_balance(4000, &rpc_client, &online_nonce_creator_signer.pubkey()); + check_balance(10, &rpc_client, &to_address); + + server.close().unwrap(); + remove_dir_all(ledger_path).unwrap(); +}