diff --git a/rpc-client/src/nonblocking/rpc_client.rs b/rpc-client/src/nonblocking/rpc_client.rs index a1cd22ca20..492dea0fe6 100644 --- a/rpc-client/src/nonblocking/rpc_client.rs +++ b/rpc-client/src/nonblocking/rpc_client.rs @@ -18,7 +18,9 @@ use { crate::{ http_sender::HttpSender, mock_sender::MockSender, - rpc_client::{GetConfirmedSignaturesForAddress2Config, RpcClientConfig}, + rpc_client::{ + GetConfirmedSignaturesForAddress2Config, RpcClientConfig, SerializableTransaction, + }, rpc_sender::*, }, bincode::serialize, @@ -48,7 +50,7 @@ use { message::Message, pubkey::Pubkey, signature::Signature, - transaction::{self, uses_durable_nonce, Transaction}, + transaction::{self}, }, solana_transaction_status::{ EncodedConfirmedBlock, EncodedConfirmedTransactionWithStatusMeta, TransactionStatus, @@ -658,7 +660,7 @@ impl RpcClient { /// ``` pub async fn send_and_confirm_transaction( &self, - transaction: &Transaction, + transaction: &impl SerializableTransaction, ) -> ClientResult { const SEND_RETRIES: usize = 1; const GET_STATUS_RETRIES: usize = usize::MAX; @@ -666,13 +668,13 @@ impl RpcClient { 'sending: for _ in 0..SEND_RETRIES { let signature = self.send_transaction(transaction).await?; - let recent_blockhash = if uses_durable_nonce(transaction).is_some() { + let recent_blockhash = if transaction.uses_durable_nonce() { let (recent_blockhash, ..) = self .get_latest_blockhash_with_commitment(CommitmentConfig::processed()) .await?; recent_blockhash } else { - transaction.message.recent_blockhash + *transaction.get_recent_blockhash() }; for status_retry in 0..GET_STATUS_RETRIES { @@ -711,7 +713,7 @@ impl RpcClient { #[cfg(feature = "spinner")] pub async fn send_and_confirm_transaction_with_spinner( &self, - transaction: &Transaction, + transaction: &impl SerializableTransaction, ) -> ClientResult { self.send_and_confirm_transaction_with_spinner_and_commitment( transaction, @@ -723,7 +725,7 @@ impl RpcClient { #[cfg(feature = "spinner")] pub async fn send_and_confirm_transaction_with_spinner_and_commitment( &self, - transaction: &Transaction, + transaction: &impl SerializableTransaction, commitment: CommitmentConfig, ) -> ClientResult { self.send_and_confirm_transaction_with_spinner_and_config( @@ -740,16 +742,16 @@ impl RpcClient { #[cfg(feature = "spinner")] pub async fn send_and_confirm_transaction_with_spinner_and_config( &self, - transaction: &Transaction, + transaction: &impl SerializableTransaction, commitment: CommitmentConfig, config: RpcSendTransactionConfig, ) -> ClientResult { - let recent_blockhash = if uses_durable_nonce(transaction).is_some() { + let recent_blockhash = if transaction.uses_durable_nonce() { self.get_latest_blockhash_with_commitment(CommitmentConfig::processed()) .await? .0 } else { - transaction.message.recent_blockhash + *transaction.get_recent_blockhash() }; let signature = self .send_transaction_with_config(transaction, config) @@ -829,7 +831,10 @@ impl RpcClient { /// # })?; /// # Ok::<(), Error>(()) /// ``` - pub async fn send_transaction(&self, transaction: &Transaction) -> ClientResult { + pub async fn send_transaction( + &self, + transaction: &impl SerializableTransaction, + ) -> ClientResult { self.send_transaction_with_config( transaction, RpcSendTransactionConfig { @@ -927,7 +932,7 @@ impl RpcClient { /// ``` pub async fn send_transaction_with_config( &self, - transaction: &Transaction, + transaction: &impl SerializableTransaction, config: RpcSendTransactionConfig, ) -> ClientResult { let encoding = if let Some(encoding) = config.encoding { @@ -944,7 +949,7 @@ impl RpcClient { preflight_commitment: Some(preflight_commitment.commitment), ..config }; - let serialized_encoded = serialize_and_encode::(transaction, encoding)?; + let serialized_encoded = serialize_and_encode(transaction, encoding)?; let signature_base58_str: String = match self .send( RpcRequest::SendTransaction, @@ -984,14 +989,15 @@ impl RpcClient { // should not be passed along to confirmation methods. The transaction may or may // not have been submitted to the cluster, so callers should verify the success of // the correct transaction signature independently. - if signature != transaction.signatures[0] { + if signature != *transaction.get_signature() { Err(RpcError::RpcRequestError(format!( "RPC node returned mismatched signature {:?}, expected {:?}", - signature, transaction.signatures[0] + signature, + transaction.get_signature() )) .into()) } else { - Ok(transaction.signatures[0]) + Ok(*transaction.get_signature()) } } @@ -1290,7 +1296,7 @@ impl RpcClient { /// ``` pub async fn simulate_transaction( &self, - transaction: &Transaction, + transaction: &impl SerializableTransaction, ) -> RpcResult { self.simulate_transaction_with_config( transaction, @@ -1377,7 +1383,7 @@ impl RpcClient { /// ``` pub async fn simulate_transaction_with_config( &self, - transaction: &Transaction, + transaction: &impl SerializableTransaction, config: RpcSimulateTransactionConfig, ) -> RpcResult { let encoding = if let Some(encoding) = config.encoding { @@ -1392,7 +1398,7 @@ impl RpcClient { commitment: Some(commitment), ..config }; - let serialized_encoded = serialize_and_encode::(transaction, encoding)?; + let serialized_encoded = serialize_and_encode(transaction, encoding)?; self.send( RpcRequest::SimulateTransaction, json!([serialized_encoded, config]), diff --git a/rpc-client/src/rpc_client.rs b/rpc-client/src/rpc_client.rs index d4705954c1..d6529c0a5e 100644 --- a/rpc-client/src/rpc_client.rs +++ b/rpc-client/src/rpc_client.rs @@ -21,6 +21,7 @@ use { nonblocking::{self, rpc_client::get_rpc_request_str}, rpc_sender::*, }, + serde::Serialize, serde_json::Value, solana_account_decoder::{ parse_token::{UiTokenAccount, UiTokenAmount}, @@ -43,7 +44,7 @@ use { message::Message, pubkey::Pubkey, signature::Signature, - transaction::{self, Transaction}, + transaction::{self, uses_durable_nonce, Transaction, VersionedTransaction}, }, solana_transaction_status::{ EncodedConfirmedBlock, EncodedConfirmedTransactionWithStatusMeta, TransactionStatus, @@ -67,6 +68,36 @@ impl RpcClientConfig { } } +/// Trait used to add support for versioned transactions to RPC APIs while +/// retaining backwards compatibility +pub trait SerializableTransaction: Serialize { + fn get_signature(&self) -> &Signature; + fn get_recent_blockhash(&self) -> &Hash; + fn uses_durable_nonce(&self) -> bool; +} +impl SerializableTransaction for Transaction { + fn get_signature(&self) -> &Signature { + &self.signatures[0] + } + fn get_recent_blockhash(&self) -> &Hash { + &self.message.recent_blockhash + } + fn uses_durable_nonce(&self) -> bool { + uses_durable_nonce(self).is_some() + } +} +impl SerializableTransaction for VersionedTransaction { + fn get_signature(&self) -> &Signature { + &self.signatures[0] + } + fn get_recent_blockhash(&self) -> &Hash { + self.message.recent_blockhash() + } + fn uses_durable_nonce(&self) -> bool { + self.uses_durable_nonce() + } +} + #[derive(Debug, Default)] pub struct GetConfirmedSignaturesForAddress2Config { pub before: Option, @@ -629,7 +660,7 @@ impl RpcClient { /// ``` pub fn send_and_confirm_transaction( &self, - transaction: &Transaction, + transaction: &impl SerializableTransaction, ) -> ClientResult { self.invoke((self.rpc_client.as_ref()).send_and_confirm_transaction(transaction)) } @@ -637,7 +668,7 @@ impl RpcClient { #[cfg(feature = "spinner")] pub fn send_and_confirm_transaction_with_spinner( &self, - transaction: &Transaction, + transaction: &impl SerializableTransaction, ) -> ClientResult { self.invoke( (self.rpc_client.as_ref()).send_and_confirm_transaction_with_spinner(transaction), @@ -647,7 +678,7 @@ impl RpcClient { #[cfg(feature = "spinner")] pub fn send_and_confirm_transaction_with_spinner_and_commitment( &self, - transaction: &Transaction, + transaction: &impl SerializableTransaction, commitment: CommitmentConfig, ) -> ClientResult { self.invoke( @@ -659,7 +690,7 @@ impl RpcClient { #[cfg(feature = "spinner")] pub fn send_and_confirm_transaction_with_spinner_and_config( &self, - transaction: &Transaction, + transaction: &impl SerializableTransaction, commitment: CommitmentConfig, config: RpcSendTransactionConfig, ) -> ClientResult { @@ -740,7 +771,10 @@ impl RpcClient { /// let signature = rpc_client.send_transaction(&tx)?; /// # Ok::<(), Error>(()) /// ``` - pub fn send_transaction(&self, transaction: &Transaction) -> ClientResult { + pub fn send_transaction( + &self, + transaction: &impl SerializableTransaction, + ) -> ClientResult { self.invoke((self.rpc_client.as_ref()).send_transaction(transaction)) } @@ -825,7 +859,7 @@ impl RpcClient { /// ``` pub fn send_transaction_with_config( &self, - transaction: &Transaction, + transaction: &impl SerializableTransaction, config: RpcSendTransactionConfig, ) -> ClientResult { self.invoke((self.rpc_client.as_ref()).send_transaction_with_config(transaction, config)) @@ -1025,7 +1059,7 @@ impl RpcClient { /// ``` pub fn simulate_transaction( &self, - transaction: &Transaction, + transaction: &impl SerializableTransaction, ) -> RpcResult { self.invoke((self.rpc_client.as_ref()).simulate_transaction(transaction)) } @@ -1102,7 +1136,7 @@ impl RpcClient { /// ``` pub fn simulate_transaction_with_config( &self, - transaction: &Transaction, + transaction: &impl SerializableTransaction, config: RpcSimulateTransactionConfig, ) -> RpcResult { self.invoke( diff --git a/sdk/src/transaction/versioned/mod.rs b/sdk/src/transaction/versioned/mod.rs index e257eb6b93..fd749c3892 100644 --- a/sdk/src/transaction/versioned/mod.rs +++ b/sdk/src/transaction/versioned/mod.rs @@ -20,6 +20,12 @@ use { mod sanitized; pub use sanitized::*; +use { + crate::program_utils::limited_deserialize, + solana_program::{ + nonce::NONCED_TX_MARKER_IX_INDEX, system_instruction::SystemInstruction, system_program, + }, +}; /// Type that serializes to the string "legacy" #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] @@ -181,6 +187,35 @@ impl VersionedTransaction { .map(|(signature, pubkey)| signature.verify(pubkey.as_ref(), message_bytes)) .collect() } + + /// Returns true if transaction begins with a valid advance nonce + /// instruction. Since dynamically loaded addresses can't have write locks + /// demoted without loading addresses, this shouldn't be used in the + /// runtime. + pub fn uses_durable_nonce(&self) -> bool { + let message = &self.message; + message + .instructions() + .get(NONCED_TX_MARKER_IX_INDEX as usize) + .filter(|instruction| { + // Is system program + matches!( + message.static_account_keys().get(instruction.program_id_index as usize), + Some(program_id) if system_program::check_id(program_id) + ) + // Is a nonce advance instruction + && matches!( + limited_deserialize(&instruction.data), + Ok(SystemInstruction::AdvanceNonceAccount) + ) + // Nonce account is writable + && matches!( + instruction.accounts.first(), + Some(index) if message.is_maybe_writable(*index as usize) + ) + }) + .is_some() + } } #[cfg(test)] @@ -190,6 +225,7 @@ mod tests { crate::{ message::Message as LegacyMessage, signer::{keypair::Keypair, Signer}, + system_instruction, sysvar, }, solana_program::{ instruction::{AccountMeta, Instruction}, @@ -240,4 +276,105 @@ mod tests { Err(err) => assert_eq!(Some(err), None), } } + + fn nonced_transfer_tx() -> (Pubkey, Pubkey, VersionedTransaction) { + let from_keypair = Keypair::new(); + let from_pubkey = from_keypair.pubkey(); + let nonce_keypair = Keypair::new(); + let nonce_pubkey = nonce_keypair.pubkey(); + let instructions = [ + system_instruction::advance_nonce_account(&nonce_pubkey, &nonce_pubkey), + system_instruction::transfer(&from_pubkey, &nonce_pubkey, 42), + ]; + let message = LegacyMessage::new(&instructions, Some(&nonce_pubkey)); + let tx = Transaction::new(&[&from_keypair, &nonce_keypair], message, Hash::default()); + (from_pubkey, nonce_pubkey, tx.into()) + } + + #[test] + fn tx_uses_nonce_ok() { + let (_, _, tx) = nonced_transfer_tx(); + assert!(tx.uses_durable_nonce()); + } + + #[test] + fn tx_uses_nonce_empty_ix_fail() { + assert!(!VersionedTransaction::default().uses_durable_nonce()); + } + + #[test] + fn tx_uses_nonce_bad_prog_id_idx_fail() { + let (_, _, mut tx) = nonced_transfer_tx(); + match &mut tx.message { + VersionedMessage::Legacy(message) => { + message.instructions.get_mut(0).unwrap().program_id_index = 255u8; + } + VersionedMessage::V0(_) => unreachable!(), + }; + assert!(!tx.uses_durable_nonce()); + } + + #[test] + fn tx_uses_nonce_first_prog_id_not_nonce_fail() { + let from_keypair = Keypair::new(); + let from_pubkey = from_keypair.pubkey(); + let nonce_keypair = Keypair::new(); + let nonce_pubkey = nonce_keypair.pubkey(); + let instructions = [ + system_instruction::transfer(&from_pubkey, &nonce_pubkey, 42), + system_instruction::advance_nonce_account(&nonce_pubkey, &nonce_pubkey), + ]; + let message = LegacyMessage::new(&instructions, Some(&from_pubkey)); + let tx = Transaction::new(&[&from_keypair, &nonce_keypair], message, Hash::default()); + let tx = VersionedTransaction::from(tx); + assert!(!tx.uses_durable_nonce()); + } + + #[test] + fn tx_uses_ro_nonce_account() { + let from_keypair = Keypair::new(); + let from_pubkey = from_keypair.pubkey(); + let nonce_keypair = Keypair::new(); + let nonce_pubkey = nonce_keypair.pubkey(); + let account_metas = vec![ + AccountMeta::new_readonly(nonce_pubkey, false), + #[allow(deprecated)] + AccountMeta::new_readonly(sysvar::recent_blockhashes::id(), false), + AccountMeta::new_readonly(nonce_pubkey, true), + ]; + let nonce_instruction = Instruction::new_with_bincode( + system_program::id(), + &system_instruction::SystemInstruction::AdvanceNonceAccount, + account_metas, + ); + let tx = Transaction::new_signed_with_payer( + &[nonce_instruction], + Some(&from_pubkey), + &[&from_keypair, &nonce_keypair], + Hash::default(), + ); + let tx = VersionedTransaction::from(tx); + assert!(!tx.uses_durable_nonce()); + } + + #[test] + fn tx_uses_nonce_wrong_first_nonce_ix_fail() { + let from_keypair = Keypair::new(); + let from_pubkey = from_keypair.pubkey(); + let nonce_keypair = Keypair::new(); + let nonce_pubkey = nonce_keypair.pubkey(); + let instructions = [ + system_instruction::withdraw_nonce_account( + &nonce_pubkey, + &nonce_pubkey, + &from_pubkey, + 42, + ), + system_instruction::transfer(&from_pubkey, &nonce_pubkey, 42), + ]; + let message = LegacyMessage::new(&instructions, Some(&nonce_pubkey)); + let tx = Transaction::new(&[&from_keypair, &nonce_keypair], message, Hash::default()); + let tx = VersionedTransaction::from(tx); + assert!(!tx.uses_durable_nonce()); + } }