diff --git a/Cargo.lock b/Cargo.lock index 7f264721b5..699c0754b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -217,6 +217,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" +[[package]] +name = "base64" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" + [[package]] name = "bincode" version = "1.3.1" @@ -3614,6 +3620,7 @@ name = "solana-client" version = "1.4.0" dependencies = [ "assert_matches", + "base64 0.13.0", "bincode", "bs58", "clap", diff --git a/client/Cargo.toml b/client/Cargo.toml index 6db4c04572..b564bb98be 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -9,6 +9,7 @@ license = "Apache-2.0" edition = "2018" [dependencies] +base64 = "0.13.0" bincode = "1.3.1" bs58 = "0.3.1" clap = "2.33.0" diff --git a/client/src/rpc_client.rs b/client/src/rpc_client.rs index 1b31f8238e..9a1cf45ca1 100644 --- a/client/src/rpc_client.rs +++ b/client/src/rpc_client.rs @@ -5,7 +5,8 @@ use crate::{ rpc_config::RpcAccountInfoConfig, rpc_config::{ RpcGetConfirmedSignaturesForAddress2Config, RpcLargestAccountsConfig, - RpcProgramAccountsConfig, RpcSendTransactionConfig, RpcTokenAccountsFilter, + RpcProgramAccountsConfig, RpcSendTransactionConfig, RpcSimulateTransactionConfig, + RpcTokenAccountsFilter, }, rpc_request::{RpcError, RpcRequest, TokenAccountsFilter}, rpc_response::*, @@ -48,6 +49,26 @@ pub struct RpcClient { sender: Box, } +fn serialize_encode_transaction( + transaction: &Transaction, + encoding: UiTransactionEncoding, +) -> ClientResult { + let serialized = serialize(transaction) + .map_err(|e| ClientErrorKind::Custom(format!("transaction serialization failed: {}", e)))?; + let encoded = match encoding { + UiTransactionEncoding::Base58 => bs58::encode(serialized).into_string(), + UiTransactionEncoding::Base64 => base64::encode(serialized), + _ => { + return Err(ClientErrorKind::Custom(format!( + "unsupported transaction encoding: {}. Supported encodings: base58, base64", + encoding + )) + .into()) + } + }; + Ok(encoded) +} + impl RpcClient { pub fn new_sender(sender: T) -> Self { Self { @@ -112,8 +133,8 @@ impl RpcClient { transaction: &Transaction, config: RpcSendTransactionConfig, ) -> ClientResult { - let serialized_encoded = bs58::encode(serialize(transaction).unwrap()).into_string(); - + let encoding = config.encoding.unwrap_or(UiTransactionEncoding::Base58); + let serialized_encoded = serialize_encode_transaction(transaction, encoding)?; let signature_base58_str: String = self.send( RpcRequest::SendTransaction, json!([serialized_encoded, config]), @@ -142,7 +163,21 @@ impl RpcClient { transaction: &Transaction, sig_verify: bool, ) -> RpcResult { - let serialized_encoded = bs58::encode(serialize(transaction).unwrap()).into_string(); + self.simulate_transaction_with_config( + transaction, + sig_verify, + RpcSimulateTransactionConfig::default(), + ) + } + + pub fn simulate_transaction_with_config( + &self, + transaction: &Transaction, + sig_verify: bool, + config: RpcSimulateTransactionConfig, + ) -> RpcResult { + let encoding = config.encoding.unwrap_or(UiTransactionEncoding::Base58); + let serialized_encoded = serialize_encode_transaction(transaction, encoding)?; self.send( RpcRequest::SimulateTransaction, json!([serialized_encoded, { "sigVerify": sig_verify }]), diff --git a/client/src/rpc_config.rs b/client/src/rpc_config.rs index 1335b302df..671da02b38 100644 --- a/client/src/rpc_config.rs +++ b/client/src/rpc_config.rs @@ -4,6 +4,7 @@ use solana_sdk::{ clock::Epoch, commitment_config::{CommitmentConfig, CommitmentLevel}, }; +use solana_transaction_status::UiTransactionEncoding; #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -17,6 +18,7 @@ pub struct RpcSendTransactionConfig { #[serde(default)] pub skip_preflight: bool, pub preflight_commitment: Option, + pub encoding: Option, } #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] @@ -26,6 +28,7 @@ pub struct RpcSimulateTransactionConfig { pub sig_verify: bool, #[serde(flatten)] pub commitment: Option, + pub encoding: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/core/Cargo.toml b/core/Cargo.toml index e63f9aeb9a..103fcc8175 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -14,6 +14,7 @@ edition = "2018" codecov = { repository = "solana-labs/solana", branch = "master", service = "github" } [dependencies] +base64 = "0.12.3" bincode = "1.3.1" bv = { version = "0.11.1", features = ["serde"] } bs58 = "0.3.1" @@ -78,7 +79,6 @@ solana-rayon-threadlimit = { path = "../rayon-threadlimit", version = "1.4.0" } trees = "0.2.1" [dev-dependencies] -base64 = "0.12.3" matches = "0.1.6" reqwest = { version = "0.10.6", default-features = false, features = ["blocking", "rustls-tls", "json"] } serial_test = "0.4.0" diff --git a/core/src/rpc.rs b/core/src/rpc.rs index 0f79beacc1..6edd1753b4 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -2268,7 +2268,8 @@ impl RpcSol for RpcSolImpl { ) -> Result { debug!("send_transaction rpc request received"); let config = config.unwrap_or_default(); - let (wire_transaction, transaction) = deserialize_bs58_transaction(data)?; + let encoding = config.encoding.unwrap_or(UiTransactionEncoding::Base58); + let (wire_transaction, transaction) = deserialize_transaction(data, encoding)?; let bank = &*meta.bank(None); let last_valid_slot = bank .get_blockhash_last_valid_slot(&transaction.message.recent_blockhash) @@ -2313,8 +2314,9 @@ impl RpcSol for RpcSolImpl { config: Option, ) -> Result> { debug!("simulate_transaction rpc request received"); - let (_, transaction) = deserialize_bs58_transaction(data)?; let config = config.unwrap_or_default(); + let encoding = config.encoding.unwrap_or(UiTransactionEncoding::Base58); + let (_, transaction) = deserialize_transaction(data, encoding)?; let mut result = if config.sig_verify { transaction.verify() @@ -2600,18 +2602,44 @@ impl RpcSol for RpcSolImpl { } const WORST_CASE_BASE58_TX: usize = 1683; // Golden, bump if PACKET_DATA_SIZE changes -fn deserialize_bs58_transaction(bs58_transaction: String) -> Result<(Vec, Transaction)> { - if bs58_transaction.len() > WORST_CASE_BASE58_TX { - return Err(Error::invalid_params(format!( - "encoded transaction too large: {} bytes (max: encoded/raw {}/{})", - bs58_transaction.len(), - WORST_CASE_BASE58_TX, - PACKET_DATA_SIZE, - ))); - } - let wire_transaction = bs58::decode(bs58_transaction) - .into_vec() - .map_err(|e| Error::invalid_params(format!("{:?}", e)))?; +const WORST_CASE_BASE64_TX: usize = 1644; // Golden, bump if PACKET_DATA_SIZE changes +fn deserialize_transaction( + encoded_transaction: String, + encoding: UiTransactionEncoding, +) -> Result<(Vec, Transaction)> { + let wire_transaction = match encoding { + UiTransactionEncoding::Base58 => { + if encoded_transaction.len() > WORST_CASE_BASE58_TX { + return Err(Error::invalid_params(format!( + "encoded transaction too large: {} bytes (max: encoded/raw {}/{})", + encoded_transaction.len(), + WORST_CASE_BASE58_TX, + PACKET_DATA_SIZE, + ))); + } + bs58::decode(encoded_transaction) + .into_vec() + .map_err(|e| Error::invalid_params(format!("{:?}", e)))? + } + UiTransactionEncoding::Base64 => { + if encoded_transaction.len() > WORST_CASE_BASE64_TX { + return Err(Error::invalid_params(format!( + "encoded transaction too large: {} bytes (max: encoded/raw {}/{})", + encoded_transaction.len(), + WORST_CASE_BASE64_TX, + PACKET_DATA_SIZE, + ))); + } + base64::decode(encoded_transaction) + .map_err(|e| Error::invalid_params(format!("{:?}", e)))? + } + _ => { + return Err(Error::invalid_params(format!( + "unsupported transaction encoding: {}. Supported encodings: base58, base64", + encoding + ))) + } + }; if wire_transaction.len() > PACKET_DATA_SIZE { let err = format!( "transaction too large: {} bytes (max: {} bytes)", @@ -5789,7 +5817,52 @@ pub mod tests { #[test] fn test_worst_case_encoded_tx_goldens() { let ff_tx = vec![0xffu8; PACKET_DATA_SIZE]; - let tx58 = bs58::encode(ff_tx).into_string(); + let tx58 = bs58::encode(&ff_tx).into_string(); assert_eq!(tx58.len(), WORST_CASE_BASE58_TX); + let tx64 = base64::encode(&ff_tx); + assert_eq!(tx64.len(), WORST_CASE_BASE64_TX); + } + + #[test] + fn test_deserialize_transacion_too_large_payloads_fail() { + // +2 because +1 still fits in base64 encoded worst-case + let too_big = PACKET_DATA_SIZE + 2; + let tx_ser = vec![0xffu8; too_big]; + let tx58 = bs58::encode(&tx_ser).into_string(); + let tx58_len = tx58.len(); + let expect58 = Error::invalid_params(format!( + "encoded transaction too large: {} bytes (max: encoded/raw {}/{})", + tx58_len, WORST_CASE_BASE58_TX, PACKET_DATA_SIZE, + )); + assert_eq!( + deserialize_transaction(tx58, UiTransactionEncoding::Base58).unwrap_err(), + expect58 + ); + let tx64 = base64::encode(&tx_ser); + let tx64_len = tx64.len(); + let expect64 = Error::invalid_params(format!( + "encoded transaction too large: {} bytes (max: encoded/raw {}/{})", + tx64_len, WORST_CASE_BASE64_TX, PACKET_DATA_SIZE, + )); + assert_eq!( + deserialize_transaction(tx64, UiTransactionEncoding::Base64).unwrap_err(), + expect64 + ); + let too_big = PACKET_DATA_SIZE + 1; + let tx_ser = vec![0x00u8; too_big]; + let tx58 = bs58::encode(&tx_ser).into_string(); + let expect = Error::invalid_params(format!( + "transaction too large: {} bytes (max: {} bytes)", + too_big, PACKET_DATA_SIZE + )); + assert_eq!( + deserialize_transaction(tx58, UiTransactionEncoding::Base58).unwrap_err(), + expect + ); + let tx64 = base64::encode(&tx_ser); + assert_eq!( + deserialize_transaction(tx64, UiTransactionEncoding::Base64).unwrap_err(), + expect + ); } } diff --git a/docs/src/apps/jsonrpc-api.md b/docs/src/apps/jsonrpc-api.md index 7a2c7a4f8d..d6e96091ff 100644 --- a/docs/src/apps/jsonrpc-api.md +++ b/docs/src/apps/jsonrpc-api.md @@ -1470,10 +1470,11 @@ Before submitting, the following preflight checks are performed: #### Parameters: -- `` - fully-signed Transaction, as base-58 encoded string +- `` - fully-signed Transaction, as encoded string - `` - (optional) Configuration object containing the following field: - `skipPreflight: ` - if true, skip the preflight transaction checks (default: false) - `preflightCommitment: ` - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) level to use for preflight (default: `"max"`). + - `encoding: ` - (optional) Encoding used for the transaction data. Either `"base58"` (*slow*, **DEPRECATED**), or `"base64"`. (default: `"base58"`). #### Results: @@ -1495,10 +1496,11 @@ 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. +- `` - Transaction, as an 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) - `commitment: ` - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) level to simulate the transaction at (default: `"max"`). + - `encoding: ` - (optional) Encoding used for the transaction data. Either `"base58"` (*slow*, **DEPRECATED**), or `"base64"`. (default: `"base58"`). #### Results: diff --git a/transaction-status/src/lib.rs b/transaction-status/src/lib.rs index b1aaff157a..03f87c7d73 100644 --- a/transaction-status/src/lib.rs +++ b/transaction-status/src/lib.rs @@ -21,6 +21,7 @@ use solana_sdk::{ signature::Signature, transaction::{Result, Transaction, TransactionError}, }; +use std::fmt; /// A duplicate representation of an Instruction for pretty JSON serialization #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -380,6 +381,14 @@ pub enum UiTransactionEncoding { JsonParsed, } +impl fmt::Display for UiTransactionEncoding { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let v = serde_json::to_value(self).map_err(|_| fmt::Error)?; + let s = v.as_str().ok_or(fmt::Error)?; + write!(f, "{}", s) + } +} + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase", untagged)] pub enum EncodedTransaction {