diff --git a/clap-utils/src/keypair.rs b/clap-utils/src/keypair.rs index 9d1895ef41..40fd5f2e01 100644 --- a/clap-utils/src/keypair.rs +++ b/clap-utils/src/keypair.rs @@ -1,4 +1,8 @@ -use crate::{input_parsers::pubkeys_sigs_of, offline::SIGNER_ARG, ArgConstant}; +use crate::{ + input_parsers::pubkeys_sigs_of, + offline::{SIGNER_ARG, SIGN_ONLY_ARG}, + ArgConstant, +}; use bip39::{Language, Mnemonic, Seed}; use clap::ArgMatches; use rpassword::prompt_password_stderr; @@ -10,7 +14,7 @@ use solana_sdk::{ pubkey::Pubkey, signature::{ keypair_from_seed, keypair_from_seed_phrase_and_passphrase, read_keypair, - read_keypair_file, Keypair, Presigner, Signature, Signer, + read_keypair_file, Keypair, NullSigner, Presigner, Signature, Signer, }, }; use std::{ @@ -101,6 +105,8 @@ pub fn signer_from_path( .and_then(|presigners| presigner_from_pubkey_sigs(&pubkey, presigners)); if let Some(presigner) = presigner { Ok(Box::new(presigner)) + } else if matches.is_present(SIGN_ONLY_ARG.name) { + Ok(Box::new(NullSigner::new(&pubkey))) } else { Err(std::io::Error::new( std::io::ErrorKind::Other, diff --git a/cli/src/cli.rs b/cli/src/cli.rs index f7bd69adfc..91be6651f4 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -1015,17 +1015,31 @@ pub fn get_blockhash_and_fee_calculator( } pub fn return_signers(tx: &Transaction) -> ProcessResult { - println_signers(tx); - let signers: Vec<_> = tx - .signatures + let verify_results = tx.verify_with_results(); + let mut signers = Vec::new(); + let mut absent = Vec::new(); + let mut bad_sig = Vec::new(); + tx.signatures .iter() - .zip(tx.message.account_keys.clone()) - .map(|(signature, pubkey)| format!("{}={}", pubkey, signature)) - .collect(); + .zip(tx.message.account_keys.iter()) + .zip(verify_results.into_iter()) + .for_each(|((sig, key), res)| { + if res { + signers.push(format!("{}={}", key, sig)) + } else if *sig == Signature::default() { + absent.push(key.to_string()); + } else { + bad_sig.push(key.to_string()); + } + }); + + println_signers(&tx.message.recent_blockhash, &signers, &absent, &bad_sig); Ok(json!({ "blockhash": tx.message.recent_blockhash.to_string(), "signers": &signers, + "absent": &absent, + "badSig": &bad_sig, }) .to_string()) } @@ -1310,11 +1324,12 @@ fn process_pay( Message::new(&[ix]) }; let mut tx = Transaction::new_unsigned(message); - tx.try_sign(&config.signers, blockhash)?; if sign_only { + tx.try_partial_sign(&config.signers, blockhash)?; return_signers(&tx) } else { + tx.try_sign(&config.signers, blockhash)?; if let Some(nonce_account) = &nonce_account { let nonce_account = rpc_client.get_account(nonce_account)?; check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &blockhash)?; @@ -1350,10 +1365,11 @@ fn process_pay( ); let message = Message::new(&ixs); let mut tx = Transaction::new_unsigned(message); - tx.try_sign(&[config.signers[0], &contract_state], blockhash)?; if sign_only { + tx.try_partial_sign(&[config.signers[0], &contract_state], blockhash)?; return_signers(&tx) } else { + tx.try_sign(&[config.signers[0], &contract_state], blockhash)?; check_account_for_fee( rpc_client, &config.signers[0].pubkey(), @@ -1395,10 +1411,11 @@ fn process_pay( ); let message = Message::new(&ixs); let mut tx = Transaction::new_unsigned(message); - tx.try_sign(&[config.signers[0], &contract_state], blockhash)?; if sign_only { + tx.try_partial_sign(&[config.signers[0], &contract_state], blockhash)?; return_signers(&tx) } else { + tx.try_sign(&[config.signers[0], &contract_state], blockhash)?; let result = rpc_client.send_and_confirm_transaction_with_spinner( &mut tx, &[config.signers[0], &contract_state], @@ -1505,11 +1522,12 @@ fn process_transfer( Message::new_with_payer(&ixs, Some(&fee_payer.pubkey())) }; let mut tx = Transaction::new_unsigned(message); - tx.try_sign(&config.signers, recent_blockhash)?; if sign_only { + tx.try_partial_sign(&config.signers, recent_blockhash)?; return_signers(&tx) } else { + tx.try_sign(&config.signers, recent_blockhash)?; if let Some(nonce_account) = &nonce_account { let nonce_account = rpc_client.get_account(nonce_account)?; check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &recent_blockhash)?; @@ -2536,7 +2554,9 @@ mod tests { use solana_client::mock_rpc_client_request::SIGNATURE; use solana_sdk::{ pubkey::Pubkey, - signature::{keypair_from_seed, read_keypair_file, write_keypair_file, Presigner}, + signature::{ + keypair_from_seed, read_keypair_file, write_keypair_file, NullSigner, Presigner, + }, transaction::TransactionError, }; use std::path::PathBuf; @@ -3664,4 +3684,52 @@ mod tests { } ); } + + #[test] + fn test_return_signers() { + struct BadSigner { + pubkey: Pubkey, + } + + impl BadSigner { + pub fn new(pubkey: Pubkey) -> Self { + Self { pubkey } + } + } + + impl Signer for BadSigner { + fn try_pubkey(&self) -> Result { + Ok(self.pubkey) + } + + fn try_sign_message(&self, _message: &[u8]) -> Result { + Ok(Signature::new(&[1u8; 64])) + } + } + + let present: Box = Box::new(keypair_from_seed(&[2u8; 32]).unwrap()); + let absent: Box = Box::new(NullSigner::new(&Pubkey::new(&[3u8; 32]))); + let bad: Box = Box::new(BadSigner::new(Pubkey::new(&[4u8; 32]))); + let to = Pubkey::new(&[5u8; 32]); + let nonce = Pubkey::new(&[6u8; 32]); + let from = present.pubkey(); + let fee_payer = absent.pubkey(); + let nonce_auth = bad.pubkey(); + let mut tx = Transaction::new_unsigned(Message::new_with_nonce( + vec![system_instruction::transfer(&from, &to, 42)], + Some(&fee_payer), + &nonce, + &nonce_auth, + )); + + let signers = vec![present.as_ref(), absent.as_ref(), bad.as_ref()]; + let blockhash = Hash::new(&[7u8; 32]); + tx.try_partial_sign(&signers, blockhash).unwrap(); + let res = return_signers(&tx).unwrap(); + let sign_only = parse_sign_only_reply_string(&res); + assert_eq!(sign_only.blockhash, blockhash); + assert_eq!(sign_only.present_signers[0].0, present.pubkey()); + assert_eq!(sign_only.absent_signers[0], absent.pubkey()); + assert_eq!(sign_only.bad_signers[0], bad.pubkey()); + } } diff --git a/cli/src/display.rs b/cli/src/display.rs index f8cf1b4690..532eaddd1e 100644 --- a/cli/src/display.rs +++ b/cli/src/display.rs @@ -1,6 +1,6 @@ use crate::cli::SettingType; use console::style; -use solana_sdk::transaction::Transaction; +use solana_sdk::hash::Hash; // Pretty print a "name value" pub fn println_name_value(name: &str, value: &str) { @@ -27,13 +27,25 @@ pub fn println_name_value_or(name: &str, value: &str, setting_type: SettingType) ); } -pub fn println_signers(tx: &Transaction) { +pub fn println_signers( + blockhash: &Hash, + signers: &[String], + absent: &[String], + bad_sig: &[String], +) { println!(); - println!("Blockhash: {}", tx.message.recent_blockhash); - println!("Signers (Pubkey=Signature):"); - tx.signatures - .iter() - .zip(tx.message.account_keys.clone()) - .for_each(|(signature, pubkey)| println!(" {:?}={:?}", pubkey, signature)); + println!("Blockhash: {}", blockhash); + if !signers.is_empty() { + println!("Signers (Pubkey=Signature):"); + signers.iter().for_each(|signer| println!(" {}", signer)) + } + if !absent.is_empty() { + println!("Absent Signers (Pubkey):"); + absent.iter().for_each(|pubkey| println!(" {}", pubkey)) + } + if !bad_sig.is_empty() { + println!("Bad Signatures (Pubkey):"); + bad_sig.iter().for_each(|pubkey| println!(" {}", pubkey)) + } println!(); } diff --git a/cli/src/main.rs b/cli/src/main.rs index a428f0d4c3..fa9baf1ae9 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -2,7 +2,8 @@ use clap::{crate_description, crate_name, AppSettings, Arg, ArgGroup, ArgMatches use console::style; use solana_clap_utils::{ - input_validators::is_url, keypair::SKIP_SEED_PHRASE_VALIDATION_ARG, DisplayError, + input_validators::is_url, keypair::SKIP_SEED_PHRASE_VALIDATION_ARG, offline::SIGN_ONLY_ARG, + DisplayError, }; use solana_cli::{ cli::{app, parse_command, process_command, CliCommandInfo, CliConfig, CliSigners}, @@ -242,7 +243,13 @@ fn do_main(matches: &ArgMatches<'_>) -> Result<(), Box> { let (mut config, signers) = parse_args(&matches, wallet_manager)?; config.signers = signers.iter().map(|s| s.as_ref()).collect(); let result = process_command(&config)?; - println!("{}", result); + let (_, submatches) = matches.subcommand(); + let sign_only = submatches + .map(|m| m.is_present(SIGN_ONLY_ARG.name)) + .unwrap_or(false); + if !sign_only { + println!("{}", result); + } }; Ok(()) } diff --git a/cli/src/offline/mod.rs b/cli/src/offline/mod.rs index b152403f06..1e43195ca8 100644 --- a/cli/src/offline/mod.rs +++ b/cli/src/offline/mod.rs @@ -6,10 +6,16 @@ use serde_json::Value; use solana_clap_utils::{ input_parsers::{pubkey_of, value_of}, input_validators::{is_hash, is_pubkey_sig}, + keypair::presigner_from_pubkey_sigs, offline::{BLOCKHASH_ARG, SIGNER_ARG, SIGN_ONLY_ARG}, }; use solana_client::rpc_client::RpcClient; -use solana_sdk::{fee_calculator::FeeCalculator, hash::Hash, pubkey::Pubkey, signature::Signature}; +use solana_sdk::{ + fee_calculator::FeeCalculator, + hash::Hash, + pubkey::Pubkey, + signature::{Presigner, Signature}, +}; use std::str::FromStr; fn blockhash_arg<'a, 'b>() -> Arg<'a, 'b> { @@ -52,12 +58,29 @@ impl OfflineArgs for App<'_, '_> { } } -pub fn parse_sign_only_reply_string(reply: &str) -> (Hash, Vec<(Pubkey, Signature)>) { +pub struct SignOnly { + pub blockhash: Hash, + pub present_signers: Vec<(Pubkey, Signature)>, + pub absent_signers: Vec, + pub bad_signers: Vec, +} + +impl SignOnly { + pub fn has_all_signers(&self) -> bool { + self.absent_signers.is_empty() && self.bad_signers.is_empty() + } + + pub fn presigner_of(&self, pubkey: &Pubkey) -> Option { + presigner_from_pubkey_sigs(pubkey, &self.present_signers) + } +} + +pub fn parse_sign_only_reply_string(reply: &str) -> SignOnly { let object: Value = serde_json::from_str(&reply).unwrap(); let blockhash_str = object.get("blockhash").unwrap().as_str().unwrap(); let blockhash = blockhash_str.parse::().unwrap(); let signer_strings = object.get("signers").unwrap().as_array().unwrap(); - let signers = signer_strings + let present_signers = signer_strings .iter() .map(|signer_string| { let mut signer = signer_string.as_str().unwrap().split('='); @@ -66,5 +89,26 @@ pub fn parse_sign_only_reply_string(reply: &str) -> (Hash, Vec<(Pubkey, Signatur (key, sig) }) .collect(); - (blockhash, signers) + let signer_strings = object.get("absent").unwrap().as_array().unwrap(); + let absent_signers = signer_strings + .iter() + .map(|val| { + let s = val.as_str().unwrap(); + Pubkey::from_str(s).unwrap() + }) + .collect(); + let signer_strings = object.get("badSig").unwrap().as_array().unwrap(); + let bad_signers = signer_strings + .iter() + .map(|val| { + let s = val.as_str().unwrap(); + Pubkey::from_str(s).unwrap() + }) + .collect(); + SignOnly { + blockhash, + present_signers, + absent_signers, + bad_signers, + } } diff --git a/cli/src/stake.rs b/cli/src/stake.rs index 851e4e5121..2faf70facf 100644 --- a/cli/src/stake.rs +++ b/cli/src/stake.rs @@ -845,11 +845,12 @@ pub fn process_create_stake_account( Message::new_with_payer(&ixs, Some(&fee_payer.pubkey())) }; let mut tx = Transaction::new_unsigned(message); - tx.try_sign(&config.signers, recent_blockhash)?; if sign_only { + tx.try_partial_sign(&config.signers, recent_blockhash)?; return_signers(&tx) } else { + tx.try_sign(&config.signers, recent_blockhash)?; if let Some(nonce_account) = &nonce_account { let nonce_account = rpc_client.get_account(nonce_account)?; check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &recent_blockhash)?; @@ -907,11 +908,12 @@ pub fn process_stake_authorize( Message::new_with_payer(&ixs, Some(&fee_payer.pubkey())) }; let mut tx = Transaction::new_unsigned(message); - tx.try_sign(&config.signers, recent_blockhash)?; if sign_only { + tx.try_partial_sign(&config.signers, recent_blockhash)?; return_signers(&tx) } else { + tx.try_sign(&config.signers, recent_blockhash)?; if let Some(nonce_account) = &nonce_account { let nonce_account = rpc_client.get_account(nonce_account)?; check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &recent_blockhash)?; @@ -960,11 +962,12 @@ pub fn process_deactivate_stake_account( Message::new_with_payer(&ixs, Some(&fee_payer.pubkey())) }; let mut tx = Transaction::new_unsigned(message); - tx.try_sign(&config.signers, recent_blockhash)?; if sign_only { + tx.try_partial_sign(&config.signers, recent_blockhash)?; return_signers(&tx) } else { + tx.try_sign(&config.signers, recent_blockhash)?; if let Some(nonce_account) = &nonce_account { let nonce_account = rpc_client.get_account(nonce_account)?; check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &recent_blockhash)?; @@ -1019,11 +1022,12 @@ pub fn process_withdraw_stake( Message::new_with_payer(&ixs, Some(&fee_payer.pubkey())) }; let mut tx = Transaction::new_unsigned(message); - tx.try_sign(&config.signers, recent_blockhash)?; if sign_only { + tx.try_partial_sign(&config.signers, recent_blockhash)?; return_signers(&tx) } else { + tx.try_sign(&config.signers, recent_blockhash)?; if let Some(nonce_account) = &nonce_account { let nonce_account = rpc_client.get_account(nonce_account)?; check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &recent_blockhash)?; @@ -1152,11 +1156,12 @@ pub fn process_split_stake( Message::new_with_payer(&ixs, Some(&fee_payer.pubkey())) }; let mut tx = Transaction::new_unsigned(message); - tx.try_sign(&config.signers, recent_blockhash)?; if sign_only { + tx.try_partial_sign(&config.signers, recent_blockhash)?; return_signers(&tx) } else { + tx.try_sign(&config.signers, recent_blockhash)?; if let Some(nonce_account) = &nonce_account { let nonce_account = rpc_client.get_account(nonce_account)?; check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &recent_blockhash)?; @@ -1208,11 +1213,12 @@ pub fn process_stake_set_lockup( Message::new_with_payer(&ixs, Some(&fee_payer.pubkey())) }; let mut tx = Transaction::new_unsigned(message); - tx.try_sign(&config.signers, recent_blockhash)?; if sign_only { + tx.try_partial_sign(&config.signers, recent_blockhash)?; return_signers(&tx) } else { + tx.try_sign(&config.signers, recent_blockhash)?; if let Some(nonce_account) = &nonce_account { let nonce_account = rpc_client.get_account(nonce_account)?; check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &recent_blockhash)?; @@ -1442,11 +1448,12 @@ pub fn process_delegate_stake( Message::new_with_payer(&ixs, Some(&fee_payer.pubkey())) }; let mut tx = Transaction::new_unsigned(message); - tx.try_sign(&config.signers, recent_blockhash)?; if sign_only { + tx.try_partial_sign(&config.signers, recent_blockhash)?; return_signers(&tx) } else { + tx.try_sign(&config.signers, recent_blockhash)?; if let Some(nonce_account) = &nonce_account { let nonce_account = rpc_client.get_account(nonce_account)?; check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &recent_blockhash)?; diff --git a/cli/tests/pay.rs b/cli/tests/pay.rs index 542586a3a0..8001e00f7d 100644 --- a/cli/tests/pay.rs +++ b/cli/tests/pay.rs @@ -1,6 +1,5 @@ use chrono::prelude::*; use serde_json::Value; -use solana_clap_utils::keypair::presigner_from_pubkey_sigs; use solana_cli::{ cli::{process_command, request_and_confirm_airdrop, CliCommand, CliConfig, PayCommand}, nonce, @@ -335,9 +334,11 @@ fn test_offline_pay_tx() { check_balance(50, &rpc_client, &config_online.signers[0].pubkey()); check_balance(0, &rpc_client, &bob_pubkey); - let (blockhash, signers) = parse_sign_only_reply_string(&sig_response); - let offline_presigner = - presigner_from_pubkey_sigs(&config_offline.signers[0].pubkey(), &signers).unwrap(); + let sign_only = parse_sign_only_reply_string(&sig_response); + assert!(sign_only.has_all_signers()); + let offline_presigner = sign_only + .presigner_of(&config_offline.signers[0].pubkey()) + .unwrap(); let online_pubkey = config_online.signers[0].pubkey(); config_online.signers = vec![&offline_presigner]; config_online.command = CliCommand::Pay(PayCommand { diff --git a/cli/tests/stake.rs b/cli/tests/stake.rs index 57ba7a5c8f..edc85311d9 100644 --- a/cli/tests/stake.rs +++ b/cli/tests/stake.rs @@ -1,4 +1,3 @@ -use solana_clap_utils::keypair::presigner_from_pubkey_sigs; use solana_cli::{ cli::{process_command, request_and_confirm_airdrop, CliCommand, CliConfig}, nonce, @@ -388,9 +387,11 @@ fn test_offline_stake_delegation_and_deactivation() { fee_payer: 0, }; let sig_response = process_command(&config_offline).unwrap(); - let (blockhash, signers) = parse_sign_only_reply_string(&sig_response); - let offline_presigner = - presigner_from_pubkey_sigs(&config_offline.signers[0].pubkey(), &signers).unwrap(); + let sign_only = parse_sign_only_reply_string(&sig_response); + assert!(sign_only.has_all_signers()); + let offline_presigner = sign_only + .presigner_of(&config_offline.signers[0].pubkey()) + .unwrap(); config_payer.signers = vec![&offline_presigner]; config_payer.command = CliCommand::DelegateStake { stake_account_pubkey: stake_keypair.pubkey(), @@ -417,9 +418,11 @@ fn test_offline_stake_delegation_and_deactivation() { fee_payer: 0, }; let sig_response = process_command(&config_offline).unwrap(); - let (blockhash, signers) = parse_sign_only_reply_string(&sig_response); - let offline_presigner = - presigner_from_pubkey_sigs(&config_offline.signers[0].pubkey(), &signers).unwrap(); + let sign_only = parse_sign_only_reply_string(&sig_response); + assert!(sign_only.has_all_signers()); + let offline_presigner = sign_only + .presigner_of(&config_offline.signers[0].pubkey()) + .unwrap(); config_payer.signers = vec![&offline_presigner]; config_payer.command = CliCommand::DeactivateStake { stake_account_pubkey: stake_keypair.pubkey(), @@ -679,9 +682,9 @@ fn test_stake_authorize() { fee_payer: 0, }; let sign_reply = process_command(&config_offline).unwrap(); - let (blockhash, signers) = parse_sign_only_reply_string(&sign_reply); - let offline_presigner = - presigner_from_pubkey_sigs(&offline_authority_pubkey, &signers).unwrap(); + let sign_only = parse_sign_only_reply_string(&sign_reply); + assert!(sign_only.has_all_signers()); + let offline_presigner = sign_only.presigner_of(&offline_authority_pubkey).unwrap(); config.signers = vec![&offline_presigner]; config.command = CliCommand::StakeAuthorize { stake_account_pubkey, @@ -739,12 +742,11 @@ fn test_stake_authorize() { fee_payer: 0, }; let sign_reply = process_command(&config_offline).unwrap(); - let (blockhash, signers) = parse_sign_only_reply_string(&sign_reply); - assert_eq!(blockhash, nonce_hash); - let offline_presigner = - presigner_from_pubkey_sigs(&offline_authority_pubkey, &signers).unwrap(); - let nonced_authority_presigner = - presigner_from_pubkey_sigs(&nonced_authority_pubkey, &signers).unwrap(); + let sign_only = parse_sign_only_reply_string(&sign_reply); + assert!(sign_only.has_all_signers()); + assert_eq!(sign_only.blockhash, nonce_hash); + let offline_presigner = sign_only.presigner_of(&offline_authority_pubkey).unwrap(); + let nonced_authority_presigner = sign_only.presigner_of(&nonced_authority_pubkey).unwrap(); config.signers = vec![&offline_presigner, &nonced_authority_presigner]; config.command = CliCommand::StakeAuthorize { stake_account_pubkey, @@ -754,7 +756,7 @@ fn test_stake_authorize() { sign_only: false, blockhash_query: BlockhashQuery::FeeCalculator( blockhash_query::Source::NonceAccount(nonce_account.pubkey()), - blockhash, + sign_only.blockhash, ), nonce_account: Some(nonce_account.pubkey()), nonce_authority: 0, @@ -816,6 +818,7 @@ fn test_stake_authorize_with_fee_payer() { let mut config_offline = CliConfig::default(); let offline_signer = Keypair::new(); config_offline.signers = vec![&offline_signer]; + config_offline.json_rpc_url = String::new(); let offline_pubkey = config_offline.signers[0].pubkey(); // Verify we're offline config_offline.command = CliCommand::ClusterVersion; @@ -886,8 +889,9 @@ fn test_stake_authorize_with_fee_payer() { fee_payer: 0, }; let sign_reply = process_command(&config_offline).unwrap(); - let (blockhash, signers) = parse_sign_only_reply_string(&sign_reply); - let offline_presigner = presigner_from_pubkey_sigs(&offline_pubkey, &signers).unwrap(); + let sign_only = parse_sign_only_reply_string(&sign_reply); + assert!(sign_only.has_all_signers()); + let offline_presigner = sign_only.presigner_of(&offline_pubkey).unwrap(); config.signers = vec![&offline_presigner]; config.command = CliCommand::StakeAuthorize { stake_account_pubkey, @@ -1023,8 +1027,9 @@ fn test_stake_split() { fee_payer: 0, }; let sig_response = process_command(&config_offline).unwrap(); - let (blockhash, signers) = parse_sign_only_reply_string(&sig_response); - let offline_presigner = presigner_from_pubkey_sigs(&offline_pubkey, &signers).unwrap(); + let sign_only = parse_sign_only_reply_string(&sig_response); + assert!(sign_only.has_all_signers()); + let offline_presigner = sign_only.presigner_of(&offline_pubkey).unwrap(); config.signers = vec![&offline_presigner, &split_account]; config.command = CliCommand::SplitStake { stake_account_pubkey: stake_account_pubkey, @@ -1032,7 +1037,7 @@ fn test_stake_split() { sign_only: false, blockhash_query: BlockhashQuery::FeeCalculator( blockhash_query::Source::NonceAccount(nonce_account.pubkey()), - blockhash, + sign_only.blockhash, ), nonce_account: Some(nonce_account.pubkey()), nonce_authority: 0, @@ -1275,8 +1280,9 @@ fn test_stake_set_lockup() { fee_payer: 0, }; let sig_response = process_command(&config_offline).unwrap(); - let (blockhash, signers) = parse_sign_only_reply_string(&sig_response); - let offline_presigner = presigner_from_pubkey_sigs(&offline_pubkey, &signers).unwrap(); + let sign_only = parse_sign_only_reply_string(&sig_response); + assert!(sign_only.has_all_signers()); + let offline_presigner = sign_only.presigner_of(&offline_pubkey).unwrap(); config.signers = vec![&offline_presigner]; config.command = CliCommand::StakeSetLockup { stake_account_pubkey, @@ -1285,7 +1291,7 @@ fn test_stake_set_lockup() { sign_only: false, blockhash_query: BlockhashQuery::FeeCalculator( blockhash_query::Source::NonceAccount(nonce_account_pubkey), - blockhash, + sign_only.blockhash, ), nonce_account: Some(nonce_account_pubkey), nonce_authority: 0, @@ -1392,9 +1398,10 @@ fn test_offline_nonced_create_stake_account_and_withdraw() { from: 0, }; let sig_response = process_command(&config_offline).unwrap(); - let (blockhash, signers) = parse_sign_only_reply_string(&sig_response); - let offline_presigner = presigner_from_pubkey_sigs(&offline_pubkey, &signers).unwrap(); - let stake_presigner = presigner_from_pubkey_sigs(&stake_pubkey, &signers).unwrap(); + let sign_only = parse_sign_only_reply_string(&sig_response); + assert!(sign_only.has_all_signers()); + let offline_presigner = sign_only.presigner_of(&offline_pubkey).unwrap(); + let stake_presigner = sign_only.presigner_of(&stake_pubkey).unwrap(); config.signers = vec![&offline_presigner, &stake_presigner]; config.command = CliCommand::CreateStakeAccount { stake_account: 1, @@ -1406,7 +1413,7 @@ fn test_offline_nonced_create_stake_account_and_withdraw() { sign_only: false, blockhash_query: BlockhashQuery::FeeCalculator( blockhash_query::Source::NonceAccount(nonce_pubkey), - blockhash, + sign_only.blockhash, ), nonce_account: Some(nonce_pubkey), nonce_authority: 0, @@ -1438,8 +1445,8 @@ fn test_offline_nonced_create_stake_account_and_withdraw() { fee_payer: 0, }; let sig_response = process_command(&config_offline).unwrap(); - let (blockhash, signers) = parse_sign_only_reply_string(&sig_response); - let offline_presigner = presigner_from_pubkey_sigs(&offline_pubkey, &signers).unwrap(); + let sign_only = parse_sign_only_reply_string(&sig_response); + let offline_presigner = sign_only.presigner_of(&offline_pubkey).unwrap(); config.signers = vec![&offline_presigner]; config.command = CliCommand::WithdrawStake { stake_account_pubkey: stake_pubkey, @@ -1449,7 +1456,7 @@ fn test_offline_nonced_create_stake_account_and_withdraw() { sign_only: false, blockhash_query: BlockhashQuery::FeeCalculator( blockhash_query::Source::NonceAccount(nonce_pubkey), - blockhash, + sign_only.blockhash, ), nonce_account: Some(nonce_pubkey), nonce_authority: 0, @@ -1482,9 +1489,9 @@ fn test_offline_nonced_create_stake_account_and_withdraw() { from: 0, }; let sig_response = process_command(&config_offline).unwrap(); - let (blockhash, signers) = parse_sign_only_reply_string(&sig_response); - let offline_presigner = presigner_from_pubkey_sigs(&offline_pubkey, &signers).unwrap(); - let stake_presigner = presigner_from_pubkey_sigs(&stake_pubkey, &signers).unwrap(); + let sign_only = parse_sign_only_reply_string(&sig_response); + let offline_presigner = sign_only.presigner_of(&offline_pubkey).unwrap(); + let stake_presigner = sign_only.presigner_of(&stake_pubkey).unwrap(); config.signers = vec![&offline_presigner, &stake_presigner]; config.command = CliCommand::CreateStakeAccount { stake_account: 1, @@ -1496,7 +1503,7 @@ fn test_offline_nonced_create_stake_account_and_withdraw() { sign_only: false, blockhash_query: BlockhashQuery::FeeCalculator( blockhash_query::Source::NonceAccount(nonce_pubkey), - blockhash, + sign_only.blockhash, ), nonce_account: Some(nonce_pubkey), nonce_authority: 0, diff --git a/cli/tests/transfer.rs b/cli/tests/transfer.rs index 0b31ea5c31..03d323352d 100644 --- a/cli/tests/transfer.rs +++ b/cli/tests/transfer.rs @@ -1,4 +1,3 @@ -use solana_clap_utils::keypair::presigner_from_pubkey_sigs; use solana_cli::{ cli::{process_command, request_and_confirm_airdrop, CliCommand, CliConfig}, nonce, @@ -13,7 +12,7 @@ use solana_faucet::faucet::run_local_faucet; use solana_sdk::{ nonce::State as NonceState, pubkey::Pubkey, - signature::{keypair_from_seed, Keypair, Signer}, + signature::{keypair_from_seed, Keypair, NullSigner, Signer}, }; use std::{fs::remove_dir_all, sync::mpsc::channel, thread::sleep, time::Duration}; @@ -102,8 +101,9 @@ fn test_transfer() { fee_payer: 0, }; let sign_only_reply = process_command(&offline).unwrap(); - let (blockhash, signers) = parse_sign_only_reply_string(&sign_only_reply); - let offline_presigner = presigner_from_pubkey_sigs(&offline_pubkey, &signers).unwrap(); + let sign_only = parse_sign_only_reply_string(&sign_only_reply); + assert!(sign_only.has_all_signers()); + let offline_presigner = sign_only.presigner_of(&offline_pubkey).unwrap(); config.signers = vec![&offline_presigner]; config.command = CliCommand::Transfer { lamports: 10, @@ -193,8 +193,9 @@ fn test_transfer() { fee_payer: 0, }; let sign_only_reply = process_command(&offline).unwrap(); - let (blockhash, signers) = parse_sign_only_reply_string(&sign_only_reply); - let offline_presigner = presigner_from_pubkey_sigs(&offline_pubkey, &signers).unwrap(); + let sign_only = parse_sign_only_reply_string(&sign_only_reply); + assert!(sign_only.has_all_signers()); + let offline_presigner = sign_only.presigner_of(&offline_pubkey).unwrap(); config.signers = vec![&offline_presigner]; config.command = CliCommand::Transfer { lamports: 10, @@ -203,7 +204,7 @@ fn test_transfer() { sign_only: false, blockhash_query: BlockhashQuery::FeeCalculator( blockhash_query::Source::NonceAccount(nonce_account.pubkey()), - blockhash, + sign_only.blockhash, ), nonce_account: Some(nonce_account.pubkey()), nonce_authority: 0, @@ -216,3 +217,114 @@ fn test_transfer() { server.close().unwrap(); remove_dir_all(ledger_path).unwrap(); } + +#[test] +fn test_transfer_multisession_signing() { + 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 to_pubkey = Pubkey::new(&[1u8; 32]); + let offline_from_signer = keypair_from_seed(&[2u8; 32]).unwrap(); + let offline_fee_payer_signer = keypair_from_seed(&[3u8; 32]).unwrap(); + let from_null_signer = NullSigner::new(&offline_from_signer.pubkey()); + + // Setup accounts + let rpc_client = RpcClient::new_socket(leader_data.rpc); + request_and_confirm_airdrop(&rpc_client, &faucet_addr, &offline_from_signer.pubkey(), 43) + .unwrap(); + request_and_confirm_airdrop( + &rpc_client, + &faucet_addr, + &offline_fee_payer_signer.pubkey(), + 3, + ) + .unwrap(); + check_balance(43, &rpc_client, &offline_from_signer.pubkey()); + check_balance(3, &rpc_client, &offline_fee_payer_signer.pubkey()); + check_balance(0, &rpc_client, &to_pubkey); + + let (blockhash, _) = rpc_client.get_recent_blockhash().unwrap(); + + // Offline fee-payer signs first + let mut fee_payer_config = CliConfig::default(); + fee_payer_config.json_rpc_url = String::default(); + fee_payer_config.signers = vec![&offline_fee_payer_signer, &from_null_signer]; + // Verify we cannot contact the cluster + fee_payer_config.command = CliCommand::ClusterVersion; + process_command(&fee_payer_config).unwrap_err(); + fee_payer_config.command = CliCommand::Transfer { + lamports: 42, + to: to_pubkey, + from: 1, + sign_only: true, + blockhash_query: BlockhashQuery::None(blockhash), + nonce_account: None, + nonce_authority: 0, + fee_payer: 0, + }; + let sign_only_reply = process_command(&fee_payer_config).unwrap(); + let sign_only = parse_sign_only_reply_string(&sign_only_reply); + assert!(!sign_only.has_all_signers()); + let fee_payer_presigner = sign_only + .presigner_of(&offline_fee_payer_signer.pubkey()) + .unwrap(); + + // Now the offline fund source + let mut from_config = CliConfig::default(); + from_config.json_rpc_url = String::default(); + from_config.signers = vec![&fee_payer_presigner, &offline_from_signer]; + // Verify we cannot contact the cluster + from_config.command = CliCommand::ClusterVersion; + process_command(&from_config).unwrap_err(); + from_config.command = CliCommand::Transfer { + lamports: 42, + to: to_pubkey, + from: 1, + sign_only: true, + blockhash_query: BlockhashQuery::None(blockhash), + nonce_account: None, + nonce_authority: 0, + fee_payer: 0, + }; + let sign_only_reply = process_command(&from_config).unwrap(); + let sign_only = parse_sign_only_reply_string(&sign_only_reply); + assert!(sign_only.has_all_signers()); + let from_presigner = sign_only + .presigner_of(&offline_from_signer.pubkey()) + .unwrap(); + + // Finally submit to the cluster + let mut config = CliConfig::default(); + config.json_rpc_url = format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port()); + config.signers = vec![&fee_payer_presigner, &from_presigner]; + config.command = CliCommand::Transfer { + lamports: 42, + to: to_pubkey, + from: 1, + sign_only: false, + blockhash_query: BlockhashQuery::FeeCalculator(blockhash_query::Source::Cluster, blockhash), + nonce_account: None, + nonce_authority: 0, + fee_payer: 0, + }; + process_command(&config).unwrap(); + + check_balance(1, &rpc_client, &offline_from_signer.pubkey()); + check_balance(1, &rpc_client, &offline_fee_payer_signer.pubkey()); + check_balance(42, &rpc_client, &to_pubkey); + + server.close().unwrap(); + remove_dir_all(ledger_path).unwrap(); +} diff --git a/docs/src/offline-signing/README.md b/docs/src/offline-signing/README.md index 1640a43b9f..4e90cd1768 100644 --- a/docs/src/offline-signing/README.md +++ b/docs/src/offline-signing/README.md @@ -82,6 +82,72 @@ Output 4vC38p4bz7XyiXrk6HtaooUqwxTWKocf45cstASGtmrD398biNJnmTcUCVEojE7wVQvgdYbjHJqRFZPpzfCQpmUN ``` +## Offline Signing Over Multiple Sessions + +Offline signing can also take place over multiple sessions. In this scenario, +pass the absent signer's public key for each role. All pubkeys that were specified, +but no signature was generated for will be listed as absent in the offline signing +output + +### Example: Transfer with Two Offline Signing Sessions + +Command (Offline Session #1) + +```text +solana@offline1$ solana transfer Fdri24WUGtrCXZ55nXiewAj6RM18hRHPGAjZk3o6vBut 10 \ + --blockhash 7ALDjLv56a8f6sH6upAZALQKkXyjAwwENH9GomyM8Dbc \ + --sign-only \ + --keypair fee_payer.json \ + --from 674RgFMgdqdRoVtMqSBg7mHFbrrNm1h1r721H1ZMquHL +``` + +Output (Offline Session #1) + +```text +Blockhash: 7ALDjLv56a8f6sH6upAZALQKkXyjAwwENH9GomyM8Dbc +Signers (Pubkey=Signature): + 3bo5YiRagwmRikuH6H1d2gkKef5nFZXE3gJeoHxJbPjy=ohGKvpRC46jAduwU9NW8tP91JkCT5r8Mo67Ysnid4zc76tiiV1Ho6jv3BKFSbBcr2NcPPCarmfTLSkTHsJCtdYi +Absent Signers (Pubkey): + 674RgFMgdqdRoVtMqSBg7mHFbrrNm1h1r721H1ZMquHL +``` + +Command (Offline Session #2) + +```text +solana@offline2$ solana transfer Fdri24WUGtrCXZ55nXiewAj6RM18hRHPGAjZk3o6vBut 10 \ + --blockhash 7ALDjLv56a8f6sH6upAZALQKkXyjAwwENH9GomyM8Dbc \ + --sign-only \ + --keypair from.json \ + --fee-payer 3bo5YiRagwmRikuH6H1d2gkKef5nFZXE3gJeoHxJbPjy +``` + +Output (Offline Session #2) + +```text +Blockhash: 7ALDjLv56a8f6sH6upAZALQKkXyjAwwENH9GomyM8Dbc +Signers (Pubkey=Signature): + 674RgFMgdqdRoVtMqSBg7mHFbrrNm1h1r721H1ZMquHL=3vJtnba4dKQmEAieAekC1rJnPUndBcpvqRPRMoPWqhLEMCty2SdUxt2yvC1wQW6wVUa5putZMt6kdwCaTv8gk7sQ +Absent Signers (Pubkey): + 3bo5YiRagwmRikuH6H1d2gkKef5nFZXE3gJeoHxJbPjy +``` + +Command (Online Submission) + +```text +solana@online$ solana transfer Fdri24WUGtrCXZ55nXiewAj6RM18hRHPGAjZk3o6vBut 10 \ + --blockhash 7ALDjLv56a8f6sH6upAZALQKkXyjAwwENH9GomyM8Dbc \ + --from 674RgFMgdqdRoVtMqSBg7mHFbrrNm1h1r721H1ZMquHL \ + --signer 674RgFMgdqdRoVtMqSBg7mHFbrrNm1h1r721H1ZMquHL=3vJtnba4dKQmEAieAekC1rJnPUndBcpvqRPRMoPWqhLEMCty2SdUxt2yvC1wQW6wVUa5putZMt6kdwCaTv8gk7sQ \ + --fee-payer 3bo5YiRagwmRikuH6H1d2gkKef5nFZXE3gJeoHxJbPjy \ + --signer 3bo5YiRagwmRikuH6H1d2gkKef5nFZXE3gJeoHxJbPjy=ohGKvpRC46jAduwU9NW8tP91JkCT5r8Mo67Ysnid4zc76tiiV1Ho6jv3BKFSbBcr2NcPPCarmfTLSkTHsJCtdYi +``` + +Output (Online Submission) + +```text +ohGKvpRC46jAduwU9NW8tP91JkCT5r8Mo67Ysnid4zc76tiiV1Ho6jv3BKFSbBcr2NcPPCarmfTLSkTHsJCtdYi +``` + ## Buying More Time to Sign Typically a Solana transaction must be signed and accepted by the network within diff --git a/sdk/src/signature.rs b/sdk/src/signature.rs index 80b1a33155..8117b69d77 100644 --- a/sdk/src/signature.rs +++ b/sdk/src/signature.rs @@ -269,6 +269,39 @@ where } } +/// NullSigner - A `Signer` implementation that always produces `Signature::default()`. +/// Used as a placeholder for absentee signers whose 'Pubkey` is required to construct +/// the transaction +#[derive(Clone, Debug, Default)] +pub struct NullSigner { + pubkey: Pubkey, +} + +impl NullSigner { + pub fn new(pubkey: &Pubkey) -> Self { + Self { pubkey: *pubkey } + } +} + +impl Signer for NullSigner { + fn try_pubkey(&self) -> Result { + Ok(self.pubkey) + } + + fn try_sign_message(&self, _message: &[u8]) -> Result { + Ok(Signature::default()) + } +} + +impl PartialEq for NullSigner +where + T: Signer, +{ + fn eq(&self, other: &T) -> bool { + self.pubkey == other.pubkey() + } +} + pub fn read_keypair(reader: &mut R) -> Result> { let bytes: Vec = serde_json::from_reader(reader)?; let dalek_keypair = ed25519_dalek::Keypair::from_bytes(&bytes) diff --git a/sdk/src/transaction.rs b/sdk/src/transaction.rs index 364e5f1c36..01972e292d 100644 --- a/sdk/src/transaction.rs +++ b/sdk/src/transaction.rs @@ -300,14 +300,20 @@ impl Transaction { Ok(()) } - /// Verify the transaction - pub fn verify(&self) -> Result<()> { - if !self - .signatures + pub fn verify_with_results(&self) -> Vec { + self.signatures .iter() .zip(&self.message.account_keys) .map(|(signature, pubkey)| signature.verify(pubkey.as_ref(), &self.message_data())) - .all(|verify_result| verify_result) + .collect() + } + + /// Verify the transaction + pub fn verify(&self) -> Result<()> { + if !self + .verify_with_results() + .iter() + .all(|verify_result| *verify_result) { Err(TransactionError::SignatureFailure) } else {