diff --git a/Cargo.lock b/Cargo.lock index 5d5607b7375..5f1de28c9e0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10337,6 +10337,7 @@ dependencies = [ "solana-inflation", "solana-pubkey 4.0.0", "solana-reward-info", + "solana-signature", "solana-transaction", "solana-transaction-error", "solana-transaction-status-client-types", diff --git a/cli/src/cluster_query.rs b/cli/src/cluster_query.rs index 0ba1b89c157..a41ee125549 100644 --- a/cli/src/cluster_query.rs +++ b/cli/src/cluster_query.rs @@ -2299,6 +2299,7 @@ pub async fn process_transaction_history( block_time, slot, transaction: transaction_with_meta, + .. } = confirmed_transaction; let decoded_transaction = diff --git a/cli/src/wallet.rs b/cli/src/wallet.rs index 0a6918bcef5..d23b8017337 100644 --- a/cli/src/wallet.rs +++ b/cli/src/wallet.rs @@ -777,6 +777,7 @@ pub async fn process_confirm( block_time, slot, transaction: transaction_with_meta, + .. } = confirmed_transaction; let decoded_transaction = diff --git a/ledger/src/blockstore.rs b/ledger/src/blockstore.rs index d68ca468d61..d71da732db0 100644 --- a/ledger/src/blockstore.rs +++ b/ledger/src/blockstore.rs @@ -3166,7 +3166,7 @@ impl Blockstore { if let Some((slot, meta)) = self.get_transaction_status(signature, confirmed_unrooted_slots)? { - let transaction = self + let (transaction, index) = self .find_transaction_in_slot(slot, signature)? .ok_or(BlockstoreError::TransactionStatusSlotMismatch)?; // Should not happen @@ -3177,32 +3177,40 @@ impl Blockstore { VersionedTransactionWithStatusMeta { transaction, meta }, ), block_time, + index, })) } else { Ok(None) } } + /// Finds a transaction by signature in the given slot and returns it along with its index. + /// + /// The index represents the transaction's 0-based position in the flattened list of all + /// transactions across all entries in this slot. This matches the `transaction_index` + /// stored in `AddressSignatures` when `write_transaction_status` is called during block + /// processing. fn find_transaction_in_slot( &self, slot: Slot, signature: Signature, - ) -> Result> { + ) -> Result> { let slot_entries = self.get_slot_entries(slot, 0)?; Ok(slot_entries - .iter() - .cloned() + .into_iter() .flat_map(|entry| entry.transactions) - .map(|transaction| { + .enumerate() + .map(|(index, transaction)| { if let Err(err) = transaction.sanitize() { warn!( "Blockstore::find_transaction_in_slot sanitize failed: {err:?}, slot: \ {slot:?}, {transaction:?}", ); } - transaction + (index, transaction) }) - .find(|transaction| transaction.signatures[0] == signature)) + .find(|(_, transaction)| transaction.signatures[0] == signature) + .map(|(index, transaction)| (transaction, index as u32))) } // DEPRECATED and decommissioned @@ -3222,9 +3230,9 @@ impl Blockstore { &self, pubkey: Pubkey, slot: Slot, - ) -> Result> { + ) -> Result> { let (lock, lowest_available_slot) = self.ensure_lowest_cleanup_slot(); - let mut signatures: Vec<(Slot, Signature)> = vec![]; + let mut signatures: Vec<(Slot, Signature, u32)> = vec![]; if slot < lowest_available_slot { return Ok(signatures); } @@ -3239,11 +3247,11 @@ impl Blockstore { ), IteratorDirection::Forward, ))?; - for ((address, transaction_slot, _transaction_index, signature), _) in index_iterator { + for ((address, transaction_slot, transaction_index, signature), _) in index_iterator { if transaction_slot > slot || address != pubkey { break; } - signatures.push((slot, signature)); + signatures.push((transaction_slot, signature, transaction_index)); } drop(lock); Ok(signatures) @@ -3346,7 +3354,7 @@ impl Blockstore { get_until_slot_timer.stop(); // Fetch the list of signatures that affect the given address - let mut address_signatures = vec![]; + let mut address_signatures: Vec<(Slot, Signature, u32)> = vec![]; // Get signatures in `slot` let mut get_initial_slot_timer = Measure::start("get_initial_slot_timer"); @@ -3356,7 +3364,7 @@ impl Blockstore { address_signatures.extend( signatures .into_iter() - .filter(|(_, signature)| !excluded_signatures.contains(signature)), + .filter(|(_, signature, _)| !excluded_signatures.contains(signature)), ) } else { address_signatures.append(&mut signatures); @@ -3378,13 +3386,13 @@ impl Blockstore { // Iterate until limit is reached while address_signatures.len() < limit { - if let Some(((key_address, slot, _transaction_index, signature), _)) = iterator.next() { + if let Some(((key_address, slot, transaction_index, signature), _)) = iterator.next() { if slot < lowest_slot { break; } if key_address == address { if self.is_root(slot) || confirmed_unrooted_slots.contains(&slot) { - address_signatures.push((slot, signature)); + address_signatures.push((slot, signature, transaction_index)); } continue; } @@ -3395,13 +3403,13 @@ impl Blockstore { let address_signatures_iter = address_signatures .into_iter() - .filter(|(_, signature)| !until_excluded_signatures.contains(signature)) + .filter(|(_, signature, _)| !until_excluded_signatures.contains(signature)) .take(limit); // Fill in the status information for each found transaction let mut get_status_info_timer = Measure::start("get_status_info_timer"); let mut infos = vec![]; - for (slot, signature) in address_signatures_iter { + for (slot, signature, index) in address_signatures_iter { let transaction_status = self.get_transaction_status(signature, &confirmed_unrooted_slots)?; let err = transaction_status.and_then(|(_slot, status)| status.status.err()); @@ -3413,6 +3421,7 @@ impl Blockstore { err, memo, block_time, + index, }); } get_status_info_timer.stop(); @@ -8868,14 +8877,15 @@ pub mod tests { }) .collect(); - for tx_with_meta in expected_transactions.clone() { + for (index, tx_with_meta) in expected_transactions.clone().into_iter().enumerate() { let signature = tx_with_meta.transaction.signatures[0]; assert_eq!( blockstore.get_rooted_transaction(signature).unwrap(), Some(ConfirmedTransactionWithStatusMeta { slot, tx_with_meta: TransactionWithStatusMeta::Complete(tx_with_meta.clone()), - block_time: None + block_time: None, + index: index as u32, }) ); assert_eq!( @@ -8885,7 +8895,8 @@ pub mod tests { Some(ConfirmedTransactionWithStatusMeta { slot, tx_with_meta: TransactionWithStatusMeta::Complete(tx_with_meta), - block_time: None + block_time: None, + index: index as u32, }) ); } @@ -8991,7 +9002,7 @@ pub mod tests { }) .collect(); - for tx_with_meta in expected_transactions.clone() { + for (index, tx_with_meta) in expected_transactions.clone().into_iter().enumerate() { let signature = tx_with_meta.transaction.signatures[0]; assert_eq!( blockstore @@ -9000,7 +9011,8 @@ pub mod tests { Some(ConfirmedTransactionWithStatusMeta { slot, tx_with_meta: TransactionWithStatusMeta::Complete(tx_with_meta), - block_time: None + block_time: None, + index: index as u32, }) ); assert_eq!(blockstore.get_rooted_transaction(signature).unwrap(), None); @@ -9135,25 +9147,28 @@ pub mod tests { let slot1_signatures = blockstore .find_address_signatures_for_slot(address0, 1) .unwrap(); - for (i, (slot, signature)) in slot1_signatures.iter().enumerate() { + for (i, (slot, signature, index)) in slot1_signatures.iter().enumerate() { assert_eq!(*slot, slot1); assert_eq!(*signature, Signature::from([i as u8 + 1; 64])); + assert_eq!(*index, (i + 1) as u32); } let slot2_signatures = blockstore .find_address_signatures_for_slot(address0, 2) .unwrap(); - for (i, (slot, signature)) in slot2_signatures.iter().enumerate() { + for (i, (slot, signature, index)) in slot2_signatures.iter().enumerate() { assert_eq!(*slot, slot2); assert_eq!(*signature, Signature::from([i as u8 + 5; 64])); + assert_eq!(*index, (i + 5) as u32); } let slot3_signatures = blockstore .find_address_signatures_for_slot(address0, 3) .unwrap(); - for (i, (slot, signature)) in slot3_signatures.iter().enumerate() { + for (i, (slot, signature, index)) in slot3_signatures.iter().enumerate() { assert_eq!(*slot, slot3); assert_eq!(*signature, Signature::from([i as u8 + 9; 64])); + assert_eq!(*index, (i + 9) as u32); } } diff --git a/rpc-client-types/Cargo.toml b/rpc-client-types/Cargo.toml index 7ab3f1c139f..ab0e58bc901 100644 --- a/rpc-client-types/Cargo.toml +++ b/rpc-client-types/Cargo.toml @@ -39,3 +39,4 @@ thiserror = { workspace = true } [dev-dependencies] const_format = { workspace = true } solana-pubkey = { workspace = true, features = ["rand"] } +solana-signature = { workspace = true } diff --git a/rpc-client-types/src/response.rs b/rpc-client-types/src/response.rs index 60553448f5a..1a461064565 100644 --- a/rpc-client-types/src/response.rs +++ b/rpc-client-types/src/response.rs @@ -496,6 +496,8 @@ pub struct RpcConfirmedTransactionStatusWithSignature { pub memo: Option, pub block_time: Option, pub confirmation_status: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub transaction_index: Option, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] @@ -545,6 +547,7 @@ impl From for RpcConfirmedTransactionSt err, memo, block_time, + index, } = value; Self { signature: signature.to_string(), @@ -553,6 +556,7 @@ impl From for RpcConfirmedTransactionSt memo, block_time, confirmation_status: None, + transaction_index: Some(index), } } } @@ -633,4 +637,27 @@ pub mod tests { assert_eq!(actual, expected); } + + #[test] + fn test_rpc_confirmed_transaction_status_with_signature_from() { + let signature = solana_signature::Signature::from([1u8; 64]); + let input = ConfirmedTransactionStatusWithSignature { + signature, + slot: 123, + err: None, + memo: Some("test memo".to_string()), + block_time: Some(1234567890), + index: 42, + }; + + let result: RpcConfirmedTransactionStatusWithSignature = input.into(); + + assert_eq!(result.signature, signature.to_string()); + assert_eq!(result.slot, 123); + assert_eq!(result.err, None); + assert_eq!(result.memo, Some("test memo".to_string())); + assert_eq!(result.block_time, Some(1234567890)); + assert_eq!(result.confirmation_status, None); + assert_eq!(result.transaction_index, Some(42)); + } } diff --git a/rpc-client/src/mock_sender.rs b/rpc-client/src/mock_sender.rs index c4f71054499..482ebec3e62 100644 --- a/rpc-client/src/mock_sender.rs +++ b/rpc-client/src/mock_sender.rs @@ -250,6 +250,7 @@ impl RpcSender for MockSender { }), }, block_time: Some(1628633791), + transaction_index: Some(0), })?, "getTransactionCount" => json![1234], "getSlot" => json![0], @@ -430,6 +431,7 @@ impl RpcSender for MockSender { memo: None, block_time: None, confirmation_status: Some(TransactionConfirmationStatus::Finalized), + transaction_index: Some(0), }])? } "getBlockTime" => serde_json::to_value(UnixTimestamp::default())?, diff --git a/storage-bigtable/src/lib.rs b/storage-bigtable/src/lib.rs index 08530fe0b52..4d3b78e72c2 100644 --- a/storage-bigtable/src/lib.rs +++ b/storage-bigtable/src/lib.rs @@ -698,6 +698,7 @@ impl LedgerStorage { slot, tx_with_meta: tx_with_meta.clone(), block_time: block.block_time, + index, }) } }) @@ -741,6 +742,7 @@ impl LedgerStorage { slot, tx_with_meta, block_time: block.block_time, + index, })) } } @@ -881,6 +883,7 @@ impl LedgerStorage { err: tx_by_addr_info.err, memo: tx_by_addr_info.memo, block_time: tx_by_addr_info.block_time, + index: tx_by_addr_info.index, }, tx_by_addr_info.index, )); diff --git a/transaction-status-client-types/src/lib.rs b/transaction-status-client-types/src/lib.rs index 0c18c1d103e..058f56c966b 100644 --- a/transaction-status-client-types/src/lib.rs +++ b/transaction-status-client-types/src/lib.rs @@ -93,6 +93,7 @@ pub struct ConfirmedTransactionStatusWithSignature { pub err: Option, pub memo: Option, pub block_time: Option, + pub index: u32, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] @@ -712,6 +713,8 @@ pub struct EncodedConfirmedTransactionWithStatusMeta { #[serde(flatten)] pub transaction: EncodedTransactionWithStatusMeta, pub block_time: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub transaction_index: Option, } #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] diff --git a/transaction-status/src/lib.rs b/transaction-status/src/lib.rs index df87583f3cb..d44d5ef9e60 100644 --- a/transaction-status/src/lib.rs +++ b/transaction-status/src/lib.rs @@ -596,6 +596,7 @@ pub struct ConfirmedTransactionWithStatusMeta { pub slot: Slot, pub tx_with_meta: TransactionWithStatusMeta, pub block_time: Option, + pub index: u32, } #[derive(Debug, Clone, PartialEq)] @@ -619,6 +620,7 @@ impl ConfirmedTransactionWithStatusMeta { true, )?, block_time: self.block_time, + transaction_index: Some(self.index), }) } @@ -964,4 +966,63 @@ mod test { expected_json_output_value ); } + + #[test] + fn test_confirmed_transaction_with_status_meta_encode() { + use solana_transaction::versioned::VersionedTransaction; + + let pubkey1 = solana_pubkey::new_rand(); + let pubkey2 = solana_pubkey::new_rand(); + + let message = solana_message::v0::Message { + header: solana_message::MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 1, + }, + account_keys: vec![pubkey1, pubkey2], + recent_blockhash: solana_hash::Hash::default(), + instructions: vec![], + address_table_lookups: vec![], + }; + + let tx = VersionedTransaction { + signatures: vec![solana_signature::Signature::from([1u8; 64])], + message: solana_message::VersionedMessage::V0(message), + }; + + let meta = TransactionStatusMeta { + status: Ok(()), + fee: 5000, + pre_balances: vec![1_000_000, 0], + post_balances: vec![994_900, 100], + inner_instructions: None, + log_messages: None, + pre_token_balances: None, + post_token_balances: None, + rewards: None, + loaded_addresses: LoadedAddresses::default(), + return_data: None, + compute_units_consumed: None, + cost_units: None, + }; + + let confirmed_tx = ConfirmedTransactionWithStatusMeta { + slot: 42, + tx_with_meta: TransactionWithStatusMeta::Complete(VersionedTransactionWithStatusMeta { + transaction: tx, + meta, + }), + block_time: Some(1234567890), + index: 7, + }; + + let encoded = confirmed_tx + .encode(UiTransactionEncoding::Base64, Some(0)) + .unwrap(); + + assert_eq!(encoded.slot, 42); + assert_eq!(encoded.block_time, Some(1234567890)); + assert_eq!(encoded.transaction_index, Some(7)); + } }