Add preflight checks to sendTransaction RPC method

This commit is contained in:
Michael Vines 2020-05-29 23:16:35 -07:00
parent 27e2e3665a
commit 189aa7962e
4 changed files with 78 additions and 19 deletions

View File

@ -6,7 +6,13 @@ pub struct RpcSignatureStatusConfig {
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")]
pub struct RpcSimulateTransactionConfig {
pub sig_verify: bool,

View File

@ -713,6 +713,19 @@ fn verify_signature(input: &str) -> Result<Signature> {
.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());
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)]
pub struct Meta {
pub request_processor: Arc<RwLock<JsonRpcRequestProcessor>>,
@ -904,7 +917,12 @@ pub trait RpcSol {
) -> Result<String>;
#[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")]
fn simulate_transaction(
@ -1406,8 +1424,36 @@ 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 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());
}
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. In these cases the user should use the
// config.skip_preflight flag
return Err(RpcCustomError::SendTransactionPreflightFailure {
message: format!("Transaction simulation failed: {}", err),
}
.into());
}
}
let transactions_socket = UdpSocket::bind("0.0.0.0:0").unwrap();
let tpu_addr = get_tpu_addr(&meta.cluster_info)?;
transactions_socket
@ -1416,7 +1462,6 @@ impl RpcSol for RpcSolImpl {
info!("send_transaction: send_to error: {:?}", err);
Error::internal_error()
})?;
let signature = transaction.signatures[0].to_string();
trace!(
"send_transaction: sent {} bytes, signature={}",
wire_transaction.len(),
@ -1432,10 +1477,7 @@ impl RpcSol for RpcSolImpl {
config: Option<RpcSimulateTransactionConfig>,
) -> RpcResponse<TransactionStatus> {
let (_, transaction) = deserialize_bs58_transaction(data)?;
let config = config.unwrap_or(RpcSimulateTransactionConfig { sig_verify: false });
let bank = &*meta.request_processor.read().unwrap().bank(None)?;
assert!(bank.is_frozen());
let config = config.unwrap_or_default();
let mut result = if config.sig_verify {
transaction.verify()
@ -1443,17 +1485,10 @@ impl RpcSol for RpcSolImpl {
Ok(())
};
let bank = &*meta.request_processor.read().unwrap().bank(None)?;
if result.is_ok() {
let transactions = [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();
result = run_transaction_simulation(&bank, &[transaction]);
}
new_response(

View File

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

View File

@ -1065,11 +1065,20 @@ curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "m
### 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:
* `<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: