diff --git a/client/src/rpc_client.rs b/client/src/rpc_client.rs index 3834549a26..1aa2b97eaa 100644 --- a/client/src/rpc_client.rs +++ b/client/src/rpc_client.rs @@ -119,6 +119,19 @@ impl RpcClient { } } + pub fn simulate_transaction( + &self, + transaction: &Transaction, + sig_verify: bool, + ) -> RpcResult { + let serialized_encoded = bs58::encode(serialize(transaction).unwrap()).into_string(); + self.send( + RpcRequest::SimulateTransaction, + json!([serialized_encoded, { "sigVerify": sig_verify }]), + 0, + ) + } + pub fn get_signature_status( &self, signature: &Signature, diff --git a/client/src/rpc_config.rs b/client/src/rpc_config.rs index e43e3d3fda..e31bdd9e49 100644 --- a/client/src/rpc_config.rs +++ b/client/src/rpc_config.rs @@ -6,6 +6,12 @@ pub struct RpcSignatureStatusConfig { pub search_transaction_history: bool, } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RpcSimulateTransactionConfig { + pub sig_verify: bool, +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum RpcLargestAccountsFilter { diff --git a/client/src/rpc_request.rs b/client/src/rpc_request.rs index 5e470c1258..2edd88ec10 100644 --- a/client/src/rpc_request.rs +++ b/client/src/rpc_request.rs @@ -40,6 +40,7 @@ pub enum RpcRequest { RegisterNode, RequestAirdrop, SendTransaction, + SimulateTransaction, SignVote, GetMinimumBalanceForRentExemption, MinimumLedgerSlot, @@ -84,6 +85,7 @@ impl fmt::Display for RpcRequest { RpcRequest::RegisterNode => "registerNode", RpcRequest::RequestAirdrop => "requestAirdrop", RpcRequest::SendTransaction => "sendTransaction", + RpcRequest::SimulateTransaction => "simulateTransaction", RpcRequest::SignVote => "signVote", RpcRequest::GetMinimumBalanceForRentExemption => "getMinimumBalanceForRentExemption", RpcRequest::MinimumLedgerSlot => "minimumLedgerSlot", diff --git a/core/src/rpc.rs b/core/src/rpc.rs index 2ab4d9c6e4..6f9be6b3ef 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -847,6 +847,14 @@ pub trait RpcSol { #[rpc(meta, name = "sendTransaction")] fn send_transaction(&self, meta: Self::Metadata, data: String) -> Result; + #[rpc(meta, name = "simulateTransaction")] + fn simulate_transaction( + &self, + meta: Self::Metadata, + data: String, + config: Option, + ) -> RpcResponse; + #[rpc(meta, name = "getSlotLeader")] fn get_slot_leader( &self, @@ -1327,41 +1335,67 @@ impl RpcSol for RpcSolImpl { } fn send_transaction(&self, meta: Self::Metadata, data: String) -> Result { - let data = bs58::decode(data).into_vec().unwrap(); - if data.len() >= PACKET_DATA_SIZE { - info!( - "send_transaction: transaction too large: {} bytes (max: {} bytes)", - data.len(), - PACKET_DATA_SIZE - ); - return Err(Error::invalid_request()); - } - let tx: Transaction = bincode::config() - .limit(PACKET_DATA_SIZE as u64) - .deserialize(&data) - .map_err(|err| { - info!("send_transaction: deserialize error: {:?}", err); - Error::invalid_request() - })?; - + let (wire_transaction, transaction) = deserialize_bs58_transaction(data)?; let transactions_socket = UdpSocket::bind("0.0.0.0:0").unwrap(); let tpu_addr = get_tpu_addr(&meta.cluster_info)?; trace!("send_transaction: leader is {:?}", &tpu_addr); transactions_socket - .send_to(&data, tpu_addr) + .send_to(&wire_transaction, tpu_addr) .map_err(|err| { info!("send_transaction: send_to error: {:?}", err); Error::internal_error() })?; - let signature = tx.signatures[0].to_string(); + let signature = transaction.signatures[0].to_string(); trace!( "send_transaction: sent {} bytes, signature={}", - data.len(), + wire_transaction.len(), signature ); Ok(signature) } + fn simulate_transaction( + &self, + meta: Self::Metadata, + data: String, + 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 mut result = if config.sig_verify { + transaction.verify() + } else { + Ok(()) + }; + + 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(); + } + + new_response( + &bank, + TransactionStatus { + slot: bank.slot(), + confirmations: Some(0), + status: result.clone(), + err: result.err(), + }, + ) + } + fn get_slot_leader( &self, meta: Self::Metadata, @@ -1498,6 +1532,26 @@ impl RpcSol for RpcSolImpl { } } +fn deserialize_bs58_transaction(bs58_transaction: String) -> Result<(Vec, Transaction)> { + let wire_transaction = bs58::decode(bs58_transaction).into_vec().unwrap(); + if wire_transaction.len() >= PACKET_DATA_SIZE { + info!( + "transaction too large: {} bytes (max: {} bytes)", + wire_transaction.len(), + PACKET_DATA_SIZE + ); + return Err(Error::invalid_request()); + } + bincode::config() + .limit(PACKET_DATA_SIZE as u64) + .deserialize(&wire_transaction) + .map_err(|err| { + info!("transaction deserialize error: {:?}", err); + Error::invalid_request() + }) + .map(|transaction| (wire_transaction, transaction)) +} + #[cfg(test)] pub mod tests { use super::*; @@ -2146,6 +2200,133 @@ pub mod tests { assert_eq!(expected, result); } + #[test] + fn test_rpc_simulate_transaction() { + let bob_pubkey = Pubkey::new_rand(); + let RpcHandler { + io, + meta, + blockhash, + alice, + bank, + .. + } = start_rpc_handler_with_tx(&bob_pubkey); + + let mut tx = system_transaction::transfer(&alice, &bob_pubkey, 1234, blockhash); + let tx_serialized_encoded = bs58::encode(serialize(&tx).unwrap()).into_string(); + tx.signatures[0] = Signature::default(); + let tx_badsig_serialized_encoded = bs58::encode(serialize(&tx).unwrap()).into_string(); + + bank.freeze(); // Ensure the root bank is frozen, `start_rpc_handler_with_tx()` doesn't do this + + // Good signature with sigVerify=true + let req = format!( + r#"{{"jsonrpc":"2.0","id":1,"method":"simulateTransaction","params":["{}", {{"sigVerify": true}}]}}"#, + tx_serialized_encoded, + ); + let res = io.handle_request_sync(&req, meta.clone()); + let expected = json!({ + "jsonrpc": "2.0", + "result": { + "context":{"slot":0}, + "value":{"confirmations":0,"slot": 0,"status":{"Ok":null},"err":null} + }, + "id": 1, + }); + let expected: Response = + serde_json::from_value(expected).expect("expected response deserialization"); + let result: Response = serde_json::from_str(&res.expect("actual response")) + .expect("actual response deserialization"); + assert_eq!(expected, result); + + // Bad signature with sigVerify=true + let req = format!( + r#"{{"jsonrpc":"2.0","id":1,"method":"simulateTransaction","params":["{}", {{"sigVerify": true}}]}}"#, + tx_badsig_serialized_encoded, + ); + let res = io.handle_request_sync(&req, meta.clone()); + let expected = json!({ + "jsonrpc": "2.0", + "result": { + "context":{"slot":0}, + "value":{"confirmations":0,"slot":0,"status":{"Err":"SignatureFailure"},"err":"SignatureFailure"} + }, + "id": 1, + }); + let expected: Response = + serde_json::from_value(expected).expect("expected response deserialization"); + let result: Response = serde_json::from_str(&res.expect("actual response")) + .expect("actual response deserialization"); + assert_eq!(expected, result); + + // Bad signature with sigVerify=false + let req = format!( + r#"{{"jsonrpc":"2.0","id":1,"method":"simulateTransaction","params":["{}", {{"sigVerify": false}}]}}"#, + tx_serialized_encoded, + ); + let res = io.handle_request_sync(&req, meta.clone()); + let expected = json!({ + "jsonrpc": "2.0", + "result": { + "context":{"slot":0}, + "value":{"confirmations":0,"slot": 0,"status":{"Ok":null},"err":null} + }, + "id": 1, + }); + let expected: Response = + serde_json::from_value(expected).expect("expected response deserialization"); + let result: Response = serde_json::from_str(&res.expect("actual response")) + .expect("actual response deserialization"); + assert_eq!(expected, result); + + // Bad signature with default sigVerify setting (false) + let req = format!( + r#"{{"jsonrpc":"2.0","id":1,"method":"simulateTransaction","params":["{}"]}}"#, + tx_serialized_encoded, + ); + let res = io.handle_request_sync(&req, meta.clone()); + let expected = json!({ + "jsonrpc": "2.0", + "result": { + "context":{"slot":0}, + "value":{"confirmations":0,"slot": 0,"status":{"Ok":null},"err":null} + }, + "id": 1, + }); + let expected: Response = + serde_json::from_value(expected).expect("expected response deserialization"); + let result: Response = serde_json::from_str(&res.expect("actual response")) + .expect("actual response deserialization"); + assert_eq!(expected, result); + } + + #[test] + #[should_panic] + fn test_rpc_simulate_transaction_panic_on_unfrozen_bank() { + let bob_pubkey = Pubkey::new_rand(); + let RpcHandler { + io, + meta, + blockhash, + alice, + bank, + .. + } = start_rpc_handler_with_tx(&bob_pubkey); + + let tx = system_transaction::transfer(&alice, &bob_pubkey, 1234, blockhash); + let tx_serialized_encoded = bs58::encode(serialize(&tx).unwrap()).into_string(); + + assert!(!bank.is_frozen()); + + let req = format!( + r#"{{"jsonrpc":"2.0","id":1,"method":"simulateTransaction","params":["{}", {{"sigVerify": true}}]}}"#, + tx_serialized_encoded, + ); + + // should panic because `bank` is not frozen + let _ = io.handle_request_sync(&req, meta.clone()); + } + #[test] fn test_rpc_confirm_tx() { let bob_pubkey = Pubkey::new_rand(); diff --git a/docs/src/apps/jsonrpc-api.md b/docs/src/apps/jsonrpc-api.md index 3fa0ae4433..48de57f361 100644 --- a/docs/src/apps/jsonrpc-api.md +++ b/docs/src/apps/jsonrpc-api.md @@ -50,6 +50,7 @@ To interact with a Solana node inside a JavaScript application, use the [solana- * [minimumLedgerSlot](jsonrpc-api.md#minimumledgerslot) * [requestAirdrop](jsonrpc-api.md#requestairdrop) * [sendTransaction](jsonrpc-api.md#sendtransaction) +* [simulateTransaction](jsonrpc-api.md#simulatetransaction) * [setLogFilter](jsonrpc-api.md#setlogfilter) * [validatorExit](jsonrpc-api.md#validatorexit) * [Subscription Websocket](jsonrpc-api.md#subscription-websocket) @@ -1112,10 +1113,34 @@ Creates new transaction ```bash // Request -curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"sendTransaction", "params":["3gKEMTuxvm3DKEJc4UyiyoNz1sxwdVRW2pyDDXqaCvUjGApnsazGh2y4W92zuaSSdJhBbWLYAkZokBt4N5oW27R7zCVaLLpLxvATL2GgheEh9DmmDR1P9r1ZqirVXM2fF3z5cafmc4EtwWc1UErFdCWj1qYvy4bDGMLXRYLURxaKytEEqrxz6JXj8rUHhDpjTZeFxmC6iAW3hZr6cmaAzewQCQfiEv2HfydriwHDtN95u3Y1EF6SuXxcRqox2aTjGye2Ln9zFj4XbnAtjCmkZhR"]}' http://localhost:8899 +curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"sendTransaction", "params":["4hXTCkRzt9WyecNzV1XPgCDfGAZzQKNxLXgynz5QDuWWPSAZBZSHptvWRL3BjCvzUXRdKvHL2b7yGrRQcWyaqsaBCncVG7BFggS8w9snUts67BSh3EqKpXLUm5UMHfD7ZBe9GhARjbNQMLJ1QD3Spr6oMTBU6EhdB4RD8CP2xUxr2u3d6fos36PD98XS6oX8TQjLpsMwncs5DAMiD4nNnR8NBfyghGCWvCVifVwvA8B8TJxE1aiyiv2L429BCWfyzAme5sZW8rDb14NeCQHhZbtNqfXhcp2tAnaAT"]}' http://localhost:8899 // Result -{"jsonrpc":"2.0","result":"2EBVM6cB8vAAD93Ktr6Vd8p67XPbQzCJX47MpReuiCXJAtcjaxpvWpcg9Ege1Nr5Tk3a2GFrByT7WPBjdsTycY9b","id":1} +{"jsonrpc":"2.0","result":"2id3YC2jK9G5Wo2phDx4gJVAew8DcY5NAojnVuao8rkxwPYPe8cSwE5GzhEgJA2y8fVjDEo6iR6ykBvDxrTQrtpb","id":1} +``` + +### simulateTransaction + +Simulate sending a transaction + +#### Parameters: + +* `` - Transaction, as base-58 encoded string. The transaction must have a valid blockhash, but is not required to be signed. +* `` - (optional) Configuration object containing the following field: + * `sigVerify: ` - if true the transaction signatures will be verified (default: false) + +#### Results: + +An RpcResponse containing a TransactionStatus object + +#### Example: + +```bash +// Request +curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"simulateTransaction", "params":["4hXTCkRzt9WyecNzV1XPgCDfGAZzQKNxLXgynz5QDuWWPSAZBZSHptvWRL3BjCvzUXRdKvHL2b7yGrRQcWyaqsaBCncVG7BFggS8w9snUts67BSh3EqKpXLUm5UMHfD7ZBe9GhARjbNQMLJ1QD3Spr6oMTBU6EhdB4RD8CP2xUxr2u3d6fos36PD98XS6oX8TQjLpsMwncs5DAMiD4nNnR8NBfyghGCWvCVifVwvA8B8TJxE1aiyiv2L429BCWfyzAme5sZW8rDb14NeCQHhZbtNqfXhcp2tAnaAT"]}' http://localhost:8899 + +// Result +{"jsonrpc":"2.0","result":{"context":{"slot":218},"value":{"confirmations":0,"err":null,"slot":218,"status":{"Ok":null}}},"id":1} ``` ### setLogFilter