Add preflight checks to sendTransaction RPC method (bp #10338) (#10363)

automerge
This commit is contained in:
mergify[bot]
2020-06-01 22:27:30 -07:00
committed by GitHub
parent df7c44bd0c
commit ae1a0f57c5
13 changed files with 562 additions and 272 deletions

View File

@ -2,7 +2,7 @@ use crate::{
checks::*, checks::*,
cli_output::{CliAccount, CliSignOnlyData, CliSignature, OutputFormat}, cli_output::{CliAccount, CliSignOnlyData, CliSignature, OutputFormat},
cluster_query::*, cluster_query::*,
display::println_name_value, display::{new_spinner_progress_bar, println_name_value, println_transaction},
nonce::{self, *}, nonce::{self, *},
offline::{blockhash_query::BlockhashQuery, *}, offline::{blockhash_query::BlockhashQuery, *},
spend_utils::*, spend_utils::*,
@ -27,7 +27,7 @@ use solana_clap_utils::{
use solana_client::{ use solana_client::{
client_error::{ClientError, ClientErrorKind, Result as ClientResult}, client_error::{ClientError, ClientErrorKind, Result as ClientResult},
rpc_client::RpcClient, rpc_client::RpcClient,
rpc_config::RpcLargestAccountsFilter, rpc_config::{RpcLargestAccountsFilter, RpcSendTransactionConfig},
rpc_response::{RpcAccount, RpcKeyedAccount}, rpc_response::{RpcAccount, RpcKeyedAccount},
}; };
#[cfg(not(test))] #[cfg(not(test))]
@ -37,7 +37,7 @@ use solana_faucet::faucet_mock::request_airdrop_transaction;
use solana_remote_wallet::remote_wallet::RemoteWalletManager; use solana_remote_wallet::remote_wallet::RemoteWalletManager;
use solana_sdk::{ use solana_sdk::{
bpf_loader, bpf_loader,
clock::{Epoch, Slot}, clock::{Epoch, Slot, DEFAULT_TICKS_PER_SECOND},
commitment_config::CommitmentConfig, commitment_config::CommitmentConfig,
fee_calculator::FeeCalculator, fee_calculator::FeeCalculator,
hash::Hash, hash::Hash,
@ -48,6 +48,7 @@ use solana_sdk::{
program_utils::DecodeError, program_utils::DecodeError,
pubkey::{Pubkey, MAX_SEED_LEN}, pubkey::{Pubkey, MAX_SEED_LEN},
signature::{Keypair, Signature, Signer, SignerError}, signature::{Keypair, Signature, Signer, SignerError},
signers::Signers,
system_instruction::{self, SystemError}, system_instruction::{self, SystemError},
system_program, system_program,
transaction::{Transaction, TransactionError}, transaction::{Transaction, TransactionError},
@ -1159,7 +1160,7 @@ fn process_confirm(
"\nTransaction executed in slot {}:", "\nTransaction executed in slot {}:",
confirmed_transaction.slot confirmed_transaction.slot
); );
crate::display::println_transaction( println_transaction(
&confirmed_transaction &confirmed_transaction
.transaction .transaction
.transaction .transaction
@ -1189,7 +1190,7 @@ fn process_confirm(
} }
fn process_decode_transaction(transaction: &Transaction) -> ProcessResult { fn process_decode_transaction(transaction: &Transaction) -> ProcessResult {
crate::display::println_transaction(transaction, &None, ""); println_transaction(transaction, &None, "");
Ok("".to_string()) Ok("".to_string())
} }
@ -1227,6 +1228,103 @@ fn process_show_account(
Ok(account_string) Ok(account_string)
} }
fn send_and_confirm_transactions_with_spinner<T: Signers>(
rpc_client: &RpcClient,
mut transactions: Vec<Transaction>,
signer_keys: &T,
) -> Result<(), Box<dyn error::Error>> {
let progress_bar = new_spinner_progress_bar();
let mut send_retries = 5;
loop {
let mut status_retries = 15;
// Send all transactions
let mut transactions_signatures = vec![];
let num_transactions = transactions.len();
for transaction in transactions {
if cfg!(not(test)) {
// Delay ~1 tick between write transactions in an attempt to reduce AccountInUse errors
// when all the write transactions modify the same program account (eg, deploying a
// new program)
sleep(Duration::from_millis(1000 / DEFAULT_TICKS_PER_SECOND));
}
let signature = rpc_client
.send_transaction_with_config(
&transaction,
RpcSendTransactionConfig {
skip_preflight: true,
},
)
.ok();
transactions_signatures.push((transaction, signature));
progress_bar.set_message(&format!(
"[{}/{}] Transactions sent",
transactions_signatures.len(),
num_transactions
));
}
// Collect statuses for all the transactions, drop those that are confirmed
while status_retries > 0 {
status_retries -= 1;
progress_bar.set_message(&format!(
"[{}/{}] Transactions confirmed",
num_transactions - transactions_signatures.len(),
num_transactions
));
if cfg!(not(test)) {
// Retry twice a second
sleep(Duration::from_millis(500));
}
transactions_signatures = transactions_signatures
.into_iter()
.filter(|(_transaction, signature)| {
if let Some(signature) = signature {
if let Ok(status) = rpc_client.get_signature_status(&signature) {
if rpc_client
.get_num_blocks_since_signature_confirmation(&signature)
.unwrap_or(0)
> 1
{
return false;
} else {
return match status {
None => true,
Some(result) => result.is_err(),
};
}
}
}
true
})
.collect();
if transactions_signatures.is_empty() {
return Ok(());
}
}
if send_retries == 0 {
return Err("Transactions failed".into());
}
send_retries -= 1;
// Re-sign any failed transactions with a new blockhash and retry
let (blockhash, _fee_calculator) = rpc_client
.get_new_blockhash(&transactions_signatures[0].0.message().recent_blockhash)?;
transactions = vec![];
for (mut transaction, _) in transactions_signatures.into_iter() {
transaction.try_sign(signer_keys, blockhash)?;
transactions.push(transaction);
}
}
}
fn process_deploy( fn process_deploy(
rpc_client: &RpcClient, rpc_client: &RpcClient,
config: &CliConfig, config: &CliConfig,
@ -1294,15 +1392,18 @@ fn process_deploy(
})?; })?;
trace!("Writing program data"); trace!("Writing program data");
rpc_client send_and_confirm_transactions_with_spinner(&rpc_client, write_transactions, &signers).map_err(
.send_and_confirm_transactions_with_spinner(write_transactions, &signers) |_| CliError::DynamicProgramError("Data writes to program account failed".to_string()),
.map_err(|_| { )?;
CliError::DynamicProgramError("Data writes to program account failed".to_string())
})?;
trace!("Finalizing program account"); trace!("Finalizing program account");
rpc_client rpc_client
.send_and_confirm_transaction_with_spinner(&finalize_tx) .send_and_confirm_transaction_with_spinner_and_config(
&finalize_tx,
RpcSendTransactionConfig {
skip_preflight: true,
},
)
.map_err(|e| { .map_err(|e| {
CliError::DynamicProgramError(format!("Finalizing program account failed: {}", e)) CliError::DynamicProgramError(format!("Finalizing program account failed: {}", e))
})?; })?;

View File

@ -1,12 +1,11 @@
use crate::{ use crate::{
cli::{CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult}, cli::{CliCommand, CliCommandInfo, CliConfig, CliError, ProcessResult},
cli_output::*, cli_output::*,
display::println_name_value, display::{new_spinner_progress_bar, println_name_value},
spend_utils::{resolve_spend_tx_and_check_account_balance, SpendAmount}, spend_utils::{resolve_spend_tx_and_check_account_balance, SpendAmount},
}; };
use clap::{value_t, value_t_or_exit, App, AppSettings, Arg, ArgMatches, SubCommand}; use clap::{value_t, value_t_or_exit, App, AppSettings, Arg, ArgMatches, SubCommand};
use console::{style, Emoji}; use console::{style, Emoji};
use indicatif::{ProgressBar, ProgressStyle};
use solana_clap_utils::{ use solana_clap_utils::{
commitment::{commitment_arg, COMMITMENT_ARG}, commitment::{commitment_arg, COMMITMENT_ARG},
input_parsers::*, input_parsers::*,
@ -467,15 +466,6 @@ pub fn parse_transaction_history(
}) })
} }
/// Creates a new process bar for processing that will take an unknown amount of time
fn new_spinner_progress_bar() -> ProgressBar {
let progress_bar = ProgressBar::new(42);
progress_bar
.set_style(ProgressStyle::default_spinner().template("{spinner:.green} {wide_msg}"));
progress_bar.enable_steady_tick(100);
progress_bar
}
pub fn process_catchup( pub fn process_catchup(
rpc_client: &RpcClient, rpc_client: &RpcClient,
node_pubkey: &Pubkey, node_pubkey: &Pubkey,

View File

@ -1,5 +1,6 @@
use crate::cli::SettingType; use crate::cli::SettingType;
use console::style; use console::style;
use indicatif::{ProgressBar, ProgressStyle};
use solana_sdk::{ use solana_sdk::{
hash::Hash, native_token::lamports_to_sol, program_utils::limited_deserialize, hash::Hash, native_token::lamports_to_sol, program_utils::limited_deserialize,
transaction::Transaction, transaction::Transaction,
@ -200,3 +201,12 @@ pub fn println_transaction(
} }
} }
} }
/// Creates a new process bar for processing that will take an unknown amount of time
pub fn new_spinner_progress_bar() -> ProgressBar {
let progress_bar = ProgressBar::new(42);
progress_bar
.set_style(ProgressStyle::default_spinner().template("{spinner:.green} {wide_msg}"));
progress_bar.enable_steady_tick(100);
progress_bar
}

View File

@ -2,7 +2,7 @@ use crate::{
client_error::{ClientError, ClientErrorKind, Result as ClientResult}, client_error::{ClientError, ClientErrorKind, Result as ClientResult},
http_sender::HttpSender, http_sender::HttpSender,
mock_sender::{MockSender, Mocks}, mock_sender::{MockSender, Mocks},
rpc_config::RpcLargestAccountsConfig, rpc_config::{RpcLargestAccountsConfig, RpcSendTransactionConfig},
rpc_request::{RpcError, RpcRequest}, rpc_request::{RpcError, RpcRequest},
rpc_response::*, rpc_response::*,
rpc_sender::RpcSender, rpc_sender::RpcSender,
@ -32,7 +32,6 @@ use solana_transaction_status::{
}; };
use solana_vote_program::vote_state::MAX_LOCKOUT_HISTORY; use solana_vote_program::vote_state::MAX_LOCKOUT_HISTORY;
use std::{ use std::{
error,
net::SocketAddr, net::SocketAddr,
thread::sleep, thread::sleep,
time::{Duration, Instant}, time::{Duration, Instant},
@ -94,10 +93,20 @@ impl RpcClient {
} }
pub fn send_transaction(&self, transaction: &Transaction) -> ClientResult<Signature> { pub fn send_transaction(&self, transaction: &Transaction) -> ClientResult<Signature> {
self.send_transaction_with_config(transaction, RpcSendTransactionConfig::default())
}
pub fn send_transaction_with_config(
&self,
transaction: &Transaction,
config: RpcSendTransactionConfig,
) -> ClientResult<Signature> {
let serialized_encoded = bs58::encode(serialize(transaction).unwrap()).into_string(); let serialized_encoded = bs58::encode(serialize(transaction).unwrap()).into_string();
let signature_base58_str: String = let signature_base58_str: String = self.send(
self.send(RpcRequest::SendTransaction, json!([serialized_encoded]))?; RpcRequest::SendTransaction,
json!([serialized_encoded, config]),
)?;
let signature = signature_base58_str let signature = signature_base58_str
.parse::<Signature>() .parse::<Signature>()
@ -406,96 +415,6 @@ impl RpcClient {
} }
} }
pub fn send_and_confirm_transactions_with_spinner<T: Signers>(
&self,
mut transactions: Vec<Transaction>,
signer_keys: &T,
) -> Result<(), Box<dyn error::Error>> {
let progress_bar = new_spinner_progress_bar();
let mut send_retries = 5;
loop {
let mut status_retries = 15;
// Send all transactions
let mut transactions_signatures = vec![];
let num_transactions = transactions.len();
for transaction in transactions {
if cfg!(not(test)) {
// Delay ~1 tick between write transactions in an attempt to reduce AccountInUse errors
// when all the write transactions modify the same program account (eg, deploying a
// new program)
sleep(Duration::from_millis(1000 / DEFAULT_TICKS_PER_SECOND));
}
let signature = self.send_transaction(&transaction).ok();
transactions_signatures.push((transaction, signature));
progress_bar.set_message(&format!(
"[{}/{}] Transactions sent",
transactions_signatures.len(),
num_transactions
));
}
// Collect statuses for all the transactions, drop those that are confirmed
while status_retries > 0 {
status_retries -= 1;
progress_bar.set_message(&format!(
"[{}/{}] Transactions confirmed",
num_transactions - transactions_signatures.len(),
num_transactions
));
if cfg!(not(test)) {
// Retry twice a second
sleep(Duration::from_millis(500));
}
transactions_signatures = transactions_signatures
.into_iter()
.filter(|(_transaction, signature)| {
if let Some(signature) = signature {
if let Ok(status) = self.get_signature_status(&signature) {
if self
.get_num_blocks_since_signature_confirmation(&signature)
.unwrap_or(0)
> 1
{
return false;
} else {
return match status {
None => true,
Some(result) => result.is_err(),
};
}
}
}
true
})
.collect();
if transactions_signatures.is_empty() {
return Ok(());
}
}
if send_retries == 0 {
return Err(RpcError::ForUser("Transactions failed".to_string()).into());
}
send_retries -= 1;
// Re-sign any failed transactions with a new blockhash and retry
let (blockhash, _fee_calculator) =
self.get_new_blockhash(&transactions_signatures[0].0.message().recent_blockhash)?;
transactions = vec![];
for (mut transaction, _) in transactions_signatures.into_iter() {
transaction.try_sign(signer_keys, blockhash)?;
transactions.push(transaction);
}
}
}
pub fn resign_transaction<T: Signers>( pub fn resign_transaction<T: Signers>(
&self, &self,
tx: &mut Transaction, tx: &mut Transaction,
@ -952,6 +871,17 @@ impl RpcClient {
pub fn send_and_confirm_transaction_with_spinner( pub fn send_and_confirm_transaction_with_spinner(
&self, &self,
transaction: &Transaction, transaction: &Transaction,
) -> ClientResult<Signature> {
self.send_and_confirm_transaction_with_spinner_and_config(
transaction,
RpcSendTransactionConfig::default(),
)
}
pub fn send_and_confirm_transaction_with_spinner_and_config(
&self,
transaction: &Transaction,
config: RpcSendTransactionConfig,
) -> ClientResult<Signature> { ) -> ClientResult<Signature> {
let mut confirmations = 0; let mut confirmations = 0;
@ -967,7 +897,7 @@ impl RpcClient {
)); ));
let mut status_retries = 15; let mut status_retries = 15;
let (signature, status) = loop { let (signature, status) = loop {
let signature = self.send_transaction(transaction)?; let signature = self.send_transaction_with_config(transaction, config.clone())?;
// Get recent commitment in order to count confirmations for successful transactions // Get recent commitment in order to count confirmations for successful transactions
let status = self let status = self

View File

@ -6,7 +6,13 @@ pub struct RpcSignatureStatusConfig {
pub search_transaction_history: bool, pub search_transaction_history: bool,
} }
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcSendTransactionConfig {
pub skip_preflight: bool,
}
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct RpcSimulateTransactionConfig { pub struct RpcSimulateTransactionConfig {
pub sig_verify: bool, pub sig_verify: bool,

View File

@ -43,6 +43,7 @@ pub mod retransmit_stage;
pub mod rewards_recorder_service; pub mod rewards_recorder_service;
pub mod rpc; pub mod rpc;
pub mod rpc_error; pub mod rpc_error;
pub mod rpc_health;
pub mod rpc_pubsub; pub mod rpc_pubsub;
pub mod rpc_pubsub_service; pub mod rpc_pubsub_service;
pub mod rpc_service; pub mod rpc_service;

View File

@ -6,6 +6,7 @@ use crate::{
contact_info::ContactInfo, contact_info::ContactInfo,
non_circulating_supply::calculate_non_circulating_supply, non_circulating_supply::calculate_non_circulating_supply,
rpc_error::RpcCustomError, rpc_error::RpcCustomError,
rpc_health::*,
validator::ValidatorExit, validator::ValidatorExit,
}; };
use bincode::serialize; use bincode::serialize;
@ -74,6 +75,7 @@ pub struct JsonRpcRequestProcessor {
blockstore: Arc<Blockstore>, blockstore: Arc<Blockstore>,
config: JsonRpcConfig, config: JsonRpcConfig,
validator_exit: Arc<RwLock<Option<ValidatorExit>>>, validator_exit: Arc<RwLock<Option<ValidatorExit>>>,
health: Arc<RpcHealth>,
} }
impl JsonRpcRequestProcessor { impl JsonRpcRequestProcessor {
@ -128,6 +130,7 @@ impl JsonRpcRequestProcessor {
block_commitment_cache: Arc<RwLock<BlockCommitmentCache>>, block_commitment_cache: Arc<RwLock<BlockCommitmentCache>>,
blockstore: Arc<Blockstore>, blockstore: Arc<Blockstore>,
validator_exit: Arc<RwLock<Option<ValidatorExit>>>, validator_exit: Arc<RwLock<Option<ValidatorExit>>>,
health: Arc<RpcHealth>,
) -> Self { ) -> Self {
JsonRpcRequestProcessor { JsonRpcRequestProcessor {
config, config,
@ -135,6 +138,7 @@ impl JsonRpcRequestProcessor {
block_commitment_cache, block_commitment_cache,
blockstore, blockstore,
validator_exit, validator_exit,
health,
} }
} }
@ -713,6 +717,19 @@ fn verify_signature(input: &str) -> Result<Signature> {
.map_err(|e| Error::invalid_params(format!("{:?}", e))) .map_err(|e| Error::invalid_params(format!("{:?}", e)))
} }
/// Run transactions against a frozen bank without committing the results
fn run_transaction_simulation(
bank: &Bank,
transactions: &[Transaction],
) -> transaction::Result<()> {
assert!(bank.is_frozen(), "simulation bank must be frozen");
let batch = bank.prepare_batch(transactions, None);
let (_loaded_accounts, executed, _retryable_transactions, _transaction_count, _signature_count) =
bank.load_and_execute_transactions(&batch, solana_sdk::clock::MAX_PROCESSING_AGE);
executed[0].0.clone().map(|_| ())
}
#[derive(Clone)] #[derive(Clone)]
pub struct Meta { pub struct Meta {
pub request_processor: Arc<RwLock<JsonRpcRequestProcessor>>, pub request_processor: Arc<RwLock<JsonRpcRequestProcessor>>,
@ -904,7 +921,12 @@ pub trait RpcSol {
) -> Result<String>; ) -> Result<String>;
#[rpc(meta, name = "sendTransaction")] #[rpc(meta, name = "sendTransaction")]
fn send_transaction(&self, meta: Self::Metadata, data: String) -> Result<String>; fn send_transaction(
&self,
meta: Self::Metadata,
data: String,
config: Option<RpcSendTransactionConfig>,
) -> Result<String>;
#[rpc(meta, name = "simulateTransaction")] #[rpc(meta, name = "simulateTransaction")]
fn simulate_transaction( fn simulate_transaction(
@ -1406,8 +1428,45 @@ impl RpcSol for RpcSolImpl {
} }
} }
fn send_transaction(&self, meta: Self::Metadata, data: String) -> Result<String> { fn send_transaction(
&self,
meta: Self::Metadata,
data: String,
config: Option<RpcSendTransactionConfig>,
) -> Result<String> {
let config = config.unwrap_or_default();
let (wire_transaction, transaction) = deserialize_bs58_transaction(data)?; let (wire_transaction, transaction) = deserialize_bs58_transaction(data)?;
let signature = transaction.signatures[0].to_string();
if !config.skip_preflight {
if transaction.verify().is_err() {
return Err(RpcCustomError::SendTransactionPreflightFailure {
message: "Transaction signature verification failed".into(),
}
.into());
}
if meta.request_processor.read().unwrap().health.check() != RpcHealthStatus::Ok {
return Err(RpcCustomError::SendTransactionPreflightFailure {
message: "RPC node is unhealthy, unable to simulate transaction".into(),
}
.into());
}
let bank = &*meta.request_processor.read().unwrap().bank(None)?;
if let Err(err) = run_transaction_simulation(&bank, &[transaction]) {
// Note: it's possible that the transaction simulation failed but the actual
// transaction would succeed, such as when a transaction depends on an earlier
// transaction that has yet to reach max confirmations. In these cases the user
// should use the config.skip_preflight flag, and potentially in the future
// additional controls over what bank is used for preflight should be exposed.
return Err(RpcCustomError::SendTransactionPreflightFailure {
message: format!("Transaction simulation failed: {}", err),
}
.into());
}
}
let transactions_socket = UdpSocket::bind("0.0.0.0:0").unwrap(); let transactions_socket = UdpSocket::bind("0.0.0.0:0").unwrap();
let tpu_addr = get_tpu_addr(&meta.cluster_info)?; let tpu_addr = get_tpu_addr(&meta.cluster_info)?;
transactions_socket transactions_socket
@ -1416,7 +1475,6 @@ impl RpcSol for RpcSolImpl {
info!("send_transaction: send_to error: {:?}", err); info!("send_transaction: send_to error: {:?}", err);
Error::internal_error() Error::internal_error()
})?; })?;
let signature = transaction.signatures[0].to_string();
trace!( trace!(
"send_transaction: sent {} bytes, signature={}", "send_transaction: sent {} bytes, signature={}",
wire_transaction.len(), wire_transaction.len(),
@ -1432,10 +1490,7 @@ impl RpcSol for RpcSolImpl {
config: Option<RpcSimulateTransactionConfig>, config: Option<RpcSimulateTransactionConfig>,
) -> RpcResponse<TransactionStatus> { ) -> RpcResponse<TransactionStatus> {
let (_, transaction) = deserialize_bs58_transaction(data)?; let (_, transaction) = deserialize_bs58_transaction(data)?;
let config = config.unwrap_or(RpcSimulateTransactionConfig { sig_verify: false }); let config = config.unwrap_or_default();
let bank = &*meta.request_processor.read().unwrap().bank(None)?;
assert!(bank.is_frozen());
let mut result = if config.sig_verify { let mut result = if config.sig_verify {
transaction.verify() transaction.verify()
@ -1443,17 +1498,10 @@ impl RpcSol for RpcSolImpl {
Ok(()) Ok(())
}; };
let bank = &*meta.request_processor.read().unwrap().bank(None)?;
if result.is_ok() { if result.is_ok() {
let transactions = [transaction]; result = run_transaction_simulation(&bank, &[transaction]);
let batch = bank.prepare_batch(&transactions, None);
let (
_loaded_accounts,
executed,
_retryable_transactions,
_transaction_count,
_signature_count,
) = bank.load_and_execute_transactions(&batch, solana_sdk::clock::MAX_PROCESSING_AGE);
result = executed[0].0.clone();
} }
new_response( new_response(
@ -1799,6 +1847,7 @@ pub mod tests {
block_commitment_cache.clone(), block_commitment_cache.clone(),
blockstore, blockstore,
validator_exit, validator_exit,
RpcHealth::stub(),
))); )));
let cluster_info = Arc::new(ClusterInfo::new_with_invalid_keypair(ContactInfo::default())); let cluster_info = Arc::new(ClusterInfo::new_with_invalid_keypair(ContactInfo::default()));
@ -1847,6 +1896,7 @@ pub mod tests {
block_commitment_cache, block_commitment_cache,
blockstore, blockstore,
validator_exit, validator_exit,
RpcHealth::stub(),
); );
thread::spawn(move || { thread::spawn(move || {
let blockhash = bank.confirmed_last_blockhash().0; let blockhash = bank.confirmed_last_blockhash().0;
@ -2791,6 +2841,7 @@ pub mod tests {
block_commitment_cache, block_commitment_cache,
blockstore, blockstore,
validator_exit, validator_exit,
RpcHealth::stub(),
); );
Arc::new(RwLock::new(request_processor)) Arc::new(RwLock::new(request_processor))
}, },
@ -2805,6 +2856,103 @@ pub mod tests {
assert_eq!(error["code"], ErrorCode::InvalidParams.code()); assert_eq!(error["code"], ErrorCode::InvalidParams.code());
} }
#[test]
fn test_rpc_send_transaction_preflight() {
let exit = Arc::new(AtomicBool::new(false));
let validator_exit = create_validator_exit(&exit);
let ledger_path = get_tmp_ledger_path!();
let blockstore = Arc::new(Blockstore::open(&ledger_path).unwrap());
let block_commitment_cache = Arc::new(RwLock::new(
BlockCommitmentCache::default_with_blockstore(blockstore.clone()),
));
let bank_forks = new_bank_forks().0;
let health = RpcHealth::stub();
// Freeze bank 0 to prevent a panic in `run_transaction_simulation()`
bank_forks.write().unwrap().get(0).unwrap().freeze();
let mut io = MetaIoHandler::default();
let rpc = RpcSolImpl;
io.extend_with(rpc.to_delegate());
let meta = Meta {
request_processor: {
let request_processor = JsonRpcRequestProcessor::new(
JsonRpcConfig::default(),
bank_forks,
block_commitment_cache,
blockstore,
validator_exit,
health.clone(),
);
Arc::new(RwLock::new(request_processor))
},
cluster_info: Arc::new(ClusterInfo::new_with_invalid_keypair(
ContactInfo::new_with_socketaddr(&socketaddr!("127.0.0.1:1234")),
)),
genesis_hash: Hash::default(),
};
let mut bad_transaction =
system_transaction::transfer(&Keypair::new(), &Pubkey::default(), 42, Hash::default());
// sendTransaction will fail because the blockhash is invalid
let req = format!(
r#"{{"jsonrpc":"2.0","id":1,"method":"sendTransaction","params":["{}"]}}"#,
bs58::encode(serialize(&bad_transaction).unwrap()).into_string()
);
let res = io.handle_request_sync(&req, meta.clone());
assert_eq!(
res,
Some(
r#"{"jsonrpc":"2.0","error":{"code":-32002,"message":"Transaction simulation failed: TransactionError::BlockhashNotFound"},"id":1}"#.to_string(),
)
);
// sendTransaction will fail due to poor node health
health.stub_set_health_status(Some(RpcHealthStatus::Behind));
let req = format!(
r#"{{"jsonrpc":"2.0","id":1,"method":"sendTransaction","params":["{}"]}}"#,
bs58::encode(serialize(&bad_transaction).unwrap()).into_string()
);
let res = io.handle_request_sync(&req, meta.clone());
assert_eq!(
res,
Some(
r#"{"jsonrpc":"2.0","error":{"code":-32002,"message":"RPC node is unhealthy, unable to simulate transaction"},"id":1}"#.to_string(),
)
);
health.stub_set_health_status(None);
// sendTransaction will fail due to invalid signature
bad_transaction.signatures[0] = Signature::default();
let req = format!(
r#"{{"jsonrpc":"2.0","id":1,"method":"sendTransaction","params":["{}"]}}"#,
bs58::encode(serialize(&bad_transaction).unwrap()).into_string()
);
let res = io.handle_request_sync(&req, meta.clone());
assert_eq!(
res,
Some(
r#"{"jsonrpc":"2.0","error":{"code":-32002,"message":"Transaction signature verification failed"},"id":1}"#.to_string(),
)
);
// sendTransaction will now succeed because skipPreflight=true even though it's a bad
// transaction
let req = format!(
r#"{{"jsonrpc":"2.0","id":1,"method":"sendTransaction","params":["{}", {{"skipPreflight": true}}]}}"#,
bs58::encode(serialize(&bad_transaction).unwrap()).into_string()
);
let res = io.handle_request_sync(&req, meta);
assert_eq!(
res,
Some(
r#"{"jsonrpc":"2.0","result":"1111111111111111111111111111111111111111111111111111111111111111","id":1}"#.to_string(),
)
);
}
#[test] #[test]
fn test_rpc_get_tpu_addr() { fn test_rpc_get_tpu_addr() {
let cluster_info = Arc::new(ClusterInfo::new_with_invalid_keypair( let cluster_info = Arc::new(ClusterInfo::new_with_invalid_keypair(
@ -2861,7 +3009,9 @@ pub mod tests {
) )
} }
pub fn create_validator_exit(exit: &Arc<AtomicBool>) -> Arc<RwLock<Option<ValidatorExit>>> { pub(crate) fn create_validator_exit(
exit: &Arc<AtomicBool>,
) -> Arc<RwLock<Option<ValidatorExit>>> {
let mut validator_exit = ValidatorExit::default(); let mut validator_exit = ValidatorExit::default();
let exit_ = exit.clone(); let exit_ = exit.clone();
validator_exit.register_exit(Box::new(move || exit_.store(true, Ordering::Relaxed))); validator_exit.register_exit(Box::new(move || exit_.store(true, Ordering::Relaxed)));
@ -2883,6 +3033,7 @@ pub mod tests {
block_commitment_cache, block_commitment_cache,
blockstore, blockstore,
validator_exit, validator_exit,
RpcHealth::stub(),
); );
assert_eq!(request_processor.validator_exit(), Ok(false)); assert_eq!(request_processor.validator_exit(), Ok(false));
assert_eq!(exit.load(Ordering::Relaxed), false); assert_eq!(exit.load(Ordering::Relaxed), false);
@ -2905,6 +3056,7 @@ pub mod tests {
block_commitment_cache, block_commitment_cache,
blockstore, blockstore,
validator_exit, validator_exit,
RpcHealth::stub(),
); );
assert_eq!(request_processor.validator_exit(), Ok(true)); assert_eq!(request_processor.validator_exit(), Ok(true));
assert_eq!(exit.load(Ordering::Relaxed), true); assert_eq!(exit.load(Ordering::Relaxed), true);
@ -2987,6 +3139,7 @@ pub mod tests {
block_commitment_cache, block_commitment_cache,
blockstore, blockstore,
validator_exit, validator_exit,
RpcHealth::stub(),
); );
assert_eq!( assert_eq!(
request_processor.get_block_commitment(0), request_processor.get_block_commitment(0),

View File

@ -3,6 +3,7 @@ use solana_sdk::clock::Slot;
const JSON_RPC_SERVER_ERROR_0: i64 = -32000; const JSON_RPC_SERVER_ERROR_0: i64 = -32000;
const JSON_RPC_SERVER_ERROR_1: i64 = -32001; const JSON_RPC_SERVER_ERROR_1: i64 = -32001;
const JSON_RPC_SERVER_ERROR_2: i64 = -32002;
pub enum RpcCustomError { pub enum RpcCustomError {
NonexistentClusterRoot { NonexistentClusterRoot {
@ -13,6 +14,9 @@ pub enum RpcCustomError {
slot: Slot, slot: Slot,
first_available_block: Slot, first_available_block: Slot,
}, },
SendTransactionPreflightFailure {
message: String,
},
} }
impl From<RpcCustomError> for Error { impl From<RpcCustomError> for Error {
@ -40,6 +44,11 @@ impl From<RpcCustomError> for Error {
), ),
data: None, data: None,
}, },
RpcCustomError::SendTransactionPreflightFailure { message } => Self {
code: ErrorCode::ServerError(JSON_RPC_SERVER_ERROR_2),
message,
data: None,
},
} }
} }
} }

120
core/src/rpc_health.rs Normal file
View File

@ -0,0 +1,120 @@
use crate::cluster_info::ClusterInfo;
use solana_sdk::pubkey::Pubkey;
use std::{
collections::HashSet,
sync::atomic::{AtomicBool, Ordering},
sync::Arc,
};
#[derive(PartialEq, Clone, Copy)]
pub enum RpcHealthStatus {
Ok,
Behind, // Validator is behind its trusted validators
}
pub struct RpcHealth {
cluster_info: Arc<ClusterInfo>,
trusted_validators: Option<HashSet<Pubkey>>,
health_check_slot_distance: u64,
override_health_check: Arc<AtomicBool>,
#[cfg(test)]
stub_health_status: std::sync::RwLock<Option<RpcHealthStatus>>,
}
impl RpcHealth {
pub fn new(
cluster_info: Arc<ClusterInfo>,
trusted_validators: Option<HashSet<Pubkey>>,
health_check_slot_distance: u64,
override_health_check: Arc<AtomicBool>,
) -> Self {
Self {
cluster_info,
trusted_validators,
health_check_slot_distance,
override_health_check,
#[cfg(test)]
stub_health_status: std::sync::RwLock::new(None),
}
}
pub fn check(&self) -> RpcHealthStatus {
#[cfg(test)]
{
if let Some(stub_health_status) = *self.stub_health_status.read().unwrap() {
return stub_health_status;
}
}
if self.override_health_check.load(Ordering::Relaxed) {
RpcHealthStatus::Ok
} else if let Some(trusted_validators) = &self.trusted_validators {
let (latest_account_hash_slot, latest_trusted_validator_account_hash_slot) = {
(
self.cluster_info
.get_accounts_hash_for_node(&self.cluster_info.id(), |hashes| {
hashes
.iter()
.max_by(|a, b| a.0.cmp(&b.0))
.map(|slot_hash| slot_hash.0)
})
.flatten()
.unwrap_or(0),
trusted_validators
.iter()
.map(|trusted_validator| {
self.cluster_info
.get_accounts_hash_for_node(&trusted_validator, |hashes| {
hashes
.iter()
.max_by(|a, b| a.0.cmp(&b.0))
.map(|slot_hash| slot_hash.0)
})
.flatten()
.unwrap_or(0)
})
.max()
.unwrap_or(0),
)
};
// This validator is considered healthy if its latest account hash slot is within
// `health_check_slot_distance` of the latest trusted validator's account hash slot
if latest_account_hash_slot > 0
&& latest_trusted_validator_account_hash_slot > 0
&& latest_account_hash_slot
> latest_trusted_validator_account_hash_slot
.saturating_sub(self.health_check_slot_distance)
{
RpcHealthStatus::Ok
} else {
warn!(
"health check: me={}, latest trusted_validator={}",
latest_account_hash_slot, latest_trusted_validator_account_hash_slot
);
RpcHealthStatus::Behind
}
} else {
// No trusted validator point of reference available, so this validator is healthy
// because it's running
RpcHealthStatus::Ok
}
}
#[cfg(test)]
pub(crate) fn stub() -> Arc<Self> {
Arc::new(Self::new(
Arc::new(ClusterInfo::new_with_invalid_keypair(
crate::contact_info::ContactInfo::default(),
)),
None,
42,
Arc::new(AtomicBool::new(false)),
))
}
#[cfg(test)]
pub(crate) fn stub_set_health_status(&self, stub_health_status: Option<RpcHealthStatus>) {
*self.stub_health_status.write().unwrap() = stub_health_status;
}
}

View File

@ -1,7 +1,8 @@
//! The `rpc_service` module implements the Solana JSON RPC service. //! The `rpc_service` module implements the Solana JSON RPC service.
use crate::{ use crate::{
cluster_info::ClusterInfo, commitment::BlockCommitmentCache, rpc::*, validator::ValidatorExit, cluster_info::ClusterInfo, commitment::BlockCommitmentCache, rpc::*, rpc_health::*,
validator::ValidatorExit,
}; };
use jsonrpc_core::MetaIoHandler; use jsonrpc_core::MetaIoHandler;
use jsonrpc_http_server::{ use jsonrpc_http_server::{
@ -19,7 +20,7 @@ use std::{
collections::HashSet, collections::HashSet,
net::SocketAddr, net::SocketAddr,
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::atomic::{AtomicBool, Ordering}, sync::atomic::AtomicBool,
sync::{mpsc::channel, Arc, RwLock}, sync::{mpsc::channel, Arc, RwLock},
thread::{self, Builder, JoinHandle}, thread::{self, Builder, JoinHandle},
}; };
@ -38,22 +39,16 @@ struct RpcRequestMiddleware {
ledger_path: PathBuf, ledger_path: PathBuf,
snapshot_archive_path_regex: Regex, snapshot_archive_path_regex: Regex,
snapshot_config: Option<SnapshotConfig>, snapshot_config: Option<SnapshotConfig>,
cluster_info: Arc<ClusterInfo>,
trusted_validators: Option<HashSet<Pubkey>>,
bank_forks: Arc<RwLock<BankForks>>, bank_forks: Arc<RwLock<BankForks>>,
health_check_slot_distance: u64, health: Arc<RpcHealth>,
override_health_check: Arc<AtomicBool>,
} }
impl RpcRequestMiddleware { impl RpcRequestMiddleware {
pub fn new( pub fn new(
ledger_path: PathBuf, ledger_path: PathBuf,
snapshot_config: Option<SnapshotConfig>, snapshot_config: Option<SnapshotConfig>,
cluster_info: Arc<ClusterInfo>,
trusted_validators: Option<HashSet<Pubkey>>,
bank_forks: Arc<RwLock<BankForks>>, bank_forks: Arc<RwLock<BankForks>>,
health_check_slot_distance: u64, health: Arc<RpcHealth>,
override_health_check: Arc<AtomicBool>,
) -> Self { ) -> Self {
Self { Self {
ledger_path, ledger_path,
@ -62,11 +57,8 @@ impl RpcRequestMiddleware {
) )
.unwrap(), .unwrap(),
snapshot_config, snapshot_config,
cluster_info,
trusted_validators,
bank_forks, bank_forks,
health_check_slot_distance, health,
override_health_check,
} }
} }
@ -137,60 +129,10 @@ impl RpcRequestMiddleware {
} }
fn health_check(&self) -> &'static str { fn health_check(&self) -> &'static str {
let response = if self.override_health_check.load(Ordering::Relaxed) { let response = match self.health.check() {
"ok" RpcHealthStatus::Ok => "ok",
} else if let Some(trusted_validators) = &self.trusted_validators { RpcHealthStatus::Behind => "behind",
let (latest_account_hash_slot, latest_trusted_validator_account_hash_slot) = {
(
self.cluster_info
.get_accounts_hash_for_node(&self.cluster_info.id(), |hashes| {
hashes
.iter()
.max_by(|a, b| a.0.cmp(&b.0))
.map(|slot_hash| slot_hash.0)
})
.flatten()
.unwrap_or(0),
trusted_validators
.iter()
.map(|trusted_validator| {
self.cluster_info
.get_accounts_hash_for_node(&trusted_validator, |hashes| {
hashes
.iter()
.max_by(|a, b| a.0.cmp(&b.0))
.map(|slot_hash| slot_hash.0)
})
.flatten()
.unwrap_or(0)
})
.max()
.unwrap_or(0),
)
}; };
// This validator is considered healthy if its latest account hash slot is within
// `health_check_slot_distance` of the latest trusted validator's account hash slot
if latest_account_hash_slot > 0
&& latest_trusted_validator_account_hash_slot > 0
&& latest_account_hash_slot
> latest_trusted_validator_account_hash_slot
.saturating_sub(self.health_check_slot_distance)
{
"ok"
} else {
warn!(
"health check: me={}, latest trusted_validator={}",
latest_account_hash_slot, latest_trusted_validator_account_hash_slot
);
"behind"
}
} else {
// No trusted validator point of reference available, so this validator is healthy
// because it's running
"ok"
};
info!("health check: {}", response); info!("health check: {}", response);
response response
} }
@ -299,13 +241,21 @@ impl JsonRpcService {
) -> Self { ) -> Self {
info!("rpc bound to {:?}", rpc_addr); info!("rpc bound to {:?}", rpc_addr);
info!("rpc configuration: {:?}", config); info!("rpc configuration: {:?}", config);
let health_check_slot_distance = config.health_check_slot_distance;
let health = Arc::new(RpcHealth::new(
cluster_info.clone(),
trusted_validators,
config.health_check_slot_distance,
override_health_check,
));
let request_processor = Arc::new(RwLock::new(JsonRpcRequestProcessor::new( let request_processor = Arc::new(RwLock::new(JsonRpcRequestProcessor::new(
config, config,
bank_forks.clone(), bank_forks.clone(),
block_commitment_cache, block_commitment_cache,
blockstore, blockstore,
validator_exit.clone(), validator_exit.clone(),
health.clone(),
))); )));
#[cfg(test)] #[cfg(test)]
@ -324,11 +274,8 @@ impl JsonRpcService {
let request_middleware = RpcRequestMiddleware::new( let request_middleware = RpcRequestMiddleware::new(
ledger_path, ledger_path,
snapshot_config, snapshot_config,
cluster_info.clone(),
trusted_validators,
bank_forks.clone(), bank_forks.clone(),
health_check_slot_distance, health.clone(),
override_health_check,
); );
let server = ServerBuilder::with_meta_extractor( let server = ServerBuilder::with_meta_extractor(
io, io,
@ -403,7 +350,10 @@ mod tests {
}; };
use solana_runtime::bank::Bank; use solana_runtime::bank::Bank;
use solana_sdk::signature::Signer; use solana_sdk::signature::Signer;
use std::net::{IpAddr, Ipv4Addr, SocketAddr}; use std::{
net::{IpAddr, Ipv4Addr, SocketAddr},
sync::atomic::Ordering,
};
#[test] #[test]
fn test_rpc_new() { fn test_rpc_new() {
@ -481,17 +431,12 @@ mod tests {
#[test] #[test]
fn test_is_file_get_path() { fn test_is_file_get_path() {
let cluster_info = Arc::new(ClusterInfo::new_with_invalid_keypair(ContactInfo::default()));
let bank_forks = create_bank_forks(); let bank_forks = create_bank_forks();
let rrm = RpcRequestMiddleware::new( let rrm = RpcRequestMiddleware::new(
PathBuf::from("/"), PathBuf::from("/"),
None, None,
cluster_info.clone(),
None,
bank_forks.clone(), bank_forks.clone(),
42, RpcHealth::stub(),
Arc::new(AtomicBool::new(false)),
); );
let rrm_with_snapshot_config = RpcRequestMiddleware::new( let rrm_with_snapshot_config = RpcRequestMiddleware::new(
PathBuf::from("/"), PathBuf::from("/"),
@ -501,11 +446,8 @@ mod tests {
snapshot_path: PathBuf::from("/"), snapshot_path: PathBuf::from("/"),
compression: CompressionType::Bzip2, compression: CompressionType::Bzip2,
}), }),
cluster_info,
None,
bank_forks, bank_forks,
42, RpcHealth::stub(),
Arc::new(AtomicBool::new(false)),
); );
assert!(rrm.is_file_get_path("/genesis.tar.bz2")); assert!(rrm.is_file_get_path("/genesis.tar.bz2"));
@ -531,16 +473,11 @@ mod tests {
#[test] #[test]
fn test_health_check_with_no_trusted_validators() { fn test_health_check_with_no_trusted_validators() {
let cluster_info = Arc::new(ClusterInfo::new_with_invalid_keypair(ContactInfo::default()));
let rm = RpcRequestMiddleware::new( let rm = RpcRequestMiddleware::new(
PathBuf::from("/"), PathBuf::from("/"),
None, None,
cluster_info,
None,
create_bank_forks(), create_bank_forks(),
42, RpcHealth::stub(),
Arc::new(AtomicBool::new(false)),
); );
assert_eq!(rm.health_check(), "ok"); assert_eq!(rm.health_check(), "ok");
} }
@ -548,20 +485,18 @@ mod tests {
#[test] #[test]
fn test_health_check_with_trusted_validators() { fn test_health_check_with_trusted_validators() {
let cluster_info = Arc::new(ClusterInfo::new_with_invalid_keypair(ContactInfo::default())); let cluster_info = Arc::new(ClusterInfo::new_with_invalid_keypair(ContactInfo::default()));
let health_check_slot_distance = 123; let health_check_slot_distance = 123;
let override_health_check = Arc::new(AtomicBool::new(false)); let override_health_check = Arc::new(AtomicBool::new(false));
let trusted_validators = vec![Pubkey::new_rand(), Pubkey::new_rand(), Pubkey::new_rand()]; let trusted_validators = vec![Pubkey::new_rand(), Pubkey::new_rand(), Pubkey::new_rand()];
let rm = RpcRequestMiddleware::new(
PathBuf::from("/"), let health = Arc::new(RpcHealth::new(
None,
cluster_info.clone(), cluster_info.clone(),
Some(trusted_validators.clone().into_iter().collect()), Some(trusted_validators.clone().into_iter().collect()),
create_bank_forks(),
health_check_slot_distance, health_check_slot_distance,
override_health_check.clone(), override_health_check.clone(),
); ));
let rm = RpcRequestMiddleware::new(PathBuf::from("/"), None, create_bank_forks(), health);
// No account hashes for this node or any trusted validators == "behind" // No account hashes for this node or any trusted validators == "behind"
assert_eq!(rm.health_check(), "behind"); assert_eq!(rm.health_check(), "behind");

View File

@ -1065,11 +1065,20 @@ curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "m
### sendTransaction ### sendTransaction
Creates new transaction Submits a signed transaction to the cluster for processing.
Before submitting, the following preflight checks are performed:
1. The transaction signatures are verified
2. The transaction is simulated against the latest max confirmed bank
and on failure an error will be returned. Preflight checks may be disabled if
desired.
#### Parameters: #### Parameters:
* `<string>` - fully-signed Transaction, as base-58 encoded string * `<string>` - fully-signed Transaction, as base-58 encoded string
* `<object>` - (optional) Configuration object containing the following field:
* `skipPreflight: <bool>` - if true, skip the preflight transaction checks (default: false)
#### Results: #### Results:

View File

@ -329,6 +329,7 @@ pub fn process_slots(rpc_client: &RpcClient, accounts_info: &mut AccountsInfo, b
mod test { mod test {
use super::*; use super::*;
use serial_test_derive::serial; use serial_test_derive::serial;
use solana_client::rpc_config::RpcSendTransactionConfig;
use solana_core::{rpc::JsonRpcConfig, validator::ValidatorConfig}; use solana_core::{rpc::JsonRpcConfig, validator::ValidatorConfig};
use solana_local_cluster::local_cluster::{ClusterConfig, LocalCluster}; use solana_local_cluster::local_cluster::{ClusterConfig, LocalCluster};
use solana_sdk::{ use solana_sdk::{
@ -443,7 +444,8 @@ mod test {
// Withdraw instruction causes non-compliance // Withdraw instruction causes non-compliance
let stake3_withdraw_signature = rpc_client let stake3_withdraw_signature = rpc_client
.send_transaction(&Transaction::new( .send_transaction_with_config(
&Transaction::new(
&[&payer, &stake3_keypair], &[&payer, &stake3_keypair],
Message::new_with_payer( Message::new_with_payer(
&[stake_instruction::withdraw( &[stake_instruction::withdraw(
@ -456,7 +458,11 @@ mod test {
Some(&payer.pubkey()), Some(&payer.pubkey()),
), ),
blockhash, blockhash,
)) ),
RpcSendTransactionConfig {
skip_preflight: true,
},
)
.unwrap(); .unwrap();
rpc_client rpc_client
@ -492,7 +498,8 @@ mod test {
// Split stake4 into stake5 // Split stake4 into stake5
let stake5_keypair = Keypair::new(); let stake5_keypair = Keypair::new();
let stake45_split_signature = rpc_client let stake45_split_signature = rpc_client
.send_transaction(&Transaction::new( .send_transaction_with_config(
&Transaction::new(
&[&payer, &stake5_keypair], &[&payer, &stake5_keypair],
Message::new_with_payer( Message::new_with_payer(
&stake_instruction::split( &stake_instruction::split(
@ -504,7 +511,11 @@ mod test {
Some(&payer.pubkey()), Some(&payer.pubkey()),
), ),
blockhash, blockhash,
)) ),
RpcSendTransactionConfig {
skip_preflight: true,
},
)
.unwrap(); .unwrap();
rpc_client rpc_client
@ -539,12 +550,17 @@ mod test {
// Withdraw 1 sol from system 1 to make it non-compliant // Withdraw 1 sol from system 1 to make it non-compliant
rpc_client rpc_client
.send_transaction(&system_transaction::transfer( .send_transaction_with_config(
&system_transaction::transfer(
&system1_keypair, &system1_keypair,
&payer.pubkey(), &payer.pubkey(),
one_sol, one_sol,
blockhash, blockhash,
)) ),
RpcSendTransactionConfig {
skip_preflight: true,
},
)
.unwrap(); .unwrap();
// System transfer 2 // System transfer 2
@ -572,12 +588,17 @@ mod test {
// Withdraw 1 sol - 1 lamport from system 2, it's still compliant // Withdraw 1 sol - 1 lamport from system 2, it's still compliant
rpc_client rpc_client
.send_transaction(&system_transaction::transfer( .send_transaction_with_config(
&system_transaction::transfer(
&system2_keypair, &system2_keypair,
&payer.pubkey(), &payer.pubkey(),
one_sol - 1, one_sol - 1,
blockhash, blockhash,
)) ),
RpcSendTransactionConfig {
skip_preflight: true,
},
)
.unwrap(); .unwrap();
// Process all the transactions // Process all the transactions

View File

@ -1,4 +1,4 @@
use solana_client::rpc_client::RpcClient; use solana_client::{rpc_client::RpcClient, rpc_config::RpcSendTransactionConfig};
use solana_runtime::bank_client::BankClient; use solana_runtime::bank_client::BankClient;
use solana_sdk::{ use solana_sdk::{
account::Account, account::Account,
@ -32,7 +32,12 @@ pub trait Client {
impl Client for RpcClient { impl Client for RpcClient {
fn send_transaction1(&self, transaction: Transaction) -> Result<Signature> { fn send_transaction1(&self, transaction: Transaction) -> Result<Signature> {
self.send_transaction(&transaction) self.send_transaction_with_config(
&transaction,
RpcSendTransactionConfig {
skip_preflight: true,
},
)
.map_err(|e| TransportError::Custom(e.to_string())) .map_err(|e| TransportError::Custom(e.to_string()))
} }