simulateTransaction can now return accounts modified by the simulation

This commit is contained in:
Michael Vines 2021-05-25 16:44:18 -07:00
parent 54f0fc9f0f
commit cbce440af4
11 changed files with 184 additions and 49 deletions

View File

@ -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<T: ReadableAccount>(
pubkey: &Pubkey,
account: T,
account: &T,
encoding: UiAccountEncoding,
additional_data: Option<AccountAdditionalData>,
data_slice_config: Option<UiDataSliceConfig>,
@ -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()
}),

View File

@ -1123,7 +1123,7 @@ fn process_show_account(
pubkey: account_pubkey.to_string(),
account: UiAccount::encode(
account_pubkey,
account,
&account,
UiAccountEncoding::Base64,
None,
None,

View File

@ -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,

View File

@ -23,6 +23,13 @@ pub struct RpcSendTransactionConfig {
pub encoding: Option<UiTransactionEncoding>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RpcSimulateTransactionAccountsConfig {
pub encoding: Option<UiAccountEncoding>,
pub addresses: Vec<String>,
}
#[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<CommitmentConfig>,
pub encoding: Option<UiTransactionEncoding>,
pub accounts: Option<RpcSimulateTransactionAccountsConfig>,
}
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]

View File

@ -318,6 +318,7 @@ pub struct RpcSignatureConfirmation {
pub struct RpcSimulateTransactionResult {
pub err: Option<TransactionError>,
pub logs: Option<Vec<String>>,
pub accounts: Option<Vec<Option<UiAccount>>>,
}
#[derive(Serialize, Deserialize, Clone, Debug)]

View File

@ -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(),
)
);

View File

@ -3233,12 +3233,16 @@ Simulate sending a transaction
#### Parameters:
- `<string>` - Transaction, as an encoded string. The transaction must have a valid blockhash, but is not required to be signed.
- `<object>` - (optional) Configuration object containing the following field:
- `<object>` - (optional) Configuration object containing the following fields:
- `sigVerify: <bool>` - if true the transaction signatures will be verified (default: false, conflicts with `replaceRecentBlockhash`)
- `commitment: <string>` - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) level to simulate the transaction at (default: `"finalized"`).
- `encoding: <string>` - (optional) Encoding used for the transaction data. Either `"base58"` (*slow*, **DEPRECATED**), or `"base64"`. (default: `"base58"`).
- `replaceRecentBlockhash: <bool>` - (optional) if true the transaction recent blockhash will be replaced with the most recent blockhash.
(default: false, conflicts with `sigVerify`)
- `accounts: <object>` - (optional) Accounts configuration object containing the following fields:
- `encoding: <string>` - (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 `<string>`.
- `addresses: <array>` - 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: <object | string | null>` - 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 | null>` - 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: <array> | null>` - array of accounts with the same length as the `accounts.addresses` array in the request
- `<null>` - if the account doesn't exist or if `err` is not null
- `<object>` - otherwise, a JSON object containing:
- `lamports: <u64>`, number of lamports assigned to this account, as a u64
- `owner: <string>`, 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 `{<program>: <state>}`, depending on encoding parameter
- `executable: <bool>`, boolean indicating if the account contains a program \(and is strictly read-only\)
- `rentEpoch: <u64>`, 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"
]

View File

@ -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,

View File

@ -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),
}),
)
};

View File

@ -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<AccountSharedData>) {
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::<Vec<_>>())
.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) {

View File

@ -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()));
}