diff --git a/cli/src/wallet.rs b/cli/src/wallet.rs index fbab92c5dbff0b..c5f1f2786717ee 100644 --- a/cli/src/wallet.rs +++ b/cli/src/wallet.rs @@ -556,6 +556,7 @@ pub fn process_confirm( RpcTransactionConfig { encoding: Some(UiTransactionEncoding::Base64), commitment: Some(CommitmentConfig::confirmed()), + enable_versioned_transactions: Some(true), }, ) { Ok(confirmed_transaction) => { diff --git a/client/src/mock_sender.rs b/client/src/mock_sender.rs index 1133e5ad4c7970..686e1b08468912 100644 --- a/client/src/mock_sender.rs +++ b/client/src/mock_sender.rs @@ -191,7 +191,7 @@ impl RpcSender for MockSender { transaction: EncodedTransaction::Json( UiTransaction { signatures: vec!["3AsdoALgZFuq2oUVWrDYhg2pNeaLJKPLf8hU2mQ6U8qJxeJ6hsrPVpMn9ma39DtfYCrDQSvngWRP8NnTpEhezJpE".to_string()], - message: UiMessage::Raw( + message: UiMessage::RawLegacy( UiRawMessage { header: MessageHeader { num_required_signatures: 1, @@ -222,6 +222,7 @@ impl RpcSender for MockSender { pre_token_balances: None, post_token_balances: None, rewards: None, + loaded_addresses: vec![], }), }, block_time: Some(1628633791), diff --git a/client/src/rpc_config.rs b/client/src/rpc_config.rs index edf3dc819877d8..e9951c4b4037b1 100644 --- a/client/src/rpc_config.rs +++ b/client/src/rpc_config.rs @@ -197,6 +197,7 @@ pub struct RpcBlockSubscribeConfig { pub encoding: Option, pub transaction_details: Option, pub show_rewards: Option, + pub enable_versioned_transactions: Option, } #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] @@ -248,6 +249,7 @@ pub struct RpcBlockConfig { pub rewards: Option, #[serde(flatten)] pub commitment: Option, + pub enable_versioned_transactions: Option, } impl EncodingConfig for RpcBlockConfig { @@ -288,6 +290,7 @@ pub struct RpcTransactionConfig { pub encoding: Option, #[serde(flatten)] pub commitment: Option, + pub enable_versioned_transactions: Option, } impl EncodingConfig for RpcTransactionConfig { diff --git a/client/src/rpc_deprecated_config.rs b/client/src/rpc_deprecated_config.rs index 9062513ba55d79..86b2922f29ac74 100644 --- a/client/src/rpc_deprecated_config.rs +++ b/client/src/rpc_deprecated_config.rs @@ -71,6 +71,7 @@ impl From for RpcBlockConfig { transaction_details: config.transaction_details, rewards: config.rewards, commitment: config.commitment, + enable_versioned_transactions: Some(false), } } } @@ -98,6 +99,7 @@ impl From for RpcTransactionConfig { Self { encoding: config.encoding, commitment: config.commitment, + enable_versioned_transactions: Some(false), } } } diff --git a/rpc/src/rpc.rs b/rpc/src/rpc.rs index 77b5db5bbcc6bf..3e05088734aed6 100644 --- a/rpc/src/rpc.rs +++ b/rpc/src/rpc.rs @@ -61,7 +61,10 @@ use { feature_set::{self, nonce_must_be_writable}, fee_calculator::FeeCalculator, hash::Hash, - message::{Message, SanitizedMessage}, + message::{ + v0::{LoadedAddresses, MessageAddressTableLookup}, + Message, SanitizedMessage, + }, pubkey::{Pubkey, PUBKEY_BYTES}, signature::{Keypair, Signature, Signer}, stake::state::{StakeActivationStatus, StakeState}, @@ -990,6 +993,8 @@ impl JsonRpcRequestProcessor { let transaction_details = config.transaction_details.unwrap_or_default(); let show_rewards = config.rewards.unwrap_or(true); let commitment = config.commitment.unwrap_or_default(); + let reject_versioned_transactions = + !config.enable_versioned_transactions.unwrap_or_default(); check_is_at_least_confirmed(commitment)?; // Block is old enough to be finalized @@ -1003,10 +1008,12 @@ impl JsonRpcRequestProcessor { self.check_status_is_complete(slot)?; let result = self.blockstore.get_rooted_block(slot, true); self.check_blockstore_root(&result, slot)?; - let configure_block = |versioned_block: VersionedConfirmedBlock| { - let confirmed_block = versioned_block - .into_legacy_block() - .ok_or(RpcCustomError::UnsupportedTransactionVersion)?; + let configure_block = |confirmed_block: VersionedConfirmedBlock| { + if reject_versioned_transactions && confirmed_block.has_versioned_transactions() + { + return Err(RpcCustomError::UnsupportedTransactionVersion); + } + let mut confirmed_block = confirmed_block.configure(encoding, transaction_details, show_rewards); if slot == 0 { @@ -1033,10 +1040,13 @@ impl JsonRpcRequestProcessor { let result = self.blockstore.get_complete_block(slot, true); return result .ok() - .map(|versioned_block| { - let mut confirmed_block = versioned_block - .into_legacy_block() - .ok_or(RpcCustomError::UnsupportedTransactionVersion)?; + .map(|mut confirmed_block| { + if reject_versioned_transactions + && confirmed_block.has_versioned_transactions() + { + return Err(RpcCustomError::UnsupportedTransactionVersion); + } + if confirmed_block.block_time.is_none() || confirmed_block.block_height.is_none() { @@ -1379,6 +1389,8 @@ impl JsonRpcRequestProcessor { .unwrap_or_default(); let encoding = config.encoding.unwrap_or(UiTransactionEncoding::Json); let commitment = config.commitment.unwrap_or_default(); + let reject_versioned_transactions = + !config.enable_versioned_transactions.unwrap_or_default(); check_is_at_least_confirmed(commitment)?; if self.config.enable_rpc_transaction_history { @@ -1392,10 +1404,12 @@ impl JsonRpcRequestProcessor { }; match versioned_confirmed_tx.unwrap_or(None) { - Some(versioned_confirmed_tx) => { - let mut confirmed_transaction = versioned_confirmed_tx - .into_legacy_confirmed_transaction() - .ok_or(RpcCustomError::UnsupportedTransactionVersion)?; + Some(confirmed_transaction) => { + if reject_versioned_transactions + && confirmed_transaction.has_versioned_transactions() + { + return Err(RpcCustomError::UnsupportedTransactionVersion); + } if commitment.is_confirmed() && confirmed_bank // should be redundant @@ -1426,10 +1440,12 @@ impl JsonRpcRequestProcessor { .get_confirmed_transaction(&signature) .await .unwrap_or(None) - .map(|versioned_confirmed_tx| { - let confirmed_tx = versioned_confirmed_tx - .into_legacy_confirmed_transaction() - .ok_or(RpcCustomError::UnsupportedTransactionVersion)?; + .map(|confirmed_tx| { + if reject_versioned_transactions + && confirmed_tx.has_versioned_transactions() + { + return Err(RpcCustomError::UnsupportedTransactionVersion); + } Ok(confirmed_tx.encode(encoding)) }) @@ -3475,6 +3491,7 @@ pub mod rpc_full { .preflight_commitment .map(|commitment| CommitmentConfig { commitment }); let preflight_bank = &*meta.bank(preflight_commitment); + let finalized_bank = &*meta.bank(Some(CommitmentConfig::finalized())); let transaction = sanitize_transaction(unsanitized_tx)?; let signature = *transaction.signature(); @@ -3583,6 +3600,7 @@ pub mod rpc_full { .set_recent_blockhash(bank.last_blockhash()); } + let finalized_bank = &*meta.bank(Some(CommitmentConfig::finalized())); let transaction = sanitize_transaction(unsanitized_tx)?; if config.sig_verify { verify_transaction(&transaction, &bank.feature_set)?; @@ -4400,12 +4418,12 @@ pub mod tests { fee_calculator::DEFAULT_BURN_PERCENT, hash::{hash, Hash}, instruction::InstructionError, - message::Message, + message::{v0, Message, MessageHeader, VersionedMessage}, nonce, rpc_port, signature::{Keypair, Signer}, system_program, system_transaction, timing::slot_duration_from_slots_per_year, - transaction::{self, Transaction, TransactionError}, + transaction::{self, AddressLookupError, Transaction, TransactionError}, }, solana_transaction_status::{ EncodedConfirmedBlock, EncodedTransaction, EncodedTransactionWithStatusMeta, @@ -6613,7 +6631,8 @@ pub mod tests { let meta = meta.unwrap(); let transaction_recent_blockhash = match transaction.message { UiMessage::Parsed(message) => message.recent_blockhash, - UiMessage::Raw(message) => message.recent_blockhash, + UiMessage::RawLegacy(message) => message.recent_blockhash, + UiMessage::RawVersioned(message) => message.recent_blockhash, }; assert_eq!(transaction_recent_blockhash, blockhash.to_string()); assert_eq!(meta.status, Ok(())); @@ -6712,6 +6731,7 @@ pub mod tests { transaction_details: Some(TransactionDetails::Signatures), rewards: Some(false), commitment: None, + enable_versioned_transactions: None, }) ); let res = io.handle_request_sync(&req, meta.clone()); @@ -6733,6 +6753,7 @@ pub mod tests { transaction_details: Some(TransactionDetails::None), rewards: Some(true), commitment: None, + enable_versioned_transactions: None, }) ); let res = io.handle_request_sync(&req, meta); @@ -8090,8 +8111,40 @@ pub mod tests { .to_string(), ); assert_eq!( - sanitize_transaction(unsanitary_versioned_tx).unwrap_err(), + sanitize_transaction(unsanitary_versioned_tx, |_| Ok(LoadedAddresses::default())) + .unwrap_err(), expect58 ); } + + #[test] + fn test_sanitize_invalid_lookups() { + let versioned_tx = VersionedTransaction { + signatures: vec![Signature::default()], + message: VersionedMessage::V0(v0::Message { + header: MessageHeader { + num_required_signatures: 1, + ..MessageHeader::default() + }, + account_keys: vec![Pubkey::new_unique()], + address_table_lookups: vec![MessageAddressTableLookup { + account_key: Pubkey::new_unique(), + writable_indexes: vec![255], + readonly_indexes: vec![], + }], + ..v0::Message::default() + }), + }; + + let sanitize_err = sanitize_transaction(versioned_tx, |_| { + Err(AddressLookupError::InvalidLookupIndex.into()) + }) + .unwrap_err(); + assert_eq!( + sanitize_err, + Error::invalid_params( + "invalid transaction: Transaction address table lookup failed".to_string(), + ) + ); + } } diff --git a/rpc/src/rpc_pubsub.rs b/rpc/src/rpc_pubsub.rs index 09e8fbd876e8e0..35a094318b7b92 100644 --- a/rpc/src/rpc_pubsub.rs +++ b/rpc/src/rpc_pubsub.rs @@ -538,6 +538,9 @@ impl RpcSolPubSubInternal for RpcSolPubSubImpl { }, transaction_details: config.transaction_details.unwrap_or_default(), show_rewards: config.show_rewards.unwrap_or_default(), + reject_versioned_transactions: !config + .enable_versioned_transactions + .unwrap_or_default(), }; self.subscribe(SubscriptionParams::Block(params)) } diff --git a/rpc/src/rpc_subscription_tracker.rs b/rpc/src/rpc_subscription_tracker.rs index c49ff6fd942a24..573bc3e485289a 100644 --- a/rpc/src/rpc_subscription_tracker.rs +++ b/rpc/src/rpc_subscription_tracker.rs @@ -140,6 +140,7 @@ pub struct BlockSubscriptionParams { pub kind: BlockSubscriptionKind, pub transaction_details: TransactionDetails, pub show_rewards: bool, + pub reject_versioned_transactions: bool, } #[derive(Debug, Clone, PartialEq, Eq, Hash)] diff --git a/rpc/src/rpc_subscriptions.rs b/rpc/src/rpc_subscriptions.rs index 4982a272e2e26b..2b2bfeee76d176 100644 --- a/rpc/src/rpc_subscriptions.rs +++ b/rpc/src/rpc_subscriptions.rs @@ -281,19 +281,26 @@ fn filter_block_result_txs( block: ConfirmedBlock, last_modified_slot: Slot, params: &BlockSubscriptionParams, -) -> Option { +) -> Result, RpcBlockUpdateError> { let transactions = match params.kind { BlockSubscriptionKind::All => block.transactions, BlockSubscriptionKind::MentionsAccountOrProgram(pk) => block .transactions .into_iter() - .filter(|tx| tx.transaction.message.account_keys.contains(&pk)) + .filter(|tx| { + tx.transaction.message.static_account_keys().contains(&pk) + || tx + .meta + .as_ref() + .map(|meta| meta.loaded_addresses.contains(&pk)) + .unwrap_or_default() + }) .collect(), }; if transactions.is_empty() { if let BlockSubscriptionKind::MentionsAccountOrProgram(_) = params.kind { - return None; + return Ok(None); } } @@ -305,16 +312,17 @@ fn filter_block_result_txs( params.encoding, params.transaction_details, params.show_rewards, - ); + params.reject_versioned_transactions, + )?; // If last_modified_slot < last_notified_slot, then the last notif was for a fork. // That's the risk clients take when subscribing to non-finalized commitments. // This code lets the logic for dealing with forks live on the client side. - Some(RpcBlockUpdate { + Ok(Some(RpcBlockUpdate { slot: last_modified_slot, block: Some(block), err: None, - }) + })) } fn filter_account_result( @@ -964,10 +972,15 @@ impl RpcSubscriptions { error!("get_complete_block error: {}", e); RpcBlockUpdateError::BlockStoreError }) - .and_then(|versioned_block| { - versioned_block.into_legacy_block().ok_or( - RpcBlockUpdateError::UnsupportedTransactionVersion, - ) + .and_then(|confirmed_block| { + if reject_versioned_transactions + && confirmed_block.has_versioned_transactions() + { + return Err( + RpcBlockUpdateError::UnsupportedTransactionVersion, + ); + } + Ok(confirmed_block) }); match block_result { diff --git a/transaction-status/src/lib.rs b/transaction-status/src/lib.rs index c69ffd66ff9d2e..f6e1b03df147f8 100644 --- a/transaction-status/src/lib.rs +++ b/transaction-status/src/lib.rs @@ -32,7 +32,8 @@ use { signature::Signature, transaction::{Result, Transaction, TransactionError, VersionedTransaction}, }, - std::fmt, + std::{fmt, str::FromStr}, + thiserror::Error, }; /// Represents types that can be encoded into one of several encoding formats @@ -41,6 +42,13 @@ pub trait Encodable { fn encode(self, encoding: UiTransactionEncoding) -> Self::Encoded; } +/// Represents types that can be encoded into one of several encoding formats +pub trait EncodableWithMeta { + type Encoded; + fn encode(self, encoding: UiTransactionEncoding, meta: &TransactionStatusMeta) + -> Self::Encoded; +} + #[derive(Serialize, Deserialize, Clone, Copy, Debug, Eq, Hash, PartialEq)] #[serde(rename_all = "camelCase")] pub enum UiTransactionEncoding { @@ -410,6 +418,25 @@ pub struct VersionedConfirmedBlock { pub block_height: Option, } +impl Encodable for VersionedConfirmedBlock { + type Encoded = EncodedConfirmedBlock; + fn encode(self, encoding: UiTransactionEncoding) -> Self::Encoded { + Self::Encoded { + previous_blockhash: self.previous_blockhash, + blockhash: self.blockhash, + parent_slot: self.parent_slot, + transactions: self + .transactions + .into_iter() + .map(|tx| tx.encode(encoding)) + .collect(), + rewards: self.rewards, + block_time: self.block_time, + block_height: self.block_height, + } + } +} + impl VersionedConfirmedBlock { /// Downgrades a versioned block into a legacy block type /// if it only contains legacy transactions @@ -469,7 +496,7 @@ impl ConfirmedBlock { encoding: UiTransactionEncoding, transaction_details: TransactionDetails, show_rewards: bool, - ) -> UiConfirmedBlock { + ) -> std::result::Result { let (transactions, signatures) = match transaction_details { TransactionDetails::Full => ( Some( @@ -491,7 +518,7 @@ impl ConfirmedBlock { ), TransactionDetails::None => (None, None), }; - UiConfirmedBlock { + Ok(UiConfirmedBlock { previous_blockhash: self.previous_blockhash, blockhash: self.blockhash, parent_slot: self.parent_slot, @@ -504,7 +531,7 @@ impl ConfirmedBlock { }, block_time: self.block_time, block_height: self.block_height, - } + }) } } @@ -568,17 +595,28 @@ impl From for UiConfirmedBlock { #[derive(Clone, Debug, PartialEq)] pub struct VersionedTransactionWithStatusMeta { pub transaction: VersionedTransaction, - pub meta: Option, + pub meta: TransactionStatusMeta, +} + +impl Encodable for VersionedTransactionWithStatusMeta { + type Encoded = EncodedTransactionWithStatusMeta; + fn encode(self, encoding: UiTransactionEncoding) -> Self::Encoded { + Self::Encoded { + transaction: self.transaction.encode(encoding, &self.meta), + meta: match encoding { + UiTransactionEncoding::JsonParsed => { + UiTransactionStatusMeta::parse(meta, &self.transaction.message) + } + _ => UiTransactionStatusMeta::from(meta), + }, + } + } } impl VersionedTransactionWithStatusMeta { pub fn account_keys_iter(&self) -> impl Iterator { let static_keys_iter = self.transaction.message.static_account_keys().iter(); - let dynamic_keys_iter = self - .meta - .iter() - .map(|meta| meta.loaded_addresses.ordered_iter()) - .flatten(); + let dynamic_keys_iter = self.meta.loaded_addresses.ordered_iter(); static_keys_iter.chain(dynamic_keys_iter) } @@ -586,7 +624,7 @@ impl VersionedTransactionWithStatusMeta { pub fn into_legacy_transaction_with_meta(self) -> Option { Some(TransactionWithStatusMeta { transaction: self.transaction.into_legacy_transaction()?, - meta: self.meta, + meta: Some(self.meta), }) } } @@ -685,6 +723,85 @@ pub enum EncodedTransaction { Json(UiTransaction), } +impl EncodableWithMeta for &VersionedTransaction { + fn encode( + self, + encoding: UiTransactionEncoding, + meta: &TransactionStatusMeta, + ) -> Self::Encoded { + match encoding { + UiTransactionEncoding::Binary => EncodedTransaction::LegacyBinary( + bs58::encode(bincode::serialize(self).unwrap()).into_string(), + ), + UiTransactionEncoding::Base58 => EncodedTransaction::Binary( + bs58::encode(bincode::serialize(self).unwrap()).into_string(), + encoding, + ), + UiTransactionEncoding::Base64 => EncodedTransaction::Binary( + base64::encode(bincode::serialize(self).unwrap()), + encoding, + ), + UiTransactionEncoding::Json | UiTransactionEncoding::JsonParsed => { + EncodedTransaction::Json(UiTransaction { + signatures: self.signatures.iter().map(ToString::to_string).collect(), + message: self.message.encode(encoding), + }) + } + UiTransactionEncoding::JsonParsed => { + let parsed_message = match transaction.message { + VersionedMessage::Legacy(message) => UiParsedMessage { + account_keys: parse_accounts(&message), + recent_blockhash: message.recent_blockhash.to_string(), + instructions: message + .instructions + .iter() + .map(|instruction| UiInstruction::parse(instruction, &message)) + .collect(), + version: None, + }, + + VersionedMessage::V0(message) => { + let loaded_addresses = LoadedAddresses::try_new( + &message.address_table_lookups, + loaded_addresses + .ok_or(EncodeTransactionError::MissingLoadedAddresses)?, + ) + .ok_or(EncodeTransactionError::InvalidLoadedAddresses)?; + + let message = SanitizedMessage::V0(LoadedMessage { + message, + loaded_addresses, + }); + + UiParsedMessage { + account_keys: parse_accounts_versioned(&message), + recent_blockhash: message.recent_blockhash().to_string(), + instructions: message + .program_instructions_iter() + .map(|(program_id, instruction)| { + UiInstruction::parse_versioned( + program_id, + instruction, + &message, + ) + }) + .collect(), + version: Some(0), + } + } + }; + + EncodedTransaction::Json(UiTransaction { + signatures: encoded_signatures, + message: UiMessage::Parsed(parsed_message), + }) + } + }; + + Ok(encoded_tx) + } +} + impl Encodable for &Transaction { type Encoded = EncodedTransaction; fn encode(self, encoding: UiTransactionEncoding) -> Self::Encoded { @@ -706,7 +823,55 @@ impl Encodable for &Transaction { message: self.message.encode(encoding), }) } - } + UiTransactionEncoding::JsonParsed => { + let parsed_message = match transaction.message { + VersionedMessage::Legacy(message) => UiParsedMessage { + account_keys: parse_accounts(&message), + recent_blockhash: message.recent_blockhash.to_string(), + instructions: message + .instructions + .iter() + .map(|instruction| UiInstruction::parse(instruction, &message)) + .collect(), + }, + VersionedMessage::V0(message) => { + let loaded_addresses = LoadedAddresses::try_new( + &message.address_table_lookups, + loaded_addresses + .ok_or(EncodeTransactionError::MissingLoadedAddresses)?, + ) + .ok_or(EncodeTransactionError::InvalidLoadedAddresses)?; + + let message = SanitizedMessage::V0(LoadedMessage { + message, + loaded_addresses, + }); + + UiParsedMessage { + account_keys: parse_accounts_versioned(&message), + recent_blockhash: message.recent_blockhash().to_string(), + instructions: message + .program_instructions_iter() + .map(|(program_id, instruction)| { + UiInstruction::parse_versioned( + program_id, + instruction, + &message, + ) + }) + .collect(), + } + } + }; + + EncodedTransaction::Json(UiTransaction { + signatures: encoded_signatures, + message: UiMessage::Parsed(parsed_message), + }) + } + }; + + Ok(encoded_tx) } } @@ -750,7 +915,7 @@ pub enum UiMessage { Raw(UiRawMessage), } -impl Encodable for &Message { +impl Encodable for &VersionedMessage { type Encoded = UiMessage; fn encode(self, encoding: UiTransactionEncoding) -> Self::Encoded { if encoding == UiTransactionEncoding::JsonParsed { @@ -769,6 +934,7 @@ impl Encodable for &Message { account_keys: self.account_keys.iter().map(ToString::to_string).collect(), recent_blockhash: self.recent_blockhash.to_string(), instructions: self.instructions.iter().map(Into::into).collect(), + address_table_lookups, }) } } @@ -782,6 +948,17 @@ pub struct UiRawMessage { pub account_keys: Vec, pub recent_blockhash: String, pub instructions: Vec, + pub version: Option, + pub address_table_lookups: Option>, +} + +/// A duplicate representation of a MessageAddressTableLookup, in raw format, for pretty JSON serialization +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UiAddressTableLookup { + pub account_key: String, + pub writable_indexes: Vec, + pub readonly_indexes: Vec, } /// A duplicate representation of a Message, in parsed format, for pretty JSON serialization @@ -791,6 +968,7 @@ pub struct UiParsedMessage { pub account_keys: Vec, pub recent_blockhash: String, pub instructions: Vec, + pub version: Option, } // A serialized `Vec` is stored in the `tx-by-addr` table. The row keys are