diff --git a/rpc-client-api/src/response.rs b/rpc-client-api/src/response.rs index fa70e89b6..f9d3085e8 100644 --- a/rpc-client-api/src/response.rs +++ b/rpc-client-api/src/response.rs @@ -424,6 +424,7 @@ pub struct RpcSimulateTransactionResult { pub units_consumed: Option, pub return_data: Option, pub inner_instructions: Option>, + pub replacement_blockhash: Option, } #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] diff --git a/rpc-client/src/mock_sender.rs b/rpc-client/src/mock_sender.rs index 44ab26359..ec093461c 100644 --- a/rpc-client/src/mock_sender.rs +++ b/rpc-client/src/mock_sender.rs @@ -351,6 +351,7 @@ impl RpcSender for MockSender { units_consumed: None, return_data: None, inner_instructions: None, + replacement_blockhash: None }, })?, "getMinimumBalanceForRentExemption" => json![20], diff --git a/rpc-test/tests/rpc.rs b/rpc-test/tests/rpc.rs index d0245608d..19bf50d2e 100644 --- a/rpc-test/tests/rpc.rs +++ b/rpc-test/tests/rpc.rs @@ -14,7 +14,7 @@ use { solana_rpc_client::rpc_client::RpcClient, solana_rpc_client_api::{ client_error::{ErrorKind as ClientErrorKind, Result as ClientResult}, - config::{RpcAccountInfoConfig, RpcSignatureSubscribeConfig}, + config::{RpcAccountInfoConfig, RpcSignatureSubscribeConfig, RpcSimulateTransactionConfig}, request::RpcError, response::{Response as RpcResponse, RpcSignatureResult, SlotUpdate}, }, @@ -139,6 +139,51 @@ fn test_rpc_send_tx() { info!("{:?}", json["result"]["value"]); } +#[test] +fn test_simulation_replaced_blockhash() -> ClientResult<()> { + solana_logger::setup(); + + let alice = Keypair::new(); + let validator = TestValidator::with_no_fees(alice.pubkey(), None, SocketAddrSpace::Unspecified); + let rpc_client = RpcClient::new(validator.rpc_url()); + + let bob = Keypair::new(); + let lamports = 50; + + let res = rpc_client.simulate_transaction_with_config( + &system_transaction::transfer(&alice, &bob.pubkey(), lamports, Hash::default()), + RpcSimulateTransactionConfig { + replace_recent_blockhash: true, + ..Default::default() + }, + )?; + assert!( + res.value.replacement_blockhash.is_some(), + "replaced_blockhash response is None" + ); + let blockhash = res.value.replacement_blockhash.unwrap(); + // ensure nothing weird is going on + assert_ne!( + blockhash.blockhash, + Hash::default().to_string(), + "replaced_blockhash is default" + ); + + let res = rpc_client.simulate_transaction_with_config( + &system_transaction::transfer(&alice, &bob.pubkey(), lamports, Hash::default()), + RpcSimulateTransactionConfig { + replace_recent_blockhash: false, + ..Default::default() + }, + )?; + assert!( + res.value.replacement_blockhash.is_none(), + "replaced_blockhash is Some when nothing should be replaced" + ); + + Ok(()) +} + #[test] fn test_rpc_invalid_requests() { solana_logger::setup(); diff --git a/rpc/src/rpc.rs b/rpc/src/rpc.rs index e6b4a2839..ae10b6d38 100644 --- a/rpc/src/rpc.rs +++ b/rpc/src/rpc.rs @@ -3723,6 +3723,7 @@ pub mod rpc_full { units_consumed: Some(units_consumed), return_data: return_data.map(|return_data| return_data.into()), inner_instructions: None, + replacement_blockhash: None, }, } .into()); @@ -3768,15 +3769,24 @@ pub mod rpc_full { commitment, min_context_slot, })?; + let mut blockhash: Option = None; if replace_recent_blockhash { if sig_verify { return Err(Error::invalid_params( "sigVerify may not be used with replaceRecentBlockhash", )); } + let recent_blockhash = bank.last_blockhash(); unsanitized_tx .message - .set_recent_blockhash(bank.last_blockhash()); + .set_recent_blockhash(recent_blockhash); + let last_valid_block_height = bank + .get_blockhash_last_valid_block_height(&recent_blockhash) + .expect("bank blockhash queue should contain blockhash"); + blockhash.replace(RpcBlockhash { + blockhash: recent_blockhash.to_string(), + last_valid_block_height, + }); } let transaction = sanitize_transaction(unsanitized_tx, bank)?; @@ -3857,6 +3867,7 @@ pub mod rpc_full { units_consumed: Some(units_consumed), return_data: return_data.map(|return_data| return_data.into()), inner_instructions, + replacement_blockhash: blockhash, }, )) } @@ -6009,6 +6020,7 @@ pub mod tests { "Program 11111111111111111111111111111111 invoke [1]", "Program 11111111111111111111111111111111 success" ], + "replacementBlockhash": null, "returnData":null, "unitsConsumed":150, } @@ -6094,6 +6106,7 @@ pub mod tests { "Program 11111111111111111111111111111111 invoke [1]", "Program 11111111111111111111111111111111 success" ], + "replacementBlockhash": null, "returnData":null, "unitsConsumed":150, } @@ -6123,7 +6136,8 @@ pub mod tests { "Program 11111111111111111111111111111111 invoke [1]", "Program 11111111111111111111111111111111 success" ], - "returnData":null, + "replacementBlockhash": null, + "returnData": null, "unitsConsumed":150, } }, @@ -6173,7 +6187,8 @@ pub mod tests { "accounts":null, "innerInstructions":null, "logs":[], - "returnData":null, + "replacementBlockhash": null, + "returnData": null, "unitsConsumed":0, } }, @@ -6191,6 +6206,11 @@ pub mod tests { r#"{{"jsonrpc":"2.0","id":1,"method":"simulateTransaction","params":["{tx_invalid_recent_blockhash}", {{"replaceRecentBlockhash": true}}]}}"#, ); let res = io.handle_request_sync(&req, meta.clone()); + let latest_blockhash = bank.confirmed_last_blockhash(); + let expiry_slot = bank + .get_blockhash_last_valid_block_height(&latest_blockhash) + .expect("blockhash exists"); + let expected = json!({ "jsonrpc": "2.0", "result": { @@ -6203,6 +6223,10 @@ pub mod tests { "Program 11111111111111111111111111111111 invoke [1]", "Program 11111111111111111111111111111111 success" ], + "replacementBlockhash": { + "blockhash": latest_blockhash.to_string(), + "lastValidBlockHeight": expiry_slot + }, "returnData":null, "unitsConsumed":150, } @@ -6347,6 +6371,7 @@ pub mod tests { "Program 11111111111111111111111111111111 invoke [1]", "Program 11111111111111111111111111111111 success" ], + "replacementBlockhash": null, "returnData": null, "unitsConsumed": 150, } @@ -6427,6 +6452,7 @@ pub mod tests { "Program 11111111111111111111111111111111 success", "Program AddressLookupTab1e1111111111111111111111111 success" ], + "replacementBlockhash": null, "returnData":null, "unitsConsumed":1200, } @@ -6470,6 +6496,7 @@ pub mod tests { "Program 11111111111111111111111111111111 success", "Program AddressLookupTab1e1111111111111111111111111 success" ], + "replacementBlockhash": null, "returnData":null, "unitsConsumed":1200, } @@ -6556,6 +6583,7 @@ pub mod tests { "Program 11111111111111111111111111111111 success", "Program AddressLookupTab1e1111111111111111111111111 success" ], + "replacementBlockhash": null, "returnData":null, "unitsConsumed":1200, } @@ -6931,7 +6959,7 @@ pub mod tests { assert_eq!( res, Some( - r#"{"jsonrpc":"2.0","error":{"code":-32002,"message":"Transaction simulation failed: Blockhash not found","data":{"accounts":null,"err":"BlockhashNotFound","innerInstructions":null,"logs":[],"returnData":null,"unitsConsumed":0}},"id":1}"#.to_string(), + r#"{"jsonrpc":"2.0","error":{"code":-32002,"message":"Transaction simulation failed: Blockhash not found","data":{"accounts":null,"err":"BlockhashNotFound","innerInstructions":null,"logs":[],"replacementBlockhash":null,"returnData":null,"unitsConsumed":0}},"id":1}"#.to_string(), ) );