From 3114c199bde434b47f255bb6fdd6492836fd9a45 Mon Sep 17 00:00:00 2001 From: Justin Starry Date: Tue, 8 Mar 2022 15:20:34 +0800 Subject: [PATCH] Add RPC support for versioned transactions (#22530) * Add RPC support for versioned transactions * fix doc tests * Add rpc test for versioned txs * Switch to preflight bank --- Cargo.lock | 1 + cli/src/cluster_query.rs | 1 + cli/src/wallet.rs | 1 + client-test/tests/client.rs | 33 +- client/src/mock_sender.rs | 6 +- client/src/nonblocking/rpc_client.rs | 2 + client/src/rpc_client.rs | 2 + client/src/rpc_config.rs | 3 + client/src/rpc_custom_error.rs | 17 +- client/src/rpc_deprecated_config.rs | 2 + client/src/rpc_response.rs | 4 +- core/src/banking_stage.rs | 4 +- core/src/replay_stage.rs | 13 +- docs/src/developing/clients/jsonrpc-api.md | 16 +- ledger-tool/src/bigtable.rs | 27 +- .../tests/common.rs | 4 +- programs/address-lookup-table/src/state.rs | 13 +- rpc/Cargo.toml | 1 + rpc/src/rpc.rs | 301 +++++++++++---- rpc/src/rpc_pubsub.rs | 1 + rpc/src/rpc_subscription_tracker.rs | 1 + rpc/src/rpc_subscriptions.rs | 129 ++++--- runtime/src/accounts.rs | 7 +- sdk/program/src/message/sanitized.rs | 30 +- sdk/program/src/message/versions/v0/loaded.rs | 86 +++-- sdk/src/transaction/sanitized.rs | 15 +- sdk/src/transaction/versioned.rs | 26 ++ transaction-status/src/lib.rs | 349 +++++++++++++----- transaction-status/src/parse_accounts.rs | 79 +++- 29 files changed, 864 insertions(+), 310 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d58c291a7..fc787b239 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5505,6 +5505,7 @@ dependencies = [ "serial_test", "soketto", "solana-account-decoder", + "solana-address-lookup-table-program", "solana-client", "solana-entry", "solana-faucet", diff --git a/cli/src/cluster_query.rs b/cli/src/cluster_query.rs index 99cc004d2..ea2872c68 100644 --- a/cli/src/cluster_query.rs +++ b/cli/src/cluster_query.rs @@ -2042,6 +2042,7 @@ pub fn process_transaction_history( RpcTransactionConfig { encoding: Some(UiTransactionEncoding::Base64), commitment: Some(CommitmentConfig::confirmed()), + max_supported_transaction_version: None, }, ) { Ok(confirmed_transaction) => { diff --git a/cli/src/wallet.rs b/cli/src/wallet.rs index 565f7711a..5b0b000d1 100644 --- a/cli/src/wallet.rs +++ b/cli/src/wallet.rs @@ -559,6 +559,7 @@ pub fn process_confirm( RpcTransactionConfig { encoding: Some(UiTransactionEncoding::Base64), commitment: Some(CommitmentConfig::confirmed()), + max_supported_transaction_version: None, }, ) { Ok(confirmed_transaction) => { diff --git a/client-test/tests/client.rs b/client-test/tests/client.rs index f29ea043a..38fe73f38 100644 --- a/client-test/tests/client.rs +++ b/client-test/tests/client.rs @@ -15,7 +15,7 @@ use { solana_ledger::{blockstore::Blockstore, get_tmp_ledger_path}, solana_rpc::{ optimistically_confirmed_bank_tracker::OptimisticallyConfirmedBank, - rpc::create_test_transactions_and_populate_blockstore, + rpc::{create_test_transaction_entries, populate_blockstore_for_tests}, rpc_pubsub_service::{PubSubConfig, PubSubService}, rpc_subscriptions::RpcSubscriptions, }, @@ -36,7 +36,9 @@ use { }, solana_streamer::socket::SocketAddrSpace, solana_test_validator::TestValidator, - solana_transaction_status::{ConfirmedBlock, TransactionDetails, UiTransactionEncoding}, + solana_transaction_status::{ + BlockEncodingOptions, ConfirmedBlock, TransactionDetails, UiTransactionEncoding, + }, std::{ collections::HashSet, net::{IpAddr, SocketAddr}, @@ -230,9 +232,12 @@ fn test_block_subscription() { let max_complete_transaction_status_slot = Arc::new(AtomicU64::new(blockstore.max_root())); bank.transfer(rent_exempt_amount, &alice, &keypair2.pubkey()) .unwrap(); - let _confirmed_block_signatures = create_test_transactions_and_populate_blockstore( - vec![&alice, &keypair1, &keypair2, &keypair3], - 0, + populate_blockstore_for_tests( + create_test_transaction_entries( + vec![&alice, &keypair1, &keypair2, &keypair3], + bank.clone(), + ) + .0, bank, blockstore.clone(), max_complete_transaction_status_slot, @@ -270,6 +275,7 @@ fn test_block_subscription() { encoding: Some(UiTransactionEncoding::Json), transaction_details: Some(TransactionDetails::Signatures), show_rewards: None, + max_supported_transaction_version: None, }), ) .unwrap(); @@ -281,14 +287,17 @@ fn test_block_subscription() { match maybe_actual { Ok(actual) => { let versioned_block = blockstore.get_complete_block(slot, false).unwrap(); - let legacy_block = ConfirmedBlock::from(versioned_block) - .into_legacy_block() + let confirmed_block = ConfirmedBlock::from(versioned_block); + let block = confirmed_block + .encode_with_options( + UiTransactionEncoding::Json, + BlockEncodingOptions { + transaction_details: TransactionDetails::Signatures, + show_rewards: false, + max_supported_transaction_version: None, + }, + ) .unwrap(); - let block = legacy_block.configure( - UiTransactionEncoding::Json, - TransactionDetails::Signatures, - false, - ); assert_eq!(actual.value.slot, slot); assert!(block.eq(&actual.value.block.unwrap())); } diff --git a/client/src/mock_sender.rs b/client/src/mock_sender.rs index ed068f8c1..064228314 100644 --- a/client/src/mock_sender.rs +++ b/client/src/mock_sender.rs @@ -28,7 +28,7 @@ use { pubkey::Pubkey, signature::Signature, sysvar::epoch_schedule::EpochSchedule, - transaction::{self, Transaction, TransactionError}, + transaction::{self, Transaction, TransactionError, TransactionVersion}, }, solana_transaction_status::{ EncodedConfirmedBlock, EncodedConfirmedTransactionWithStatusMeta, EncodedTransaction, @@ -192,6 +192,7 @@ impl RpcSender for MockSender { "getTransaction" => serde_json::to_value(EncodedConfirmedTransactionWithStatusMeta { slot: 2, transaction: EncodedTransactionWithStatusMeta { + version: Some(TransactionVersion::LEGACY), transaction: EncodedTransaction::Json( UiTransaction { signatures: vec!["3AsdoALgZFuq2oUVWrDYhg2pNeaLJKPLf8hU2mQ6U8qJxeJ6hsrPVpMn9ma39DtfYCrDQSvngWRP8NnTpEhezJpE".to_string()], @@ -213,6 +214,7 @@ impl RpcSender for MockSender { accounts: vec![0, 1], data: "3Bxs49DitAvXtoDR".to_string(), }], + address_table_lookups: None, }) }), meta: Some(UiTransactionStatusMeta { @@ -226,6 +228,7 @@ impl RpcSender for MockSender { pre_token_balances: None, post_token_balances: None, rewards: None, + loaded_addresses: None, }), }, block_time: Some(1628633791), @@ -381,6 +384,7 @@ impl RpcSender for MockSender { UiTransactionEncoding::Base58, ), meta: None, + version: Some(TransactionVersion::LEGACY), }], rewards: Rewards::new(), block_time: None, diff --git a/client/src/nonblocking/rpc_client.rs b/client/src/nonblocking/rpc_client.rs index 1a35d85f1..d62203d31 100644 --- a/client/src/nonblocking/rpc_client.rs +++ b/client/src/nonblocking/rpc_client.rs @@ -2411,6 +2411,7 @@ impl RpcClient { /// transaction_details: Some(TransactionDetails::None), /// rewards: Some(true), /// commitment: None, + /// max_supported_transaction_version: Some(0), /// }; /// let block = rpc_client.get_block_with_config( /// slot, @@ -3051,6 +3052,7 @@ impl RpcClient { /// let config = RpcTransactionConfig { /// encoding: Some(UiTransactionEncoding::Json), /// commitment: Some(CommitmentConfig::confirmed()), + /// max_supported_transaction_version: Some(0), /// }; /// let transaction = rpc_client.get_transaction_with_config( /// &signature, diff --git a/client/src/rpc_client.rs b/client/src/rpc_client.rs index 152a78e59..79b013578 100644 --- a/client/src/rpc_client.rs +++ b/client/src/rpc_client.rs @@ -2034,6 +2034,7 @@ impl RpcClient { /// transaction_details: Some(TransactionDetails::None), /// rewards: Some(true), /// commitment: None, + /// max_supported_transaction_version: Some(0), /// }; /// let block = rpc_client.get_block_with_config( /// slot, @@ -2596,6 +2597,7 @@ impl RpcClient { /// let config = RpcTransactionConfig { /// encoding: Some(UiTransactionEncoding::Json), /// commitment: Some(CommitmentConfig::confirmed()), + /// max_supported_transaction_version: Some(0), /// }; /// let transaction = rpc_client.get_transaction_with_config( /// &signature, diff --git a/client/src/rpc_config.rs b/client/src/rpc_config.rs index edf3dc819..380d180a5 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 max_supported_transaction_version: Option, } #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] @@ -248,6 +249,7 @@ pub struct RpcBlockConfig { pub rewards: Option, #[serde(flatten)] pub commitment: Option, + pub max_supported_transaction_version: Option, } impl EncodingConfig for RpcBlockConfig { @@ -288,6 +290,7 @@ pub struct RpcTransactionConfig { pub encoding: Option, #[serde(flatten)] pub commitment: Option, + pub max_supported_transaction_version: Option, } impl EncodingConfig for RpcTransactionConfig { diff --git a/client/src/rpc_custom_error.rs b/client/src/rpc_custom_error.rs index 2f1bcd7e1..ae8fc545c 100644 --- a/client/src/rpc_custom_error.rs +++ b/client/src/rpc_custom_error.rs @@ -3,6 +3,7 @@ use { crate::rpc_response::RpcSimulateTransactionResult, jsonrpc_core::{Error, ErrorCode}, solana_sdk::clock::Slot, + solana_transaction_status::EncodeError, thiserror::Error, }; @@ -59,7 +60,7 @@ pub enum RpcCustomError { #[error("BlockStatusNotAvailableYet")] BlockStatusNotAvailableYet { slot: Slot }, #[error("UnsupportedTransactionVersion")] - UnsupportedTransactionVersion, + UnsupportedTransactionVersion(u8), } #[derive(Debug, Serialize, Deserialize)] @@ -68,6 +69,16 @@ pub struct NodeUnhealthyErrorData { pub num_slots_behind: Option, } +impl From for RpcCustomError { + fn from(err: EncodeError) -> Self { + match err { + EncodeError::UnsupportedTransactionVersion(version) => { + Self::UnsupportedTransactionVersion(version) + } + } + } +} + impl From for Error { fn from(e: RpcCustomError) -> Self { match e { @@ -172,9 +183,9 @@ impl From for Error { message: format!("Block status not yet available for slot {}", slot), data: None, }, - RpcCustomError::UnsupportedTransactionVersion => Self { + RpcCustomError::UnsupportedTransactionVersion(version) => Self { code: ErrorCode::ServerError(JSON_RPC_SERVER_ERROR_UNSUPPORTED_TRANSACTION_VERSION), - message: "Versioned transactions are not supported".to_string(), + message: format!("Transaction version ({}) is not supported", version), data: None, }, } diff --git a/client/src/rpc_deprecated_config.rs b/client/src/rpc_deprecated_config.rs index 9062513ba..eb4fbaea8 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, + max_supported_transaction_version: None, } } } @@ -98,6 +99,7 @@ impl From for RpcTransactionConfig { Self { encoding: config.encoding, commitment: config.commitment, + max_supported_transaction_version: None, } } } diff --git a/client/src/rpc_response.rs b/client/src/rpc_response.rs index abed3d539..78a79a867 100644 --- a/client/src/rpc_response.rs +++ b/client/src/rpc_response.rs @@ -432,8 +432,8 @@ pub enum RpcBlockUpdateError { #[error("block store error")] BlockStoreError, - #[error("unsupported transaction version")] - UnsupportedTransactionVersion, + #[error("unsupported transaction version ({0})")] + UnsupportedTransactionVersion(u8), } #[derive(Serialize, Deserialize, Debug)] diff --git a/core/src/banking_stage.rs b/core/src/banking_stage.rs index 4e43408d0..123107ee9 100644 --- a/core/src/banking_stage.rs +++ b/core/src/banking_stage.rs @@ -3384,9 +3384,7 @@ mod tests { account_address: Pubkey, address_lookup_table: AddressLookupTable<'static>, ) -> AccountSharedData { - let mut data = Vec::new(); - address_lookup_table.serialize_for_tests(&mut data).unwrap(); - + let data = address_lookup_table.serialize_for_tests().unwrap(); let mut account = AccountSharedData::new(1, data.len(), &solana_address_lookup_table_program::id()); account.set_data(data); diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index db5064c8a..98f910346 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -3129,7 +3129,7 @@ pub mod tests { }, solana_rpc::{ optimistically_confirmed_bank_tracker::OptimisticallyConfirmedBank, - rpc::create_test_transactions_and_populate_blockstore, + rpc::{create_test_transaction_entries, populate_blockstore_for_tests}, }, solana_runtime::{ accounts_background_service::AbsRequestSender, @@ -3998,15 +3998,18 @@ pub mod tests { let bank1 = Arc::new(Bank::new_from_parent(&bank0, &Pubkey::default(), 1)); let slot = bank1.slot(); - let mut test_signatures_iter = create_test_transactions_and_populate_blockstore( + let (entries, test_signatures) = create_test_transaction_entries( vec![&mint_keypair, &keypair1, &keypair2, &keypair3], - bank0.slot(), + bank1.clone(), + ); + populate_blockstore_for_tests( + entries, bank1, blockstore.clone(), Arc::new(AtomicU64::default()), - ) - .into_iter(); + ); + let mut test_signatures_iter = test_signatures.into_iter(); let confirmed_block = blockstore.get_rooted_block(slot, false).unwrap(); let actual_tx_results: Vec<_> = confirmed_block .transactions diff --git a/docs/src/developing/clients/jsonrpc-api.md b/docs/src/developing/clients/jsonrpc-api.md index df423c43a..3a134e340 100644 --- a/docs/src/developing/clients/jsonrpc-api.md +++ b/docs/src/developing/clients/jsonrpc-api.md @@ -389,6 +389,7 @@ Returns identity and transaction information about a confirmed block in the ledg - (optional) `transactionDetails: ` - level of transaction detail to return, either "full", "signatures", or "none". If parameter not provided, the default detail level is "full". - (optional) `rewards: bool` - whether to populate the `rewards` array. If parameter not provided, the default includes rewards. - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment); "processed" is not supported. If parameter not provided, the default is "finalized". + - (optional) `maxSupportedTransactionVersion: ` - set the max transaction version to return in responses. If the requested block contains a transaction with a higher version, an error will be returned. #### Results: @@ -413,6 +414,10 @@ The result field will be an object with the following fields: - DEPRECATED: `status: ` - Transaction status - `"Ok": ` - Transaction was successful - `"Err": ` - Transaction failed with TransactionError + - `loadedAddresses: ` - Transaction addresses loaded from address lookup tables. Undefined if `maxSupportedTransactionVersion` is not set in request params. + - `writable: ` - Ordered list of base-58 encoded addresses for writable loaded accounts + - `readonly: ` - Ordered list of base-58 encoded addresses for readonly loaded accounts + - `version: <"legacy"|number|undefined>` - Transaction version. Undefined if `maxSupportedTransactionVersion` is not set in request params. - `signatures: ` - present if "signatures" are requested for transaction details; an array of signatures strings, corresponding to the transaction order in the block - `rewards: ` - present if rewards are requested; an array of JSON objects containing: - `pubkey: ` - The public key, as base-58 encoded string, of the account that received the reward @@ -559,6 +564,10 @@ The JSON structure of a transaction is defined as follows: - `programIdIndex: ` - Index into the `message.accountKeys` array indicating the program account that executes this instruction. - `accounts: ` - List of ordered indices into the `message.accountKeys` array indicating which accounts to pass to the program. - `data: ` - The program input data encoded in a base-58 string. + - `addressTableLookups: ` - List of address table lookups used by a transaction to dynamically load addresses from on-chain address lookup tables. Undefined if `maxSupportedTransactionVersion` is not set. + - `accountKey: ` - base-58 encoded public key for an address lookup table account. + - `writableIndexes: ` - List of indices used to load addresses of writable accounts from a lookup table. + - `readonlyIndexes: ` - List of indices used to load addresses of readonly accounts from a lookup table. #### Inner Instructions Structure @@ -2313,7 +2322,7 @@ Returns the slot leaders for a given slot range #### Results: -- `>` - Node identity public keys as base-58 encoded strings +- `` - Node identity public keys as base-58 encoded strings #### Example: @@ -2847,6 +2856,7 @@ Returns transaction details for a confirmed transaction - (optional) `encoding: ` - encoding for each returned Transaction, either "json", "jsonParsed", "base58" (_slow_), "base64". If parameter not provided, the default encoding is "json". "jsonParsed" encoding attempts to use program-specific instruction parsers to return more human-readable and explicit data in the `transaction.message.instructions` list. If "jsonParsed" is requested but a parser cannot be found, the instruction falls back to regular JSON encoding (`accounts`, `data`, and `programIdIndex` fields). - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment); "processed" is not supported. If parameter not provided, the default is "finalized". + - (optional) `maxSupportedTransactionVersion: ` - set the max transaction version to return in responses. If the requested transaction is a higher version, an error will be returned. #### Results: @@ -2873,6 +2883,10 @@ Returns transaction details for a confirmed transaction - `postBalance: ` - account balance in lamports after the reward was applied - `rewardType: ` - type of reward: currently only "rent", other types may be added in the future - `commission: ` - vote account commission when the reward was credited, only present for voting and staking rewards + - `loadedAddresses: ` - Transaction addresses loaded from address lookup tables. Undefined if `maxSupportedTransactionVersion` is not set in request params. + - `writable: ` - Ordered list of base-58 encoded addresses for writable loaded accounts + - `readonly: ` - Ordered list of base-58 encoded addresses for readonly loaded accounts + - `version: <"legacy"|number|undefined>` - Transaction version. Undefined if `maxSupportedTransactionVersion` is not set in request params. #### Example: diff --git a/ledger-tool/src/bigtable.rs b/ledger-tool/src/bigtable.rs index 77144cac9..e1c15c570 100644 --- a/ledger-tool/src/bigtable.rs +++ b/ledger-tool/src/bigtable.rs @@ -16,7 +16,10 @@ use { }, solana_ledger::{blockstore::Blockstore, blockstore_db::AccessType}, solana_sdk::{clock::Slot, pubkey::Pubkey, signature::Signature}, - solana_transaction_status::{Encodable, LegacyConfirmedBlock, UiTransactionEncoding}, + solana_transaction_status::{ + BlockEncodingOptions, Encodable, EncodeError, LegacyConfirmedBlock, TransactionDetails, + UiTransactionEncoding, + }, std::{ collections::HashSet, path::Path, @@ -72,12 +75,26 @@ async fn block(slot: Slot, output_format: OutputFormat) -> Result<(), Box { + format!( + "Failed to process unsupported transaction version ({}) in block", + version + ) + } + })?; let cli_block = CliBlock { - encoded_confirmed_block: legacy_block.encode(UiTransactionEncoding::Base64), + encoded_confirmed_block: encoded_block.into(), slot, }; println!("{}", output_format.formatted_string(&cli_block)); diff --git a/programs/address-lookup-table-tests/tests/common.rs b/programs/address-lookup-table-tests/tests/common.rs index 3aa21bc89..d5c77c208 100644 --- a/programs/address-lookup-table-tests/tests/common.rs +++ b/programs/address-lookup-table-tests/tests/common.rs @@ -76,9 +76,7 @@ pub async fn add_lookup_table_account( account_address: Pubkey, address_lookup_table: AddressLookupTable<'static>, ) -> AccountSharedData { - let mut data = Vec::new(); - address_lookup_table.serialize_for_tests(&mut data).unwrap(); - + let data = address_lookup_table.serialize_for_tests().unwrap(); let rent = context.banks_client.get_rent().await.unwrap(); let rent_exempt_balance = rent.minimum_balance(data.len()); diff --git a/programs/address-lookup-table/src/state.rs b/programs/address-lookup-table/src/state.rs index 78f4c1d93..4768cd563 100644 --- a/programs/address-lookup-table/src/state.rs +++ b/programs/address-lookup-table/src/state.rs @@ -178,13 +178,13 @@ impl<'a> AddressLookupTable<'a> { } /// Serialize an address table including its addresses - pub fn serialize_for_tests(self, data: &mut Vec) -> Result<(), InstructionError> { - data.resize(LOOKUP_TABLE_META_SIZE, 0); - Self::overwrite_meta_data(data, self.meta)?; + pub fn serialize_for_tests(self) -> Result, InstructionError> { + let mut data = vec![0; LOOKUP_TABLE_META_SIZE]; + Self::overwrite_meta_data(&mut data, self.meta)?; self.addresses.iter().for_each(|address| { data.extend_from_slice(address.as_ref()); }); - Ok(()) + Ok(data) } /// Efficiently deserialize an address table without allocating @@ -352,9 +352,8 @@ mod tests { fn test_case(num_addresses: usize) { let lookup_table_meta = LookupTableMeta::new_for_tests(); let address_table = AddressLookupTable::new_for_tests(lookup_table_meta, num_addresses); - let mut address_table_data = Vec::new(); - AddressLookupTable::serialize_for_tests(address_table.clone(), &mut address_table_data) - .unwrap(); + let address_table_data = + AddressLookupTable::serialize_for_tests(address_table.clone()).unwrap(); assert_eq!( AddressLookupTable::deserialize(&address_table_data).unwrap(), address_table, diff --git a/rpc/Cargo.toml b/rpc/Cargo.toml index e74f58b2e..147428153 100644 --- a/rpc/Cargo.toml +++ b/rpc/Cargo.toml @@ -56,6 +56,7 @@ tokio-util = { version = "0.6", features = ["codec", "compat"] } [dev-dependencies] serial_test = "0.6.0" +solana-address-lookup-table-program = { path = "../programs/address-lookup-table", version = "=1.10.1" } solana-net-utils = { path = "../net-utils", version = "=1.10.1" } solana-stake-program = { path = "../programs/stake", version = "=1.10.1" } symlink = "0.1.0" diff --git a/rpc/src/rpc.rs b/rpc/src/rpc.rs index a4e143ba9..cb62c8485 100644 --- a/rpc/src/rpc.rs +++ b/rpc/src/rpc.rs @@ -29,6 +29,7 @@ use { }, rpc_response::{Response as RpcResponse, *}, }, + solana_entry::entry::Entry, solana_faucet::faucet::request_airdrop_transaction, solana_gossip::{cluster_info::ClusterInfo, contact_info::ContactInfo}, solana_ledger::{ @@ -69,8 +70,7 @@ use { system_instruction, sysvar::stake_history, transaction::{ - self, DisabledAddressLoader, SanitizedTransaction, TransactionError, - VersionedTransaction, + self, AddressLoader, SanitizedTransaction, TransactionError, VersionedTransaction, }, }, solana_send_transaction_service::{ @@ -80,9 +80,9 @@ use { solana_storage_bigtable::Error as StorageError, solana_streamer::socket::SocketAddrSpace, solana_transaction_status::{ - ConfirmedBlock, ConfirmedTransactionStatusWithSignature, - ConfirmedTransactionWithStatusMeta, Encodable, EncodedConfirmedTransactionWithStatusMeta, - Reward, RewardType, TransactionConfirmationStatus, TransactionStatus, UiConfirmedBlock, + BlockEncodingOptions, ConfirmedBlock, ConfirmedTransactionStatusWithSignature, + ConfirmedTransactionWithStatusMeta, EncodedConfirmedTransactionWithStatusMeta, Reward, + RewardType, TransactionConfirmationStatus, TransactionStatus, UiConfirmedBlock, UiTransactionEncoding, }, solana_vote_program::vote_state::{VoteState, MAX_LOCKOUT_HISTORY}, @@ -991,8 +991,11 @@ impl JsonRpcRequestProcessor { .map(|config| config.convert_to_current()) .unwrap_or_default(); let encoding = config.encoding.unwrap_or(UiTransactionEncoding::Json); - let transaction_details = config.transaction_details.unwrap_or_default(); - let show_rewards = config.rewards.unwrap_or(true); + let encoding_options = BlockEncodingOptions { + transaction_details: config.transaction_details.unwrap_or_default(), + show_rewards: config.rewards.unwrap_or(true), + max_supported_transaction_version: config.max_supported_transaction_version, + }; let commitment = config.commitment.unwrap_or_default(); check_is_at_least_confirmed(commitment)?; @@ -1007,31 +1010,29 @@ 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 = |confirmed_block: ConfirmedBlock| { - let legacy_block = confirmed_block - .into_legacy_block() - .ok_or(RpcCustomError::UnsupportedTransactionVersion)?; - let mut confirmed_block = - legacy_block.configure(encoding, transaction_details, show_rewards); + let encode_block = |confirmed_block: ConfirmedBlock| -> Result { + let mut encoded_block = confirmed_block + .encode_with_options(encoding, encoding_options) + .map_err(RpcCustomError::from)?; if slot == 0 { - confirmed_block.block_time = Some(self.genesis_creation_time()); - confirmed_block.block_height = Some(0); + encoded_block.block_time = Some(self.genesis_creation_time()); + encoded_block.block_height = Some(0); } - Ok(confirmed_block) + Ok(encoded_block) }; if result.is_err() { if let Some(bigtable_ledger_storage) = &self.bigtable_ledger_storage { let bigtable_result = bigtable_ledger_storage.get_confirmed_block(slot).await; self.check_bigtable_result(&bigtable_result)?; - return bigtable_result.ok().map(configure_block).transpose(); + return bigtable_result.ok().map(encode_block).transpose(); } } self.check_slot_cleaned_up(&result, slot)?; return result .ok() .map(ConfirmedBlock::from) - .map(configure_block) + .map(encode_block) .transpose(); } else if commitment.is_confirmed() { // Check if block is confirmed @@ -1042,27 +1043,26 @@ impl JsonRpcRequestProcessor { return result .ok() .map(ConfirmedBlock::from) - .map(|confirmed_block| -> Result { - let mut legacy_block = confirmed_block - .into_legacy_block() - .ok_or(RpcCustomError::UnsupportedTransactionVersion)?; - - if legacy_block.block_time.is_none() - || legacy_block.block_height.is_none() + .map(|mut confirmed_block| -> Result { + if confirmed_block.block_time.is_none() + || confirmed_block.block_height.is_none() { let r_bank_forks = self.bank_forks.read().unwrap(); let bank = r_bank_forks.get(slot).cloned(); if let Some(bank) = bank { - if legacy_block.block_time.is_none() { - legacy_block.block_time = Some(bank.clock().unix_timestamp); + if confirmed_block.block_time.is_none() { + confirmed_block.block_time = + Some(bank.clock().unix_timestamp); } - if legacy_block.block_height.is_none() { - legacy_block.block_height = Some(bank.block_height()); + if confirmed_block.block_height.is_none() { + confirmed_block.block_height = Some(bank.block_height()); } } } - Ok(legacy_block.configure(encoding, transaction_details, show_rewards)) + Ok(confirmed_block + .encode_with_options(encoding, encoding_options) + .map_err(RpcCustomError::from)?) }) .transpose(); } @@ -1384,6 +1384,7 @@ impl JsonRpcRequestProcessor { .map(|config| config.convert_to_current()) .unwrap_or_default(); let encoding = config.encoding.unwrap_or(UiTransactionEncoding::Json); + let max_supported_transaction_version = config.max_supported_transaction_version; let commitment = config.commitment.unwrap_or_default(); check_is_at_least_confirmed(commitment)?; @@ -1399,10 +1400,7 @@ impl JsonRpcRequestProcessor { let encode_transaction = |confirmed_tx_with_meta: ConfirmedTransactionWithStatusMeta| -> Result { - let legacy_tx_with_meta = confirmed_tx_with_meta.into_legacy_confirmed_transaction() - .ok_or(RpcCustomError::UnsupportedTransactionVersion)?; - - Ok(legacy_tx_with_meta.encode(encoding)) + Ok(confirmed_tx_with_meta.encode(encoding, max_supported_transaction_version).map_err(RpcCustomError::from)?) }; match confirmed_transaction.unwrap_or(None) { @@ -3478,7 +3476,7 @@ pub mod rpc_full { .preflight_commitment .map(|commitment| CommitmentConfig { commitment }); let preflight_bank = &*meta.bank(preflight_commitment); - let transaction = sanitize_transaction(unsanitized_tx)?; + let transaction = sanitize_transaction(unsanitized_tx, preflight_bank)?; let signature = *transaction.signature(); let mut last_valid_block_height = preflight_bank @@ -3586,7 +3584,7 @@ pub mod rpc_full { .set_recent_blockhash(bank.last_blockhash()); } - let transaction = sanitize_transaction(unsanitized_tx)?; + let transaction = sanitize_transaction(unsanitized_tx, bank)?; if config.sig_verify { verify_transaction(&transaction, &bank.feature_set)?; } @@ -4271,9 +4269,12 @@ where .map(|output| (wire_output, output)) } -fn sanitize_transaction(transaction: VersionedTransaction) -> Result { +fn sanitize_transaction( + transaction: VersionedTransaction, + address_loader: &impl AddressLoader, +) -> Result { let message_hash = transaction.message.hash(); - SanitizedTransaction::try_create(transaction, message_hash, None, &DisabledAddressLoader) + SanitizedTransaction::try_create(transaction, message_hash, None, address_loader) .map_err(|err| Error::invalid_params(format!("invalid transaction: {}", err))) } @@ -4284,22 +4285,18 @@ pub(crate) fn create_validator_exit(exit: &Arc) -> Arc> Arc::new(RwLock::new(validator_exit)) } -// Used for tests -pub fn create_test_transactions_and_populate_blockstore( +pub fn create_test_transaction_entries( keypairs: Vec<&Keypair>, - previous_slot: Slot, bank: Arc, - blockstore: Arc, - max_complete_transaction_status_slot: Arc, -) -> Vec { +) -> (Vec, Vec) { let mint_keypair = keypairs[0]; let keypair1 = keypairs[1]; let keypair2 = keypairs[2]; let keypair3 = keypairs[3]; - let slot = bank.slot(); let blockhash = bank.confirmed_last_blockhash(); let rent_exempt_amount = bank.get_minimum_balance_for_rent_exemption(0); + let mut signatures = Vec::new(); // Generate transactions for processing // Successful transaction let success_tx = solana_sdk::system_transaction::transfer( @@ -4308,7 +4305,7 @@ pub fn create_test_transactions_and_populate_blockstore( rent_exempt_amount, blockhash, ); - let success_signature = success_tx.signatures[0]; + signatures.push(success_tx.signatures[0]); let entry_1 = solana_entry::entry::next_entry(&blockhash, 1, vec![success_tx]); // Failed transaction, InstructionError let ix_error_tx = solana_sdk::system_transaction::transfer( @@ -4317,12 +4314,21 @@ pub fn create_test_transactions_and_populate_blockstore( 2 * rent_exempt_amount, blockhash, ); - let ix_error_signature = ix_error_tx.signatures[0]; + signatures.push(ix_error_tx.signatures[0]); let entry_2 = solana_entry::entry::next_entry(&entry_1.hash, 1, vec![ix_error_tx]); - let entries = vec![entry_1, entry_2]; + (vec![entry_1, entry_2], signatures) +} +pub fn populate_blockstore_for_tests( + entries: Vec, + bank: Arc, + blockstore: Arc, + max_complete_transaction_status_slot: Arc, +) { + let slot = bank.slot(); + let parent_slot = bank.parent_slot(); let shreds = - solana_ledger::blockstore::entries_to_test_shreds(&entries, slot, previous_slot, true, 0); + solana_ledger::blockstore::entries_to_test_shreds(&entries, slot, parent_slot, true, 0); blockstore.insert_shreds(shreds, None, false).unwrap(); blockstore.set_roots(std::iter::once(&slot)).unwrap(); @@ -4357,8 +4363,6 @@ pub fn create_test_transactions_and_populate_blockstore( ); transaction_status_service.join().unwrap(); - - vec![success_signature, ix_error_signature] } #[cfg(test)] @@ -4377,13 +4381,16 @@ pub mod tests { jsonrpc_core::{futures, ErrorCode, MetaIoHandler, Output, Response, Value}, jsonrpc_core_client::transports::local, serde::de::DeserializeOwned, + solana_address_lookup_table_program::state::{AddressLookupTable, LookupTableMeta}, solana_client::{ rpc_custom_error::{ JSON_RPC_SERVER_ERROR_BLOCK_NOT_AVAILABLE, JSON_RPC_SERVER_ERROR_TRANSACTION_HISTORY_NOT_AVAILABLE, + JSON_RPC_SERVER_ERROR_UNSUPPORTED_TRANSACTION_VERSION, }, rpc_filter::{Memcmp, MemcmpEncodedBytes}, }, + solana_entry::entry::next_versioned_entry, solana_gossip::{contact_info::ContactInfo, socketaddr}, solana_ledger::{ blockstore_meta::PerfSample, @@ -4400,11 +4407,15 @@ pub mod tests { fee_calculator::DEFAULT_BURN_PERCENT, hash::{hash, Hash}, instruction::InstructionError, + message::{v0, v0::MessageAddressTableLookup, MessageHeader, VersionedMessage}, nonce, rpc_port, signature::{Keypair, Signer}, + slot_hashes::SlotHashes, system_program, system_transaction, timing::slot_duration_from_slots_per_year, - transaction::{self, Transaction, TransactionError}, + transaction::{ + self, DisabledAddressLoader, Transaction, TransactionError, TransactionVersion, + }, }, solana_transaction_status::{ EncodedConfirmedBlock, EncodedTransaction, EncodedTransactionWithStatusMeta, @@ -4418,7 +4429,7 @@ pub mod tests { solana_program::{program_option::COption, pubkey::Pubkey as SplTokenPubkey}, state::{AccountState as TokenAccountState, Mint}, }, - std::collections::HashMap, + std::{borrow::Cow, collections::HashMap}, }; fn spl_token_id() -> Pubkey { @@ -4554,22 +4565,107 @@ pub mod tests { serde_json::from_str(response).expect("failed to deserialize response") } + fn overwrite_working_bank_entries(&self, entries: Vec) { + populate_blockstore_for_tests( + entries, + self.working_bank(), + self.blockstore.clone(), + self.max_complete_transaction_status_slot.clone(), + ); + } + fn create_test_transactions_and_populate_blockstore(&self) -> Vec { let mint_keypair = &self.mint_keypair; let keypair1 = Keypair::new(); let keypair2 = Keypair::new(); let keypair3 = Keypair::new(); - let bank = self.bank_forks.read().unwrap().working_bank(); + let bank = self.working_bank(); let rent_exempt_amount = bank.get_minimum_balance_for_rent_exemption(0); bank.transfer(rent_exempt_amount, mint_keypair, &keypair2.pubkey()) .unwrap(); - create_test_transactions_and_populate_blockstore( + + let (entries, signatures) = create_test_transaction_entries( vec![&self.mint_keypair, &keypair1, &keypair2, &keypair3], - 0, bank, - self.blockstore.clone(), - self.max_complete_transaction_status_slot.clone(), - ) + ); + self.overwrite_working_bank_entries(entries); + signatures + } + + fn create_test_versioned_transactions_and_populate_blockstore( + &self, + address_table_key: Option, + ) -> Vec { + let address_table_key = + address_table_key.unwrap_or_else(|| self.store_address_lookup_table()); + + let bank = self.working_bank(); + let recent_blockhash = bank.confirmed_last_blockhash(); + let legacy_message = VersionedMessage::Legacy(Message { + header: MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 0, + }, + recent_blockhash, + account_keys: vec![self.mint_keypair.pubkey()], + instructions: vec![], + }); + let version_0_message = VersionedMessage::V0(v0::Message { + header: MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 0, + }, + recent_blockhash, + account_keys: vec![self.mint_keypair.pubkey()], + address_table_lookups: vec![MessageAddressTableLookup { + account_key: address_table_key, + writable_indexes: vec![0], + readonly_indexes: vec![], + }], + instructions: vec![], + }); + + let mut signatures = Vec::new(); + let legacy_tx = + VersionedTransaction::try_new(legacy_message, &[&self.mint_keypair]).unwrap(); + signatures.push(legacy_tx.signatures[0]); + let version_0_tx = + VersionedTransaction::try_new(version_0_message, &[&self.mint_keypair]).unwrap(); + signatures.push(version_0_tx.signatures[0]); + let entry1 = next_versioned_entry(&recent_blockhash, 1, vec![legacy_tx]); + let entry2 = next_versioned_entry(&entry1.hash, 1, vec![version_0_tx]); + let entries = vec![entry1, entry2]; + self.overwrite_working_bank_entries(entries); + signatures + } + + fn store_address_lookup_table(&self) -> Pubkey { + let bank = self.working_bank(); + let address_table_pubkey = Pubkey::new_unique(); + let address_table_account = { + let address_table_state = AddressLookupTable { + meta: LookupTableMeta { + // ensure that active address length is 1 at slot 0 + last_extended_slot_start_index: 1, + ..LookupTableMeta::default() + }, + addresses: Cow::Owned(vec![Pubkey::new_unique()]), + }; + let address_table_data = address_table_state.serialize_for_tests().unwrap(); + let min_balance_lamports = + bank.get_minimum_balance_for_rent_exemption(address_table_data.len()); + AccountSharedData::create( + min_balance_lamports, + address_table_data, + solana_address_lookup_table_program::id(), + false, + 0, + ) + }; + bank.store_account(&address_table_pubkey, &address_table_account); + address_table_pubkey } fn add_roots_to_blockstore(&self, mut roots: Vec) { @@ -6307,6 +6403,45 @@ pub mod tests { assert_eq!(result, expected); } + #[test] + fn test_get_block_with_versioned_tx() { + let rpc = RpcHandler::start(); + + let bank = rpc.working_bank(); + // Slot hashes is necessary for processing versioned txs. + bank.set_sysvar_for_tests(&SlotHashes::default()); + // Add both legacy and version #0 transactions to the block + rpc.create_test_versioned_transactions_and_populate_blockstore(None); + + let request = create_test_request( + "getBlock", + Some(json!([ + 0u64, + {"maxSupportedTransactionVersion": 0}, + ])), + ); + let result: Option = + parse_success_result(rpc.handle_request_sync(request)); + let confirmed_block = result.unwrap(); + assert_eq!(confirmed_block.transactions.len(), 2); + assert_eq!( + confirmed_block.transactions[0].version, + Some(TransactionVersion::LEGACY) + ); + assert_eq!( + confirmed_block.transactions[1].version, + Some(TransactionVersion::Number(0)) + ); + + let request = create_test_request("getBlock", Some(json!([0u64,]))); + let response = parse_failure_response(rpc.handle_request_sync(request)); + let expected = ( + JSON_RPC_SERVER_ERROR_UNSUPPORTED_TRANSACTION_VERSION, + String::from("Transaction version (0) is not supported"), + ); + assert_eq!(response, expected); + } + #[test] fn test_get_block() { let mut rpc = RpcHandler::start(); @@ -6320,9 +6455,16 @@ pub mod tests { assert_eq!(confirmed_block.transactions.len(), 2); assert_eq!(confirmed_block.rewards, vec![]); - for EncodedTransactionWithStatusMeta { transaction, meta } in - confirmed_block.transactions.into_iter() + for EncodedTransactionWithStatusMeta { + transaction, + meta, + version, + } in confirmed_block.transactions.into_iter() { + assert_eq!( + version, None, + "requests which don't set max_supported_transaction_version shouldn't receive a version" + ); if let EncodedTransaction::Json(transaction) = transaction { if transaction.signatures[0] == confirmed_block_signatures[0].to_string() { let meta = meta.unwrap(); @@ -6357,9 +6499,16 @@ pub mod tests { assert_eq!(confirmed_block.transactions.len(), 2); assert_eq!(confirmed_block.rewards, vec![]); - for EncodedTransactionWithStatusMeta { transaction, meta } in - confirmed_block.transactions.into_iter() + for EncodedTransactionWithStatusMeta { + transaction, + meta, + version, + } in confirmed_block.transactions.into_iter() { + assert_eq!( + version, None, + "requests which don't set max_supported_transaction_version shouldn't receive a version" + ); if let EncodedTransaction::LegacyBinary(transaction) = transaction { let decoded_transaction: Transaction = deserialize(&bs58::decode(&transaction).into_vec().unwrap()).unwrap(); @@ -6414,6 +6563,7 @@ pub mod tests { transaction_details: Some(TransactionDetails::Signatures), rewards: Some(false), commitment: None, + max_supported_transaction_version: None, }, ])), ); @@ -6436,6 +6586,7 @@ pub mod tests { transaction_details: Some(TransactionDetails::None), rewards: Some(true), commitment: None, + max_supported_transaction_version: None, }, ])), ); @@ -7662,8 +7813,30 @@ pub mod tests { .to_string(), ); assert_eq!( - sanitize_transaction(unsanitary_versioned_tx).unwrap_err(), + sanitize_transaction(unsanitary_versioned_tx, &DisabledAddressLoader).unwrap_err(), expect58 ); } + + #[test] + fn test_sanitize_unsupported_transaction_version() { + 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()], + ..v0::Message::default() + }), + }; + + assert_eq!( + sanitize_transaction(versioned_tx, &DisabledAddressLoader).unwrap_err(), + Error::invalid_params( + "invalid transaction: Transaction version is unsupported".to_string(), + ) + ); + } } diff --git a/rpc/src/rpc_pubsub.rs b/rpc/src/rpc_pubsub.rs index 550eca4ea..143c36f4d 100644 --- a/rpc/src/rpc_pubsub.rs +++ b/rpc/src/rpc_pubsub.rs @@ -538,6 +538,7 @@ impl RpcSolPubSubInternal for RpcSolPubSubImpl { }, transaction_details: config.transaction_details.unwrap_or_default(), show_rewards: config.show_rewards.unwrap_or_default(), + max_supported_transaction_version: config.max_supported_transaction_version, }; self.subscribe(SubscriptionParams::Block(params)) } diff --git a/rpc/src/rpc_subscription_tracker.rs b/rpc/src/rpc_subscription_tracker.rs index c49ff6fd9..ffeffcb7b 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 max_supported_transaction_version: Option, } #[derive(Debug, Clone, PartialEq, Eq, Hash)] diff --git a/rpc/src/rpc_subscriptions.rs b/rpc/src/rpc_subscriptions.rs index 1768a3a33..1d350b2b4 100644 --- a/rpc/src/rpc_subscriptions.rs +++ b/rpc/src/rpc_subscriptions.rs @@ -40,7 +40,9 @@ use { timing::timestamp, transaction, }, - solana_transaction_status::{ConfirmedBlock, LegacyConfirmedBlock}, + solana_transaction_status::{ + BlockEncodingOptions, ConfirmedBlock, EncodeError, VersionedConfirmedBlock, + }, std::{ cell::RefCell, collections::{HashMap, VecDeque}, @@ -278,39 +280,48 @@ impl RpcNotifier { } fn filter_block_result_txs( - mut block: LegacyConfirmedBlock, + mut block: VersionedConfirmedBlock, last_modified_slot: Slot, params: &BlockSubscriptionParams, -) -> Option { +) -> Result, RpcBlockUpdateError> { block.transactions = match params.kind { BlockSubscriptionKind::All => block.transactions, BlockSubscriptionKind::MentionsAccountOrProgram(pk) => block .transactions .into_iter() - .filter(|tx_with_meta| tx_with_meta.transaction.message.account_keys.contains(&pk)) + .filter(|tx| tx.account_keys().iter().any(|key| key == &pk)) .collect(), }; if block.transactions.is_empty() { if let BlockSubscriptionKind::MentionsAccountOrProgram(_) = params.kind { - return None; + return Ok(None); } } - let block = block.configure( - params.encoding, - params.transaction_details, - params.show_rewards, - ); + let block = ConfirmedBlock::from(block) + .encode_with_options( + params.encoding, + BlockEncodingOptions { + transaction_details: params.transaction_details, + show_rewards: params.show_rewards, + max_supported_transaction_version: params.max_supported_transaction_version, + }, + ) + .map_err(|err| match err { + EncodeError::UnsupportedTransactionVersion(version) => { + RpcBlockUpdateError::UnsupportedTransactionVersion(version) + } + })?; // 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,19 +975,11 @@ impl RpcSubscriptions { error!("get_complete_block error: {}", e); RpcBlockUpdateError::BlockStoreError }) - .and_then(|versioned_block| { - ConfirmedBlock::from(versioned_block) - .into_legacy_block() - .ok_or( - RpcBlockUpdateError::UnsupportedTransactionVersion, - ) - }); + .and_then(|block| filter_block_result_txs(block, s, params)); match block_update_result { Ok(block_update) => { - if let Some(block_update) = - filter_block_result_txs(block_update, s, params) - { + if let Some(block_update) = block_update { notifier.notify( Response { context: RpcResponseContext { slot: s }, @@ -1189,7 +1192,7 @@ pub(crate) mod tests { optimistically_confirmed_bank_tracker::{ BankNotification, OptimisticallyConfirmedBank, OptimisticallyConfirmedBankTracker, }, - rpc::create_test_transactions_and_populate_blockstore, + rpc::{create_test_transaction_entries, populate_blockstore_for_tests}, rpc_pubsub::RpcSolPubSubInternal, rpc_pubsub_service, }, @@ -1385,6 +1388,7 @@ pub(crate) mod tests { encoding: Some(UiTransactionEncoding::Json), transaction_details: Some(TransactionDetails::Signatures), show_rewards: None, + max_supported_transaction_version: None, }; let params = BlockSubscriptionParams { kind: BlockSubscriptionKind::All, @@ -1392,6 +1396,7 @@ pub(crate) mod tests { encoding: config.encoding.unwrap(), transaction_details: config.transaction_details.unwrap(), show_rewards: config.show_rewards.unwrap_or_default(), + max_supported_transaction_version: config.max_supported_transaction_version, }; let sub_id = rpc.block_subscribe(filter, Some(config)).unwrap(); @@ -1406,9 +1411,12 @@ pub(crate) mod tests { let max_complete_transaction_status_slot = Arc::new(AtomicU64::new(blockstore.max_root())); bank.transfer(rent_exempt_amount, &mint_keypair, &keypair2.pubkey()) .unwrap(); - let _confirmed_block_signatures = create_test_transactions_and_populate_blockstore( - vec![&mint_keypair, &keypair1, &keypair2, &keypair3], - 0, + populate_blockstore_for_tests( + create_test_transaction_entries( + vec![&mint_keypair, &keypair1, &keypair2, &keypair3], + bank.clone(), + ) + .0, bank, blockstore.clone(), max_complete_transaction_status_slot, @@ -1421,8 +1429,16 @@ pub(crate) mod tests { let confirmed_block = ConfirmedBlock::from(blockstore.get_complete_block(slot, false).unwrap()); - let legacy_block = confirmed_block.into_legacy_block().unwrap(); - let block = legacy_block.configure(params.encoding, params.transaction_details, false); + let block = confirmed_block + .encode_with_options( + params.encoding, + BlockEncodingOptions { + transaction_details: params.transaction_details, + show_rewards: false, + max_supported_transaction_version: None, + }, + ) + .unwrap(); let expected_resp = RpcBlockUpdate { slot, block: Some(block), @@ -1492,6 +1508,7 @@ pub(crate) mod tests { encoding: Some(UiTransactionEncoding::Json), transaction_details: Some(TransactionDetails::Signatures), show_rewards: None, + max_supported_transaction_version: None, }; let params = BlockSubscriptionParams { kind: BlockSubscriptionKind::MentionsAccountOrProgram(keypair1.pubkey()), @@ -1499,6 +1516,7 @@ pub(crate) mod tests { encoding: config.encoding.unwrap(), transaction_details: config.transaction_details.unwrap(), show_rewards: config.show_rewards.unwrap_or_default(), + max_supported_transaction_version: config.max_supported_transaction_version, }; let sub_id = rpc.block_subscribe(filter, Some(config)).unwrap(); @@ -1512,9 +1530,12 @@ pub(crate) mod tests { let max_complete_transaction_status_slot = Arc::new(AtomicU64::new(blockstore.max_root())); bank.transfer(rent_exempt_amount, &mint_keypair, &keypair2.pubkey()) .unwrap(); - let _confirmed_block_signatures = create_test_transactions_and_populate_blockstore( - vec![&mint_keypair, &keypair1, &keypair2, &keypair3], - 0, + populate_blockstore_for_tests( + create_test_transaction_entries( + vec![&mint_keypair, &keypair1, &keypair2, &keypair3], + bank.clone(), + ) + .0, bank, blockstore.clone(), max_complete_transaction_status_slot, @@ -1526,17 +1547,24 @@ pub(crate) mod tests { let actual_resp = serde_json::from_str::(&actual_resp).unwrap(); // make sure it filtered out the other keypairs - let confirmed_block = + let mut confirmed_block = ConfirmedBlock::from(blockstore.get_complete_block(slot, false).unwrap()); - let mut legacy_block = confirmed_block.into_legacy_block().unwrap(); - legacy_block.transactions.retain(|tx_with_meta| { + confirmed_block.transactions.retain(|tx_with_meta| { tx_with_meta - .transaction - .message - .account_keys - .contains(&keypair1.pubkey()) + .account_keys() + .iter() + .any(|key| key == &keypair1.pubkey()) }); - let block = legacy_block.configure(params.encoding, params.transaction_details, false); + let block = confirmed_block + .encode_with_options( + params.encoding, + BlockEncodingOptions { + transaction_details: params.transaction_details, + show_rewards: false, + max_supported_transaction_version: None, + }, + ) + .unwrap(); let expected_resp = RpcBlockUpdate { slot, block: Some(block), @@ -1594,6 +1622,7 @@ pub(crate) mod tests { encoding: Some(UiTransactionEncoding::Json), transaction_details: Some(TransactionDetails::Signatures), show_rewards: None, + max_supported_transaction_version: None, }; let params = BlockSubscriptionParams { kind: BlockSubscriptionKind::All, @@ -1601,6 +1630,7 @@ pub(crate) mod tests { encoding: config.encoding.unwrap(), transaction_details: config.transaction_details.unwrap(), show_rewards: config.show_rewards.unwrap_or_default(), + max_supported_transaction_version: config.max_supported_transaction_version, }; let sub_id = rpc.block_subscribe(filter, Some(config)).unwrap(); subscriptions @@ -1614,9 +1644,12 @@ pub(crate) mod tests { let max_complete_transaction_status_slot = Arc::new(AtomicU64::new(blockstore.max_root())); bank.transfer(rent_exempt_amount, &mint_keypair, &keypair2.pubkey()) .unwrap(); - let _confirmed_block_signatures = create_test_transactions_and_populate_blockstore( - vec![&mint_keypair, &keypair1, &keypair2, &keypair3], - 0, + populate_blockstore_for_tests( + create_test_transaction_entries( + vec![&mint_keypair, &keypair1, &keypair2, &keypair3], + bank.clone(), + ) + .0, bank, blockstore.clone(), max_complete_transaction_status_slot, @@ -1634,8 +1667,16 @@ pub(crate) mod tests { let confirmed_block = ConfirmedBlock::from(blockstore.get_complete_block(slot, false).unwrap()); - let legacy_block = confirmed_block.into_legacy_block().unwrap(); - let block = legacy_block.configure(params.encoding, params.transaction_details, false); + let block = confirmed_block + .encode_with_options( + params.encoding, + BlockEncodingOptions { + transaction_details: params.transaction_details, + show_rewards: false, + max_supported_transaction_version: None, + }, + ) + .unwrap(); let expected_resp = RpcBlockUpdate { slot, block: Some(block), diff --git a/runtime/src/accounts.rs b/runtime/src/accounts.rs index b92b0440f..f384e6907 100644 --- a/runtime/src/accounts.rs +++ b/runtime/src/accounts.rs @@ -2057,14 +2057,9 @@ mod tests { meta: LookupTableMeta::default(), addresses: Cow::Owned(table_addresses.clone()), }; - let table_data = { - let mut data = vec![]; - table_state.serialize_for_tests(&mut data).unwrap(); - data - }; AccountSharedData::create( 1, - table_data, + table_state.serialize_for_tests().unwrap(), solana_address_lookup_table_program::id(), false, 0, diff --git a/sdk/program/src/message/sanitized.rs b/sdk/program/src/message/sanitized.rs index 9f28e28a1..88e1d3bf8 100644 --- a/sdk/program/src/message/sanitized.rs +++ b/sdk/program/src/message/sanitized.rs @@ -24,7 +24,7 @@ pub enum SanitizedMessage { /// Sanitized legacy message Legacy(LegacyMessage), /// Sanitized version #0 message with dynamically loaded addresses - V0(v0::LoadedMessage), + V0(v0::LoadedMessage<'static>), } #[derive(PartialEq, Debug, Error, Eq, Clone)] @@ -69,7 +69,7 @@ impl SanitizedMessage { pub fn header(&self) -> &MessageHeader { match self { Self::Legacy(message) => &message.header, - Self::V0(message) => &message.header, + Self::V0(loaded_msg) => &loaded_msg.message.header, } } @@ -93,7 +93,7 @@ impl SanitizedMessage { pub fn recent_blockhash(&self) -> &Hash { match self { Self::Legacy(message) => &message.recent_blockhash, - Self::V0(message) => &message.recent_blockhash, + Self::V0(loaded_msg) => &loaded_msg.message.recent_blockhash, } } @@ -102,7 +102,7 @@ impl SanitizedMessage { pub fn instructions(&self) -> &[CompiledInstruction] { match self { Self::Legacy(message) => &message.instructions, - Self::V0(message) => &message.instructions, + Self::V0(loaded_msg) => &loaded_msg.message.instructions, } } @@ -111,11 +111,7 @@ impl SanitizedMessage { pub fn program_instructions_iter( &self, ) -> impl Iterator { - match self { - Self::Legacy(message) => message.instructions.iter(), - Self::V0(message) => message.instructions.iter(), - } - .map(move |ix| { + self.instructions().iter().map(move |ix| { ( self.account_keys() .get(usize::from(ix.program_id_index)) @@ -347,8 +343,8 @@ mod tests { assert_eq!(legacy_message.num_readonly_accounts(), 2); - let v0_message = SanitizedMessage::V0(v0::LoadedMessage { - message: v0::Message { + let v0_message = SanitizedMessage::V0(v0::LoadedMessage::new( + v0::Message { header: MessageHeader { num_required_signatures: 2, num_readonly_signed_accounts: 1, @@ -357,11 +353,11 @@ mod tests { account_keys: vec![key0, key1, key2, key3], ..v0::Message::default() }, - loaded_addresses: LoadedAddresses { + LoadedAddresses { writable: vec![key4], readonly: vec![key5], }, - }); + )); assert_eq!(v0_message.num_readonly_accounts(), 3); } @@ -414,8 +410,8 @@ mod tests { }) .unwrap(); - let v0_message = SanitizedMessage::V0(v0::LoadedMessage { - message: v0::Message { + let v0_message = SanitizedMessage::V0(v0::LoadedMessage::new( + v0::Message { header: MessageHeader { num_required_signatures: 1, num_readonly_signed_accounts: 0, @@ -424,11 +420,11 @@ mod tests { account_keys: vec![key0, key1], ..v0::Message::default() }, - loaded_addresses: LoadedAddresses { + LoadedAddresses { writable: vec![key2], readonly: vec![program_id], }, - }); + )); for message in vec![legacy_message, v0_message] { assert_eq!( diff --git a/sdk/program/src/message/versions/v0/loaded.rs b/sdk/program/src/message/versions/v0/loaded.rs index 4684f17eb..826f0bc1d 100644 --- a/sdk/program/src/message/versions/v0/loaded.rs +++ b/sdk/program/src/message/versions/v0/loaded.rs @@ -5,23 +5,16 @@ use { pubkey::Pubkey, sysvar, }, - std::{collections::HashSet, ops::Deref}, + std::{borrow::Cow, collections::HashSet}, }; /// Combination of a version #0 message and its loaded addresses #[derive(Debug, Clone)] -pub struct LoadedMessage { +pub struct LoadedMessage<'a> { /// Message which loaded a collection of lookup table addresses - pub message: v0::Message, + pub message: Cow<'a, v0::Message>, /// Addresses loaded with on-chain address lookup tables - pub loaded_addresses: LoadedAddresses, -} - -impl Deref for LoadedMessage { - type Target = v0::Message; - fn deref(&self) -> &Self::Target { - &self.message - } + pub loaded_addresses: Cow<'a, LoadedAddresses>, } /// Collection of addresses loaded from on-chain lookup tables, split @@ -59,10 +52,29 @@ impl LoadedAddresses { } } -impl LoadedMessage { - /// Returns the list of account keys that are loaded for this message. +impl<'a> LoadedMessage<'a> { + pub fn new(message: v0::Message, loaded_addresses: LoadedAddresses) -> Self { + Self { + message: Cow::Owned(message), + loaded_addresses: Cow::Owned(loaded_addresses), + } + } + + pub fn new_borrowed(message: &'a v0::Message, loaded_addresses: &'a LoadedAddresses) -> Self { + Self { + message: Cow::Borrowed(message), + loaded_addresses: Cow::Borrowed(loaded_addresses), + } + } + + /// Returns the full list of static and dynamic account keys that are loaded for this message. pub fn account_keys(&self) -> AccountKeys { - AccountKeys::new(&self.account_keys, Some(&self.loaded_addresses)) + AccountKeys::new(&self.message.account_keys, Some(&self.loaded_addresses)) + } + + /// Returns the list of static account keys that are loaded for this message. + pub fn static_account_keys(&self) -> &[Pubkey] { + &self.message.account_keys } /// Returns true if any account keys are duplicates @@ -107,6 +119,10 @@ impl LoadedMessage { false } + pub fn is_signer(&self, i: usize) -> bool { + i < self.message.header.num_required_signatures as usize + } + /// Returns true if the account at the specified index is called as a program by an instruction pub fn is_key_called_as_program(&self, key_index: usize) -> bool { if let Ok(key_index) = u8::try_from(key_index) { @@ -135,7 +151,7 @@ mod tests { itertools::Itertools, }; - fn check_test_loaded_message() -> (LoadedMessage, [Pubkey; 6]) { + fn check_test_loaded_message() -> (LoadedMessage<'static>, [Pubkey; 6]) { let key0 = Pubkey::new_unique(); let key1 = Pubkey::new_unique(); let key2 = Pubkey::new_unique(); @@ -143,8 +159,8 @@ mod tests { let key4 = Pubkey::new_unique(); let key5 = Pubkey::new_unique(); - let message = LoadedMessage { - message: v0::Message { + let message = LoadedMessage::new( + v0::Message { header: MessageHeader { num_required_signatures: 2, num_readonly_signed_accounts: 1, @@ -153,11 +169,11 @@ mod tests { account_keys: vec![key0, key1, key2, key3], ..v0::Message::default() }, - loaded_addresses: LoadedAddresses { + LoadedAddresses { writable: vec![key4], readonly: vec![key5], }, - }; + ); (message, [key0, key1, key2, key3, key4, key5]) } @@ -171,15 +187,17 @@ mod tests { #[test] fn test_has_duplicates_with_dupe_keys() { - let create_message_with_dupe_keys = |mut keys: Vec| LoadedMessage { - message: v0::Message { - account_keys: keys.split_off(2), - ..v0::Message::default() - }, - loaded_addresses: LoadedAddresses { - writable: keys.split_off(2), - readonly: keys, - }, + let create_message_with_dupe_keys = |mut keys: Vec| { + LoadedMessage::new( + v0::Message { + account_keys: keys.split_off(2), + ..v0::Message::default() + }, + LoadedAddresses { + writable: keys.split_off(2), + readonly: keys, + }, + ) }; let key0 = Pubkey::new_unique(); @@ -212,11 +230,11 @@ mod tests { fn test_is_writable() { let mut message = check_test_loaded_message().0; - message.message.account_keys[0] = sysvar::clock::id(); + message.message.to_mut().account_keys[0] = sysvar::clock::id(); assert!(message.is_writable_index(0)); assert!(!message.is_writable(0)); - message.message.account_keys[0] = system_program::id(); + message.message.to_mut().account_keys[0] = system_program::id(); assert!(message.is_writable_index(0)); assert!(!message.is_writable(0)); } @@ -226,8 +244,8 @@ mod tests { let key0 = Pubkey::new_unique(); let key1 = Pubkey::new_unique(); let key2 = Pubkey::new_unique(); - let message = LoadedMessage { - message: v0::Message { + let message = LoadedMessage::new( + v0::Message { header: MessageHeader { num_required_signatures: 1, num_readonly_signed_accounts: 0, @@ -241,11 +259,11 @@ mod tests { }], ..v0::Message::default() }, - loaded_addresses: LoadedAddresses { + LoadedAddresses { writable: vec![key1, key2], readonly: vec![], }, - }; + ); assert!(message.is_writable_index(2)); assert!(!message.is_writable(2)); diff --git a/sdk/src/transaction/sanitized.rs b/sdk/src/transaction/sanitized.rs index 8b7cb2994..a3f4fbce1 100644 --- a/sdk/src/transaction/sanitized.rs +++ b/sdk/src/transaction/sanitized.rs @@ -65,10 +65,11 @@ impl SanitizedTransaction { let signatures = tx.signatures; let message = match tx.message { VersionedMessage::Legacy(message) => SanitizedMessage::Legacy(message), - VersionedMessage::V0(message) => SanitizedMessage::V0(v0::LoadedMessage { - loaded_addresses: address_loader.load_addresses(&message.address_table_lookups)?, - message, - }), + VersionedMessage::V0(message) => { + let loaded_addresses = + address_loader.load_addresses(&message.address_table_lookups)?; + SanitizedMessage::V0(v0::LoadedMessage::new(message, loaded_addresses)) + } }; let is_simple_vote_tx = is_simple_vote_tx.unwrap_or_else(|| { @@ -139,7 +140,7 @@ impl SanitizedTransaction { match &self.message { SanitizedMessage::V0(sanitized_msg) => VersionedTransaction { signatures, - message: VersionedMessage::V0(sanitized_msg.message.clone()), + message: VersionedMessage::V0(v0::Message::clone(&sanitized_msg.message)), }, SanitizedMessage::Legacy(message) => VersionedTransaction { signatures, @@ -191,7 +192,7 @@ impl SanitizedTransaction { pub fn get_loaded_addresses(&self) -> LoadedAddresses { match &self.message { SanitizedMessage::Legacy(_) => LoadedAddresses::default(), - SanitizedMessage::V0(message) => message.loaded_addresses.clone(), + SanitizedMessage::V0(message) => LoadedAddresses::clone(&message.loaded_addresses), } } @@ -204,7 +205,7 @@ impl SanitizedTransaction { fn message_data(&self) -> Vec { match &self.message { SanitizedMessage::Legacy(message) => message.serialize(), - SanitizedMessage::V0(message) => message.serialize(), + SanitizedMessage::V0(loaded_msg) => loaded_msg.message.serialize(), } } diff --git a/sdk/src/transaction/versioned.rs b/sdk/src/transaction/versioned.rs index 2e36f21dd..650f0810e 100644 --- a/sdk/src/transaction/versioned.rs +++ b/sdk/src/transaction/versioned.rs @@ -17,6 +17,24 @@ use { std::cmp::Ordering, }; +/// Type that serializes to the string "legacy" +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub enum Legacy { + Legacy, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase", untagged)] +pub enum TransactionVersion { + Legacy(Legacy), + Number(u8), +} + +impl TransactionVersion { + pub const LEGACY: Self = Self::Legacy(Legacy::Legacy); +} + // NOTE: Serialization-related changes must be paired with the direct read at sigverify. /// An atomic transaction #[derive(Debug, PartialEq, Default, Eq, Clone, Serialize, Deserialize, AbiExample)] @@ -93,6 +111,14 @@ impl VersionedTransaction { }) } + /// Returns the version of the transaction + pub fn version(&self) -> TransactionVersion { + match self.message { + VersionedMessage::Legacy(_) => TransactionVersion::LEGACY, + VersionedMessage::V0(_) => TransactionVersion::Number(0), + } + } + /// Returns a legacy transaction if the transaction message is legacy. pub fn into_legacy_transaction(self) -> Option { match self.message { diff --git a/transaction-status/src/lib.rs b/transaction-status/src/lib.rs index 52a0e6e3f..865b69b07 100644 --- a/transaction-status/src/lib.rs +++ b/transaction-status/src/lib.rs @@ -18,7 +18,7 @@ pub mod token_balances; pub use {crate::extract_memos::extract_and_fmt_memos, solana_runtime::bank::RewardType}; use { crate::{ - parse_accounts::{parse_accounts, ParsedAccount}, + parse_accounts::{parse_accounts, parse_static_accounts, ParsedAccount}, parse_instruction::{parse, ParsedInstruction}, }, solana_account_decoder::parse_token::UiTokenAmount, @@ -26,18 +26,48 @@ use { clock::{Slot, UnixTimestamp}, commitment_config::CommitmentConfig, instruction::CompiledInstruction, - message::{v0::LoadedAddresses, AccountKeys, Message, MessageHeader}, + message::{ + v0::{self, LoadedAddresses, LoadedMessage, MessageAddressTableLookup}, + AccountKeys, Message, MessageHeader, VersionedMessage, + }, + pubkey::Pubkey, sanitize::Sanitize, signature::Signature, - transaction::{Result, Transaction, TransactionError, VersionedTransaction}, + transaction::{ + Result as TransactionResult, Transaction, TransactionError, TransactionVersion, + VersionedTransaction, + }, }, std::fmt, + thiserror::Error, }; +pub struct BlockEncodingOptions { + pub transaction_details: TransactionDetails, + pub show_rewards: bool, + pub max_supported_transaction_version: Option, +} + +#[derive(Error, Debug, PartialEq, Eq, Clone)] +pub enum EncodeError { + #[error("Encoding does not support transaction version {0}")] + UnsupportedTransactionVersion(u8), +} + /// Represents types that can be encoded into one of several encoding formats pub trait Encodable { type Encoded; - fn encode(self, encoding: UiTransactionEncoding) -> Self::Encoded; + 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_with_meta( + &self, + encoding: UiTransactionEncoding, + meta: &TransactionStatusMeta, + ) -> Self::Encoded; } #[derive(Serialize, Deserialize, Clone, Copy, Debug, Eq, Hash, PartialEq)] @@ -160,14 +190,13 @@ pub struct UiInnerInstructions { } impl UiInnerInstructions { - fn parse(inner_instructions: InnerInstructions, message: &Message) -> Self { - let account_keys = AccountKeys::new(&message.account_keys, None); + fn parse(inner_instructions: InnerInstructions, account_keys: &AccountKeys) -> Self { Self { index: inner_instructions.index, instructions: inner_instructions .instructions .iter() - .map(|ix| UiInstruction::parse(ix, &account_keys)) + .map(|ix| UiInstruction::parse(ix, account_keys)) .collect(), } } @@ -221,7 +250,7 @@ impl From for UiTransactionTokenBalance { #[derive(Clone, Debug, PartialEq)] pub struct TransactionStatusMeta { - pub status: Result<()>, + pub status: TransactionResult<()>, pub fee: u64, pub pre_balances: Vec, pub post_balances: Vec, @@ -255,7 +284,7 @@ impl Default for TransactionStatusMeta { #[serde(rename_all = "camelCase")] pub struct UiTransactionStatusMeta { pub err: Option, - pub status: Result<()>, // This field is deprecated. See https://github.com/solana-labs/solana/issues/9302 + pub status: TransactionResult<()>, // This field is deprecated. See https://github.com/solana-labs/solana/issues/9302 pub fee: u64, pub pre_balances: Vec, pub post_balances: Vec, @@ -264,10 +293,38 @@ pub struct UiTransactionStatusMeta { pub pre_token_balances: Option>, pub post_token_balances: Option>, pub rewards: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub loaded_addresses: Option, +} + +/// A duplicate representation of LoadedAddresses +#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UiLoadedAddresses { + pub writable: Vec, + pub readonly: Vec, +} + +impl From<&LoadedAddresses> for UiLoadedAddresses { + fn from(loaded_addresses: &LoadedAddresses) -> Self { + Self { + writable: loaded_addresses + .writable + .iter() + .map(ToString::to_string) + .collect(), + readonly: loaded_addresses + .readonly + .iter() + .map(ToString::to_string) + .collect(), + } + } } impl UiTransactionStatusMeta { - fn parse(meta: TransactionStatusMeta, message: &Message) -> Self { + fn parse(meta: TransactionStatusMeta, static_keys: &[Pubkey]) -> Self { + let account_keys = AccountKeys::new(static_keys, Some(&meta.loaded_addresses)); Self { err: meta.status.clone().err(), status: meta.status, @@ -276,7 +333,7 @@ impl UiTransactionStatusMeta { post_balances: meta.post_balances, inner_instructions: meta.inner_instructions.map(|ixs| { ixs.into_iter() - .map(|ix| UiInnerInstructions::parse(ix, message)) + .map(|ix| UiInnerInstructions::parse(ix, &account_keys)) .collect() }), log_messages: meta.log_messages, @@ -287,6 +344,7 @@ impl UiTransactionStatusMeta { .post_token_balances .map(|balance| balance.into_iter().map(Into::into).collect()), rewards: meta.rewards, + loaded_addresses: Some(UiLoadedAddresses::from(&meta.loaded_addresses)), } } } @@ -310,6 +368,7 @@ impl From for UiTransactionStatusMeta { .post_token_balances .map(|balance| balance.into_iter().map(Into::into).collect()), rewards: meta.rewards, + loaded_addresses: Some(UiLoadedAddresses::from(&meta.loaded_addresses)), } } } @@ -326,8 +385,8 @@ pub enum TransactionConfirmationStatus { #[serde(rename_all = "camelCase")] pub struct TransactionStatus { pub slot: Slot, - pub confirmations: Option, // None = rooted - pub status: Result<()>, // legacy field + pub confirmations: Option, // None = rooted + pub status: TransactionResult<()>, // legacy field pub err: Option, pub confirmation_status: Option, } @@ -462,39 +521,21 @@ impl From for ConfirmedBlock { } } -impl Encodable for LegacyConfirmedBlock { - 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 LegacyConfirmedBlock { - pub fn configure( +impl ConfirmedBlock { + pub fn encode_with_options( self, encoding: UiTransactionEncoding, - transaction_details: TransactionDetails, - show_rewards: bool, - ) -> UiConfirmedBlock { - let (transactions, signatures) = match transaction_details { + options: BlockEncodingOptions, + ) -> Result { + let (transactions, signatures) = match options.transaction_details { TransactionDetails::Full => ( Some( self.transactions .into_iter() - .map(|tx_with_meta| tx_with_meta.encode(encoding)) - .collect(), + .map(|tx_with_meta| { + tx_with_meta.encode(encoding, options.max_supported_transaction_version) + }) + .collect::, _>>()?, ), None, ), @@ -503,26 +544,26 @@ impl LegacyConfirmedBlock { Some( self.transactions .into_iter() - .map(|tx| tx.transaction.signatures[0].to_string()) + .map(|tx_with_meta| tx_with_meta.transaction_signature().to_string()) .collect(), ), ), TransactionDetails::None => (None, None), }; - UiConfirmedBlock { + Ok(UiConfirmedBlock { previous_blockhash: self.previous_blockhash, blockhash: self.blockhash, parent_slot: self.parent_slot, transactions, signatures, - rewards: if show_rewards { + rewards: if options.show_rewards { Some(self.rewards) } else { None }, block_time: self.block_time, block_height: self.block_height, - } + }) } } @@ -568,21 +609,6 @@ pub struct UiConfirmedBlock { pub block_height: Option, } -impl From for UiConfirmedBlock { - fn from(block: EncodedConfirmedBlock) -> Self { - Self { - previous_blockhash: block.previous_blockhash, - blockhash: block.blockhash, - parent_slot: block.parent_slot, - transactions: Some(block.transactions), - signatures: None, - rewards: Some(block.rewards), - block_time: block.block_time, - block_height: block.block_height, - } - } -} - #[derive(Clone, Debug, PartialEq)] #[allow(clippy::large_enum_variant)] pub enum TransactionWithStatusMeta { @@ -613,6 +639,23 @@ impl TransactionWithStatusMeta { } } + pub fn encode( + self, + encoding: UiTransactionEncoding, + max_supported_transaction_version: Option, + ) -> Result { + match self { + Self::MissingMetadata(ref transaction) => Ok(EncodedTransactionWithStatusMeta { + version: None, + transaction: transaction.encode(encoding), + meta: None, + }), + Self::Complete(tx_with_meta) => { + tx_with_meta.encode(encoding, max_supported_transaction_version) + } + } + } + pub fn into_legacy_transaction_with_meta(self) -> Option { match self { TransactionWithStatusMeta::MissingMetadata(transaction) => { @@ -626,9 +669,53 @@ impl TransactionWithStatusMeta { } } } + + pub fn account_keys(&self) -> AccountKeys { + match self { + Self::MissingMetadata(tx) => AccountKeys::new(&tx.message.account_keys, None), + Self::Complete(tx_with_meta) => tx_with_meta.account_keys(), + } + } } impl VersionedTransactionWithStatusMeta { + pub fn encode( + self, + encoding: UiTransactionEncoding, + max_supported_transaction_version: Option, + ) -> Result { + let version = match ( + max_supported_transaction_version, + self.transaction.version(), + ) { + // Set to none because old clients can't handle this field + (None, TransactionVersion::LEGACY) => Ok(None), + (None, TransactionVersion::Number(version)) => { + Err(EncodeError::UnsupportedTransactionVersion(version)) + } + (Some(_), TransactionVersion::LEGACY) => Ok(Some(TransactionVersion::LEGACY)), + (Some(max_version), TransactionVersion::Number(version)) => { + if version <= max_version { + Ok(Some(TransactionVersion::Number(version))) + } else { + Err(EncodeError::UnsupportedTransactionVersion(version)) + } + } + }?; + + Ok(EncodedTransactionWithStatusMeta { + transaction: self.transaction.encode_with_meta(encoding, &self.meta), + meta: Some(match encoding { + UiTransactionEncoding::JsonParsed => UiTransactionStatusMeta::parse( + self.meta, + self.transaction.message.static_account_keys(), + ), + _ => UiTransactionStatusMeta::from(self.meta), + }), + version, + }) + } + pub fn account_keys(&self) -> AccountKeys { AccountKeys::new( self.transaction.message.static_account_keys(), @@ -636,7 +723,7 @@ impl VersionedTransactionWithStatusMeta { ) } - pub fn into_legacy_transaction_with_meta(self) -> Option { + fn into_legacy_transaction_with_meta(self) -> Option { Some(LegacyTransactionWithStatusMeta { transaction: self.transaction.into_legacy_transaction()?, meta: Some(self.meta), @@ -644,26 +731,13 @@ impl VersionedTransactionWithStatusMeta { } } -impl Encodable for LegacyTransactionWithStatusMeta { - type Encoded = EncodedTransactionWithStatusMeta; - fn encode(self, encoding: UiTransactionEncoding) -> Self::Encoded { - Self::Encoded { - transaction: self.transaction.encode(encoding), - meta: self.meta.map(|meta| match encoding { - UiTransactionEncoding::JsonParsed => { - UiTransactionStatusMeta::parse(meta, &self.transaction.message) - } - _ => UiTransactionStatusMeta::from(meta), - }), - } - } -} - #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct EncodedTransactionWithStatusMeta { pub transaction: EncodedTransaction, pub meta: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub version: Option, } #[derive(Debug, Clone, PartialEq)] @@ -686,17 +760,6 @@ pub struct LegacyConfirmedTransactionWithStatusMeta { pub block_time: Option, } -impl Encodable for LegacyConfirmedTransactionWithStatusMeta { - type Encoded = EncodedConfirmedTransactionWithStatusMeta; - fn encode(self, encoding: UiTransactionEncoding) -> Self::Encoded { - Self::Encoded { - slot: self.slot, - transaction: self.tx_with_meta.encode(encoding), - block_time: self.block_time, - } - } -} - impl ConfirmedTransactionWithStatusMeta { /// Downgrades a versioned confirmed transaction into a legacy /// confirmed transaction if it contains a legacy transaction. @@ -709,6 +772,20 @@ impl ConfirmedTransactionWithStatusMeta { slot: self.slot, }) } + + pub fn encode( + self, + encoding: UiTransactionEncoding, + max_supported_transaction_version: Option, + ) -> Result { + Ok(EncodedConfirmedTransactionWithStatusMeta { + slot: self.slot, + transaction: self + .tx_with_meta + .encode(encoding, max_supported_transaction_version)?, + block_time: self.block_time, + }) + } } #[derive(Debug, PartialEq, Serialize, Deserialize)] @@ -728,9 +805,41 @@ pub enum EncodedTransaction { Json(UiTransaction), } -impl Encodable for &Transaction { +impl EncodableWithMeta for VersionedTransaction { type Encoded = EncodedTransaction; - fn encode(self, encoding: UiTransactionEncoding) -> Self::Encoded { + fn encode_with_meta( + &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: match &self.message { + VersionedMessage::Legacy(message) => message.encode(encoding), + VersionedMessage::V0(message) => message.encode_with_meta(encoding, meta), + }, + }) + } + } + } +} + +impl Encodable for Transaction { + type Encoded = EncodedTransaction; + fn encode(&self, encoding: UiTransactionEncoding) -> Self::Encoded { match encoding { UiTransactionEncoding::Binary => EncodedTransaction::LegacyBinary( bs58::encode(bincode::serialize(self).unwrap()).into_string(), @@ -793,9 +902,9 @@ pub enum UiMessage { Raw(UiRawMessage), } -impl Encodable for &Message { +impl Encodable for Message { type Encoded = UiMessage; - fn encode(self, encoding: UiTransactionEncoding) -> Self::Encoded { + fn encode(&self, encoding: UiTransactionEncoding) -> Self::Encoded { if encoding == UiTransactionEncoding::JsonParsed { let account_keys = AccountKeys::new(&self.account_keys, None); UiMessage::Parsed(UiParsedMessage { @@ -806,6 +915,7 @@ impl Encodable for &Message { .iter() .map(|instruction| UiInstruction::parse(instruction, &account_keys)) .collect(), + address_table_lookups: None, }) } else { UiMessage::Raw(UiRawMessage { @@ -813,6 +923,43 @@ 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: None, + }) + } + } +} + +impl EncodableWithMeta for v0::Message { + type Encoded = UiMessage; + fn encode_with_meta( + &self, + encoding: UiTransactionEncoding, + meta: &TransactionStatusMeta, + ) -> Self::Encoded { + if encoding == UiTransactionEncoding::JsonParsed { + let account_keys = AccountKeys::new(&self.account_keys, Some(&meta.loaded_addresses)); + let loaded_message = LoadedMessage::new_borrowed(self, &meta.loaded_addresses); + UiMessage::Parsed(UiParsedMessage { + account_keys: parse_static_accounts(&loaded_message), + recent_blockhash: self.recent_blockhash.to_string(), + instructions: self + .instructions + .iter() + .map(|instruction| UiInstruction::parse(instruction, &account_keys)) + .collect(), + address_table_lookups: Some( + self.address_table_lookups.iter().map(Into::into).collect(), + ), + }) + } else { + UiMessage::Raw(UiRawMessage { + header: self.header, + 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: Some( + self.address_table_lookups.iter().map(Into::into).collect(), + ), }) } } @@ -826,6 +973,27 @@ pub struct UiRawMessage { pub account_keys: Vec, pub recent_blockhash: String, pub instructions: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + 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, +} + +impl From<&MessageAddressTableLookup> for UiAddressTableLookup { + fn from(lookup: &MessageAddressTableLookup) -> Self { + Self { + account_key: lookup.account_key.to_string(), + writable_indexes: lookup.writable_indexes.clone(), + readonly_indexes: lookup.readonly_indexes.clone(), + } + } } /// A duplicate representation of a Message, in parsed format, for pretty JSON serialization @@ -835,6 +1003,7 @@ pub struct UiParsedMessage { pub account_keys: Vec, pub recent_blockhash: String, pub instructions: Vec, + pub address_table_lookups: Option>, } // A serialized `Vec` is stored in the `tx-by-addr` table. The row keys are diff --git a/transaction-status/src/parse_accounts.rs b/transaction-status/src/parse_accounts.rs index 35d8d4c9e..300e7fec0 100644 --- a/transaction-status/src/parse_accounts.rs +++ b/transaction-status/src/parse_accounts.rs @@ -1,4 +1,4 @@ -use solana_sdk::message::Message; +use solana_sdk::message::{v0::LoadedMessage, Message}; #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] #[serde(rename_all = "camelCase")] @@ -20,16 +20,34 @@ pub fn parse_accounts(message: &Message) -> Vec { accounts } +pub fn parse_static_accounts(message: &LoadedMessage) -> Vec { + let mut accounts: Vec = vec![]; + for (i, account_key) in message.static_account_keys().iter().enumerate() { + accounts.push(ParsedAccount { + pubkey: account_key.to_string(), + writable: message.is_writable(i), + signer: message.is_signer(i), + }); + } + accounts +} + #[cfg(test)] mod test { - use {super::*, solana_sdk::message::MessageHeader}; + use { + super::*, + solana_sdk::{ + message::{v0, v0::LoadedAddresses, MessageHeader}, + pubkey::Pubkey, + }, + }; #[test] fn test_parse_accounts() { - let pubkey0 = solana_sdk::pubkey::new_rand(); - let pubkey1 = solana_sdk::pubkey::new_rand(); - let pubkey2 = solana_sdk::pubkey::new_rand(); - let pubkey3 = solana_sdk::pubkey::new_rand(); + let pubkey0 = Pubkey::new_unique(); + let pubkey1 = Pubkey::new_unique(); + let pubkey2 = Pubkey::new_unique(); + let pubkey3 = Pubkey::new_unique(); let message = Message { header: MessageHeader { num_required_signatures: 2, @@ -66,4 +84,53 @@ mod test { ] ); } + + #[test] + fn test_parse_static_accounts() { + let pubkey0 = Pubkey::new_unique(); + let pubkey1 = Pubkey::new_unique(); + let pubkey2 = Pubkey::new_unique(); + let pubkey3 = Pubkey::new_unique(); + let message = LoadedMessage::new( + v0::Message { + header: MessageHeader { + num_required_signatures: 2, + num_readonly_signed_accounts: 1, + num_readonly_unsigned_accounts: 1, + }, + account_keys: vec![pubkey0, pubkey1, pubkey2, pubkey3], + ..v0::Message::default() + }, + LoadedAddresses { + writable: vec![Pubkey::new_unique()], + readonly: vec![Pubkey::new_unique()], + }, + ); + + assert_eq!( + parse_static_accounts(&message), + vec![ + ParsedAccount { + pubkey: pubkey0.to_string(), + writable: true, + signer: true, + }, + ParsedAccount { + pubkey: pubkey1.to_string(), + writable: false, + signer: true, + }, + ParsedAccount { + pubkey: pubkey2.to_string(), + writable: true, + signer: false, + }, + ParsedAccount { + pubkey: pubkey3.to_string(), + writable: false, + signer: false, + }, + ] + ); + } }