diff --git a/core/src/rpc.rs b/core/src/rpc.rs index 4c7c26f30854d8..e044fa408053a3 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -39,6 +39,7 @@ use std::{ }; const MAX_QUERY_ITEMS: usize = 256; +const MAX_SLOT_RANGE: u64 = 10_000; type RpcResponse = Result>; @@ -523,6 +524,22 @@ impl JsonRpcRequestProcessor { Ok(None) } } + + pub fn get_confirmed_signatures_for_address( + &self, + pubkey: Pubkey, + start_slot: Slot, + end_slot: Slot, + ) -> Result> { + if self.config.enable_rpc_transaction_history { + Ok(self + .blockstore + .get_confirmed_signatures_for_address(pubkey, start_slot, end_slot) + .unwrap_or_else(|_| vec![])) + } else { + Ok(vec![]) + } + } } fn get_tpu_addr(cluster_info: &Arc>) -> Result { @@ -772,6 +789,15 @@ pub trait RpcSol { signature_str: String, encoding: Option, ) -> Result>; + + #[rpc(meta, name = "getConfirmedSignaturesForAddress")] + fn get_confirmed_signatures_for_address( + &self, + meta: Self::Metadata, + pubkey_str: String, + start_slot: Slot, + end_slot: Slot, + ) -> Result>; } pub struct RpcSolImpl; @@ -1328,6 +1354,38 @@ impl RpcSol for RpcSolImpl { .unwrap() .get_confirmed_transaction(signature, encoding) } + + fn get_confirmed_signatures_for_address( + &self, + meta: Self::Metadata, + pubkey_str: String, + start_slot: Slot, + end_slot: Slot, + ) -> Result> { + let pubkey = verify_pubkey(pubkey_str)?; + if end_slot <= start_slot { + return Err(Error::invalid_params(format!( + "start_slot {} must be smaller than end_slot {}", + start_slot, end_slot + ))); + } + if end_slot - start_slot > MAX_SLOT_RANGE { + return Err(Error::invalid_params(format!( + "Slot range too large; max {}", + MAX_SLOT_RANGE + ))); + } + meta.request_processor + .read() + .unwrap() + .get_confirmed_signatures_for_address(pubkey, start_slot, end_slot) + .map(|signatures| { + signatures + .iter() + .map(|signature| signature.to_string()) + .collect() + }) + } } #[cfg(test)] diff --git a/docs/src/apps/jsonrpc-api.md b/docs/src/apps/jsonrpc-api.md index cd6a5fdc4442a5..7cd76dc1be4121 100644 --- a/docs/src/apps/jsonrpc-api.md +++ b/docs/src/apps/jsonrpc-api.md @@ -21,6 +21,7 @@ To interact with a Solana node inside a JavaScript application, use the [solana- * [getClusterNodes](jsonrpc-api.md#getclusternodes) * [getConfirmedBlock](jsonrpc-api.md#getconfirmedblock) * [getConfirmedBlocks](jsonrpc-api.md#getconfirmedblocks) +* [getConfirmedSignaturesForAddress](jsonrpc-api.md#getconfirmedsignaturesforaddress) * [getConfirmedTransaction](jsonrpc-api.md#getconfirmedtransaction) * [getEpochInfo](jsonrpc-api.md#getepochinfo) * [getEpochSchedule](jsonrpc-api.md#getepochschedule) @@ -346,6 +347,33 @@ curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0","id":1,"m {"jsonrpc":"2.0","result":[5,6,7,8,9,10],"id":1} ``` +### getConfirmedSignaturesForAddress + +Returns a list of all the confirmed signatures for transactions involving an address, within a specified Slot range. Max range allowed is 10_000 Slots. + +#### Parameters: + +* `` - account address as base-58 encoded string +* `` - start slot, inclusive +* `` - end slot, inclusive + +#### Results: + +The result field will be an array of: +* `` - transaction signature as base-58 encoded string + +The signatures will be ordered based on the Slot in which they were confirmed in, from lowest to highest Slot + +#### Example: + +```bash +// Request +curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0","id":1,"method":"getConfirmedSignaturesForAddress","params":["6H94zdiaYfRfPfKjYLjyr2VFBg6JHXygy84r3qhc3NsC", 0, 100]}' localhost:8899 + +// Result +{"jsonrpc":"2.0","result":{["35YGay1Lwjwgxe9zaH6APSHbt9gYQUCtBWTNL3aVwVGn9xTFw2fgds7qK5AL29mP63A9j3rh8KpN1TgSR62XCaby","4bJdGN8Tt2kLWZ3Fa1dpwPSEkXWWTSszPSf1rRVsCwNjxbbUdwTeiWtmi8soA26YmwnKD4aAxNp8ci1Gjpdv4gsr","4LQ14a7BYY27578Uj8LPCaVhSdJGLn9DJqnUJHpy95FMqdKf9acAhUhecPQNjNUy6VoNFUbvwYkPociFSf87cWbG"]},"id":1} +``` + ### getConfirmedTransaction Returns transaction details for a confirmed transaction diff --git a/ledger/src/blockstore.rs b/ledger/src/blockstore.rs index d884cdbccef6d5..bb0da04fe8416a 100644 --- a/ledger/src/blockstore.rs +++ b/ledger/src/blockstore.rs @@ -1667,20 +1667,14 @@ impl Blockstore { .put((primary_index, signature, slot), status)?; for address in writable_keys { self.address_signatures_cf.put( - (primary_index, *address, slot), - &AddressSignatureMeta { - signature, - writeable: true, - }, + (primary_index, *address, slot, signature), + &AddressSignatureMeta { writeable: true }, )?; } for address in readonly_keys { self.address_signatures_cf.put( - (primary_index, *address, slot), - &AddressSignatureMeta { - signature, - writeable: false, - }, + (primary_index, *address, slot, signature), + &AddressSignatureMeta { writeable: false }, )?; } Ok(()) @@ -1757,6 +1751,49 @@ impl Blockstore { .find(|transaction| transaction.signatures[0] == signature)) } + // Returns all cached signatures for an address, ordered by slot that the transaction was + // processed in + fn find_address_signatures( + &self, + pubkey: Pubkey, + start_slot: Slot, + end_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, + start_slot, + Signature::default(), + ), + IteratorDirection::Forward, + ))?; + for ((i, address, slot, signature), _) in index_iterator { + if i != transaction_status_cf_primary_index || slot > end_slot || address != pubkey + { + break; + } + if self.is_root(slot) { + signatures.push((slot, signature)); + } + } + } + signatures.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); + Ok(signatures) + } + + pub fn get_confirmed_signatures_for_address( + &self, + pubkey: Pubkey, + start_slot: Slot, + end_slot: Slot, + ) -> Result> { + self.find_address_signatures(pubkey, start_slot, end_slot) + .map(|signatures| signatures.iter().map(|(_, signature)| *signature).collect()) + } + pub fn read_rewards(&self, index: Slot) -> Result> { self.rewards_cf.get(index) } @@ -2887,7 +2924,7 @@ pub mod tests { .iter::(IteratorMode::Start) .unwrap() .next() - .map(|((primary_index, _, slot), _)| { + .map(|((primary_index, _, slot, _), _)| { slot >= min_slot || (primary_index == 2 && slot == 0) }) .unwrap_or(true) @@ -5492,7 +5529,7 @@ pub mod tests { let first_status_entry = blockstore .db .iter::(IteratorMode::From( - (0, Signature::default(), 0), + cf::TransactionStatus::as_index(0), IteratorDirection::Forward, )) .unwrap() @@ -5504,7 +5541,7 @@ pub mod tests { let first_address_entry = blockstore .db .iter::(IteratorMode::From( - (0, Pubkey::default(), 0), + cf::AddressSignatures::as_index(0), IteratorDirection::Forward, )) .unwrap() @@ -5562,7 +5599,7 @@ pub mod tests { let first_status_entry = blockstore .db .iter::(IteratorMode::From( - (0, Signature::default(), 0), + cf::TransactionStatus::as_index(0), IteratorDirection::Forward, )) .unwrap() @@ -5574,7 +5611,7 @@ pub mod tests { let first_address_entry = blockstore .db .iter::(IteratorMode::From( - (0, Pubkey::default(), 0), + cf::AddressSignatures::as_index(0), IteratorDirection::Forward, )) .unwrap() @@ -5587,7 +5624,7 @@ pub mod tests { let index1_first_status_entry = blockstore .db .iter::(IteratorMode::From( - (1, Signature::default(), 0), + cf::TransactionStatus::as_index(1), IteratorDirection::Forward, )) .unwrap() @@ -5599,7 +5636,7 @@ pub mod tests { let index1_first_address_entry = blockstore .db .iter::(IteratorMode::From( - (1, Pubkey::default(), 0), + cf::AddressSignatures::as_index(1), IteratorDirection::Forward, )) .unwrap() @@ -5630,7 +5667,7 @@ pub mod tests { let first_status_entry = blockstore .db .iter::(IteratorMode::From( - (0, Signature::default(), 0), + cf::TransactionStatus::as_index(0), IteratorDirection::Forward, )) .unwrap() @@ -5642,7 +5679,7 @@ pub mod tests { let first_address_entry = blockstore .db .iter::(IteratorMode::From( - (0, Pubkey::default(), 0), + cf::AddressSignatures::as_index(0), IteratorDirection::Forward, )) .unwrap() @@ -5679,7 +5716,7 @@ pub mod tests { let mut status_entry_iterator = blockstore .db .iter::(IteratorMode::From( - (0, Signature::default(), 0), + cf::TransactionStatus::as_index(0), IteratorDirection::Forward, )) .unwrap(); @@ -5691,7 +5728,7 @@ pub mod tests { let mut address_transactions_iterator = blockstore .db .iter::(IteratorMode::From( - (0, Pubkey::default(), 0), + (0, Pubkey::default(), 0, Signature::default()), IteratorDirection::Forward, )) .unwrap(); @@ -5713,7 +5750,7 @@ pub mod tests { let mut status_entry_iterator = blockstore .db .iter::(IteratorMode::From( - (0, Signature::default(), 0), + cf::TransactionStatus::as_index(0), IteratorDirection::Forward, )) .unwrap(); @@ -5725,7 +5762,7 @@ pub mod tests { let mut address_transactions_iterator = blockstore .db .iter::(IteratorMode::From( - (0, Pubkey::default(), 0), + cf::AddressSignatures::as_index(0), IteratorDirection::Forward, )) .unwrap(); @@ -5747,7 +5784,7 @@ pub mod tests { let mut status_entry_iterator = blockstore .db .iter::(IteratorMode::From( - (0, Signature::default(), 0), + cf::TransactionStatus::as_index(0), IteratorDirection::Forward, )) .unwrap(); @@ -5759,7 +5796,7 @@ pub mod tests { let mut address_transactions_iterator = blockstore .db .iter::(IteratorMode::From( - (0, Pubkey::default(), 0), + cf::AddressSignatures::as_index(0), IteratorDirection::Forward, )) .unwrap(); @@ -5780,7 +5817,7 @@ pub mod tests { let mut status_entry_iterator = blockstore .db .iter::(IteratorMode::From( - (0, Signature::default(), 0), + cf::TransactionStatus::as_index(0), IteratorDirection::Forward, )) .unwrap(); @@ -5791,7 +5828,7 @@ pub mod tests { let mut address_transactions_iterator = blockstore .db .iter::(IteratorMode::From( - (0, Pubkey::default(), 0), + cf::AddressSignatures::as_index(0), IteratorDirection::Forward, )) .unwrap(); @@ -6021,6 +6058,144 @@ pub mod tests { } } + #[test] + fn test_get_confirmed_signatures_for_address() { + let blockstore_path = get_tmp_ledger_path!(); + { + let blockstore = Blockstore::open(&blockstore_path).unwrap(); + + let address0 = Pubkey::new_rand(); + let address1 = Pubkey::new_rand(); + + let slot0 = 10; + for x in 1..5 { + let signature = Signature::new(&[x; 64]); + blockstore + .write_transaction_status( + slot0, + signature, + vec![&address0], + vec![&address1], + &TransactionStatusMeta::default(), + ) + .unwrap(); + } + // Purge to freeze index 0 + blockstore.run_purge(0, 1).unwrap(); + let slot1 = 20; + for x in 5..9 { + let signature = Signature::new(&[x; 64]); + blockstore + .write_transaction_status( + slot1, + signature, + vec![&address0], + vec![&address1], + &TransactionStatusMeta::default(), + ) + .unwrap(); + } + blockstore.set_roots(&[slot0, slot1]).unwrap(); + + let all0 = blockstore + .get_confirmed_signatures_for_address(address0, 0, 50) + .unwrap(); + assert_eq!(all0.len(), 8); + for x in 1..9 { + let expected_signature = Signature::new(&[x; 64]); + assert_eq!(all0[x as usize - 1], expected_signature); + } + assert_eq!( + blockstore + .get_confirmed_signatures_for_address(address0, 20, 50) + .unwrap() + .len(), + 4 + ); + assert_eq!( + blockstore + .get_confirmed_signatures_for_address(address0, 0, 10) + .unwrap() + .len(), + 4 + ); + assert!(blockstore + .get_confirmed_signatures_for_address(address0, 1, 5) + .unwrap() + .is_empty()); + assert_eq!( + blockstore + .get_confirmed_signatures_for_address(address0, 1, 15) + .unwrap() + .len(), + 4 + ); + + let all1 = blockstore + .get_confirmed_signatures_for_address(address1, 0, 50) + .unwrap(); + assert_eq!(all1.len(), 8); + for x in 1..9 { + let expected_signature = Signature::new(&[x; 64]); + assert_eq!(all1[x as usize - 1], expected_signature); + } + + // Purge index 0 + blockstore.run_purge(0, 10).unwrap(); + assert_eq!( + blockstore + .get_confirmed_signatures_for_address(address0, 0, 50) + .unwrap() + .len(), + 4 + ); + assert_eq!( + blockstore + .get_confirmed_signatures_for_address(address0, 20, 50) + .unwrap() + .len(), + 4 + ); + assert!(blockstore + .get_confirmed_signatures_for_address(address0, 0, 10) + .unwrap() + .is_empty()); + assert!(blockstore + .get_confirmed_signatures_for_address(address0, 1, 5) + .unwrap() + .is_empty()); + assert_eq!( + blockstore + .get_confirmed_signatures_for_address(address0, 1, 25) + .unwrap() + .len(), + 4 + ); + + // Test sort, regardless of entry order or signature value + for slot in (21..25).rev() { + let random_bytes: Vec = (0..64).map(|_| rand::random::()).collect(); + let signature = Signature::new(&random_bytes); + blockstore + .write_transaction_status( + slot, + signature, + vec![&address0], + vec![&address1], + &TransactionStatusMeta::default(), + ) + .unwrap(); + } + blockstore.set_roots(&[21, 22, 23, 24]).unwrap(); + let mut past_slot = 0; + for (slot, _) in blockstore.find_address_signatures(address0, 1, 25).unwrap() { + assert!(slot >= past_slot); + past_slot = slot; + } + } + Blockstore::destroy(&blockstore_path).expect("Expected successful database destruction"); + } + #[test] fn test_get_last_hash() { let mut entries: Vec = vec![]; diff --git a/ledger/src/blockstore_db.rs b/ledger/src/blockstore_db.rs index 90bf4e7746a37d..9f6519d4ba1fb7 100644 --- a/ledger/src/blockstore_db.rs +++ b/ledger/src/blockstore_db.rs @@ -356,21 +356,23 @@ impl ColumnName for columns::TransactionStatus { } impl Column for columns::AddressSignatures { - type Index = (u64, Pubkey, Slot); + type Index = (u64, Pubkey, Slot, Signature); - fn key((index, pubkey, slot): (u64, Pubkey, Slot)) -> Vec { - let mut key = vec![0; 8 + 32 + 8]; // size_of u64 + size_of Pubkey + size_of Slot + fn key((index, pubkey, slot, signature): (u64, Pubkey, Slot, Signature)) -> Vec { + let mut key = vec![0; 8 + 32 + 8 + 64]; // size_of u64 + size_of Pubkey + size_of Slot + size_of Signature BigEndian::write_u64(&mut key[0..8], index); key[8..40].clone_from_slice(&pubkey.as_ref()[0..32]); BigEndian::write_u64(&mut key[40..48], slot); + key[48..112].clone_from_slice(&signature.as_ref()[0..64]); key } - fn index(key: &[u8]) -> (u64, Pubkey, Slot) { + fn index(key: &[u8]) -> (u64, Pubkey, Slot, Signature) { let index = BigEndian::read_u64(&key[0..8]); let pubkey = Pubkey::new(&key[8..40]); let slot = BigEndian::read_u64(&key[40..48]); - (index, pubkey, slot) + let signature = Signature::new(&key[48..112]); + (index, pubkey, slot, signature) } fn primary_index(index: Self::Index) -> u64 { @@ -378,7 +380,7 @@ impl Column for columns::AddressSignatures { } fn as_index(index: u64) -> Self::Index { - (index, Pubkey::default(), 0) + (index, Pubkey::default(), 0, Signature::default()) } } diff --git a/ledger/src/blockstore_meta.rs b/ledger/src/blockstore_meta.rs index eb6599dd1198be..0488f24c30b076 100644 --- a/ledger/src/blockstore_meta.rs +++ b/ledger/src/blockstore_meta.rs @@ -1,6 +1,6 @@ use crate::erasure::ErasureConfig; use serde::{Deserialize, Serialize}; -use solana_sdk::{clock::Slot, signature::Signature}; +use solana_sdk::clock::Slot; use std::{collections::BTreeSet, ops::RangeBounds}; #[derive(Clone, Debug, Default, Deserialize, Serialize, Eq, PartialEq)] @@ -230,7 +230,6 @@ pub struct TransactionStatusIndexMeta { #[derive(Debug, Default, Deserialize, Serialize, PartialEq)] pub struct AddressSignatureMeta { - pub signature: Signature, pub writeable: bool, }