CLI: Add multi-session signing support (#8927)
* SDK: Add `NullSigner` implementation * SDK: Split `Transaction::verify()` to gain access to results * CLI: Minor refactor of --sign_only result parsing * CLI: Enable paritial signing Signers specified by pubkey, but without a matching --signer arg supplied fall back to a `NullSigner` when --sign-only is in effect. This allows their pubkey to be used for TX construction as usual, but leaves their `sign_message()` a NOP. As such, with --sign-only in effect, signing and verification must be done separately, with the latter's per-signature results considered * CLI: Surface/report missing/bad signers to user * CLI: Suppress --sign-only JSON output * nits * Docs for multi-session offline signing
This commit is contained in:
parent
aeb7278b00
commit
98228c392e
|
@ -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 bip39::{Language, Mnemonic, Seed};
|
||||||
use clap::ArgMatches;
|
use clap::ArgMatches;
|
||||||
use rpassword::prompt_password_stderr;
|
use rpassword::prompt_password_stderr;
|
||||||
|
@ -10,7 +14,7 @@ use solana_sdk::{
|
||||||
pubkey::Pubkey,
|
pubkey::Pubkey,
|
||||||
signature::{
|
signature::{
|
||||||
keypair_from_seed, keypair_from_seed_phrase_and_passphrase, read_keypair,
|
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::{
|
use std::{
|
||||||
|
@ -101,6 +105,8 @@ pub fn signer_from_path(
|
||||||
.and_then(|presigners| presigner_from_pubkey_sigs(&pubkey, presigners));
|
.and_then(|presigners| presigner_from_pubkey_sigs(&pubkey, presigners));
|
||||||
if let Some(presigner) = presigner {
|
if let Some(presigner) = presigner {
|
||||||
Ok(Box::new(presigner))
|
Ok(Box::new(presigner))
|
||||||
|
} else if matches.is_present(SIGN_ONLY_ARG.name) {
|
||||||
|
Ok(Box::new(NullSigner::new(&pubkey)))
|
||||||
} else {
|
} else {
|
||||||
Err(std::io::Error::new(
|
Err(std::io::Error::new(
|
||||||
std::io::ErrorKind::Other,
|
std::io::ErrorKind::Other,
|
||||||
|
|
|
@ -1019,17 +1019,31 @@ pub fn get_blockhash_and_fee_calculator(
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn return_signers(tx: &Transaction) -> ProcessResult {
|
pub fn return_signers(tx: &Transaction) -> ProcessResult {
|
||||||
println_signers(tx);
|
let verify_results = tx.verify_with_results();
|
||||||
let signers: Vec<_> = tx
|
let mut signers = Vec::new();
|
||||||
.signatures
|
let mut absent = Vec::new();
|
||||||
|
let mut bad_sig = Vec::new();
|
||||||
|
tx.signatures
|
||||||
.iter()
|
.iter()
|
||||||
.zip(tx.message.account_keys.clone())
|
.zip(tx.message.account_keys.iter())
|
||||||
.map(|(signature, pubkey)| format!("{}={}", pubkey, signature))
|
.zip(verify_results.into_iter())
|
||||||
.collect();
|
.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!({
|
Ok(json!({
|
||||||
"blockhash": tx.message.recent_blockhash.to_string(),
|
"blockhash": tx.message.recent_blockhash.to_string(),
|
||||||
"signers": &signers,
|
"signers": &signers,
|
||||||
|
"absent": &absent,
|
||||||
|
"badSig": &bad_sig,
|
||||||
})
|
})
|
||||||
.to_string())
|
.to_string())
|
||||||
}
|
}
|
||||||
|
@ -1314,11 +1328,12 @@ fn process_pay(
|
||||||
Message::new(&[ix])
|
Message::new(&[ix])
|
||||||
};
|
};
|
||||||
let mut tx = Transaction::new_unsigned(message);
|
let mut tx = Transaction::new_unsigned(message);
|
||||||
tx.try_sign(&config.signers, blockhash)?;
|
|
||||||
|
|
||||||
if sign_only {
|
if sign_only {
|
||||||
|
tx.try_partial_sign(&config.signers, blockhash)?;
|
||||||
return_signers(&tx)
|
return_signers(&tx)
|
||||||
} else {
|
} else {
|
||||||
|
tx.try_sign(&config.signers, blockhash)?;
|
||||||
if let Some(nonce_account) = &nonce_account {
|
if let Some(nonce_account) = &nonce_account {
|
||||||
let nonce_account = rpc_client.get_account(nonce_account)?;
|
let nonce_account = rpc_client.get_account(nonce_account)?;
|
||||||
check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &blockhash)?;
|
check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &blockhash)?;
|
||||||
|
@ -1354,10 +1369,11 @@ fn process_pay(
|
||||||
);
|
);
|
||||||
let message = Message::new(&ixs);
|
let message = Message::new(&ixs);
|
||||||
let mut tx = Transaction::new_unsigned(message);
|
let mut tx = Transaction::new_unsigned(message);
|
||||||
tx.try_sign(&[config.signers[0], &contract_state], blockhash)?;
|
|
||||||
if sign_only {
|
if sign_only {
|
||||||
|
tx.try_partial_sign(&[config.signers[0], &contract_state], blockhash)?;
|
||||||
return_signers(&tx)
|
return_signers(&tx)
|
||||||
} else {
|
} else {
|
||||||
|
tx.try_sign(&[config.signers[0], &contract_state], blockhash)?;
|
||||||
check_account_for_fee(
|
check_account_for_fee(
|
||||||
rpc_client,
|
rpc_client,
|
||||||
&config.signers[0].pubkey(),
|
&config.signers[0].pubkey(),
|
||||||
|
@ -1399,10 +1415,11 @@ fn process_pay(
|
||||||
);
|
);
|
||||||
let message = Message::new(&ixs);
|
let message = Message::new(&ixs);
|
||||||
let mut tx = Transaction::new_unsigned(message);
|
let mut tx = Transaction::new_unsigned(message);
|
||||||
tx.try_sign(&[config.signers[0], &contract_state], blockhash)?;
|
|
||||||
if sign_only {
|
if sign_only {
|
||||||
|
tx.try_partial_sign(&[config.signers[0], &contract_state], blockhash)?;
|
||||||
return_signers(&tx)
|
return_signers(&tx)
|
||||||
} else {
|
} else {
|
||||||
|
tx.try_sign(&[config.signers[0], &contract_state], blockhash)?;
|
||||||
let result = rpc_client.send_and_confirm_transaction_with_spinner(
|
let result = rpc_client.send_and_confirm_transaction_with_spinner(
|
||||||
&mut tx,
|
&mut tx,
|
||||||
&[config.signers[0], &contract_state],
|
&[config.signers[0], &contract_state],
|
||||||
|
@ -1509,11 +1526,12 @@ fn process_transfer(
|
||||||
Message::new_with_payer(&ixs, Some(&fee_payer.pubkey()))
|
Message::new_with_payer(&ixs, Some(&fee_payer.pubkey()))
|
||||||
};
|
};
|
||||||
let mut tx = Transaction::new_unsigned(message);
|
let mut tx = Transaction::new_unsigned(message);
|
||||||
tx.try_sign(&config.signers, recent_blockhash)?;
|
|
||||||
|
|
||||||
if sign_only {
|
if sign_only {
|
||||||
|
tx.try_partial_sign(&config.signers, recent_blockhash)?;
|
||||||
return_signers(&tx)
|
return_signers(&tx)
|
||||||
} else {
|
} else {
|
||||||
|
tx.try_sign(&config.signers, recent_blockhash)?;
|
||||||
if let Some(nonce_account) = &nonce_account {
|
if let Some(nonce_account) = &nonce_account {
|
||||||
let nonce_account = rpc_client.get_account(nonce_account)?;
|
let nonce_account = rpc_client.get_account(nonce_account)?;
|
||||||
check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &recent_blockhash)?;
|
check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &recent_blockhash)?;
|
||||||
|
@ -2543,7 +2561,9 @@ mod tests {
|
||||||
use solana_client::mock_rpc_client_request::SIGNATURE;
|
use solana_client::mock_rpc_client_request::SIGNATURE;
|
||||||
use solana_sdk::{
|
use solana_sdk::{
|
||||||
pubkey::Pubkey,
|
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,
|
transaction::TransactionError,
|
||||||
};
|
};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
@ -3671,4 +3691,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<Pubkey, SignerError> {
|
||||||
|
Ok(self.pubkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_sign_message(&self, _message: &[u8]) -> Result<Signature, SignerError> {
|
||||||
|
Ok(Signature::new(&[1u8; 64]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let present: Box<dyn Signer> = Box::new(keypair_from_seed(&[2u8; 32]).unwrap());
|
||||||
|
let absent: Box<dyn Signer> = Box::new(NullSigner::new(&Pubkey::new(&[3u8; 32])));
|
||||||
|
let bad: Box<dyn Signer> = 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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::cli::SettingType;
|
use crate::cli::SettingType;
|
||||||
use console::style;
|
use console::style;
|
||||||
use solana_sdk::transaction::Transaction;
|
use solana_sdk::hash::Hash;
|
||||||
|
|
||||||
// Pretty print a "name value"
|
// Pretty print a "name value"
|
||||||
pub fn println_name_value(name: &str, value: &str) {
|
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!();
|
||||||
println!("Blockhash: {}", tx.message.recent_blockhash);
|
println!("Blockhash: {}", blockhash);
|
||||||
println!("Signers (Pubkey=Signature):");
|
if !signers.is_empty() {
|
||||||
tx.signatures
|
println!("Signers (Pubkey=Signature):");
|
||||||
.iter()
|
signers.iter().for_each(|signer| println!(" {}", signer))
|
||||||
.zip(tx.message.account_keys.clone())
|
}
|
||||||
.for_each(|(signature, pubkey)| println!(" {:?}={:?}", pubkey, signature));
|
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!();
|
println!();
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,8 @@ use clap::{crate_description, crate_name, AppSettings, Arg, ArgGroup, ArgMatches
|
||||||
use console::style;
|
use console::style;
|
||||||
|
|
||||||
use solana_clap_utils::{
|
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::{
|
use solana_cli::{
|
||||||
cli::{app, parse_command, process_command, CliCommandInfo, CliConfig, CliSigners},
|
cli::{app, parse_command, process_command, CliCommandInfo, CliConfig, CliSigners},
|
||||||
|
@ -242,7 +243,13 @@ fn do_main(matches: &ArgMatches<'_>) -> Result<(), Box<dyn error::Error>> {
|
||||||
let (mut config, signers) = parse_args(&matches, wallet_manager)?;
|
let (mut config, signers) = parse_args(&matches, wallet_manager)?;
|
||||||
config.signers = signers.iter().map(|s| s.as_ref()).collect();
|
config.signers = signers.iter().map(|s| s.as_ref()).collect();
|
||||||
let result = process_command(&config)?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,10 +6,16 @@ use serde_json::Value;
|
||||||
use solana_clap_utils::{
|
use solana_clap_utils::{
|
||||||
input_parsers::{pubkey_of, value_of},
|
input_parsers::{pubkey_of, value_of},
|
||||||
input_validators::{is_hash, is_pubkey_sig},
|
input_validators::{is_hash, is_pubkey_sig},
|
||||||
|
keypair::presigner_from_pubkey_sigs,
|
||||||
offline::{BLOCKHASH_ARG, SIGNER_ARG, SIGN_ONLY_ARG},
|
offline::{BLOCKHASH_ARG, SIGNER_ARG, SIGN_ONLY_ARG},
|
||||||
};
|
};
|
||||||
use solana_client::rpc_client::RpcClient;
|
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;
|
use std::str::FromStr;
|
||||||
|
|
||||||
fn blockhash_arg<'a, 'b>() -> Arg<'a, 'b> {
|
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<Pubkey>,
|
||||||
|
pub bad_signers: Vec<Pubkey>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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> {
|
||||||
|
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 object: Value = serde_json::from_str(&reply).unwrap();
|
||||||
let blockhash_str = object.get("blockhash").unwrap().as_str().unwrap();
|
let blockhash_str = object.get("blockhash").unwrap().as_str().unwrap();
|
||||||
let blockhash = blockhash_str.parse::<Hash>().unwrap();
|
let blockhash = blockhash_str.parse::<Hash>().unwrap();
|
||||||
let signer_strings = object.get("signers").unwrap().as_array().unwrap();
|
let signer_strings = object.get("signers").unwrap().as_array().unwrap();
|
||||||
let signers = signer_strings
|
let present_signers = signer_strings
|
||||||
.iter()
|
.iter()
|
||||||
.map(|signer_string| {
|
.map(|signer_string| {
|
||||||
let mut signer = signer_string.as_str().unwrap().split('=');
|
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)
|
(key, sig)
|
||||||
})
|
})
|
||||||
.collect();
|
.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,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -845,11 +845,12 @@ pub fn process_create_stake_account(
|
||||||
Message::new_with_payer(&ixs, Some(&fee_payer.pubkey()))
|
Message::new_with_payer(&ixs, Some(&fee_payer.pubkey()))
|
||||||
};
|
};
|
||||||
let mut tx = Transaction::new_unsigned(message);
|
let mut tx = Transaction::new_unsigned(message);
|
||||||
tx.try_sign(&config.signers, recent_blockhash)?;
|
|
||||||
|
|
||||||
if sign_only {
|
if sign_only {
|
||||||
|
tx.try_partial_sign(&config.signers, recent_blockhash)?;
|
||||||
return_signers(&tx)
|
return_signers(&tx)
|
||||||
} else {
|
} else {
|
||||||
|
tx.try_sign(&config.signers, recent_blockhash)?;
|
||||||
if let Some(nonce_account) = &nonce_account {
|
if let Some(nonce_account) = &nonce_account {
|
||||||
let nonce_account = rpc_client.get_account(nonce_account)?;
|
let nonce_account = rpc_client.get_account(nonce_account)?;
|
||||||
check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &recent_blockhash)?;
|
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()))
|
Message::new_with_payer(&ixs, Some(&fee_payer.pubkey()))
|
||||||
};
|
};
|
||||||
let mut tx = Transaction::new_unsigned(message);
|
let mut tx = Transaction::new_unsigned(message);
|
||||||
tx.try_sign(&config.signers, recent_blockhash)?;
|
|
||||||
|
|
||||||
if sign_only {
|
if sign_only {
|
||||||
|
tx.try_partial_sign(&config.signers, recent_blockhash)?;
|
||||||
return_signers(&tx)
|
return_signers(&tx)
|
||||||
} else {
|
} else {
|
||||||
|
tx.try_sign(&config.signers, recent_blockhash)?;
|
||||||
if let Some(nonce_account) = &nonce_account {
|
if let Some(nonce_account) = &nonce_account {
|
||||||
let nonce_account = rpc_client.get_account(nonce_account)?;
|
let nonce_account = rpc_client.get_account(nonce_account)?;
|
||||||
check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &recent_blockhash)?;
|
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()))
|
Message::new_with_payer(&ixs, Some(&fee_payer.pubkey()))
|
||||||
};
|
};
|
||||||
let mut tx = Transaction::new_unsigned(message);
|
let mut tx = Transaction::new_unsigned(message);
|
||||||
tx.try_sign(&config.signers, recent_blockhash)?;
|
|
||||||
|
|
||||||
if sign_only {
|
if sign_only {
|
||||||
|
tx.try_partial_sign(&config.signers, recent_blockhash)?;
|
||||||
return_signers(&tx)
|
return_signers(&tx)
|
||||||
} else {
|
} else {
|
||||||
|
tx.try_sign(&config.signers, recent_blockhash)?;
|
||||||
if let Some(nonce_account) = &nonce_account {
|
if let Some(nonce_account) = &nonce_account {
|
||||||
let nonce_account = rpc_client.get_account(nonce_account)?;
|
let nonce_account = rpc_client.get_account(nonce_account)?;
|
||||||
check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &recent_blockhash)?;
|
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()))
|
Message::new_with_payer(&ixs, Some(&fee_payer.pubkey()))
|
||||||
};
|
};
|
||||||
let mut tx = Transaction::new_unsigned(message);
|
let mut tx = Transaction::new_unsigned(message);
|
||||||
tx.try_sign(&config.signers, recent_blockhash)?;
|
|
||||||
|
|
||||||
if sign_only {
|
if sign_only {
|
||||||
|
tx.try_partial_sign(&config.signers, recent_blockhash)?;
|
||||||
return_signers(&tx)
|
return_signers(&tx)
|
||||||
} else {
|
} else {
|
||||||
|
tx.try_sign(&config.signers, recent_blockhash)?;
|
||||||
if let Some(nonce_account) = &nonce_account {
|
if let Some(nonce_account) = &nonce_account {
|
||||||
let nonce_account = rpc_client.get_account(nonce_account)?;
|
let nonce_account = rpc_client.get_account(nonce_account)?;
|
||||||
check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &recent_blockhash)?;
|
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()))
|
Message::new_with_payer(&ixs, Some(&fee_payer.pubkey()))
|
||||||
};
|
};
|
||||||
let mut tx = Transaction::new_unsigned(message);
|
let mut tx = Transaction::new_unsigned(message);
|
||||||
tx.try_sign(&config.signers, recent_blockhash)?;
|
|
||||||
|
|
||||||
if sign_only {
|
if sign_only {
|
||||||
|
tx.try_partial_sign(&config.signers, recent_blockhash)?;
|
||||||
return_signers(&tx)
|
return_signers(&tx)
|
||||||
} else {
|
} else {
|
||||||
|
tx.try_sign(&config.signers, recent_blockhash)?;
|
||||||
if let Some(nonce_account) = &nonce_account {
|
if let Some(nonce_account) = &nonce_account {
|
||||||
let nonce_account = rpc_client.get_account(nonce_account)?;
|
let nonce_account = rpc_client.get_account(nonce_account)?;
|
||||||
check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &recent_blockhash)?;
|
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()))
|
Message::new_with_payer(&ixs, Some(&fee_payer.pubkey()))
|
||||||
};
|
};
|
||||||
let mut tx = Transaction::new_unsigned(message);
|
let mut tx = Transaction::new_unsigned(message);
|
||||||
tx.try_sign(&config.signers, recent_blockhash)?;
|
|
||||||
|
|
||||||
if sign_only {
|
if sign_only {
|
||||||
|
tx.try_partial_sign(&config.signers, recent_blockhash)?;
|
||||||
return_signers(&tx)
|
return_signers(&tx)
|
||||||
} else {
|
} else {
|
||||||
|
tx.try_sign(&config.signers, recent_blockhash)?;
|
||||||
if let Some(nonce_account) = &nonce_account {
|
if let Some(nonce_account) = &nonce_account {
|
||||||
let nonce_account = rpc_client.get_account(nonce_account)?;
|
let nonce_account = rpc_client.get_account(nonce_account)?;
|
||||||
check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &recent_blockhash)?;
|
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()))
|
Message::new_with_payer(&ixs, Some(&fee_payer.pubkey()))
|
||||||
};
|
};
|
||||||
let mut tx = Transaction::new_unsigned(message);
|
let mut tx = Transaction::new_unsigned(message);
|
||||||
tx.try_sign(&config.signers, recent_blockhash)?;
|
|
||||||
|
|
||||||
if sign_only {
|
if sign_only {
|
||||||
|
tx.try_partial_sign(&config.signers, recent_blockhash)?;
|
||||||
return_signers(&tx)
|
return_signers(&tx)
|
||||||
} else {
|
} else {
|
||||||
|
tx.try_sign(&config.signers, recent_blockhash)?;
|
||||||
if let Some(nonce_account) = &nonce_account {
|
if let Some(nonce_account) = &nonce_account {
|
||||||
let nonce_account = rpc_client.get_account(nonce_account)?;
|
let nonce_account = rpc_client.get_account(nonce_account)?;
|
||||||
check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &recent_blockhash)?;
|
check_nonce_account(&nonce_account, &nonce_authority.pubkey(), &recent_blockhash)?;
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
use chrono::prelude::*;
|
use chrono::prelude::*;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use solana_clap_utils::keypair::presigner_from_pubkey_sigs;
|
|
||||||
use solana_cli::{
|
use solana_cli::{
|
||||||
cli::{process_command, request_and_confirm_airdrop, CliCommand, CliConfig, PayCommand},
|
cli::{process_command, request_and_confirm_airdrop, CliCommand, CliConfig, PayCommand},
|
||||||
nonce,
|
nonce,
|
||||||
|
@ -335,9 +334,11 @@ fn test_offline_pay_tx() {
|
||||||
check_balance(50, &rpc_client, &config_online.signers[0].pubkey());
|
check_balance(50, &rpc_client, &config_online.signers[0].pubkey());
|
||||||
check_balance(0, &rpc_client, &bob_pubkey);
|
check_balance(0, &rpc_client, &bob_pubkey);
|
||||||
|
|
||||||
let (blockhash, signers) = parse_sign_only_reply_string(&sig_response);
|
let sign_only = parse_sign_only_reply_string(&sig_response);
|
||||||
let offline_presigner =
|
assert!(sign_only.has_all_signers());
|
||||||
presigner_from_pubkey_sigs(&config_offline.signers[0].pubkey(), &signers).unwrap();
|
let offline_presigner = sign_only
|
||||||
|
.presigner_of(&config_offline.signers[0].pubkey())
|
||||||
|
.unwrap();
|
||||||
let online_pubkey = config_online.signers[0].pubkey();
|
let online_pubkey = config_online.signers[0].pubkey();
|
||||||
config_online.signers = vec![&offline_presigner];
|
config_online.signers = vec![&offline_presigner];
|
||||||
config_online.command = CliCommand::Pay(PayCommand {
|
config_online.command = CliCommand::Pay(PayCommand {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
use solana_clap_utils::keypair::presigner_from_pubkey_sigs;
|
|
||||||
use solana_cli::{
|
use solana_cli::{
|
||||||
cli::{process_command, request_and_confirm_airdrop, CliCommand, CliConfig},
|
cli::{process_command, request_and_confirm_airdrop, CliCommand, CliConfig},
|
||||||
nonce,
|
nonce,
|
||||||
|
@ -388,9 +387,11 @@ fn test_offline_stake_delegation_and_deactivation() {
|
||||||
fee_payer: 0,
|
fee_payer: 0,
|
||||||
};
|
};
|
||||||
let sig_response = process_command(&config_offline).unwrap();
|
let sig_response = process_command(&config_offline).unwrap();
|
||||||
let (blockhash, signers) = parse_sign_only_reply_string(&sig_response);
|
let sign_only = parse_sign_only_reply_string(&sig_response);
|
||||||
let offline_presigner =
|
assert!(sign_only.has_all_signers());
|
||||||
presigner_from_pubkey_sigs(&config_offline.signers[0].pubkey(), &signers).unwrap();
|
let offline_presigner = sign_only
|
||||||
|
.presigner_of(&config_offline.signers[0].pubkey())
|
||||||
|
.unwrap();
|
||||||
config_payer.signers = vec![&offline_presigner];
|
config_payer.signers = vec![&offline_presigner];
|
||||||
config_payer.command = CliCommand::DelegateStake {
|
config_payer.command = CliCommand::DelegateStake {
|
||||||
stake_account_pubkey: stake_keypair.pubkey(),
|
stake_account_pubkey: stake_keypair.pubkey(),
|
||||||
|
@ -417,9 +418,11 @@ fn test_offline_stake_delegation_and_deactivation() {
|
||||||
fee_payer: 0,
|
fee_payer: 0,
|
||||||
};
|
};
|
||||||
let sig_response = process_command(&config_offline).unwrap();
|
let sig_response = process_command(&config_offline).unwrap();
|
||||||
let (blockhash, signers) = parse_sign_only_reply_string(&sig_response);
|
let sign_only = parse_sign_only_reply_string(&sig_response);
|
||||||
let offline_presigner =
|
assert!(sign_only.has_all_signers());
|
||||||
presigner_from_pubkey_sigs(&config_offline.signers[0].pubkey(), &signers).unwrap();
|
let offline_presigner = sign_only
|
||||||
|
.presigner_of(&config_offline.signers[0].pubkey())
|
||||||
|
.unwrap();
|
||||||
config_payer.signers = vec![&offline_presigner];
|
config_payer.signers = vec![&offline_presigner];
|
||||||
config_payer.command = CliCommand::DeactivateStake {
|
config_payer.command = CliCommand::DeactivateStake {
|
||||||
stake_account_pubkey: stake_keypair.pubkey(),
|
stake_account_pubkey: stake_keypair.pubkey(),
|
||||||
|
@ -679,9 +682,9 @@ fn test_stake_authorize() {
|
||||||
fee_payer: 0,
|
fee_payer: 0,
|
||||||
};
|
};
|
||||||
let sign_reply = process_command(&config_offline).unwrap();
|
let sign_reply = process_command(&config_offline).unwrap();
|
||||||
let (blockhash, signers) = parse_sign_only_reply_string(&sign_reply);
|
let sign_only = parse_sign_only_reply_string(&sign_reply);
|
||||||
let offline_presigner =
|
assert!(sign_only.has_all_signers());
|
||||||
presigner_from_pubkey_sigs(&offline_authority_pubkey, &signers).unwrap();
|
let offline_presigner = sign_only.presigner_of(&offline_authority_pubkey).unwrap();
|
||||||
config.signers = vec![&offline_presigner];
|
config.signers = vec![&offline_presigner];
|
||||||
config.command = CliCommand::StakeAuthorize {
|
config.command = CliCommand::StakeAuthorize {
|
||||||
stake_account_pubkey,
|
stake_account_pubkey,
|
||||||
|
@ -739,12 +742,11 @@ fn test_stake_authorize() {
|
||||||
fee_payer: 0,
|
fee_payer: 0,
|
||||||
};
|
};
|
||||||
let sign_reply = process_command(&config_offline).unwrap();
|
let sign_reply = process_command(&config_offline).unwrap();
|
||||||
let (blockhash, signers) = parse_sign_only_reply_string(&sign_reply);
|
let sign_only = parse_sign_only_reply_string(&sign_reply);
|
||||||
assert_eq!(blockhash, nonce_hash);
|
assert!(sign_only.has_all_signers());
|
||||||
let offline_presigner =
|
assert_eq!(sign_only.blockhash, nonce_hash);
|
||||||
presigner_from_pubkey_sigs(&offline_authority_pubkey, &signers).unwrap();
|
let offline_presigner = sign_only.presigner_of(&offline_authority_pubkey).unwrap();
|
||||||
let nonced_authority_presigner =
|
let nonced_authority_presigner = sign_only.presigner_of(&nonced_authority_pubkey).unwrap();
|
||||||
presigner_from_pubkey_sigs(&nonced_authority_pubkey, &signers).unwrap();
|
|
||||||
config.signers = vec![&offline_presigner, &nonced_authority_presigner];
|
config.signers = vec![&offline_presigner, &nonced_authority_presigner];
|
||||||
config.command = CliCommand::StakeAuthorize {
|
config.command = CliCommand::StakeAuthorize {
|
||||||
stake_account_pubkey,
|
stake_account_pubkey,
|
||||||
|
@ -754,7 +756,7 @@ fn test_stake_authorize() {
|
||||||
sign_only: false,
|
sign_only: false,
|
||||||
blockhash_query: BlockhashQuery::FeeCalculator(
|
blockhash_query: BlockhashQuery::FeeCalculator(
|
||||||
blockhash_query::Source::NonceAccount(nonce_account.pubkey()),
|
blockhash_query::Source::NonceAccount(nonce_account.pubkey()),
|
||||||
blockhash,
|
sign_only.blockhash,
|
||||||
),
|
),
|
||||||
nonce_account: Some(nonce_account.pubkey()),
|
nonce_account: Some(nonce_account.pubkey()),
|
||||||
nonce_authority: 0,
|
nonce_authority: 0,
|
||||||
|
@ -816,6 +818,7 @@ fn test_stake_authorize_with_fee_payer() {
|
||||||
let mut config_offline = CliConfig::default();
|
let mut config_offline = CliConfig::default();
|
||||||
let offline_signer = Keypair::new();
|
let offline_signer = Keypair::new();
|
||||||
config_offline.signers = vec![&offline_signer];
|
config_offline.signers = vec![&offline_signer];
|
||||||
|
config_offline.json_rpc_url = String::new();
|
||||||
let offline_pubkey = config_offline.signers[0].pubkey();
|
let offline_pubkey = config_offline.signers[0].pubkey();
|
||||||
// Verify we're offline
|
// Verify we're offline
|
||||||
config_offline.command = CliCommand::ClusterVersion;
|
config_offline.command = CliCommand::ClusterVersion;
|
||||||
|
@ -886,8 +889,9 @@ fn test_stake_authorize_with_fee_payer() {
|
||||||
fee_payer: 0,
|
fee_payer: 0,
|
||||||
};
|
};
|
||||||
let sign_reply = process_command(&config_offline).unwrap();
|
let sign_reply = process_command(&config_offline).unwrap();
|
||||||
let (blockhash, signers) = parse_sign_only_reply_string(&sign_reply);
|
let sign_only = parse_sign_only_reply_string(&sign_reply);
|
||||||
let offline_presigner = presigner_from_pubkey_sigs(&offline_pubkey, &signers).unwrap();
|
assert!(sign_only.has_all_signers());
|
||||||
|
let offline_presigner = sign_only.presigner_of(&offline_pubkey).unwrap();
|
||||||
config.signers = vec![&offline_presigner];
|
config.signers = vec![&offline_presigner];
|
||||||
config.command = CliCommand::StakeAuthorize {
|
config.command = CliCommand::StakeAuthorize {
|
||||||
stake_account_pubkey,
|
stake_account_pubkey,
|
||||||
|
@ -1023,8 +1027,9 @@ fn test_stake_split() {
|
||||||
fee_payer: 0,
|
fee_payer: 0,
|
||||||
};
|
};
|
||||||
let sig_response = process_command(&config_offline).unwrap();
|
let sig_response = process_command(&config_offline).unwrap();
|
||||||
let (blockhash, signers) = parse_sign_only_reply_string(&sig_response);
|
let sign_only = parse_sign_only_reply_string(&sig_response);
|
||||||
let offline_presigner = presigner_from_pubkey_sigs(&offline_pubkey, &signers).unwrap();
|
assert!(sign_only.has_all_signers());
|
||||||
|
let offline_presigner = sign_only.presigner_of(&offline_pubkey).unwrap();
|
||||||
config.signers = vec![&offline_presigner, &split_account];
|
config.signers = vec![&offline_presigner, &split_account];
|
||||||
config.command = CliCommand::SplitStake {
|
config.command = CliCommand::SplitStake {
|
||||||
stake_account_pubkey: stake_account_pubkey,
|
stake_account_pubkey: stake_account_pubkey,
|
||||||
|
@ -1032,7 +1037,7 @@ fn test_stake_split() {
|
||||||
sign_only: false,
|
sign_only: false,
|
||||||
blockhash_query: BlockhashQuery::FeeCalculator(
|
blockhash_query: BlockhashQuery::FeeCalculator(
|
||||||
blockhash_query::Source::NonceAccount(nonce_account.pubkey()),
|
blockhash_query::Source::NonceAccount(nonce_account.pubkey()),
|
||||||
blockhash,
|
sign_only.blockhash,
|
||||||
),
|
),
|
||||||
nonce_account: Some(nonce_account.pubkey()),
|
nonce_account: Some(nonce_account.pubkey()),
|
||||||
nonce_authority: 0,
|
nonce_authority: 0,
|
||||||
|
@ -1275,8 +1280,9 @@ fn test_stake_set_lockup() {
|
||||||
fee_payer: 0,
|
fee_payer: 0,
|
||||||
};
|
};
|
||||||
let sig_response = process_command(&config_offline).unwrap();
|
let sig_response = process_command(&config_offline).unwrap();
|
||||||
let (blockhash, signers) = parse_sign_only_reply_string(&sig_response);
|
let sign_only = parse_sign_only_reply_string(&sig_response);
|
||||||
let offline_presigner = presigner_from_pubkey_sigs(&offline_pubkey, &signers).unwrap();
|
assert!(sign_only.has_all_signers());
|
||||||
|
let offline_presigner = sign_only.presigner_of(&offline_pubkey).unwrap();
|
||||||
config.signers = vec![&offline_presigner];
|
config.signers = vec![&offline_presigner];
|
||||||
config.command = CliCommand::StakeSetLockup {
|
config.command = CliCommand::StakeSetLockup {
|
||||||
stake_account_pubkey,
|
stake_account_pubkey,
|
||||||
|
@ -1285,7 +1291,7 @@ fn test_stake_set_lockup() {
|
||||||
sign_only: false,
|
sign_only: false,
|
||||||
blockhash_query: BlockhashQuery::FeeCalculator(
|
blockhash_query: BlockhashQuery::FeeCalculator(
|
||||||
blockhash_query::Source::NonceAccount(nonce_account_pubkey),
|
blockhash_query::Source::NonceAccount(nonce_account_pubkey),
|
||||||
blockhash,
|
sign_only.blockhash,
|
||||||
),
|
),
|
||||||
nonce_account: Some(nonce_account_pubkey),
|
nonce_account: Some(nonce_account_pubkey),
|
||||||
nonce_authority: 0,
|
nonce_authority: 0,
|
||||||
|
@ -1392,9 +1398,10 @@ fn test_offline_nonced_create_stake_account_and_withdraw() {
|
||||||
from: 0,
|
from: 0,
|
||||||
};
|
};
|
||||||
let sig_response = process_command(&config_offline).unwrap();
|
let sig_response = process_command(&config_offline).unwrap();
|
||||||
let (blockhash, signers) = parse_sign_only_reply_string(&sig_response);
|
let sign_only = parse_sign_only_reply_string(&sig_response);
|
||||||
let offline_presigner = presigner_from_pubkey_sigs(&offline_pubkey, &signers).unwrap();
|
assert!(sign_only.has_all_signers());
|
||||||
let stake_presigner = presigner_from_pubkey_sigs(&stake_pubkey, &signers).unwrap();
|
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.signers = vec![&offline_presigner, &stake_presigner];
|
||||||
config.command = CliCommand::CreateStakeAccount {
|
config.command = CliCommand::CreateStakeAccount {
|
||||||
stake_account: 1,
|
stake_account: 1,
|
||||||
|
@ -1406,7 +1413,7 @@ fn test_offline_nonced_create_stake_account_and_withdraw() {
|
||||||
sign_only: false,
|
sign_only: false,
|
||||||
blockhash_query: BlockhashQuery::FeeCalculator(
|
blockhash_query: BlockhashQuery::FeeCalculator(
|
||||||
blockhash_query::Source::NonceAccount(nonce_pubkey),
|
blockhash_query::Source::NonceAccount(nonce_pubkey),
|
||||||
blockhash,
|
sign_only.blockhash,
|
||||||
),
|
),
|
||||||
nonce_account: Some(nonce_pubkey),
|
nonce_account: Some(nonce_pubkey),
|
||||||
nonce_authority: 0,
|
nonce_authority: 0,
|
||||||
|
@ -1438,8 +1445,8 @@ fn test_offline_nonced_create_stake_account_and_withdraw() {
|
||||||
fee_payer: 0,
|
fee_payer: 0,
|
||||||
};
|
};
|
||||||
let sig_response = process_command(&config_offline).unwrap();
|
let sig_response = process_command(&config_offline).unwrap();
|
||||||
let (blockhash, signers) = parse_sign_only_reply_string(&sig_response);
|
let sign_only = parse_sign_only_reply_string(&sig_response);
|
||||||
let offline_presigner = presigner_from_pubkey_sigs(&offline_pubkey, &signers).unwrap();
|
let offline_presigner = sign_only.presigner_of(&offline_pubkey).unwrap();
|
||||||
config.signers = vec![&offline_presigner];
|
config.signers = vec![&offline_presigner];
|
||||||
config.command = CliCommand::WithdrawStake {
|
config.command = CliCommand::WithdrawStake {
|
||||||
stake_account_pubkey: stake_pubkey,
|
stake_account_pubkey: stake_pubkey,
|
||||||
|
@ -1449,7 +1456,7 @@ fn test_offline_nonced_create_stake_account_and_withdraw() {
|
||||||
sign_only: false,
|
sign_only: false,
|
||||||
blockhash_query: BlockhashQuery::FeeCalculator(
|
blockhash_query: BlockhashQuery::FeeCalculator(
|
||||||
blockhash_query::Source::NonceAccount(nonce_pubkey),
|
blockhash_query::Source::NonceAccount(nonce_pubkey),
|
||||||
blockhash,
|
sign_only.blockhash,
|
||||||
),
|
),
|
||||||
nonce_account: Some(nonce_pubkey),
|
nonce_account: Some(nonce_pubkey),
|
||||||
nonce_authority: 0,
|
nonce_authority: 0,
|
||||||
|
@ -1482,9 +1489,9 @@ fn test_offline_nonced_create_stake_account_and_withdraw() {
|
||||||
from: 0,
|
from: 0,
|
||||||
};
|
};
|
||||||
let sig_response = process_command(&config_offline).unwrap();
|
let sig_response = process_command(&config_offline).unwrap();
|
||||||
let (blockhash, signers) = parse_sign_only_reply_string(&sig_response);
|
let sign_only = parse_sign_only_reply_string(&sig_response);
|
||||||
let offline_presigner = presigner_from_pubkey_sigs(&offline_pubkey, &signers).unwrap();
|
let offline_presigner = sign_only.presigner_of(&offline_pubkey).unwrap();
|
||||||
let stake_presigner = presigner_from_pubkey_sigs(&stake_pubkey, &signers).unwrap();
|
let stake_presigner = sign_only.presigner_of(&stake_pubkey).unwrap();
|
||||||
config.signers = vec![&offline_presigner, &stake_presigner];
|
config.signers = vec![&offline_presigner, &stake_presigner];
|
||||||
config.command = CliCommand::CreateStakeAccount {
|
config.command = CliCommand::CreateStakeAccount {
|
||||||
stake_account: 1,
|
stake_account: 1,
|
||||||
|
@ -1496,7 +1503,7 @@ fn test_offline_nonced_create_stake_account_and_withdraw() {
|
||||||
sign_only: false,
|
sign_only: false,
|
||||||
blockhash_query: BlockhashQuery::FeeCalculator(
|
blockhash_query: BlockhashQuery::FeeCalculator(
|
||||||
blockhash_query::Source::NonceAccount(nonce_pubkey),
|
blockhash_query::Source::NonceAccount(nonce_pubkey),
|
||||||
blockhash,
|
sign_only.blockhash,
|
||||||
),
|
),
|
||||||
nonce_account: Some(nonce_pubkey),
|
nonce_account: Some(nonce_pubkey),
|
||||||
nonce_authority: 0,
|
nonce_authority: 0,
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
use solana_clap_utils::keypair::presigner_from_pubkey_sigs;
|
|
||||||
use solana_cli::{
|
use solana_cli::{
|
||||||
cli::{process_command, request_and_confirm_airdrop, CliCommand, CliConfig},
|
cli::{process_command, request_and_confirm_airdrop, CliCommand, CliConfig},
|
||||||
nonce,
|
nonce,
|
||||||
|
@ -13,7 +12,7 @@ use solana_faucet::faucet::run_local_faucet;
|
||||||
use solana_sdk::{
|
use solana_sdk::{
|
||||||
nonce::State as NonceState,
|
nonce::State as NonceState,
|
||||||
pubkey::Pubkey,
|
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};
|
use std::{fs::remove_dir_all, sync::mpsc::channel, thread::sleep, time::Duration};
|
||||||
|
|
||||||
|
@ -102,8 +101,9 @@ fn test_transfer() {
|
||||||
fee_payer: 0,
|
fee_payer: 0,
|
||||||
};
|
};
|
||||||
let sign_only_reply = process_command(&offline).unwrap();
|
let sign_only_reply = process_command(&offline).unwrap();
|
||||||
let (blockhash, signers) = parse_sign_only_reply_string(&sign_only_reply);
|
let sign_only = parse_sign_only_reply_string(&sign_only_reply);
|
||||||
let offline_presigner = presigner_from_pubkey_sigs(&offline_pubkey, &signers).unwrap();
|
assert!(sign_only.has_all_signers());
|
||||||
|
let offline_presigner = sign_only.presigner_of(&offline_pubkey).unwrap();
|
||||||
config.signers = vec![&offline_presigner];
|
config.signers = vec![&offline_presigner];
|
||||||
config.command = CliCommand::Transfer {
|
config.command = CliCommand::Transfer {
|
||||||
lamports: 10,
|
lamports: 10,
|
||||||
|
@ -193,8 +193,9 @@ fn test_transfer() {
|
||||||
fee_payer: 0,
|
fee_payer: 0,
|
||||||
};
|
};
|
||||||
let sign_only_reply = process_command(&offline).unwrap();
|
let sign_only_reply = process_command(&offline).unwrap();
|
||||||
let (blockhash, signers) = parse_sign_only_reply_string(&sign_only_reply);
|
let sign_only = parse_sign_only_reply_string(&sign_only_reply);
|
||||||
let offline_presigner = presigner_from_pubkey_sigs(&offline_pubkey, &signers).unwrap();
|
assert!(sign_only.has_all_signers());
|
||||||
|
let offline_presigner = sign_only.presigner_of(&offline_pubkey).unwrap();
|
||||||
config.signers = vec![&offline_presigner];
|
config.signers = vec![&offline_presigner];
|
||||||
config.command = CliCommand::Transfer {
|
config.command = CliCommand::Transfer {
|
||||||
lamports: 10,
|
lamports: 10,
|
||||||
|
@ -203,7 +204,7 @@ fn test_transfer() {
|
||||||
sign_only: false,
|
sign_only: false,
|
||||||
blockhash_query: BlockhashQuery::FeeCalculator(
|
blockhash_query: BlockhashQuery::FeeCalculator(
|
||||||
blockhash_query::Source::NonceAccount(nonce_account.pubkey()),
|
blockhash_query::Source::NonceAccount(nonce_account.pubkey()),
|
||||||
blockhash,
|
sign_only.blockhash,
|
||||||
),
|
),
|
||||||
nonce_account: Some(nonce_account.pubkey()),
|
nonce_account: Some(nonce_account.pubkey()),
|
||||||
nonce_authority: 0,
|
nonce_authority: 0,
|
||||||
|
@ -216,3 +217,114 @@ fn test_transfer() {
|
||||||
server.close().unwrap();
|
server.close().unwrap();
|
||||||
remove_dir_all(ledger_path).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();
|
||||||
|
}
|
||||||
|
|
|
@ -82,6 +82,72 @@ Output
|
||||||
4vC38p4bz7XyiXrk6HtaooUqwxTWKocf45cstASGtmrD398biNJnmTcUCVEojE7wVQvgdYbjHJqRFZPpzfCQpmUN
|
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
|
## Buying More Time to Sign
|
||||||
|
|
||||||
Typically a Solana transaction must be signed and accepted by the network within
|
Typically a Solana transaction must be signed and accepted by the network within
|
||||||
|
|
|
@ -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<Pubkey, SignerError> {
|
||||||
|
Ok(self.pubkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn try_sign_message(&self, _message: &[u8]) -> Result<Signature, SignerError> {
|
||||||
|
Ok(Signature::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> PartialEq<T> for NullSigner
|
||||||
|
where
|
||||||
|
T: Signer,
|
||||||
|
{
|
||||||
|
fn eq(&self, other: &T) -> bool {
|
||||||
|
self.pubkey == other.pubkey()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn read_keypair<R: Read>(reader: &mut R) -> Result<Keypair, Box<dyn error::Error>> {
|
pub fn read_keypair<R: Read>(reader: &mut R) -> Result<Keypair, Box<dyn error::Error>> {
|
||||||
let bytes: Vec<u8> = serde_json::from_reader(reader)?;
|
let bytes: Vec<u8> = serde_json::from_reader(reader)?;
|
||||||
let dalek_keypair = ed25519_dalek::Keypair::from_bytes(&bytes)
|
let dalek_keypair = ed25519_dalek::Keypair::from_bytes(&bytes)
|
||||||
|
|
|
@ -300,14 +300,20 @@ impl Transaction {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Verify the transaction
|
pub fn verify_with_results(&self) -> Vec<bool> {
|
||||||
pub fn verify(&self) -> Result<()> {
|
self.signatures
|
||||||
if !self
|
|
||||||
.signatures
|
|
||||||
.iter()
|
.iter()
|
||||||
.zip(&self.message.account_keys)
|
.zip(&self.message.account_keys)
|
||||||
.map(|(signature, pubkey)| signature.verify(pubkey.as_ref(), &self.message_data()))
|
.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)
|
Err(TransactionError::SignatureFailure)
|
||||||
} else {
|
} else {
|
||||||
|
|
Loading…
Reference in New Issue