diff --git a/cli/src/cluster_query.rs b/cli/src/cluster_query.rs index 61045a2903..dce9907719 100644 --- a/cli/src/cluster_query.rs +++ b/cli/src/cluster_query.rs @@ -24,9 +24,9 @@ use solana_client::{ pubsub_client::PubsubClient, rpc_client::{GetConfirmedSignaturesForAddress2Config, RpcClient}, rpc_config::{ - RpcAccountInfoConfig, RpcConfirmedBlockConfig, RpcLargestAccountsConfig, - RpcLargestAccountsFilter, RpcProgramAccountsConfig, RpcTransactionLogsConfig, - RpcTransactionLogsFilter, + RpcAccountInfoConfig, RpcConfirmedBlockConfig, RpcConfirmedTransactionConfig, + RpcLargestAccountsConfig, RpcLargestAccountsFilter, RpcProgramAccountsConfig, + RpcTransactionLogsConfig, RpcTransactionLogsFilter, }, rpc_filter, rpc_response::SlotInfo, @@ -1826,6 +1826,7 @@ pub fn process_transaction_history( before, until, limit: Some(limit), + commitment: Some(CommitmentConfig::confirmed()), }, )?; @@ -1842,9 +1843,13 @@ pub fn process_transaction_history( Some(block_time) => format!("timestamp={} ", unix_timestamp_to_string(block_time)), }, - match result.err { - None => "Confirmed".to_string(), - Some(err) => format!("Failed: {:?}", err), + if let Some(err) = result.err { + format!("Failed: {:?}", err) + } else { + match result.confirmation_status { + None => "Finalized".to_string(), + Some(status) => format!("{:?}", status), + } }, result.memo.unwrap_or_else(|| "".to_string()), ); @@ -1854,9 +1859,13 @@ pub fn process_transaction_history( if show_transactions { if let Ok(signature) = result.signature.parse::() { - match rpc_client - .get_confirmed_transaction(&signature, UiTransactionEncoding::Base64) - { + match rpc_client.get_confirmed_transaction_with_config( + &signature, + RpcConfirmedTransactionConfig { + encoding: Some(UiTransactionEncoding::Base64), + commitment: Some(CommitmentConfig::confirmed()), + }, + ) { Ok(confirmed_transaction) => { println_transaction( &confirmed_transaction diff --git a/client/src/rpc_client.rs b/client/src/rpc_client.rs index ee4e6fa484..6c22d4a22c 100644 --- a/client/src/rpc_client.rs +++ b/client/src/rpc_client.rs @@ -652,6 +652,7 @@ impl RpcClient { before: config.before.map(|signature| signature.to_string()), until: config.until.map(|signature| signature.to_string()), limit: config.limit, + commitment: config.commitment, }; let result: Vec = self.send( @@ -1632,6 +1633,7 @@ pub struct GetConfirmedSignaturesForAddress2Config { pub before: Option, pub until: Option, pub limit: Option, + pub commitment: Option, } fn new_spinner_progress_bar() -> ProgressBar { diff --git a/client/src/rpc_config.rs b/client/src/rpc_config.rs index 03b0a9cda6..a168872c36 100644 --- a/client/src/rpc_config.rs +++ b/client/src/rpc_config.rs @@ -109,6 +109,8 @@ pub struct RpcGetConfirmedSignaturesForAddress2Config { pub before: Option, // Signature as base-58 string pub until: Option, // Signature as base-58 string pub limit: Option, + #[serde(flatten)] + pub commitment: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] diff --git a/client/src/rpc_response.rs b/client/src/rpc_response.rs index 73b39f8666..068d18d7b8 100644 --- a/client/src/rpc_response.rs +++ b/client/src/rpc_response.rs @@ -7,7 +7,9 @@ use { inflation::Inflation, transaction::{Result, TransactionError}, }, - solana_transaction_status::ConfirmedTransactionStatusWithSignature, + solana_transaction_status::{ + ConfirmedTransactionStatusWithSignature, TransactionConfirmationStatus, + }, std::{collections::HashMap, fmt, net::SocketAddr}, }; @@ -348,6 +350,7 @@ pub struct RpcConfirmedTransactionStatusWithSignature { pub err: Option, pub memo: Option, pub block_time: Option, + pub confirmation_status: Option, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -374,6 +377,7 @@ impl From for RpcConfirmedTransactionSt err, memo, block_time, + confirmation_status: None, } } } diff --git a/core/src/rpc.rs b/core/src/rpc.rs index 168460db1e..7345095348 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -1163,23 +1163,27 @@ impl JsonRpcRequestProcessor { mut before: Option, until: Option, mut limit: usize, + commitment: Option, ) -> Result> { + let commitment = commitment.unwrap_or_default(); + check_is_at_least_confirmed(commitment)?; + if self.config.enable_rpc_transaction_history { let highest_confirmed_root = self .block_commitment_cache .read() .unwrap() .highest_confirmed_root(); + let highest_slot = if commitment.is_confirmed() { + let confirmed_bank = self.bank(Some(CommitmentConfig::confirmed())); + confirmed_bank.slot() + } else { + highest_confirmed_root + }; let mut results = self .blockstore - .get_confirmed_signatures_for_address2( - address, - highest_confirmed_root, - before, - until, - limit, - ) + .get_confirmed_signatures_for_address2(address, highest_slot, before, until, limit) .map_err(|err| Error::invalid_params(format!("{}", err)))?; if results.len() < limit { @@ -1208,7 +1212,24 @@ impl JsonRpcRequestProcessor { } } - Ok(results.into_iter().map(|x| x.into()).collect()) + Ok(results + .into_iter() + .map(|x| { + let mut item: RpcConfirmedTransactionStatusWithSignature = x.into(); + if item.slot <= highest_confirmed_root { + item.confirmation_status = Some(TransactionConfirmationStatus::Finalized); + } else { + item.confirmation_status = Some(TransactionConfirmationStatus::Confirmed); + if item.block_time.is_none() { + let r_bank_forks = self.bank_forks.read().unwrap(); + item.block_time = r_bank_forks + .get(item.slot) + .map(|bank| bank.clock().unix_timestamp); + } + } + item + }) + .collect()) } else { Ok(vec![]) } @@ -2341,6 +2362,7 @@ pub mod rpc_full { config: Option>, ) -> Result>; + // DEPRECATED #[rpc(meta, name = "getConfirmedSignaturesForAddress")] fn get_confirmed_signatures_for_address( &self, @@ -3087,7 +3109,13 @@ pub mod rpc_full { ))); } - meta.get_confirmed_signatures_for_address2(address, before, until, limit) + meta.get_confirmed_signatures_for_address2( + address, + before, + until, + limit, + config.commitment, + ) } fn get_first_available_block(&self, meta: Self::Metadata) -> Result { diff --git a/docs/src/developing/clients/jsonrpc-api.md b/docs/src/developing/clients/jsonrpc-api.md index 2fc254fbcf..14b6e8f6ec 100644 --- a/docs/src/developing/clients/jsonrpc-api.md +++ b/docs/src/developing/clients/jsonrpc-api.md @@ -803,6 +803,7 @@ address backwards in time from the provided signature or most recent confirmed b * `before: ` - (optional) start searching backwards from this transaction signature. If not provided the search starts from the top of the highest max confirmed block. * `until: ` - (optional) search until this transaction signature, if found before limit reached. + * (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment); "processed" is not supported. If parameter not provided, the default is "finalized". #### Results: The result field will be an array of transaction signature information, ordered diff --git a/ledger/src/blockstore.rs b/ledger/src/blockstore.rs index a666d06ecd..01920ffc0d 100644 --- a/ledger/src/blockstore.rs +++ b/ledger/src/blockstore.rs @@ -2112,6 +2112,8 @@ impl Blockstore { // Returns all rooted signatures for an address, ordered by slot that the transaction was // processed in. Within each slot the transactions will be ordered by signature, and NOT by // the order in which the transactions exist in the block + // + // DEPRECATED fn find_address_signatures( &self, pubkey: Pubkey, @@ -2143,6 +2145,40 @@ impl Blockstore { Ok(signatures) } + // Returns all signatures for an address in a particular slot, regardless of whether that slot + // has been rooted. The transactions will be ordered by signature, and NOT by the order in + // which the transactions exist in the block + fn find_address_signatures_for_slot( + &self, + pubkey: Pubkey, + slot: Slot, + ) -> Result> { + let mut signatures: Vec<(Slot, Signature)> = vec![]; + for transaction_status_cf_primary_index in 0..=1 { + let index_iterator = self.address_signatures_cf.iter(IteratorMode::From( + ( + transaction_status_cf_primary_index, + pubkey, + slot, + Signature::default(), + ), + IteratorDirection::Forward, + ))?; + for ((i, address, transaction_slot, signature), _) in index_iterator { + if i != transaction_status_cf_primary_index + || transaction_slot > slot + || address != pubkey + { + break; + } + signatures.push((slot, signature)); + } + } + signatures.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap().then(a.1.cmp(&b.1))); + Ok(signatures) + } + + // DEPRECATED pub fn get_confirmed_signatures_for_address( &self, pubkey: Pubkey, @@ -2164,7 +2200,7 @@ impl Blockstore { pub fn get_confirmed_signatures_for_address2( &self, address: Pubkey, - highest_confirmed_root: Slot, + highest_slot: Slot, // highest_confirmed_root or highest_confirmed_slot before: Option, until: Option, limit: usize, @@ -2177,28 +2213,31 @@ impl Blockstore { String ) ); + let confirmed_unrooted_slots: Vec<_> = AncestorIterator::new_inclusive(highest_slot, self) + .filter(|&slot| slot > self.last_root()) + .collect(); // Figure the `slot` to start listing signatures at, based on the ledger location of the // `before` signature if present. Also generate a HashSet of signatures that should // be excluded from the results. let mut get_before_slot_timer = Measure::start("get_before_slot_timer"); let (slot, mut before_excluded_signatures) = match before { - None => (highest_confirmed_root, None), + None => (highest_slot, None), Some(before) => { - let transaction_status = self.get_transaction_status(before, &[])?; + let transaction_status = + self.get_transaction_status(before, &confirmed_unrooted_slots)?; match transaction_status { None => return Ok(vec![]), Some((slot, _)) => { - let confirmed_block = - self.get_rooted_block(slot, false).map_err(|err| { - BlockstoreError::Io(IoError::new( - ErrorKind::Other, - format!("Unable to get confirmed block: {}", err), - )) - })?; + let block = self.get_complete_block(slot, false).map_err(|err| { + BlockstoreError::Io(IoError::new( + ErrorKind::Other, + format!("Unable to get block: {}", err), + )) + })?; // Load all signatures for the block - let mut slot_signatures: Vec<_> = confirmed_block + let mut slot_signatures: Vec<_> = block .transactions .into_iter() .filter_map(|transaction_with_meta| { @@ -2236,20 +2275,20 @@ impl Blockstore { let (lowest_slot, until_excluded_signatures) = match until { None => (0, HashSet::new()), Some(until) => { - let transaction_status = self.get_transaction_status(until, &[])?; + let transaction_status = + self.get_transaction_status(until, &confirmed_unrooted_slots)?; match transaction_status { None => (0, HashSet::new()), Some((slot, _)) => { - let confirmed_block = - self.get_rooted_block(slot, false).map_err(|err| { - BlockstoreError::Io(IoError::new( - ErrorKind::Other, - format!("Unable to get confirmed block: {}", err), - )) - })?; + let block = self.get_complete_block(slot, false).map_err(|err| { + BlockstoreError::Io(IoError::new( + ErrorKind::Other, + format!("Unable to get block: {}", err), + )) + })?; // Load all signatures for the block - let mut slot_signatures: Vec<_> = confirmed_block + let mut slot_signatures: Vec<_> = block .transactions .into_iter() .filter_map(|transaction_with_meta| { @@ -2284,7 +2323,7 @@ impl Blockstore { // Get signatures in `slot` let mut get_initial_slot_timer = Measure::start("get_initial_slot_timer"); - let mut signatures = self.find_address_signatures(address, slot, slot)?; + let mut signatures = self.find_address_signatures_for_slot(address, slot)?; signatures.reverse(); if let Some(excluded_signatures) = before_excluded_signatures.take() { address_signatures.extend( @@ -2324,7 +2363,7 @@ impl Blockstore { && key_address == address && slot >= first_available_block { - if self.is_root(slot) { + if self.is_root(slot) || confirmed_unrooted_slots.contains(&slot) { address_signatures.push((slot, signature)); } continue; @@ -2336,7 +2375,7 @@ impl Blockstore { // Handle slots that cross primary indexes if next_max_slot >= lowest_slot { let mut signatures = - self.find_address_signatures(address, next_max_slot, next_max_slot)?; + self.find_address_signatures_for_slot(address, next_max_slot)?; signatures.reverse(); address_signatures.append(&mut signatures); } @@ -2362,7 +2401,7 @@ impl Blockstore { && key_address == address && slot >= first_available_block { - if self.is_root(slot) { + if self.is_root(slot) || confirmed_unrooted_slots.contains(&slot) { address_signatures.push((slot, signature)); } continue; @@ -2381,7 +2420,8 @@ impl Blockstore { let mut get_status_info_timer = Measure::start("get_status_info_timer"); let mut infos = vec![]; for (slot, signature) in address_signatures.into_iter() { - let transaction_status = self.get_transaction_status(signature, &[])?; + let transaction_status = + self.get_transaction_status(signature, &confirmed_unrooted_slots)?; let err = match transaction_status { None => None, Some((_slot, status)) => status.status.err(), @@ -6821,6 +6861,96 @@ pub mod tests { Blockstore::destroy(&blockstore_path).expect("Expected successful database destruction"); } + #[test] + fn test_find_address_signatures_for_slot() { + let blockstore_path = get_tmp_ledger_path!(); + { + let blockstore = Blockstore::open(&blockstore_path).unwrap(); + + let address0 = solana_sdk::pubkey::new_rand(); + let address1 = solana_sdk::pubkey::new_rand(); + + let slot1 = 1; + for x in 1..5 { + let signature = Signature::new(&[x; 64]); + blockstore + .write_transaction_status( + slot1, + signature, + vec![&address0], + vec![&address1], + TransactionStatusMeta::default(), + ) + .unwrap(); + } + let slot2 = 2; + for x in 5..7 { + let signature = Signature::new(&[x; 64]); + blockstore + .write_transaction_status( + slot2, + signature, + vec![&address0], + vec![&address1], + TransactionStatusMeta::default(), + ) + .unwrap(); + } + // Purge to freeze index 0 + blockstore.run_purge(0, 1, PurgeType::PrimaryIndex).unwrap(); + for x in 7..9 { + let signature = Signature::new(&[x; 64]); + blockstore + .write_transaction_status( + slot2, + signature, + vec![&address0], + vec![&address1], + TransactionStatusMeta::default(), + ) + .unwrap(); + } + let slot3 = 3; + for x in 9..13 { + let signature = Signature::new(&[x; 64]); + blockstore + .write_transaction_status( + slot3, + signature, + vec![&address0], + vec![&address1], + TransactionStatusMeta::default(), + ) + .unwrap(); + } + blockstore.set_roots(&[slot1]).unwrap(); + + let slot1_signatures = blockstore + .find_address_signatures_for_slot(address0, 1) + .unwrap(); + for (i, (slot, signature)) in slot1_signatures.iter().enumerate() { + assert_eq!(*slot, slot1); + assert_eq!(*signature, Signature::new(&[i as u8 + 1; 64])); + } + + let slot2_signatures = blockstore + .find_address_signatures_for_slot(address0, 2) + .unwrap(); + for (i, (slot, signature)) in slot2_signatures.iter().enumerate() { + assert_eq!(*slot, slot2); + assert_eq!(*signature, Signature::new(&[i as u8 + 5; 64])); + } + + let slot3_signatures = blockstore + .find_address_signatures_for_slot(address0, 3) + .unwrap(); + for (i, (slot, signature)) in slot3_signatures.iter().enumerate() { + assert_eq!(*slot, slot3); + assert_eq!(*signature, Signature::new(&[i as u8 + 9; 64])); + } + } + } + #[test] fn test_get_confirmed_signatures_for_address2() { let blockstore_path = get_tmp_ledger_path!(); @@ -6873,11 +7003,36 @@ pub mod tests { } } } + + // Add 2 slots that both descend from slot 8 + for slot in 9..=10 { + let entries = make_slot_entries_with_transaction_addresses(&[ + address0, address1, address0, address1, + ]); + let shreds = entries_to_test_shreds(entries.clone(), slot, 8, true, 0); + blockstore.insert_shreds(shreds, None, false).unwrap(); + + for entry in entries.iter() { + for transaction in &entry.transactions { + assert_eq!(transaction.signatures.len(), 1); + blockstore + .write_transaction_status( + slot, + transaction.signatures[0], + transaction.message.account_keys.iter().collect(), + vec![], + TransactionStatusMeta::default(), + ) + .unwrap(); + } + } + } + // Leave one slot unrooted to test only returns confirmed signatures blockstore.set_roots(&[1, 2, 4, 5, 6, 7, 8]).unwrap(); let highest_confirmed_root = 8; - // Fetch all signatures for address 0 at once... + // Fetch all rooted signatures for address 0 at once... let all0 = blockstore .get_confirmed_signatures_for_address2( address0, @@ -6889,7 +7044,7 @@ pub mod tests { .unwrap(); assert_eq!(all0.len(), 12); - // Fetch all signatures for address 1 at once... + // Fetch all rooted signatures for address 1 at once... let all1 = blockstore .get_confirmed_signatures_for_address2( address1, @@ -6901,8 +7056,6 @@ pub mod tests { .unwrap(); assert_eq!(all1.len(), 12); - assert!(all0 != all1); - // Fetch all signatures for address 0 individually for i in 0..all0.len() { let results = blockstore @@ -7035,6 +7188,170 @@ pub mod tests { ) .unwrap(); assert!(results2.len() < results.len()); + + // Duplicate all tests using confirmed signatures + let highest_confirmed_slot = 10; + + // Fetch all signatures for address 0 at once... + let all0 = blockstore + .get_confirmed_signatures_for_address2( + address0, + highest_confirmed_slot, + None, + None, + usize::MAX, + ) + .unwrap(); + assert_eq!(all0.len(), 14); + + // Fetch all signatures for address 1 at once... + let all1 = blockstore + .get_confirmed_signatures_for_address2( + address1, + highest_confirmed_slot, + None, + None, + usize::MAX, + ) + .unwrap(); + assert_eq!(all1.len(), 14); + + // Fetch all signatures for address 0 individually + for i in 0..all0.len() { + let results = blockstore + .get_confirmed_signatures_for_address2( + address0, + highest_confirmed_slot, + if i == 0 { + None + } else { + Some(all0[i - 1].signature) + }, + None, + 1, + ) + .unwrap(); + assert_eq!(results.len(), 1); + assert_eq!(results[0], all0[i], "Unexpected result for {}", i); + } + // Fetch all signatures for address 0 individually using `until` + for i in 0..all0.len() { + let results = blockstore + .get_confirmed_signatures_for_address2( + address0, + highest_confirmed_slot, + if i == 0 { + None + } else { + Some(all0[i - 1].signature) + }, + if i == all0.len() - 1 || i == all0.len() { + None + } else { + Some(all0[i + 1].signature) + }, + 10, + ) + .unwrap(); + assert_eq!(results.len(), 1); + assert_eq!(results[0], all0[i], "Unexpected result for {}", i); + } + + assert!(blockstore + .get_confirmed_signatures_for_address2( + address0, + highest_confirmed_slot, + Some(all0[all0.len() - 1].signature), + None, + 1, + ) + .unwrap() + .is_empty()); + + assert!(blockstore + .get_confirmed_signatures_for_address2( + address0, + highest_confirmed_slot, + None, + Some(all0[0].signature), + 2, + ) + .unwrap() + .is_empty()); + + // Fetch all signatures for address 0, three at a time + assert!(all0.len() % 3 == 2); + for i in (0..all0.len()).step_by(3) { + let results = blockstore + .get_confirmed_signatures_for_address2( + address0, + highest_confirmed_slot, + if i == 0 { + None + } else { + Some(all0[i - 1].signature) + }, + None, + 3, + ) + .unwrap(); + if i < 12 { + assert_eq!(results.len(), 3); + assert_eq!(results[2], all0[i + 2]); + } else { + assert_eq!(results.len(), 2); + } + assert_eq!(results[0], all0[i]); + assert_eq!(results[1], all0[i + 1]); + } + + // Ensure that the signatures within a slot are reverse ordered by signature + // (current limitation of the .get_confirmed_signatures_for_address2()) + for i in (0..all1.len()).step_by(2) { + let results = blockstore + .get_confirmed_signatures_for_address2( + address1, + highest_confirmed_slot, + if i == 0 { + None + } else { + Some(all1[i - 1].signature) + }, + None, + 2, + ) + .unwrap(); + assert_eq!(results.len(), 2); + assert_eq!(results[0].slot, results[1].slot); + assert!(results[0].signature >= results[1].signature); + assert_eq!(results[0], all1[i]); + assert_eq!(results[1], all1[i + 1]); + } + + // A search for address 0 with `before` and/or `until` signatures from address1 should also work + let results = blockstore + .get_confirmed_signatures_for_address2( + address0, + highest_confirmed_slot, + Some(all1[0].signature), + None, + usize::MAX, + ) + .unwrap(); + // The exact number of results returned is variable, based on the sort order of the + // random signatures that are generated + assert!(!results.is_empty()); + + let results2 = blockstore + .get_confirmed_signatures_for_address2( + address0, + highest_confirmed_slot, + Some(all1[0].signature), + Some(all1[4].signature), + usize::MAX, + ) + .unwrap(); + assert!(results2.len() < results.len()); } Blockstore::destroy(&blockstore_path).expect("Expected successful database destruction"); }