diff --git a/client/src/rpc_config.rs b/client/src/rpc_config.rs index 9773bb7a1..2f0427ce1 100644 --- a/client/src/rpc_config.rs +++ b/client/src/rpc_config.rs @@ -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, diff --git a/core/src/rpc.rs b/core/src/rpc.rs index 4078fe9a4..bb7efc5d7 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -713,6 +713,19 @@ fn verify_signature(input: &str) -> Result { .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>, @@ -904,7 +917,12 @@ pub trait RpcSol { ) -> Result; #[rpc(meta, name = "sendTransaction")] - fn send_transaction(&self, meta: Self::Metadata, data: String) -> Result; + fn send_transaction( + &self, + meta: Self::Metadata, + data: String, + config: Option, + ) -> Result; #[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 { + fn send_transaction( + &self, + meta: Self::Metadata, + data: String, + config: Option, + ) -> Result { + 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, ) -> RpcResponse { 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( diff --git a/core/src/rpc_error.rs b/core/src/rpc_error.rs index ab727776e..276ad96c5 100644 --- a/core/src/rpc_error.rs +++ b/core/src/rpc_error.rs @@ -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 for Error { @@ -40,6 +44,11 @@ impl From for Error { ), data: None, }, + RpcCustomError::SendTransactionPreflightFailure { message } => Self { + code: ErrorCode::ServerError(JSON_RPC_SERVER_ERROR_2), + message, + data: None, + }, } } } diff --git a/docs/src/apps/jsonrpc-api.md b/docs/src/apps/jsonrpc-api.md index 1a92877f6..0eda6a9d7 100644 --- a/docs/src/apps/jsonrpc-api.md +++ b/docs/src/apps/jsonrpc-api.md @@ -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: * `` - fully-signed Transaction, as base-58 encoded string +* `` - (optional) Configuration object containing the following field: + * `skipPreflight: ` - if true, skip the preflight transaction checks (default: false) + #### Results: