automerge
This commit is contained in:
123
cli/src/cli.rs
123
cli/src/cli.rs
@ -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))
|
||||||
})?;
|
})?;
|
||||||
|
@ -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,
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
189
core/src/rpc.rs
189
core/src/rpc.rs
@ -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),
|
||||||
|
@ -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
120
core/src/rpc_health.rs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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");
|
||||||
|
@ -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:
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user