Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Retry on blockhash not found #5

Merged
merged 4 commits into from
Mar 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
members = [
"libs/*"
]
resolver = "2"

[profile.release]
lto = true
164 changes: 141 additions & 23 deletions libs/marinade-client-rs/src/transactions/transaction_executors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@ use solana_client::client_error::ClientError as SolanaClientError;
use solana_client::client_error::ClientErrorKind;
use solana_client::rpc_client::RpcClient;
use solana_client::rpc_config::{RpcSendTransactionConfig, RpcSimulateTransactionConfig};
use solana_client::rpc_request::RpcError::ForUser;
use solana_client::rpc_request::{RpcError, RpcResponseErrorData};
use solana_client::rpc_response::{RpcResult, RpcSimulateTransactionResult};
use solana_sdk::commitment_config::{CommitmentConfig, CommitmentLevel};
use solana_sdk::signature::Signature;
use solana_sdk::signer::Signer;
use solana_sdk::transaction::TransactionError;
use std::ops::Deref;

pub fn log_execution(
Expand Down Expand Up @@ -55,8 +57,11 @@ pub trait TransactionSimulator {
impl<'a, C: Deref<Target = impl Signer> + Clone> TransactionSimulator for RequestBuilder<'a, C> {
fn simulate(&self, rpc_client: &RpcClient) -> RpcResult<RpcSimulateTransactionResult> {
let tx = self.signed_transaction().map_err(|err| {
error!("Cannot build transactions from builder: {:?}", err);
RpcError::ForUser(format!("Request builder transaction error: {}", err))
error!(
"RequestBuilder#simulate: cannot build transactions from builder: {:?}",
err
);
ForUser(format!("Building transaction error: {}", err))
})?;
rpc_client.simulate_transaction(&tx)
}
Expand Down Expand Up @@ -202,6 +207,7 @@ pub fn execute_transaction_builder(
blockhash_commitment: CommitmentLevel,
simulate: bool,
print_only: bool,
blockhash_failure_retries: Option<u16>,
) -> anyhow::Result<()> {
warn_text_simulate_print_only(simulate, print_only);

Expand Down Expand Up @@ -247,11 +253,12 @@ pub fn execute_transaction_builder(
}
} else {
for mut prepared_transaction in transaction_builder.sequence_combined() {
let execution_result = execute_prepared_transaction(
let execution_result = execute_prepared_transaction_blockhash_retry(
&mut prepared_transaction,
rpc_client,
preflight_config,
blockhash_commitment,
blockhash_failure_retries,
);
log_execution(&execution_result)?;
}
Expand All @@ -260,6 +267,107 @@ pub fn execute_transaction_builder(
Ok(())
}

fn execute_prepared_transaction_internal(
prepared_transaction: &mut PreparedTransaction,
rpc_client: &RpcClient,
preflight_config: RpcSendTransactionConfig,
) -> Result<Signature, solana_client::client_error::ClientError> {
let latest_hash = rpc_client.get_latest_blockhash()?;
let tx = prepared_transaction.sign(latest_hash).map_err(|e| {
error!(
"execute_prepared_transaction: error signing transaction with blockhash: {}: {:?}",
latest_hash, e
);
SolanaClientError::from(e)
})?;

rpc_client.send_and_confirm_transaction_with_spinner_and_config(
tx,
rpc_client.commitment(),
preflight_config,
)
}

fn execute_prepared_transaction_retry_blockhash_internal(
prepared_transaction: &mut PreparedTransaction,
rpc_client: &RpcClient,
preflight_config: RpcSendTransactionConfig,
blockhash_failure_retries: Option<u16>,
) -> Result<Signature, anchor_client::ClientError> {
let mut retry_count: u16 = 0;
let blockhash_failure_retries = blockhash_failure_retries.unwrap_or(0);
let mut last_error = anchor_client::ClientError::SolanaClientError(SolanaClientError::from(
RpcError::RpcRequestError("send_transaction: unknown retry failure".to_string()),
));
while retry_count <= blockhash_failure_retries {
let send_result = execute_prepared_transaction_internal(
prepared_transaction,
rpc_client,
preflight_config,
);
match send_result {
Ok(signature) => {
return Ok(signature);
}
Err(err) => {
last_error =
anchor_client::ClientError::SolanaClientError(SolanaClientError::from(err));
if let anchor_client::ClientError::SolanaClientError(ce) = &last_error {
let to_check_err: Option<&TransactionError> = match ce.kind() {
ClientErrorKind::RpcError(RpcError::RpcResponseError {
data:
RpcResponseErrorData::SendTransactionPreflightFailure(
RpcSimulateTransactionResult {
err: transaction_error,
logs,
accounts,
..
},
),
..
}) => {
debug!(
"Failed to send transaction: {:?}, logs: {:?}, accounts: {:?}",
transaction_error, logs, accounts
);
transaction_error.as_ref()
}
ClientErrorKind::RpcError(ForUser(message)) => {
// unable to confirm transaction. This can happen in situations such as transaction expiration and insufficient fee-payer funds
if message
.to_lowercase()
.contains("unable to confirm transaction")
{
Some(&TransactionError::BlockhashNotFound)
} else {
None
}
}
ClientErrorKind::TransactionError(te) => Some(te),
_ => None,
};

if let Some(tx_err) = to_check_err {
if *tx_err == TransactionError::BlockhashNotFound {
debug!(
"Retried attempt #{}/{} to send transaction with error: {:?} ",
retry_count, blockhash_failure_retries, tx_err
);
// retry
retry_count += 1;
continue;
}
}
// No Error to retry, let's break the loop and use the last error
break;
}
}
}
}
error!("Transaction ERR send_transaction: {:?}", last_error);
Err(last_error)
}

pub fn execute_prepared_transaction(
prepared_transaction: &mut PreparedTransaction,
rpc_client: &RpcClient,
Expand All @@ -272,26 +380,36 @@ pub fn execute_prepared_transaction(
commitment: blockhash_commitment,
},
);
let latest_hash = rpc_client_blockhash.get_latest_blockhash()?;
let tx = prepared_transaction.sign(latest_hash).map_err(|e| {
error!(
"execute_prepared_transaction: error signing transaction with blockhash: {}: {:?}",
latest_hash, e
);
anchor_client::ClientError::SolanaClientError(SolanaClientError::from(e))
})?;

rpc_client
.send_and_confirm_transaction_with_spinner_and_config(
tx,
rpc_client.commitment(),
preflight_config,
)
.map_err(|e|{
error!("execute_prepared_transaction: error send_and_confirm transaction '{:?}', signers: '{:?}': {:?}",
execute_prepared_transaction_internal(
prepared_transaction,
&rpc_client_blockhash,
preflight_config,
).map_err(|e|{
error!("execute_prepared_transaction: error send_and_confirm transaction '{:?}', signers: '{:?}': {:?}",
prepared_transaction.transaction, prepared_transaction.signers.iter().map(|s| s.pubkey()), e);
e.into()
})
e.into()
})
}

pub fn execute_prepared_transaction_blockhash_retry(
prepared_transaction: &mut PreparedTransaction,
rpc_client: &RpcClient,
preflight_config: RpcSendTransactionConfig,
blockhash_commitment: CommitmentLevel,
blockhash_failure_retries: Option<u16>,
) -> Result<Signature, anchor_client::ClientError> {
let rpc_client_blockhash = RpcClient::new_with_commitment(
rpc_client.url(),
CommitmentConfig {
commitment: blockhash_commitment,
},
);
execute_prepared_transaction_retry_blockhash_internal(
prepared_transaction,
&rpc_client_blockhash,
preflight_config,
blockhash_failure_retries,
)
}

pub fn simulate_prepared_transaction(
Expand All @@ -312,7 +430,7 @@ pub fn simulate_prepared_transaction(
"simulate_prepared_transaction: error signing transaction with blockhash: {}: {:?}",
latest_hash, e
);
RpcError::ForUser(format!("Signature error: {}", e))
ForUser(format!("Signing transaction error: {}", e))
})?;

rpc_client.simulate_transaction_with_config(tx, simulate_config)
Expand Down
29 changes: 29 additions & 0 deletions libs/marinade-common-cli/src/config_args.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,3 +147,32 @@ pub fn print_only_arg<'a, 'b>() -> Arg<'a, 'b> {
.takes_value(false)
.help(PRINT_ONLY_ARG.help)
}

pub const WITH_COMPUTE_UNIT_PRICE_ARG: ArgConstant<'static> = ArgConstant {
name: "with_compute_unit_price",
long: "with-compute-unit-price",
help: "Set compute unit price for transaction, in increments of 0.000001 lamports per compute unit.",
};
pub fn with_compute_unit_price<'a, 'b>() -> Arg<'a, 'b> {
Arg::with_name(WITH_COMPUTE_UNIT_PRICE_ARG.name)
.value_name("COMPUTE-UNIT-PRICE")
.takes_value(true)
.long(WITH_COMPUTE_UNIT_PRICE_ARG.long)
.help(WITH_COMPUTE_UNIT_PRICE_ARG.help)
.default_value("0")
}

pub const BLOCKHASH_NOT_FOUND_RETRIES_ARG: ArgConstant<'static> = ArgConstant {
name: "blockhash_not_found_retries",
long: "blockhash-not-found-retries",
help: "Number of retries to get a blockhash when exception BlockhashNotFound is thrown on execution.",
};

pub fn blockhash_not_found_retries_arg<'a, 'b>() -> Arg<'a, 'b> {
Arg::with_name(BLOCKHASH_NOT_FOUND_RETRIES_ARG.name)
.long(BLOCKHASH_NOT_FOUND_RETRIES_ARG.long)
.value_name("NUMBER")
.takes_value(true)
.help(BLOCKHASH_NOT_FOUND_RETRIES_ARG.help)
.default_value("0")
}
26 changes: 23 additions & 3 deletions libs/marinade-common-cli/src/matchers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,29 @@ pub fn pubkey_or_signer(
})
}

pub fn match_u16(matches: &ArgMatches<'_>, name: &str) -> anyhow::Result<u16> {
crate::matchers::match_u16_option(matches, name)?
.ok_or_else(|| anyhow::Error::msg(format!("match_u16: argument '{}' missing", name)))
}

pub fn match_u16_option(matches: &ArgMatches<'_>, name: &str) -> anyhow::Result<Option<u16>> {
if let Some(value) = matches.value_of(name) {
let value = u16::from_str(value).map_err(|e| {
anyhow!(
"Failed to convert argument {} of value {} to u16: {:?}",
name,
value,
e
)
})?;
return Ok(Some(value));
}
Ok(None)
}

pub fn match_u32(matches: &ArgMatches<'_>, name: &str) -> anyhow::Result<u32> {
match_u32_option(matches, name)?
.ok_or_else(|| anyhow::Error::msg(format!("argument '{}' missing", name)))
.ok_or_else(|| anyhow::Error::msg(format!("match_u32: argument '{}' missing", name)))
}

pub fn match_u32_option(matches: &ArgMatches<'_>, name: &str) -> anyhow::Result<Option<u32>> {
Expand All @@ -166,7 +186,7 @@ pub fn match_u32_option(matches: &ArgMatches<'_>, name: &str) -> anyhow::Result<

pub fn match_u64(matches: &ArgMatches<'_>, name: &str) -> anyhow::Result<u64> {
match_u64_option(matches, name)?
.ok_or_else(|| anyhow::Error::msg(format!("argument '{}' missing", name)))
.ok_or_else(|| anyhow::Error::msg(format!("match_u64: argument '{}' missing", name)))
}

pub fn match_u64_option(matches: &ArgMatches<'_>, name: &str) -> anyhow::Result<Option<u64>> {
Expand All @@ -186,7 +206,7 @@ pub fn match_u64_option(matches: &ArgMatches<'_>, name: &str) -> anyhow::Result<

pub fn match_f64(matches: &ArgMatches<'_>, name: &str) -> anyhow::Result<f64> {
match_f64_option(matches, name)?
.ok_or_else(|| anyhow::Error::msg(format!("argument '{}' missing", name)))
.ok_or_else(|| anyhow::Error::msg(format!("match_f64: argument '{}' missing", name)))
}

pub fn match_f64_option(matches: &ArgMatches<'_>, name: &str) -> anyhow::Result<Option<f64>> {
Expand Down
2 changes: 2 additions & 0 deletions rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[toolchain]
channel = "1.69.0"
Loading