Add offline signing support to CLI (#7104)

This commit is contained in:
Jack May 2019-11-25 21:09:57 -08:00 committed by GitHub
parent 294662a1ce
commit 88cb0c6ae3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 892 additions and 70 deletions

View File

@ -3,8 +3,9 @@ use clap::ArgMatches;
use solana_sdk::{
native_token::sol_to_lamports,
pubkey::Pubkey,
signature::{read_keypair_file, Keypair, KeypairUtil},
signature::{read_keypair_file, Keypair, KeypairUtil, Signature},
};
use std::str::FromStr;
// Return parsed values from matches at `name`
pub fn values_of<T>(matches: &ArgMatches<'_>, name: &str) -> Option<Vec<T>>
@ -50,6 +51,20 @@ pub fn pubkey_of(matches: &ArgMatches<'_>, name: &str) -> Option<Pubkey> {
value_of(matches, name).or_else(|| keypair_of(matches, name).map(|keypair| keypair.pubkey()))
}
// Return pubkey/signature pairs for a string of the form pubkey=signature
pub fn pubkeys_sigs_of(matches: &ArgMatches<'_>, name: &str) -> Option<Vec<(Pubkey, Signature)>> {
matches.values_of(name).map(|values| {
values
.map(|pubkey_signer_string| {
let mut signer = pubkey_signer_string.split('=');
let key = Pubkey::from_str(signer.next().unwrap()).unwrap();
let sig = Signature::from_str(signer.next().unwrap()).unwrap();
(key, sig)
})
.collect()
})
}
pub fn amount_of(matches: &ArgMatches<'_>, name: &str, unit: &str) -> Option<u64> {
if matches.value_of(unit) == Some("lamports") {
value_of(matches, name)
@ -172,4 +187,25 @@ mod tests {
fs::remove_file(&outfile).unwrap();
}
#[test]
fn test_pubkeys_sigs_of() {
let key1 = Pubkey::new_rand();
let key2 = Pubkey::new_rand();
let sig1 = Keypair::new().sign_message(&[0u8]);
let sig2 = Keypair::new().sign_message(&[1u8]);
let signer1 = format!("{}={}", key1, sig1);
let signer2 = format!("{}={}", key2, sig2);
let matches = app().clone().get_matches_from(vec![
"test",
"--multiple",
&signer1,
"--multiple",
&signer2,
]);
assert_eq!(
pubkeys_sigs_of(&matches, "multiple"),
Some(vec![(key1, sig1), (key2, sig2)])
);
}
}

View File

@ -1,6 +1,8 @@
use crate::keypair::ASK_KEYWORD;
use solana_sdk::hash::Hash;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::read_keypair_file;
use solana_sdk::signature::{read_keypair_file, Signature};
use std::str::FromStr;
// Return an error if a pubkey cannot be parsed.
pub fn is_pubkey(string: String) -> Result<(), String> {
@ -10,6 +12,14 @@ pub fn is_pubkey(string: String) -> Result<(), String> {
}
}
// Return an error if a hash cannot be parsed.
pub fn is_hash(string: String) -> Result<(), String> {
match string.parse::<Hash>() {
Ok(_) => Ok(()),
Err(err) => Err(format!("{:?}", err)),
}
}
// Return an error if a keypair file cannot be parsed.
pub fn is_keypair(string: String) -> Result<(), String> {
read_keypair_file(&string)
@ -32,6 +42,28 @@ pub fn is_pubkey_or_keypair(string: String) -> Result<(), String> {
is_pubkey(string.clone()).or_else(|_| is_keypair(string))
}
// Return an error if string cannot be parsed as pubkey=signature string
pub fn is_pubkey_sig(string: String) -> Result<(), String> {
let mut signer = string.split('=');
match Pubkey::from_str(
signer
.next()
.ok_or_else(|| "Malformed signer string".to_string())?,
) {
Ok(_) => {
match Signature::from_str(
signer
.next()
.ok_or_else(|| "Malformed signer string".to_string())?,
) {
Ok(_) => Ok(()),
Err(err) => Err(format!("{:?}", err)),
}
}
Err(err) => Err(format!("{:?}", err)),
}
}
// Return an error if a url cannot be parsed.
pub fn is_url(string: String) -> Result<(), String> {
match url::Url::parse(&string) {

View File

@ -1,5 +1,10 @@
use crate::{
cluster_query::*, display::println_name_value, stake::*, storage::*, validator_info::*, vote::*,
cluster_query::*,
display::{println_name_value, println_signers},
stake::*,
storage::*,
validator_info::*,
vote::*,
};
use chrono::prelude::*;
use clap::{App, AppSettings, Arg, ArgMatches, SubCommand};
@ -105,8 +110,20 @@ pub enum CliCommand {
lockup: Lockup,
lamports: u64,
},
DeactivateStake(Pubkey),
DelegateStake(Pubkey, Pubkey, bool),
DeactivateStake {
stake_account_pubkey: Pubkey,
sign_only: bool,
signers: Option<Vec<(Pubkey, Signature)>>,
blockhash: Option<Hash>,
},
DelegateStake {
stake_account_pubkey: Pubkey,
vote_account_pubkey: Pubkey,
force: bool,
sign_only: bool,
signers: Option<Vec<(Pubkey, Signature)>>,
blockhash: Option<Hash>,
},
RedeemVoteCredits(Pubkey, Pubkey),
ShowStakeHistory {
use_lamports_unit: bool,
@ -174,6 +191,9 @@ pub enum CliCommand {
timestamp_pubkey: Option<Pubkey>,
witnesses: Option<Vec<Pubkey>>,
cancelable: bool,
sign_only: bool,
signers: Option<Vec<(Pubkey, Signature)>>,
blockhash: Option<Hash>,
},
ShowAccount {
pubkey: Pubkey,
@ -413,6 +433,9 @@ pub fn parse_command(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, Box<dyn
let timestamp_pubkey = value_of(&matches, "timestamp_pubkey");
let witnesses = values_of(&matches, "witness");
let cancelable = matches.is_present("cancelable");
let sign_only = matches.is_present("sign_only");
let signers = pubkeys_sigs_of(&matches, "signer");
let blockhash = value_of(&matches, "blockhash");
Ok(CliCommandInfo {
command: CliCommand::Pay {
@ -422,8 +445,11 @@ pub fn parse_command(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, Box<dyn
timestamp_pubkey,
witnesses,
cancelable,
sign_only,
signers,
blockhash,
},
require_keypair: true,
require_keypair: !sign_only,
})
}
("show-account", Some(matches)) => {
@ -522,6 +548,48 @@ pub fn check_unique_pubkeys(
}
}
pub fn get_blockhash_fee_calculator(
rpc_client: &RpcClient,
sign_only: bool,
blockhash: Option<Hash>,
) -> Result<(Hash, FeeCalculator), Box<dyn std::error::Error>> {
Ok(if let Some(blockhash) = blockhash {
if sign_only {
(blockhash, FeeCalculator::default())
} else {
(blockhash, rpc_client.get_recent_blockhash()?.1)
}
} else {
rpc_client.get_recent_blockhash()?
})
}
pub fn return_signers(tx: &Transaction) -> ProcessResult {
println_signers(tx);
let signers: Vec<_> = tx
.signatures
.iter()
.zip(tx.message.account_keys.clone())
.map(|(signature, pubkey)| format!("{}={}", pubkey, signature))
.collect();
Ok(json!({
"blockhash": tx.message.recent_blockhash.to_string(),
"signers": &signers,
})
.to_string())
}
pub fn replace_signatures(tx: &mut Transaction, signers: &[(Pubkey, Signature)]) -> ProcessResult {
tx.replace_signatures(signers).map_err(|_| {
CliError::BadParameter(
"Transaction construction failed, incorrect signature or public key provided"
.to_string(),
)
})?;
Ok("".to_string())
}
fn process_airdrop(
rpc_client: &RpcClient,
config: &CliConfig,
@ -694,6 +762,7 @@ fn process_deploy(
.to_string())
}
#[allow(clippy::too_many_arguments)]
fn process_pay(
rpc_client: &RpcClient,
config: &CliConfig,
@ -703,12 +772,17 @@ fn process_pay(
timestamp_pubkey: Option<Pubkey>,
witnesses: &Option<Vec<Pubkey>>,
cancelable: bool,
sign_only: bool,
signers: &Option<Vec<(Pubkey, Signature)>>,
blockhash: Option<Hash>,
) -> ProcessResult {
check_unique_pubkeys(
(&config.keypair.pubkey(), "cli keypair".to_string()),
(to, "to".to_string()),
)?;
let (blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?;
let (blockhash, fee_calculator) =
get_blockhash_fee_calculator(rpc_client, sign_only, blockhash)?;
let cancelable = if cancelable {
Some(config.keypair.pubkey())
@ -718,9 +792,17 @@ fn process_pay(
if timestamp == None && *witnesses == None {
let mut tx = system_transaction::transfer(&config.keypair, to, lamports, blockhash);
check_account_for_fee(rpc_client, config, &fee_calculator, &tx.message)?;
let result = rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair]);
log_instruction_custom_error::<SystemError>(result)
if let Some(signers) = signers {
replace_signatures(&mut tx, &signers)?;
}
if sign_only {
return_signers(&tx)
} else {
check_account_for_fee(rpc_client, config, &fee_calculator, &tx.message)?;
let result = rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair]);
log_instruction_custom_error::<SystemError>(result)
}
} else if *witnesses == None {
let dt = timestamp.unwrap();
let dt_pubkey = match timestamp_pubkey {
@ -745,19 +827,24 @@ fn process_pay(
ixs,
blockhash,
);
check_account_for_fee(rpc_client, config, &fee_calculator, &tx.message)?;
let result =
rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair, &contract_state]);
let signature_str = log_instruction_custom_error::<BudgetError>(result)?;
if let Some(signers) = signers {
replace_signatures(&mut tx, &signers)?;
}
if sign_only {
return_signers(&tx)
} else {
check_account_for_fee(rpc_client, config, &fee_calculator, &tx.message)?;
let result = rpc_client
.send_and_confirm_transaction(&mut tx, &[&config.keypair, &contract_state]);
let signature_str = log_instruction_custom_error::<BudgetError>(result)?;
Ok(json!({
"signature": signature_str,
"processId": format!("{}", contract_state.pubkey()),
})
.to_string())
Ok(json!({
"signature": signature_str,
"processId": format!("{}", contract_state.pubkey()),
})
.to_string())
}
} else if timestamp == None {
let (blockhash, _fee_calculator) = rpc_client.get_recent_blockhash()?;
let witness = if let Some(ref witness_vec) = *witnesses {
witness_vec[0]
} else {
@ -783,16 +870,23 @@ fn process_pay(
ixs,
blockhash,
);
let result =
rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair, &contract_state]);
check_account_for_fee(rpc_client, config, &fee_calculator, &tx.message)?;
let signature_str = log_instruction_custom_error::<BudgetError>(result)?;
if let Some(signers) = signers {
replace_signatures(&mut tx, &signers)?;
}
if sign_only {
return_signers(&tx)
} else {
let result = rpc_client
.send_and_confirm_transaction(&mut tx, &[&config.keypair, &contract_state]);
check_account_for_fee(rpc_client, config, &fee_calculator, &tx.message)?;
let signature_str = log_instruction_custom_error::<BudgetError>(result)?;
Ok(json!({
"signature": signature_str,
"processId": format!("{}", contract_state.pubkey()),
})
.to_string())
Ok(json!({
"signature": signature_str,
"processId": format!("{}", contract_state.pubkey()),
})
.to_string())
}
} else {
Ok("Combo transactions not yet handled".to_string())
}
@ -926,18 +1020,36 @@ pub fn process_command(config: &CliConfig) -> ProcessResult {
*lamports,
),
// Deactivate stake account
CliCommand::DeactivateStake(stake_account_pubkey) => {
process_deactivate_stake_account(&rpc_client, config, &stake_account_pubkey)
}
CliCommand::DelegateStake(stake_account_pubkey, vote_account_pubkey, force) => {
process_delegate_stake(
&rpc_client,
config,
&stake_account_pubkey,
&vote_account_pubkey,
*force,
)
}
CliCommand::DeactivateStake {
stake_account_pubkey,
sign_only,
ref signers,
blockhash,
} => process_deactivate_stake_account(
&rpc_client,
config,
&stake_account_pubkey,
*sign_only,
signers,
*blockhash,
),
CliCommand::DelegateStake {
stake_account_pubkey,
vote_account_pubkey,
force,
sign_only,
ref signers,
blockhash,
} => process_delegate_stake(
&rpc_client,
config,
&stake_account_pubkey,
&vote_account_pubkey,
*force,
*sign_only,
signers,
*blockhash,
),
CliCommand::RedeemVoteCredits(stake_account_pubkey, vote_account_pubkey) => {
process_redeem_vote_credits(
&rpc_client,
@ -1118,6 +1230,9 @@ pub fn process_command(config: &CliConfig) -> ProcessResult {
timestamp_pubkey,
ref witnesses,
cancelable,
sign_only,
ref signers,
blockhash,
} => process_pay(
&rpc_client,
config,
@ -1127,6 +1242,9 @@ pub fn process_command(config: &CliConfig) -> ProcessResult {
*timestamp_pubkey,
witnesses,
*cancelable,
*sign_only,
signers,
*blockhash,
),
CliCommand::ShowAccount {
pubkey,
@ -1410,6 +1528,29 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, '
Arg::with_name("cancelable")
.long("cancelable")
.takes_value(false),
)
.arg(
Arg::with_name("sign_only")
.long("sign-only")
.takes_value(false)
.help("Sign the transaction offline"),
)
.arg(
Arg::with_name("signer")
.long("signer")
.value_name("PUBKEY=BASE58_SIG")
.takes_value(true)
.validator(is_pubkey_sig)
.multiple(true)
.help("Provide a public-key/signature pair for the transaction"),
)
.arg(
Arg::with_name("blockhash")
.long("blockhash")
.value_name("BLOCKHASH")
.takes_value(true)
.validator(is_hash)
.help("Use the supplied blockhash"),
),
)
.subcommand(
@ -1667,6 +1808,9 @@ mod tests {
timestamp_pubkey: None,
witnesses: None,
cancelable: false,
sign_only: false,
signers: None,
blockhash: None,
},
require_keypair: true
}
@ -1694,6 +1838,9 @@ mod tests {
timestamp_pubkey: None,
witnesses: Some(vec![witness0, witness1]),
cancelable: false,
sign_only: false,
signers: None,
blockhash: None,
},
require_keypair: true
}
@ -1717,6 +1864,9 @@ mod tests {
timestamp_pubkey: None,
witnesses: Some(vec![witness0]),
cancelable: false,
sign_only: false,
signers: None,
blockhash: None,
},
require_keypair: true
}
@ -1744,6 +1894,130 @@ mod tests {
timestamp_pubkey: Some(witness0),
witnesses: None,
cancelable: false,
sign_only: false,
signers: None,
blockhash: None,
},
require_keypair: true
}
);
// Test Pay Subcommand w/ sign-only
let test_pay = test_commands.clone().get_matches_from(vec![
"test",
"pay",
&pubkey_string,
"50",
"lamports",
"--sign-only",
]);
assert_eq!(
parse_command(&test_pay).unwrap(),
CliCommandInfo {
command: CliCommand::Pay {
lamports: 50,
to: pubkey,
timestamp: None,
timestamp_pubkey: None,
witnesses: None,
cancelable: false,
sign_only: true,
signers: None,
blockhash: None,
},
require_keypair: false
}
);
// Test Pay Subcommand w/ signer
let key1 = Pubkey::new_rand();
let sig1 = Keypair::new().sign_message(&[0u8]);
let signer1 = format!("{}={}", key1, sig1);
let test_pay = test_commands.clone().get_matches_from(vec![
"test",
"pay",
&pubkey_string,
"50",
"lamports",
"--signer",
&signer1,
]);
assert_eq!(
parse_command(&test_pay).unwrap(),
CliCommandInfo {
command: CliCommand::Pay {
lamports: 50,
to: pubkey,
timestamp: None,
timestamp_pubkey: None,
witnesses: None,
cancelable: false,
sign_only: false,
signers: Some(vec![(key1, sig1)]),
blockhash: None,
},
require_keypair: true
}
);
// Test Pay Subcommand w/ signers
let key2 = Pubkey::new_rand();
let sig2 = Keypair::new().sign_message(&[1u8]);
let signer2 = format!("{}={}", key2, sig2);
let test_pay = test_commands.clone().get_matches_from(vec![
"test",
"pay",
&pubkey_string,
"50",
"lamports",
"--signer",
&signer1,
"--signer",
&signer2,
]);
assert_eq!(
parse_command(&test_pay).unwrap(),
CliCommandInfo {
command: CliCommand::Pay {
lamports: 50,
to: pubkey,
timestamp: None,
timestamp_pubkey: None,
witnesses: None,
cancelable: false,
sign_only: false,
signers: Some(vec![(key1, sig1), (key2, sig2)]),
blockhash: None,
},
require_keypair: true
}
);
// Test Pay Subcommand w/ Blockhash
let blockhash = Hash::default();
let blockhash_string = format!("{}", blockhash);
let test_pay = test_commands.clone().get_matches_from(vec![
"test",
"pay",
&pubkey_string,
"50",
"lamports",
"--blockhash",
&blockhash_string,
]);
assert_eq!(
parse_command(&test_pay).unwrap(),
CliCommandInfo {
command: CliCommand::Pay {
lamports: 50,
to: pubkey,
timestamp: None,
timestamp_pubkey: None,
witnesses: None,
cancelable: false,
sign_only: false,
signers: None,
blockhash: Some(blockhash),
},
require_keypair: true
}
@ -1788,6 +2062,9 @@ mod tests {
timestamp_pubkey: Some(witness0),
witnesses: Some(vec![witness0, witness1]),
cancelable: false,
sign_only: false,
signers: None,
blockhash: None,
},
require_keypair: true
}
@ -1894,7 +2171,12 @@ mod tests {
assert_eq!(signature.unwrap(), SIGNATURE.to_string());
let stake_pubkey = Pubkey::new_rand();
config.command = CliCommand::DeactivateStake(stake_pubkey);
config.command = CliCommand::DeactivateStake {
stake_account_pubkey: stake_pubkey,
sign_only: false,
signers: None,
blockhash: None,
};
let signature = process_command(&config);
assert_eq!(signature.unwrap(), SIGNATURE.to_string());
@ -1915,6 +2197,9 @@ mod tests {
timestamp_pubkey: None,
witnesses: None,
cancelable: false,
sign_only: false,
signers: None,
blockhash: None,
};
let signature = process_command(&config);
assert_eq!(signature.unwrap(), SIGNATURE.to_string());
@ -1928,6 +2213,9 @@ mod tests {
timestamp_pubkey: Some(config.keypair.pubkey()),
witnesses: None,
cancelable: false,
sign_only: false,
signers: None,
blockhash: None,
};
let result = process_command(&config);
let json: Value = serde_json::from_str(&result.unwrap()).unwrap();
@ -1949,6 +2237,9 @@ mod tests {
timestamp_pubkey: None,
witnesses: Some(vec![witness]),
cancelable: true,
sign_only: false,
signers: None,
blockhash: None,
};
let result = process_command(&config);
let json: Value = serde_json::from_str(&result.unwrap()).unwrap();
@ -2056,6 +2347,9 @@ mod tests {
timestamp_pubkey: None,
witnesses: None,
cancelable: false,
sign_only: false,
signers: None,
blockhash: None,
};
assert!(process_command(&config).is_err());
@ -2066,6 +2360,9 @@ mod tests {
timestamp_pubkey: Some(config.keypair.pubkey()),
witnesses: None,
cancelable: false,
sign_only: false,
signers: None,
blockhash: None,
};
assert!(process_command(&config).is_err());
@ -2076,6 +2373,9 @@ mod tests {
timestamp_pubkey: None,
witnesses: Some(vec![witness]),
cancelable: true,
sign_only: false,
signers: None,
blockhash: None,
};
assert!(process_command(&config).is_err());

View File

@ -1,4 +1,5 @@
use console::style;
use solana_sdk::transaction::Transaction;
// Pretty print a "name value"
pub fn println_name_value(name: &str, value: &str) {
@ -22,3 +23,14 @@ pub fn println_name_value_or(name: &str, value: &str, default_value: &str) {
println!("{} {}", style(name).bold(), style(value));
};
}
pub fn println_signers(tx: &Transaction) {
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!();
}

View File

@ -1,14 +1,16 @@
use crate::cli::{
build_balance_message, check_account_for_fee, check_unique_pubkeys,
log_instruction_custom_error, CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult,
get_blockhash_fee_calculator, log_instruction_custom_error, replace_signatures, return_signers,
CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult,
};
use clap::{App, Arg, ArgMatches, SubCommand};
use console::style;
use solana_clap_utils::{input_parsers::*, input_validators::*};
use solana_client::rpc_client::RpcClient;
use solana_sdk::signature::Keypair;
use solana_sdk::signature::{Keypair, Signature};
use solana_sdk::{
account_utils::State,
hash::Hash,
pubkey::Pubkey,
signature::KeypairUtil,
system_instruction::SystemError,
@ -120,6 +122,29 @@ impl StakeSubCommands for App<'_, '_> {
.validator(is_pubkey_or_keypair)
.help("The vote account to which the stake will be delegated")
)
.arg(
Arg::with_name("sign_only")
.long("sign-only")
.takes_value(false)
.help("Sign the transaction offline"),
)
.arg(
Arg::with_name("signer")
.long("signer")
.value_name("PUBKEY=BASE58_SIG")
.takes_value(true)
.validator(is_pubkey_sig)
.multiple(true)
.help("Provide a public-key/signature pair for the transaction"),
)
.arg(
Arg::with_name("blockhash")
.long("blockhash")
.value_name("BLOCKHASH")
.takes_value(true)
.validator(is_hash)
.help("Use the supplied blockhash"),
),
)
.subcommand(
SubCommand::with_name("stake-authorize-staker")
@ -176,6 +201,29 @@ impl StakeSubCommands for App<'_, '_> {
.required(true)
.help("Stake account to be deactivated.")
)
.arg(
Arg::with_name("sign_only")
.long("sign-only")
.takes_value(false)
.help("Sign the transaction offline"),
)
.arg(
Arg::with_name("signer")
.long("signer")
.value_name("PUBKEY=BASE58_SIG")
.takes_value(true)
.validator(is_pubkey_sig)
.multiple(true)
.help("Provide a public-key/signature pair for the transaction"),
)
.arg(
Arg::with_name("blockhash")
.long("blockhash")
.value_name("BLOCKHASH")
.takes_value(true)
.validator(is_hash)
.help("Use the supplied blockhash"),
),
)
.subcommand(
SubCommand::with_name("withdraw-stake")
@ -293,10 +341,20 @@ pub fn parse_stake_delegate_stake(matches: &ArgMatches<'_>) -> Result<CliCommand
let stake_account_pubkey = pubkey_of(matches, "stake_account_pubkey").unwrap();
let vote_account_pubkey = pubkey_of(matches, "vote_account_pubkey").unwrap();
let force = matches.is_present("force");
let sign_only = matches.is_present("sign_only");
let signers = pubkeys_sigs_of(&matches, "signer");
let blockhash = value_of(matches, "blockhash");
Ok(CliCommandInfo {
command: CliCommand::DelegateStake(stake_account_pubkey, vote_account_pubkey, force),
require_keypair: true,
command: CliCommand::DelegateStake {
stake_account_pubkey,
vote_account_pubkey,
force,
sign_only,
signers,
blockhash,
},
require_keypair: !sign_only,
})
}
@ -328,9 +386,17 @@ pub fn parse_redeem_vote_credits(matches: &ArgMatches<'_>) -> Result<CliCommandI
pub fn parse_stake_deactivate_stake(matches: &ArgMatches<'_>) -> Result<CliCommandInfo, CliError> {
let stake_account_pubkey = pubkey_of(matches, "stake_account_pubkey").unwrap();
let sign_only = matches.is_present("sign_only");
let signers = pubkeys_sigs_of(&matches, "signer");
let blockhash = value_of(matches, "blockhash");
Ok(CliCommandInfo {
command: CliCommand::DeactivateStake(stake_account_pubkey),
require_keypair: true,
command: CliCommand::DeactivateStake {
stake_account_pubkey,
sign_only,
signers,
blockhash,
},
require_keypair: !sign_only,
})
}
@ -463,8 +529,12 @@ pub fn process_deactivate_stake_account(
rpc_client: &RpcClient,
config: &CliConfig,
stake_account_pubkey: &Pubkey,
sign_only: bool,
signers: &Option<Vec<(Pubkey, Signature)>>,
blockhash: Option<Hash>,
) -> ProcessResult {
let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?;
let (recent_blockhash, fee_calculator) =
get_blockhash_fee_calculator(rpc_client, sign_only, blockhash)?;
let ixs = vec![stake_instruction::deactivate_stake(
stake_account_pubkey,
&config.keypair.pubkey(),
@ -475,9 +545,16 @@ pub fn process_deactivate_stake_account(
&[&config.keypair],
recent_blockhash,
);
check_account_for_fee(rpc_client, config, &fee_calculator, &tx.message)?;
let result = rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair]);
log_instruction_custom_error::<StakeError>(result)
if let Some(signers) = signers {
replace_signatures(&mut tx, &signers)?;
}
if sign_only {
return_signers(&tx)
} else {
check_account_for_fee(rpc_client, config, &fee_calculator, &tx.message)?;
let result = rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair]);
log_instruction_custom_error::<StakeError>(result)
}
}
pub fn process_withdraw_stake(
@ -644,6 +721,9 @@ pub fn process_delegate_stake(
stake_account_pubkey: &Pubkey,
vote_account_pubkey: &Pubkey,
force: bool,
sign_only: bool,
signers: &Option<Vec<(Pubkey, Signature)>>,
blockhash: Option<Hash>,
) -> ProcessResult {
check_unique_pubkeys(
(&config.keypair.pubkey(), "cli keypair".to_string()),
@ -690,7 +770,8 @@ pub fn process_delegate_stake(
}
}
let (recent_blockhash, fee_calculator) = rpc_client.get_recent_blockhash()?;
let (recent_blockhash, fee_calculator) =
get_blockhash_fee_calculator(rpc_client, sign_only, blockhash)?;
let ixs = vec![stake_instruction::delegate_stake(
stake_account_pubkey,
@ -704,9 +785,16 @@ pub fn process_delegate_stake(
&[&config.keypair],
recent_blockhash,
);
check_account_for_fee(rpc_client, config, &fee_calculator, &tx.message)?;
let result = rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair]);
log_instruction_custom_error::<StakeError>(result)
if let Some(signers) = signers {
replace_signatures(&mut tx, &signers)?;
}
if sign_only {
return_signers(&tx)
} else {
check_account_for_fee(rpc_client, config, &fee_calculator, &tx.message)?;
let result = rpc_client.send_and_confirm_transaction(&mut tx, &[&config.keypair]);
log_instruction_custom_error::<StakeError>(result)
}
}
#[cfg(test)]
@ -831,18 +919,25 @@ mod tests {
);
// Test DelegateStake Subcommand
let stake_pubkey = Pubkey::new_rand();
let stake_pubkey_string = stake_pubkey.to_string();
let vote_account_pubkey = Pubkey::new_rand();
let vote_account_string = vote_account_pubkey.to_string();
let test_delegate_stake = test_commands.clone().get_matches_from(vec![
"test",
"delegate-stake",
&stake_pubkey_string,
&stake_account_string,
&vote_account_string,
]);
assert_eq!(
parse_command(&test_delegate_stake).unwrap(),
CliCommandInfo {
command: CliCommand::DelegateStake(stake_pubkey, stake_account_pubkey, false),
command: CliCommand::DelegateStake {
stake_account_pubkey,
vote_account_pubkey,
force: false,
sign_only: false,
signers: None,
blockhash: None
},
require_keypair: true
}
);
@ -851,13 +946,124 @@ mod tests {
"test",
"delegate-stake",
"--force",
&stake_pubkey_string,
&stake_account_string,
&vote_account_string,
]);
assert_eq!(
parse_command(&test_delegate_stake).unwrap(),
CliCommandInfo {
command: CliCommand::DelegateStake(stake_pubkey, stake_account_pubkey, true),
command: CliCommand::DelegateStake {
stake_account_pubkey,
vote_account_pubkey,
force: true,
sign_only: false,
signers: None,
blockhash: None
},
require_keypair: true
}
);
// Test Delegate Subcommand w/ Blockhash
let blockhash = Hash::default();
let blockhash_string = format!("{}", blockhash);
let test_delegate_stake = test_commands.clone().get_matches_from(vec![
"test",
"delegate-stake",
&stake_account_string,
&vote_account_string,
"--blockhash",
&blockhash_string,
]);
assert_eq!(
parse_command(&test_delegate_stake).unwrap(),
CliCommandInfo {
command: CliCommand::DelegateStake {
stake_account_pubkey,
vote_account_pubkey,
force: false,
sign_only: false,
signers: None,
blockhash: Some(blockhash)
},
require_keypair: true
}
);
let test_delegate_stake = test_commands.clone().get_matches_from(vec![
"test",
"delegate-stake",
&stake_account_string,
&vote_account_string,
"--sign-only",
]);
assert_eq!(
parse_command(&test_delegate_stake).unwrap(),
CliCommandInfo {
command: CliCommand::DelegateStake {
stake_account_pubkey,
vote_account_pubkey,
force: false,
sign_only: true,
signers: None,
blockhash: None
},
require_keypair: false
}
);
// Test Delegate Subcommand w/ signer
let key1 = Pubkey::new_rand();
let sig1 = Keypair::new().sign_message(&[0u8]);
let signer1 = format!("{}={}", key1, sig1);
let test_delegate_stake = test_commands.clone().get_matches_from(vec![
"test",
"delegate-stake",
&stake_account_string,
&vote_account_string,
"--signer",
&signer1,
]);
assert_eq!(
parse_command(&test_delegate_stake).unwrap(),
CliCommandInfo {
command: CliCommand::DelegateStake {
stake_account_pubkey,
vote_account_pubkey,
force: false,
sign_only: false,
signers: Some(vec![(key1, sig1)]),
blockhash: None
},
require_keypair: true
}
);
// Test Delegate Subcommand w/ signers
let key2 = Pubkey::new_rand();
let sig2 = Keypair::new().sign_message(&[0u8]);
let signer2 = format!("{}={}", key2, sig2);
let test_delegate_stake = test_commands.clone().get_matches_from(vec![
"test",
"delegate-stake",
&stake_account_string,
&vote_account_string,
"--signer",
&signer1,
"--signer",
&signer2,
]);
assert_eq!(
parse_command(&test_delegate_stake).unwrap(),
CliCommandInfo {
command: CliCommand::DelegateStake {
stake_account_pubkey,
vote_account_pubkey,
force: false,
sign_only: false,
signers: Some(vec![(key1, sig1), (key2, sig2)]),
blockhash: None
},
require_keypair: true
}
);
@ -866,7 +1072,7 @@ mod tests {
let test_withdraw_stake = test_commands.clone().get_matches_from(vec![
"test",
"withdraw-stake",
&stake_pubkey_string,
&stake_account_string,
&stake_account_string,
"42",
"lamports",
@ -875,7 +1081,7 @@ mod tests {
assert_eq!(
parse_command(&test_withdraw_stake).unwrap(),
CliCommandInfo {
command: CliCommand::WithdrawStake(stake_pubkey, stake_account_pubkey, 42),
command: CliCommand::WithdrawStake(stake_account_pubkey, stake_account_pubkey, 42),
require_keypair: true
}
);
@ -884,12 +1090,109 @@ mod tests {
let test_deactivate_stake = test_commands.clone().get_matches_from(vec![
"test",
"deactivate-stake",
&stake_pubkey_string,
&stake_account_string,
]);
assert_eq!(
parse_command(&test_deactivate_stake).unwrap(),
CliCommandInfo {
command: CliCommand::DeactivateStake(stake_pubkey),
command: CliCommand::DeactivateStake {
stake_account_pubkey,
sign_only: false,
signers: None,
blockhash: None
},
require_keypair: true
}
);
// Test Deactivate Subcommand w/ Blockhash
let blockhash = Hash::default();
let blockhash_string = format!("{}", blockhash);
let test_deactivate_stake = test_commands.clone().get_matches_from(vec![
"test",
"deactivate-stake",
&stake_account_string,
"--blockhash",
&blockhash_string,
]);
assert_eq!(
parse_command(&test_deactivate_stake).unwrap(),
CliCommandInfo {
command: CliCommand::DeactivateStake {
stake_account_pubkey,
sign_only: false,
signers: None,
blockhash: Some(blockhash)
},
require_keypair: true
}
);
let test_deactivate_stake = test_commands.clone().get_matches_from(vec![
"test",
"deactivate-stake",
&stake_account_string,
"--sign-only",
]);
assert_eq!(
parse_command(&test_deactivate_stake).unwrap(),
CliCommandInfo {
command: CliCommand::DeactivateStake {
stake_account_pubkey,
sign_only: true,
signers: None,
blockhash: None
},
require_keypair: false
}
);
// Test Deactivate Subcommand w/ signers
let key1 = Pubkey::new_rand();
let sig1 = Keypair::new().sign_message(&[0u8]);
let signer1 = format!("{}={}", key1, sig1);
let test_deactivate_stake = test_commands.clone().get_matches_from(vec![
"test",
"deactivate-stake",
&stake_account_string,
"--signer",
&signer1,
]);
assert_eq!(
parse_command(&test_deactivate_stake).unwrap(),
CliCommandInfo {
command: CliCommand::DeactivateStake {
stake_account_pubkey,
sign_only: false,
signers: Some(vec![(key1, sig1)]),
blockhash: None
},
require_keypair: true
}
);
// Test Deactivate Subcommand w/ signers
let key2 = Pubkey::new_rand();
let sig2 = Keypair::new().sign_message(&[0u8]);
let signer2 = format!("{}={}", key2, sig2);
let test_deactivate_stake = test_commands.clone().get_matches_from(vec![
"test",
"deactivate-stake",
&stake_account_string,
"--signer",
&signer1,
"--signer",
&signer2,
]);
assert_eq!(
parse_command(&test_deactivate_stake).unwrap(),
CliCommandInfo {
command: CliCommand::DeactivateStake {
stake_account_pubkey,
sign_only: false,
signers: Some(vec![(key1, sig1), (key2, sig2)]),
blockhash: None
},
require_keypair: true
}
);

View File

@ -3,9 +3,9 @@ use serde_json::Value;
use solana_cli::cli::{process_command, request_and_confirm_airdrop, CliCommand, CliConfig};
use solana_client::rpc_client::RpcClient;
use solana_drone::drone::run_local_drone;
use solana_sdk::pubkey::Pubkey;
use solana_sdk::signature::KeypairUtil;
use solana_sdk::{hash::Hash, pubkey::Pubkey, signature::KeypairUtil, signature::Signature};
use std::fs::remove_dir_all;
use std::str::FromStr;
use std::sync::mpsc::channel;
#[cfg(test)]
@ -71,6 +71,9 @@ fn test_cli_timestamp_tx() {
timestamp_pubkey: Some(config_witness.keypair.pubkey()),
witnesses: None,
cancelable: false,
sign_only: false,
signers: None,
blockhash: None,
};
let sig_response = process_command(&config_payer);
@ -138,6 +141,9 @@ fn test_cli_witness_tx() {
timestamp_pubkey: None,
witnesses: Some(vec![config_witness.keypair.pubkey()]),
cancelable: false,
sign_only: false,
signers: None,
blockhash: None,
};
let sig_response = process_command(&config_payer);
@ -198,6 +204,9 @@ fn test_cli_cancel_tx() {
timestamp_pubkey: None,
witnesses: Some(vec![config_witness.keypair.pubkey()]),
cancelable: true,
sign_only: false,
signers: None,
blockhash: None,
};
let sig_response = process_command(&config_payer).unwrap();
@ -223,3 +232,94 @@ fn test_cli_cancel_tx() {
server.close().unwrap();
remove_dir_all(ledger_path).unwrap();
}
#[test]
fn test_offline_pay_tx() {
let (server, leader_data, alice, ledger_path) = new_validator_for_tests();
let bob_pubkey = Pubkey::new_rand();
let (sender, receiver) = channel();
run_local_drone(alice, sender, None);
let drone_addr = receiver.recv().unwrap();
let rpc_client = RpcClient::new_socket(leader_data.rpc);
let mut config_offline = CliConfig::default();
config_offline.json_rpc_url =
format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port());
let mut config_online = CliConfig::default();
config_online.json_rpc_url =
format!("http://{}:{}", leader_data.rpc.ip(), leader_data.rpc.port());
assert_ne!(
config_offline.keypair.pubkey(),
config_online.keypair.pubkey()
);
request_and_confirm_airdrop(
&rpc_client,
&drone_addr,
&config_offline.keypair.pubkey(),
50,
)
.unwrap();
request_and_confirm_airdrop(
&rpc_client,
&drone_addr,
&config_online.keypair.pubkey(),
50,
)
.unwrap();
check_balance(50, &rpc_client, &config_offline.keypair.pubkey());
check_balance(50, &rpc_client, &config_online.keypair.pubkey());
config_offline.command = CliCommand::Pay {
lamports: 10,
to: bob_pubkey,
timestamp: None,
timestamp_pubkey: None,
witnesses: None,
cancelable: false,
sign_only: true,
signers: None,
blockhash: None,
};
let sig_response = process_command(&config_offline).unwrap();
check_balance(50, &rpc_client, &config_offline.keypair.pubkey());
check_balance(50, &rpc_client, &config_online.keypair.pubkey());
check_balance(0, &rpc_client, &bob_pubkey);
let object: Value = serde_json::from_str(&sig_response).unwrap();
let blockhash_str = object.get("blockhash").unwrap().as_str().unwrap();
let signer_strings = object.get("signers").unwrap().as_array().unwrap();
let signers: Vec<_> = signer_strings
.iter()
.map(|signer_string| {
let mut signer = signer_string.as_str().unwrap().split('=');
let key = Pubkey::from_str(signer.next().unwrap()).unwrap();
let sig = Signature::from_str(signer.next().unwrap()).unwrap();
(key, sig)
})
.collect();
config_online.command = CliCommand::Pay {
lamports: 10,
to: bob_pubkey,
timestamp: None,
timestamp_pubkey: None,
witnesses: None,
cancelable: false,
sign_only: false,
signers: Some(signers),
blockhash: Some(blockhash_str.parse::<Hash>().unwrap()),
};
process_command(&config_online).unwrap();
check_balance(40, &rpc_client, &config_offline.keypair.pubkey());
check_balance(50, &rpc_client, &config_online.keypair.pubkey());
check_balance(10, &rpc_client, &bob_pubkey);
server.close().unwrap();
remove_dir_all(ledger_path).unwrap();
}

View File

@ -117,7 +117,7 @@ impl Instruction {
pub struct AccountMeta {
/// An account's public key
pub pubkey: Pubkey,
/// True if an Instruciton requires a Transaction signature matching `pubkey`.
/// True if an Instruction requires a Transaction signature matching `pubkey`.
pub is_signer: bool,
/// True if the `pubkey` can be loaded as a read-write account.
pub is_writable: bool,

View File

@ -51,6 +51,9 @@ pub enum TransactionError {
/// Transaction contains an invalid account reference
InvalidAccountIndex,
/// Transaction did not pass signature verification
SignatureFailure,
}
pub type Result<T> = result::Result<T, TransactionError>;
@ -225,6 +228,21 @@ impl Transaction {
}
}
/// Verify the transaction
pub fn verify(&self) -> Result<()> {
if !self
.signatures
.iter()
.zip(&self.message.account_keys)
.map(|(signature, pubkey)| signature.verify(pubkey.as_ref(), &self.message_data()))
.all(|verify_result| verify_result)
{
Err(TransactionError::SignatureFailure)
} else {
Ok(())
}
}
/// Get the positions of the pubkeys in `account_keys` associated with signing keypairs
pub fn get_signing_keypair_positions<T: KeypairUtil>(
&self,
@ -246,6 +264,27 @@ impl Transaction {
.collect())
}
/// Replace all the signatures and pubkeys
pub fn replace_signatures(&mut self, signers: &[(Pubkey, Signature)]) -> Result<()> {
let num_required_signatures = self.message.header.num_required_signatures as usize;
if signers.len() != num_required_signatures
|| self.signatures.len() != num_required_signatures
|| self.message.account_keys.len() < num_required_signatures
{
return Err(TransactionError::InvalidAccountIndex);
}
signers
.iter()
.enumerate()
.for_each(|(i, (pubkey, signature))| {
self.signatures[i] = *signature;
self.message.account_keys[i] = *pubkey;
});
self.verify()
}
pub fn is_signed(&self) -> bool {
self.signatures
.iter()