diff --git a/account-decoder/src/lib.rs b/account-decoder/src/lib.rs index 336908a729..7f1e7c40c7 100644 --- a/account-decoder/src/lib.rs +++ b/account-decoder/src/lib.rs @@ -48,7 +48,7 @@ pub enum UiAccountData { Binary(String, UiAccountEncoding), } -#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq)] #[serde(rename_all = "camelCase")] pub enum UiAccountEncoding { Binary, // Legacy. Retained for RPC backwards compatibility @@ -62,7 +62,7 @@ pub enum UiAccountEncoding { impl UiAccount { pub fn encode( pubkey: &Pubkey, - account: T, + account: &T, encoding: UiAccountEncoding, additional_data: Option, data_slice_config: Option, @@ -224,7 +224,7 @@ mod test { fn test_base64_zstd() { let encoded_account = UiAccount::encode( &Pubkey::default(), - AccountSharedData::from(Account { + &AccountSharedData::from(Account { data: vec![0; 1024], ..Account::default() }), diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 1cde72eb0e..e1092f7df5 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -1123,7 +1123,7 @@ fn process_show_account( pubkey: account_pubkey.to_string(), account: UiAccount::encode( account_pubkey, - account, + &account, UiAccountEncoding::Base64, None, None, diff --git a/client/src/blockhash_query.rs b/client/src/blockhash_query.rs index 1a33a79ff4..ced886b04b 100644 --- a/client/src/blockhash_query.rs +++ b/client/src/blockhash_query.rs @@ -361,7 +361,7 @@ mod tests { let nonce_pubkey = Pubkey::new(&[4u8; 32]); let rpc_nonce_account = UiAccount::encode( &nonce_pubkey, - nonce_account, + &nonce_account, UiAccountEncoding::Base64, None, None, diff --git a/client/src/rpc_config.rs b/client/src/rpc_config.rs index 6faa96e558..5597384e30 100644 --- a/client/src/rpc_config.rs +++ b/client/src/rpc_config.rs @@ -23,6 +23,13 @@ pub struct RpcSendTransactionConfig { pub encoding: Option, } +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RpcSimulateTransactionAccountsConfig { + pub encoding: Option, + pub addresses: Vec, +} + #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RpcSimulateTransactionConfig { @@ -33,6 +40,7 @@ pub struct RpcSimulateTransactionConfig { #[serde(flatten)] pub commitment: Option, pub encoding: Option, + pub accounts: Option, } #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] diff --git a/client/src/rpc_response.rs b/client/src/rpc_response.rs index 9e60266468..a712739e60 100644 --- a/client/src/rpc_response.rs +++ b/client/src/rpc_response.rs @@ -318,6 +318,7 @@ pub struct RpcSignatureConfirmation { pub struct RpcSimulateTransactionResult { pub err: Option, pub logs: Option>, + pub accounts: Option>>, } #[derive(Serialize, Deserialize, Clone, Debug)] diff --git a/core/src/rpc.rs b/core/src/rpc.rs index 0d702304b3..5d16a703e7 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -341,7 +341,7 @@ impl JsonRpcRequestProcessor { for pubkey in pubkeys { let response_account = - get_encoded_account(&bank, &pubkey, encoding.clone(), config.data_slice)?; + get_encoded_account(&bank, &pubkey, encoding, config.data_slice)?; accounts.push(response_account) } Ok(new_response(&bank, accounts)) @@ -387,8 +387,8 @@ impl JsonRpcRequestProcessor { pubkey: pubkey.to_string(), account: UiAccount::encode( &pubkey, - account, - encoding.clone(), + &account, + encoding, None, data_slice_config, ), @@ -1647,8 +1647,8 @@ impl JsonRpcRequestProcessor { pubkey: pubkey.to_string(), account: UiAccount::encode( &pubkey, - account, - encoding.clone(), + &account, + encoding, None, data_slice_config, ), @@ -1706,8 +1706,8 @@ impl JsonRpcRequestProcessor { pubkey: pubkey.to_string(), account: UiAccount::encode( &pubkey, - account, - encoding.clone(), + &account, + encoding, None, data_slice_config, ), @@ -1957,7 +1957,7 @@ fn get_encoded_account( }); } else { response = Some(UiAccount::encode( - pubkey, account, encoding, None, data_slice, + pubkey, &account, encoding, None, data_slice, )); } } @@ -2970,12 +2970,13 @@ pub mod rpc_full { } } - if let (Err(err), logs) = preflight_bank.simulate_transaction(transaction.clone()) { + if let (Err(err), logs, _) = preflight_bank.simulate_transaction(&transaction) { return Err(RpcCustomError::SendTransactionPreflightFailure { message: format!("Transaction simulation failed: {}", err), result: RpcSimulateTransactionResult { err: Some(err), logs: Some(logs), + accounts: None, }, } .into()); @@ -3013,18 +3014,59 @@ pub mod rpc_full { return Err(e); } } - let bank = &*meta.bank(config.commitment); if config.replace_recent_blockhash { transaction.message.recent_blockhash = bank.last_blockhash(); } - let (result, logs) = bank.simulate_transaction(transaction); + let (result, logs, post_simulation_accounts) = bank.simulate_transaction(&transaction); + + let accounts = if let Some(config_accounts) = config.accounts { + let accounts_encoding = config_accounts + .encoding + .unwrap_or(UiAccountEncoding::Base64); + + if accounts_encoding == UiAccountEncoding::Binary + || accounts_encoding == UiAccountEncoding::Base58 + { + return Err(Error::invalid_params("base58 encoding not supported")); + } + + if config_accounts.addresses.len() > post_simulation_accounts.len() { + return Err(Error::invalid_params(format!( + "Too many accounts provided; max {}", + post_simulation_accounts.len() + ))); + } + + let mut accounts = vec![]; + for address_str in config_accounts.addresses { + let address = verify_pubkey(&address_str)?; + accounts.push(if result.is_err() { + None + } else { + transaction + .message + .account_keys + .iter() + .position(|pubkey| *pubkey == address) + .map(|i| post_simulation_accounts.get(i)) + .flatten() + .map(|account| { + UiAccount::encode(&address, account, accounts_encoding, None, None) + }) + }); + } + Some(accounts) + } else { + None + }; Ok(new_response( &bank, RpcSimulateTransactionResult { err: result.err(), logs: Some(logs), + accounts, }, )) } @@ -4928,7 +4970,6 @@ pub mod tests { #[test] fn test_rpc_simulate_transaction() { - let bob_pubkey = solana_sdk::pubkey::new_rand(); let RpcHandler { io, meta, @@ -4936,8 +4977,9 @@ pub mod tests { alice, bank, .. - } = start_rpc_handler_with_tx(&bob_pubkey); + } = start_rpc_handler_with_tx(&solana_sdk::pubkey::new_rand()); + let bob_pubkey = solana_sdk::pubkey::new_rand(); 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(); @@ -4949,18 +4991,46 @@ pub mod tests { // Good signature with sigVerify=true let req = format!( - r#"{{"jsonrpc":"2.0","id":1,"method":"simulateTransaction","params":["{}", {{"sigVerify": true}}]}}"#, + r#"{{"jsonrpc":"2.0", + "id":1, + "method":"simulateTransaction", + "params":[ + "{}", + {{ + "sigVerify": true, + "accounts": {{ + "encoding": "jsonParsed", + "addresses": ["{}", "{}"] + }} + }} + ] + }}"#, tx_serialized_encoded, + solana_sdk::pubkey::new_rand(), + bob_pubkey, ); let res = io.handle_request_sync(&req, meta.clone()); let expected = json!({ "jsonrpc": "2.0", "result": { "context":{"slot":0}, - "value":{"err":null, "logs":[ - "Program 11111111111111111111111111111111 invoke [1]", - "Program 11111111111111111111111111111111 success" - ]} + "value":{ + "accounts": [ + null, + { + "data": ["", "base64"], + "executable": false, + "owner": "11111111111111111111111111111111", + "lamports": 1234, + "rentEpoch": 0 + } + ], + "err":null, + "logs":[ + "Program 11111111111111111111111111111111 invoke [1]", + "Program 11111111111111111111111111111111 success" + ] + } }, "id": 1, }); @@ -4970,6 +5040,43 @@ pub mod tests { .expect("actual response deserialization"); assert_eq!(expected, result); + // Too many input accounts... + let req = format!( + r#"{{"jsonrpc":"2.0", + "id":1, + "method":"simulateTransaction", + "params":[ + "{}", + {{ + "sigVerify": true, + "accounts": {{ + "addresses": [ + "11111111111111111111111111111111", + "11111111111111111111111111111111", + "11111111111111111111111111111111", + "11111111111111111111111111111111" + ] + }} + }} + ] + }}"#, + tx_serialized_encoded, + ); + let res = io.handle_request_sync(&req, meta.clone()); + let expected = json!({ + "jsonrpc":"2.0", + "error": { + "code": error::ErrorCode::InvalidParams.code(), + "message": "Too many accounts provided; max 3" + }, + "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}}]}}"#, @@ -5001,7 +5108,7 @@ pub mod tests { "jsonrpc": "2.0", "result": { "context":{"slot":0}, - "value":{"err":null, "logs":[ + "value":{"accounts": null, "err":null, "logs":[ "Program 11111111111111111111111111111111 invoke [1]", "Program 11111111111111111111111111111111 success" ]} @@ -5024,7 +5131,7 @@ pub mod tests { "jsonrpc": "2.0", "result": { "context":{"slot":0}, - "value":{"err":null, "logs":[ + "value":{"accounts": null, "err":null, "logs":[ "Program 11111111111111111111111111111111 invoke [1]", "Program 11111111111111111111111111111111 success" ]} @@ -5072,7 +5179,7 @@ pub mod tests { "jsonrpc":"2.0", "result": { "context":{"slot":0}, - "value":{"err": "BlockhashNotFound", "logs":[]} + "value":{"err": "BlockhashNotFound", "accounts": null, "logs":[]} }, "id":1 }); @@ -5093,7 +5200,7 @@ pub mod tests { "jsonrpc": "2.0", "result": { "context":{"slot":0}, - "value":{"err":null, "logs":[ + "value":{"accounts": null, "err":null, "logs":[ "Program 11111111111111111111111111111111 invoke [1]", "Program 11111111111111111111111111111111 success" ]} @@ -5437,7 +5544,7 @@ pub mod tests { assert_eq!( res, Some( - r#"{"jsonrpc":"2.0","error":{"code":-32002,"message":"Transaction simulation failed: Blockhash not found","data":{"err":"BlockhashNotFound","logs":[]}},"id":1}"#.to_string(), + r#"{"jsonrpc":"2.0","error":{"code":-32002,"message":"Transaction simulation failed: Blockhash not found","data":{"accounts":null,"err":"BlockhashNotFound","logs":[]}},"id":1}"#.to_string(), ) ); diff --git a/docs/src/developing/clients/jsonrpc-api.md b/docs/src/developing/clients/jsonrpc-api.md index b37bd3f6e9..608257910b 100644 --- a/docs/src/developing/clients/jsonrpc-api.md +++ b/docs/src/developing/clients/jsonrpc-api.md @@ -3233,12 +3233,16 @@ Simulate sending a transaction #### Parameters: - `` - 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: +- `` - (optional) Configuration object containing the following fields: - `sigVerify: ` - if true the transaction signatures will be verified (default: false, conflicts with `replaceRecentBlockhash`) - `commitment: ` - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) level to simulate the transaction at (default: `"finalized"`). - `encoding: ` - (optional) Encoding used for the transaction data. Either `"base58"` (*slow*, **DEPRECATED**), or `"base64"`. (default: `"base58"`). - `replaceRecentBlockhash: ` - (optional) if true the transaction recent blockhash will be replaced with the most recent blockhash. (default: false, conflicts with `sigVerify`) + - `accounts: ` - (optional) Accounts configuration object containing the following fields: + - `encoding: ` - (optional) encoding for returned Account data, either "base64" (default), "base64+zstd" or "jsonParsed". + "jsonParsed" encoding attempts to use program-specific state parsers to return more human-readable and explicit account state data. If "jsonParsed" is requested but a parser cannot be found, the field falls back to binary encoding, detectable when the `data` field is type ``. + - `addresses: ` - An array of accounts to return, as base-58 encoded strings #### Results: @@ -3247,6 +3251,14 @@ The result will be an RpcResponse JSON object with `value` set to a JSON object - `err: ` - Error if transaction failed, null if transaction succeeded. [TransactionError definitions](https://github.com/solana-labs/solana/blob/master/sdk/src/transaction.rs#L24) - `logs: ` - Array of log messages the transaction instructions output during execution, null if simulation failed before the transaction was able to execute (for example due to an invalid blockhash or signature verification failure) +- `accounts: | null>` - array of accounts with the same length as the `accounts.addresses` array in the request + - `` - if the account doesn't exist or if `err` is not null + - `` - otherwise, a JSON object containing: + - `lamports: `, number of lamports assigned to this account, as a u64 + - `owner: `, base-58 encoded Pubkey of the program this account has been assigned to + - `data: <[string, encoding]|object>`, data associated with the account, either as encoded binary data or JSON format `{: }`, depending on encoding parameter + - `executable: `, boolean indicating if the account contains a program \(and is strictly read-only\) + - `rentEpoch: `, the epoch at which this account will next owe rent, as u64 #### Example: @@ -3273,6 +3285,7 @@ Result: }, "value": { "err": null, + "accounts": null, "logs": [ "BPF program 83astBRguLMdt2h5U1Tpdq5tjFoJ6noeGwaY3mDLVcri success" ] diff --git a/rpc/src/parsed_token_accounts.rs b/rpc/src/parsed_token_accounts.rs index c10b75e23a..bfcc9a1fa7 100644 --- a/rpc/src/parsed_token_accounts.rs +++ b/rpc/src/parsed_token_accounts.rs @@ -28,7 +28,7 @@ pub fn get_parsed_token_account( UiAccount::encode( pubkey, - account, + &account, UiAccountEncoding::JsonParsed, additional_data, None, @@ -55,7 +55,7 @@ where let maybe_encoded_account = UiAccount::encode( &pubkey, - account, + &account, UiAccountEncoding::JsonParsed, additional_data, None, diff --git a/rpc/src/rpc_subscriptions.rs b/rpc/src/rpc_subscriptions.rs index 70d7e50ba3..d2aedd5ead 100644 --- a/rpc/src/rpc_subscriptions.rs +++ b/rpc/src/rpc_subscriptions.rs @@ -296,7 +296,7 @@ fn filter_account_result( Box::new(iter::once(get_parsed_token_account(bank, pubkey, account))) } else { Box::new(iter::once(UiAccount::encode( - pubkey, account, encoding, None, None, + pubkey, &account, encoding, None, None, ))) } } else { @@ -347,7 +347,7 @@ fn filter_program_results( Box::new( keyed_accounts.map(move |(pubkey, account)| RpcKeyedAccount { pubkey: pubkey.to_string(), - account: UiAccount::encode(&pubkey, account, encoding.clone(), None, None), + account: UiAccount::encode(&pubkey, &account, encoding, None, None), }), ) }; diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 6588618724..7d06a2013d 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -2576,16 +2576,15 @@ impl Bank { TransactionBatch::new(lock_results, &self, Cow::Borrowed(hashed_txs)) } - pub fn prepare_simulation_batch<'a, 'b>( + pub(crate) fn prepare_simulation_batch<'a, 'b>( &'a self, - txs: &'b [Transaction], + tx: &'b Transaction, ) -> TransactionBatch<'a, 'b> { - let lock_results: Vec<_> = txs - .iter() - .map(|tx| tx.sanitize().map_err(|e| e.into())) - .collect(); - let hashed_txs = txs.iter().map(HashedTransaction::from).collect(); - let mut batch = TransactionBatch::new(lock_results, &self, hashed_txs); + let mut batch = TransactionBatch::new( + vec![tx.sanitize().map_err(|e| e.into())], + &self, + Cow::Owned(vec![HashedTransaction::from(tx)]), + ); batch.needs_unlock = false; batch } @@ -2593,17 +2592,16 @@ impl Bank { /// Run transactions against a frozen bank without committing the results pub fn simulate_transaction( &self, - transaction: Transaction, - ) -> (Result<()>, TransactionLogMessages) { + transaction: &Transaction, + ) -> (Result<()>, TransactionLogMessages, Vec) { assert!(self.is_frozen(), "simulation bank must be frozen"); - let txs = &[transaction]; - let batch = self.prepare_simulation_batch(txs); + let batch = self.prepare_simulation_batch(&transaction); let mut timings = ExecuteTimings::default(); let ( - _loaded_accounts, + loaded_accounts, executed, _inner_instructions, log_messages, @@ -2625,10 +2623,18 @@ impl Bank { let log_messages = log_messages .get(0) .map_or(vec![], |messages| messages.to_vec()); + let post_transaction_accounts = loaded_accounts + .into_iter() + .next() + .unwrap() + .0 + .ok() + .map(|loaded_transaction| loaded_transaction.accounts.into_iter().collect::>()) + .unwrap_or_default(); debug!("simulate_transaction: {:?}", timings); - (transaction_result, log_messages) + (transaction_result, log_messages, post_transaction_accounts) } pub fn unlock_accounts(&self, batch: &mut TransactionBatch) { diff --git a/runtime/src/transaction_batch.rs b/runtime/src/transaction_batch.rs index 076ed77d6d..ba4b963174 100644 --- a/runtime/src/transaction_batch.rs +++ b/runtime/src/transaction_batch.rs @@ -83,7 +83,7 @@ mod tests { let (bank, txs) = setup(); // Prepare batch without locks - let batch = bank.prepare_simulation_batch(&txs); + let batch = bank.prepare_simulation_batch(&txs[0]); assert!(batch.lock_results().iter().all(|x| x.is_ok())); // Grab locks @@ -91,7 +91,7 @@ mod tests { assert!(batch2.lock_results().iter().all(|x| x.is_ok())); // Prepare another batch without locks - let batch3 = bank.prepare_simulation_batch(&txs); + let batch3 = bank.prepare_simulation_batch(&txs[0]); assert!(batch3.lock_results().iter().all(|x| x.is_ok())); }