Improve solana deploy (#12621)
* Check program account before attempting to create it * Use last_valid_slot to timeout status checks * Include transaction history in RpcClient::get_signature_statuses requests * Improve solana-deploy send-transactions * Clippy * Improve mock deploy test * Review comments
This commit is contained in:
parent
978b26a9c5
commit
19f385db76
209
cli/src/cli.rs
209
cli/src/cli.rs
|
@ -29,7 +29,8 @@ use solana_client::{
|
||||||
nonce_utils,
|
nonce_utils,
|
||||||
rpc_client::RpcClient,
|
rpc_client::RpcClient,
|
||||||
rpc_config::{RpcLargestAccountsFilter, RpcSendTransactionConfig},
|
rpc_config::{RpcLargestAccountsFilter, RpcSendTransactionConfig},
|
||||||
rpc_response::{Response, RpcKeyedAccount},
|
rpc_request::MAX_GET_SIGNATURE_STATUSES_QUERY_ITEMS,
|
||||||
|
rpc_response::RpcKeyedAccount,
|
||||||
};
|
};
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
use solana_faucet::faucet::request_airdrop_transaction;
|
use solana_faucet::faucet::request_airdrop_transaction;
|
||||||
|
@ -42,7 +43,7 @@ use solana_sdk::{
|
||||||
commitment_config::CommitmentConfig,
|
commitment_config::CommitmentConfig,
|
||||||
decode_error::DecodeError,
|
decode_error::DecodeError,
|
||||||
hash::Hash,
|
hash::Hash,
|
||||||
instruction::InstructionError,
|
instruction::{Instruction, InstructionError},
|
||||||
loader_instruction,
|
loader_instruction,
|
||||||
message::Message,
|
message::Message,
|
||||||
pubkey::{Pubkey, MAX_SEED_LEN},
|
pubkey::{Pubkey, MAX_SEED_LEN},
|
||||||
|
@ -1021,6 +1022,7 @@ fn send_and_confirm_transactions_with_spinner<T: Signers>(
|
||||||
mut transactions: Vec<Transaction>,
|
mut transactions: Vec<Transaction>,
|
||||||
signer_keys: &T,
|
signer_keys: &T,
|
||||||
commitment: CommitmentConfig,
|
commitment: CommitmentConfig,
|
||||||
|
mut last_valid_slot: Slot,
|
||||||
) -> Result<(), Box<dyn error::Error>> {
|
) -> Result<(), Box<dyn error::Error>> {
|
||||||
let progress_bar = new_spinner_progress_bar();
|
let progress_bar = new_spinner_progress_bar();
|
||||||
let mut send_retries = 5;
|
let mut send_retries = 5;
|
||||||
|
@ -1028,7 +1030,7 @@ fn send_and_confirm_transactions_with_spinner<T: Signers>(
|
||||||
let mut status_retries = 15;
|
let mut status_retries = 15;
|
||||||
|
|
||||||
// Send all transactions
|
// Send all transactions
|
||||||
let mut transactions_signatures = vec![];
|
let mut pending_transactions = HashMap::new();
|
||||||
let num_transactions = transactions.len();
|
let num_transactions = transactions.len();
|
||||||
for transaction in transactions {
|
for transaction in transactions {
|
||||||
if cfg!(not(test)) {
|
if cfg!(not(test)) {
|
||||||
|
@ -1038,7 +1040,7 @@ fn send_and_confirm_transactions_with_spinner<T: Signers>(
|
||||||
sleep(Duration::from_millis(1000 / DEFAULT_TICKS_PER_SECOND));
|
sleep(Duration::from_millis(1000 / DEFAULT_TICKS_PER_SECOND));
|
||||||
}
|
}
|
||||||
|
|
||||||
let signature = rpc_client
|
let _result = rpc_client
|
||||||
.send_transaction_with_config(
|
.send_transaction_with_config(
|
||||||
&transaction,
|
&transaction,
|
||||||
RpcSendTransactionConfig {
|
RpcSendTransactionConfig {
|
||||||
|
@ -1047,11 +1049,11 @@ fn send_and_confirm_transactions_with_spinner<T: Signers>(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.ok();
|
.ok();
|
||||||
transactions_signatures.push((transaction, signature));
|
pending_transactions.insert(transaction.signatures[0], transaction);
|
||||||
|
|
||||||
progress_bar.set_message(&format!(
|
progress_bar.set_message(&format!(
|
||||||
"[{}/{}] Transactions sent",
|
"[{}/{}] Transactions sent",
|
||||||
transactions_signatures.len(),
|
pending_transactions.len(),
|
||||||
num_transactions
|
num_transactions
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
@ -1062,34 +1064,45 @@ fn send_and_confirm_transactions_with_spinner<T: Signers>(
|
||||||
|
|
||||||
progress_bar.set_message(&format!(
|
progress_bar.set_message(&format!(
|
||||||
"[{}/{}] Transactions confirmed",
|
"[{}/{}] Transactions confirmed",
|
||||||
num_transactions - transactions_signatures.len(),
|
num_transactions - pending_transactions.len(),
|
||||||
num_transactions
|
num_transactions
|
||||||
));
|
));
|
||||||
|
|
||||||
|
let mut statuses = vec![];
|
||||||
|
let pending_signatures = pending_transactions.keys().cloned().collect::<Vec<_>>();
|
||||||
|
for pending_signatures_chunk in
|
||||||
|
pending_signatures.chunks(MAX_GET_SIGNATURE_STATUSES_QUERY_ITEMS - 1)
|
||||||
|
{
|
||||||
|
statuses.extend(
|
||||||
|
rpc_client
|
||||||
|
.get_signature_statuses_with_history(pending_signatures_chunk)?
|
||||||
|
.value
|
||||||
|
.into_iter(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
assert_eq!(statuses.len(), pending_signatures.len());
|
||||||
|
|
||||||
|
for (signature, status) in pending_signatures.into_iter().zip(statuses.into_iter()) {
|
||||||
|
if let Some(status) = status {
|
||||||
|
if status.confirmations.is_none() || status.confirmations.unwrap() > 1 {
|
||||||
|
let _ = pending_transactions.remove(&signature);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if pending_transactions.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let slot = rpc_client.get_slot_with_commitment(commitment)?;
|
||||||
|
if slot > last_valid_slot {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if cfg!(not(test)) {
|
if cfg!(not(test)) {
|
||||||
// Retry twice a second
|
// Retry twice a second
|
||||||
sleep(Duration::from_millis(500));
|
sleep(Duration::from_millis(500));
|
||||||
}
|
}
|
||||||
|
|
||||||
transactions_signatures = transactions_signatures
|
|
||||||
.into_iter()
|
|
||||||
.filter(|(_transaction, signature)| {
|
|
||||||
signature
|
|
||||||
.and_then(|signature| rpc_client.get_signature_statuses(&[signature]).ok())
|
|
||||||
.map(|Response { context: _, value }| match &value[0] {
|
|
||||||
None => true,
|
|
||||||
Some(transaction_status) => {
|
|
||||||
!(transaction_status.confirmations.is_none()
|
|
||||||
|| transaction_status.confirmations.unwrap() > 1)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap_or(true)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if transactions_signatures.is_empty() {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if send_retries == 0 {
|
if send_retries == 0 {
|
||||||
|
@ -1098,10 +1111,12 @@ fn send_and_confirm_transactions_with_spinner<T: Signers>(
|
||||||
send_retries -= 1;
|
send_retries -= 1;
|
||||||
|
|
||||||
// Re-sign any failed transactions with a new blockhash and retry
|
// Re-sign any failed transactions with a new blockhash and retry
|
||||||
let (blockhash, _fee_calculator) = rpc_client
|
let (blockhash, _fee_calculator, new_last_valid_slot) = rpc_client
|
||||||
.get_new_blockhash(&transactions_signatures[0].0.message().recent_blockhash)?;
|
.get_recent_blockhash_with_commitment(commitment)?
|
||||||
|
.value;
|
||||||
|
last_valid_slot = new_last_valid_slot;
|
||||||
transactions = vec![];
|
transactions = vec![];
|
||||||
for (mut transaction, _) in transactions_signatures.into_iter() {
|
for (_, mut transaction) in pending_transactions.into_iter() {
|
||||||
transaction.try_sign(signer_keys, blockhash)?;
|
transaction.try_sign(signer_keys, blockhash)?;
|
||||||
transactions.push(transaction);
|
transactions.push(transaction);
|
||||||
}
|
}
|
||||||
|
@ -1135,24 +1150,70 @@ fn process_deploy(
|
||||||
bpf_loader::id()
|
bpf_loader::id()
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let minimum_balance = rpc_client.get_minimum_balance_for_rent_exemption(program_data.len())?;
|
||||||
|
let signers = [config.signers[0], program_id];
|
||||||
|
|
||||||
|
// Check program account to see if partial initialization has occurred
|
||||||
|
let initial_instructions = if let Some(account) = rpc_client
|
||||||
|
.get_account_with_commitment(&program_id.pubkey(), config.commitment)?
|
||||||
|
.value
|
||||||
|
{
|
||||||
|
let mut instructions: Vec<Instruction> = vec![];
|
||||||
|
if account.executable {
|
||||||
|
return Err(CliError::DynamicProgramError(
|
||||||
|
"Program account is already executable".to_string(),
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
if account.owner != loader_id && !system_program::check_id(&account.owner) {
|
||||||
|
return Err(CliError::DynamicProgramError(
|
||||||
|
"Program account is already owned by another account".to_string(),
|
||||||
|
)
|
||||||
|
.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
if account.data.is_empty() && system_program::check_id(&account.owner) {
|
||||||
|
instructions.push(system_instruction::allocate(
|
||||||
|
&program_id.pubkey(),
|
||||||
|
program_data.len() as u64,
|
||||||
|
));
|
||||||
|
if account.owner != loader_id {
|
||||||
|
instructions.push(system_instruction::assign(&program_id.pubkey(), &loader_id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if account.lamports < minimum_balance {
|
||||||
|
instructions.push(system_instruction::transfer(
|
||||||
|
&config.signers[0].pubkey(),
|
||||||
|
&program_id.pubkey(),
|
||||||
|
minimum_balance - account.lamports,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
instructions
|
||||||
|
} else {
|
||||||
|
vec![system_instruction::create_account(
|
||||||
|
&config.signers[0].pubkey(),
|
||||||
|
&program_id.pubkey(),
|
||||||
|
minimum_balance,
|
||||||
|
program_data.len() as u64,
|
||||||
|
&loader_id,
|
||||||
|
)]
|
||||||
|
};
|
||||||
|
let initial_message = if !initial_instructions.is_empty() {
|
||||||
|
Some(Message::new(
|
||||||
|
&initial_instructions,
|
||||||
|
Some(&config.signers[0].pubkey()),
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
// Build transactions to calculate fees
|
// Build transactions to calculate fees
|
||||||
let mut messages: Vec<&Message> = Vec::new();
|
let mut messages: Vec<&Message> = Vec::new();
|
||||||
let (blockhash, fee_calculator, _) = rpc_client
|
|
||||||
.get_recent_blockhash_with_commitment(config.commitment)?
|
if let Some(message) = &initial_message {
|
||||||
.value;
|
messages.push(message);
|
||||||
let minimum_balance = rpc_client.get_minimum_balance_for_rent_exemption(program_data.len())?;
|
}
|
||||||
let ix = system_instruction::create_account(
|
|
||||||
&config.signers[0].pubkey(),
|
|
||||||
&program_id.pubkey(),
|
|
||||||
minimum_balance.max(1),
|
|
||||||
program_data.len() as u64,
|
|
||||||
&loader_id,
|
|
||||||
);
|
|
||||||
let message = Message::new(&[ix], Some(&config.signers[0].pubkey()));
|
|
||||||
let mut create_account_tx = Transaction::new_unsigned(message);
|
|
||||||
let signers = [config.signers[0], program_id];
|
|
||||||
create_account_tx.try_sign(&signers, blockhash)?;
|
|
||||||
messages.push(&create_account_tx.message);
|
|
||||||
let mut write_messages = vec![];
|
let mut write_messages = vec![];
|
||||||
for (chunk, i) in program_data.chunks(DATA_CHUNK_SIZE).zip(0..) {
|
for (chunk, i) in program_data.chunks(DATA_CHUNK_SIZE).zip(0..) {
|
||||||
let instruction = loader_instruction::write(
|
let instruction = loader_instruction::write(
|
||||||
|
@ -1174,6 +1235,10 @@ fn process_deploy(
|
||||||
let finalize_message = Message::new(&[instruction], Some(&signers[0].pubkey()));
|
let finalize_message = Message::new(&[instruction], Some(&signers[0].pubkey()));
|
||||||
messages.push(&finalize_message);
|
messages.push(&finalize_message);
|
||||||
|
|
||||||
|
let (blockhash, fee_calculator, _) = rpc_client
|
||||||
|
.get_recent_blockhash_with_commitment(config.commitment)?
|
||||||
|
.value;
|
||||||
|
|
||||||
check_account_for_multiple_fees_with_commitment(
|
check_account_for_multiple_fees_with_commitment(
|
||||||
rpc_client,
|
rpc_client,
|
||||||
&config.signers[0].pubkey(),
|
&config.signers[0].pubkey(),
|
||||||
|
@ -1182,17 +1247,31 @@ fn process_deploy(
|
||||||
config.commitment,
|
config.commitment,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
trace!("Creating program account");
|
if let Some(message) = initial_message {
|
||||||
let result = rpc_client.send_and_confirm_transaction_with_spinner_and_config(
|
trace!("Creating or modifying program account");
|
||||||
&create_account_tx,
|
let num_required_signatures = message.header.num_required_signatures;
|
||||||
config.commitment,
|
|
||||||
config.send_transaction_config,
|
|
||||||
);
|
|
||||||
log_instruction_custom_error::<SystemError>(result, &config).map_err(|_| {
|
|
||||||
CliError::DynamicProgramError("Program account allocation failed".to_string())
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let (blockhash, _, _) = rpc_client
|
let mut initial_transaction = Transaction::new_unsigned(message);
|
||||||
|
// Most of the initial_transaction combinations require both the fee-payer and new program
|
||||||
|
// account to sign the transaction. One (transfer) only requires the fee-payer signature.
|
||||||
|
// This check is to ensure signing does not fail on a KeypairPubkeyMismatch error from an
|
||||||
|
// extraneous signature.
|
||||||
|
if num_required_signatures == 2 {
|
||||||
|
initial_transaction.try_sign(&signers, blockhash)?;
|
||||||
|
} else {
|
||||||
|
initial_transaction.try_sign(&[signers[0]], blockhash)?;
|
||||||
|
}
|
||||||
|
let result = rpc_client.send_and_confirm_transaction_with_spinner_and_config(
|
||||||
|
&initial_transaction,
|
||||||
|
config.commitment,
|
||||||
|
config.send_transaction_config,
|
||||||
|
);
|
||||||
|
log_instruction_custom_error::<SystemError>(result, &config).map_err(|_| {
|
||||||
|
CliError::DynamicProgramError("Program account allocation failed".to_string())
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let (blockhash, _, last_valid_slot) = rpc_client
|
||||||
.get_recent_blockhash_with_commitment(config.commitment)?
|
.get_recent_blockhash_with_commitment(config.commitment)?
|
||||||
.value;
|
.value;
|
||||||
|
|
||||||
|
@ -1209,6 +1288,7 @@ fn process_deploy(
|
||||||
write_transactions,
|
write_transactions,
|
||||||
&signers,
|
&signers,
|
||||||
config.commitment,
|
config.commitment,
|
||||||
|
last_valid_slot,
|
||||||
)
|
)
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
CliError::DynamicProgramError("Data writes to program account failed".to_string())
|
CliError::DynamicProgramError("Data writes to program account failed".to_string())
|
||||||
|
@ -2225,7 +2305,12 @@ pub fn app<'ab, 'v>(name: &str, about: &'ab str, version: &'v str) -> App<'ab, '
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
use solana_client::{blockhash_query, mock_sender::SIGNATURE};
|
use solana_client::{
|
||||||
|
blockhash_query,
|
||||||
|
mock_sender::SIGNATURE,
|
||||||
|
rpc_request::RpcRequest,
|
||||||
|
rpc_response::{Response, RpcResponseContext},
|
||||||
|
};
|
||||||
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, Presigner},
|
||||||
|
@ -2797,7 +2882,15 @@ mod tests {
|
||||||
|
|
||||||
// Success case
|
// Success case
|
||||||
let mut config = CliConfig::default();
|
let mut config = CliConfig::default();
|
||||||
config.rpc_client = Some(RpcClient::new_mock("deploy_succeeds".to_string()));
|
let account_info_response = json!(Response {
|
||||||
|
context: RpcResponseContext { slot: 1 },
|
||||||
|
value: Value::Null,
|
||||||
|
});
|
||||||
|
let mut mocks = HashMap::new();
|
||||||
|
mocks.insert(RpcRequest::GetAccountInfo, account_info_response);
|
||||||
|
let rpc_client = RpcClient::new_mock_with_mocks("".to_string(), mocks);
|
||||||
|
|
||||||
|
config.rpc_client = Some(rpc_client);
|
||||||
let default_keypair = Keypair::new();
|
let default_keypair = Keypair::new();
|
||||||
config.signers = vec![&default_keypair];
|
config.signers = vec![&default_keypair];
|
||||||
|
|
||||||
|
|
|
@ -94,9 +94,15 @@ impl RpcSender for MockSender {
|
||||||
err,
|
err,
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
let statuses: Vec<Option<TransactionStatus>> = params.as_array().unwrap()[0]
|
||||||
|
.as_array()
|
||||||
|
.unwrap()
|
||||||
|
.iter()
|
||||||
|
.map(|_| status.clone())
|
||||||
|
.collect();
|
||||||
serde_json::to_value(Response {
|
serde_json::to_value(Response {
|
||||||
context: RpcResponseContext { slot: 1 },
|
context: RpcResponseContext { slot: 1 },
|
||||||
value: vec![status],
|
value: statuses,
|
||||||
})?
|
})?
|
||||||
}
|
}
|
||||||
RpcRequest::GetTransactionCount => Value::Number(Number::from(1234)),
|
RpcRequest::GetTransactionCount => Value::Number(Number::from(1234)),
|
||||||
|
|
|
@ -164,6 +164,19 @@ impl RpcClient {
|
||||||
self.send(RpcRequest::GetSignatureStatuses, json!([signatures]))
|
self.send(RpcRequest::GetSignatureStatuses, json!([signatures]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_signature_statuses_with_history(
|
||||||
|
&self,
|
||||||
|
signatures: &[Signature],
|
||||||
|
) -> RpcResult<Vec<Option<TransactionStatus>>> {
|
||||||
|
let signatures: Vec<_> = signatures.iter().map(|s| s.to_string()).collect();
|
||||||
|
self.send(
|
||||||
|
RpcRequest::GetSignatureStatuses,
|
||||||
|
json!([signatures, {
|
||||||
|
"searchTransactionHistory": true
|
||||||
|
}]),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_signature_status_with_commitment(
|
pub fn get_signature_status_with_commitment(
|
||||||
&self,
|
&self,
|
||||||
signature: &Signature,
|
signature: &Signature,
|
||||||
|
|
Loading…
Reference in New Issue