diff --git a/client/src/rpc_client.rs b/client/src/rpc_client.rs index ecd0becaa9..bd9550af7e 100644 --- a/client/src/rpc_client.rs +++ b/client/src/rpc_client.rs @@ -2,7 +2,10 @@ use crate::{ client_error::{ClientError, ClientErrorKind, Result as ClientResult}, http_sender::HttpSender, mock_sender::{MockSender, Mocks}, - rpc_config::{RpcLargestAccountsConfig, RpcSendTransactionConfig, RpcTokenAccountsFilter}, + rpc_config::{ + RpcGetConfirmedSignaturesForAddress2Config, RpcLargestAccountsConfig, + RpcSendTransactionConfig, RpcTokenAccountsFilter, + }, rpc_request::{RpcError, RpcRequest, TokenAccountsFilter}, rpc_response::*, rpc_sender::RpcSender, @@ -289,6 +292,32 @@ impl RpcClient { Ok(signatures) } + pub fn get_confirmed_signatures_for_address2( + &self, + address: &Pubkey, + ) -> ClientResult> { + self.get_confirmed_signatures_for_address2_with_config(address, None, None) + } + + pub fn get_confirmed_signatures_for_address2_with_config( + &self, + address: &Pubkey, + start_after: Option, + limit: Option, + ) -> ClientResult> { + let config = RpcGetConfirmedSignaturesForAddress2Config { + start_after: start_after.map(|signature| signature.to_string()), + limit, + }; + + let result: Vec = self.send( + RpcRequest::GetConfirmedSignaturesForAddress2, + json!([address.to_string(), config]), + )?; + + Ok(result) + } + pub fn get_confirmed_transaction( &self, signature: &Signature, diff --git a/client/src/rpc_config.rs b/client/src/rpc_config.rs index e10a3c1e3c..f3d6743083 100644 --- a/client/src/rpc_config.rs +++ b/client/src/rpc_config.rs @@ -65,3 +65,10 @@ pub enum RpcTokenAccountsFilter { Mint(String), ProgramId(String), } + +#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RpcGetConfirmedSignaturesForAddress2Config { + pub start_after: Option, // Signature as base-58 string + pub limit: Option, +} diff --git a/client/src/rpc_request.rs b/client/src/rpc_request.rs index 50f9788fd4..f27e70e2de 100644 --- a/client/src/rpc_request.rs +++ b/client/src/rpc_request.rs @@ -14,6 +14,7 @@ pub enum RpcRequest { GetConfirmedBlock, GetConfirmedBlocks, GetConfirmedSignaturesForAddress, + GetConfirmedSignaturesForAddress2, GetConfirmedTransaction, GetEpochInfo, GetEpochSchedule, @@ -65,6 +66,7 @@ impl fmt::Display for RpcRequest { RpcRequest::GetConfirmedBlock => "getConfirmedBlock", RpcRequest::GetConfirmedBlocks => "getConfirmedBlocks", RpcRequest::GetConfirmedSignaturesForAddress => "getConfirmedSignaturesForAddress", + RpcRequest::GetConfirmedSignaturesForAddress2 => "getConfirmedSignaturesForAddress2", RpcRequest::GetConfirmedTransaction => "getConfirmedTransaction", RpcRequest::GetEpochInfo => "getEpochInfo", RpcRequest::GetEpochSchedule => "getEpochSchedule", @@ -112,6 +114,7 @@ pub const NUM_LARGEST_ACCOUNTS: usize = 20; pub const MAX_GET_SIGNATURE_STATUSES_QUERY_ITEMS: usize = 256; pub const MAX_GET_CONFIRMED_SIGNATURES_FOR_ADDRESS_SLOT_RANGE: u64 = 10_000; pub const MAX_GET_CONFIRMED_BLOCKS_RANGE: u64 = 500_000; +pub const MAX_GET_CONFIRMED_SIGNATURES_FOR_ADDRESS2_LIMIT: usize = 1_000; // Validators that are this number of slots behind are considered delinquent pub const DELINQUENT_VALIDATOR_SLOT_DISTANCE: u64 = 128; diff --git a/client/src/rpc_response.rs b/client/src/rpc_response.rs index 37d390e93d..f90447b409 100644 --- a/client/src/rpc_response.rs +++ b/client/src/rpc_response.rs @@ -6,6 +6,7 @@ use solana_sdk::{ inflation::Inflation, transaction::{Result, TransactionError}, }; +use solana_transaction_status::ConfirmedTransactionStatusWithSignature; use std::{collections::HashMap, net::SocketAddr}; pub type RpcResult = client_error::Result>; @@ -236,3 +237,29 @@ pub struct RpcTokenAccountBalance { #[serde(flatten)] pub amount: RpcTokenAmount, } + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RpcConfirmedTransactionStatusWithSignature { + pub signature: String, + pub slot: Slot, + pub err: Option, + pub memo: Option, +} + +impl From for RpcConfirmedTransactionStatusWithSignature { + fn from(value: ConfirmedTransactionStatusWithSignature) -> Self { + let ConfirmedTransactionStatusWithSignature { + signature, + slot, + err, + memo, + } = value; + Self { + signature: signature.to_string(), + slot, + err, + memo, + } + } +} diff --git a/core/src/rpc.rs b/core/src/rpc.rs index e29c5ed221..69ab71f7c7 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -17,6 +17,7 @@ use solana_client::{ rpc_filter::{Memcmp, MemcmpEncodedBytes, RpcFilterType}, rpc_request::{ TokenAccountsFilter, DELINQUENT_VALIDATOR_SLOT_DISTANCE, MAX_GET_CONFIRMED_BLOCKS_RANGE, + MAX_GET_CONFIRMED_SIGNATURES_FOR_ADDRESS2_LIMIT, MAX_GET_CONFIRMED_SIGNATURES_FOR_ADDRESS_SLOT_RANGE, MAX_GET_SIGNATURE_STATUSES_QUERY_ITEMS, NUM_LARGEST_ACCOUNTS, }, @@ -835,6 +836,35 @@ impl JsonRpcRequestProcessor { } } + pub fn get_confirmed_signatures_for_address2( + &self, + address: Pubkey, + start_after: Option, + limit: usize, + ) -> Result> { + if self.config.enable_rpc_transaction_history { + let highest_confirmed_root = self + .block_commitment_cache + .read() + .unwrap() + .highest_confirmed_root(); + + let results = self + .blockstore + .get_confirmed_signatures_for_address2( + address, + highest_confirmed_root, + start_after, + limit, + ) + .map_err(|err| Error::invalid_params(format!("{}", err)))?; + + Ok(results.into_iter().map(|x| x.into()).collect()) + } else { + Ok(vec![]) + } + } + pub fn get_first_available_block(&self) -> Slot { let slot = self .blockstore @@ -1485,6 +1515,14 @@ pub trait RpcSol { end_slot: Slot, ) -> Result>; + #[rpc(meta, name = "getConfirmedSignaturesForAddress2")] + fn get_confirmed_signatures_for_address2( + &self, + meta: Self::Metadata, + address: String, + config: Option, + ) -> Result>; + #[rpc(meta, name = "getFirstAvailableBlock")] fn get_first_available_block(&self, meta: Self::Metadata) -> Result; @@ -2115,6 +2153,39 @@ impl RpcSol for RpcSolImpl { .collect()) } + fn get_confirmed_signatures_for_address2( + &self, + meta: Self::Metadata, + address: String, + config: Option, + ) -> Result> { + let address = verify_pubkey(address)?; + + let (start_after, limit) = + if let Some(RpcGetConfirmedSignaturesForAddress2Config { start_after, limit }) = config + { + ( + if let Some(start_after) = start_after { + Some(verify_signature(&start_after)?) + } else { + None + }, + limit.unwrap_or(MAX_GET_CONFIRMED_SIGNATURES_FOR_ADDRESS2_LIMIT), + ) + } else { + (None, MAX_GET_CONFIRMED_SIGNATURES_FOR_ADDRESS2_LIMIT) + }; + + if limit == 0 || limit > MAX_GET_CONFIRMED_SIGNATURES_FOR_ADDRESS2_LIMIT { + return Err(Error::invalid_params(format!( + "Invalid limit; max {}", + MAX_GET_CONFIRMED_SIGNATURES_FOR_ADDRESS2_LIMIT + ))); + } + + meta.get_confirmed_signatures_for_address2(address, start_after, limit) + } + fn get_first_available_block(&self, meta: Self::Metadata) -> Result { debug!("get_first_available_block rpc request received"); Ok(meta.get_first_available_block()) diff --git a/docs/src/apps/jsonrpc-api.md b/docs/src/apps/jsonrpc-api.md index ec5304f8dc..11428dbb17 100644 --- a/docs/src/apps/jsonrpc-api.md +++ b/docs/src/apps/jsonrpc-api.md @@ -24,6 +24,7 @@ To interact with a Solana node inside a JavaScript application, use the [solana- - [getConfirmedBlock](jsonrpc-api.md#getconfirmedblock) - [getConfirmedBlocks](jsonrpc-api.md#getconfirmedblocks) - [getConfirmedSignaturesForAddress](jsonrpc-api.md#getconfirmedsignaturesforaddress) +- [getConfirmedSignaturesForAddress2](jsonrpc-api.md#getconfirmedsignaturesforaddress2) - [getConfirmedTransaction](jsonrpc-api.md#getconfirmedtransaction) - [getEpochInfo](jsonrpc-api.md#getepochinfo) - [getEpochSchedule](jsonrpc-api.md#getepochschedule) @@ -389,6 +390,8 @@ curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0","id":1,"m ### getConfirmedSignaturesForAddress +**DEPRECATED: Please use getConfirmedSignaturesForAddress2 instead** + 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 @@ -416,6 +419,39 @@ curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0","id":1,"m {"jsonrpc":"2.0","result":{["35YGay1Lwjwgxe9zaH6APSHbt9gYQUCtBWTNL3aVwVGn9xTFw2fgds7qK5AL29mP63A9j3rh8KpN1TgSR62XCaby","4bJdGN8Tt2kLWZ3Fa1dpwPSEkXWWTSszPSf1rRVsCwNjxbbUdwTeiWtmi8soA26YmwnKD4aAxNp8ci1Gjpdv4gsr","4LQ14a7BYY27578Uj8LPCaVhSdJGLn9DJqnUJHpy95FMqdKf9acAhUhecPQNjNUy6VoNFUbvwYkPociFSf87cWbG"]},"id":1} ``` + +### getConfirmedSignaturesForAddress2 + +Returns confirmed signatures for transactions involving an +address backwards in time from the provided signature or most recent confirmed block + +#### Parameters: +* `` - account address as base-58 encoded string +* `` - (optional) Configuration object containing the following fields: + * `startAfter: ` - (optional) start searching backwards from this transaction signature, + which must be a confirmed signature for the account + address. If not provided the search starts from + the highest max confirmed block. + * `limit: ` - (optional) maximum transaction signatures to return (between 1 and 1,000, default: 1,000). + +#### Results: +The result field will be an array of transaction signature information, ordered +from newest to oldest transaction: +* `` + * `signature: ` - transaction signature as base-58 encoded string + * `slot: ` - The slot that contains the block with the transaction + * `err: ` - Error if transaction failed, null if transaction succeeded. [TransactionError definitions](https://github.com/solana-labs/solana/blob/master/sdk/src/transaction.rs#L14) + * `memo: ` - Memo associated with the transaction, null if no memo is present + +#### Example: +```bash +// Request +curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0","id":1,"method":"getConfirmedSignaturesForAddress2","params":["Vote111111111111111111111111111111111111111", {"limit": 1}]}' localhost:8899 + +// Result +{"jsonrpc":"2.0","result":[{"err":null,"memo":null,"signature":"5h6xBEauJ3PK6SWCZ1PGjBvj8vDdWG3KpwATGy1ARAXFSDwt8GFXM7W5Ncn16wmqokgpiKRLuS83KUxyZyv2sUYv","slot":114}],"id":1} +``` + ### getConfirmedTransaction Returns transaction details for a confirmed transaction diff --git a/ledger/src/blockstore.rs b/ledger/src/blockstore.rs index 725b039a96..e294abc373 100644 --- a/ledger/src/blockstore.rs +++ b/ledger/src/blockstore.rs @@ -37,8 +37,9 @@ use solana_sdk::{ transaction::Transaction, }; use solana_transaction_status::{ - ConfirmedBlock, ConfirmedTransaction, EncodedTransaction, Rewards, TransactionStatusMeta, - TransactionWithStatusMeta, UiTransactionEncoding, UiTransactionStatusMeta, + ConfirmedBlock, ConfirmedTransaction, ConfirmedTransactionStatusWithSignature, + EncodedTransaction, Rewards, TransactionStatusMeta, TransactionWithStatusMeta, UiMessage, + UiTransactionEncoding, UiTransactionStatusMeta, }; use solana_vote_program::{vote_instruction::VoteInstruction, vote_state::TIMESTAMP_SLOT_INTERVAL}; use std::{ @@ -1921,6 +1922,127 @@ impl Blockstore { .map(|signatures| signatures.iter().map(|(_, signature)| *signature).collect()) } + pub fn get_confirmed_signatures_for_address2( + &self, + address: Pubkey, + highest_confirmed_root: Slot, + start_after: Option, + limit: usize, + ) -> Result> { + datapoint_info!( + "blockstore-rpc-api", + ( + "method", + "get_confirmed_signatures_for_address2".to_string(), + String + ) + ); + + let (mut slot, mut start_after) = match start_after { + None => (highest_confirmed_root, None), + Some(start_after) => { + let confirmed_transaction = + self.get_confirmed_transaction(start_after, Some(UiTransactionEncoding::Json))?; + match confirmed_transaction { + None => return Ok(vec![]), + Some(ConfirmedTransaction { slot, transaction }) => { + // Ensure that `start_after` is from a transaction that contains `address` + match transaction.transaction { + EncodedTransaction::Json(ui_transaction) => { + match ui_transaction.message { + UiMessage::Raw(message) => { + let address = address.to_string(); + if !message + .account_keys + .iter() + .any(|account_address| *account_address == address) + { + return Err(BlockstoreError::IO(IOError::new( + ErrorKind::Other, + format!( + "Invalid start_after signature: {}", + start_after + ), + ))); + } + } + _ => { + // Should never happen... + return Err(BlockstoreError::IO(IOError::new( + ErrorKind::Other, + "Unexpected transaction message encoding".to_string(), + ))); + } + } + } + _ => { + // Should never happen... + return Err(BlockstoreError::IO(IOError::new( + ErrorKind::Other, + "Unexpected transaction encoding".to_string(), + ))); + } + } + (slot, Some(start_after)) + } + } + } + }; + + // Fetch the list of signatures that affect the given address + let first_available_block = self.get_first_available_block()?; + let mut address_signatures = vec![]; + loop { + if address_signatures.len() >= limit { + address_signatures.truncate(limit); + break; + } + + let mut signatures = self.find_address_signatures(address, slot, slot)?; + if let Some(start_after) = start_after { + if let Some(index) = signatures + .iter() + .position(|(_slot, signature)| *signature == start_after) + { + address_signatures.append(&mut signatures.split_off(index + 1)); + signatures.clear(); + } else { + // Should never happen... + warn!( + "Blockstore corruption? Expected to find {} in slot {}", + start_after, slot + ); + } + } + address_signatures.append(&mut signatures); + start_after = None; + + if slot == first_available_block { + break; + } + slot -= 1; + } + address_signatures.truncate(limit); + + // Fill in the status information for each found transaction + let mut infos = vec![]; + for (slot, signature) in address_signatures.into_iter() { + let transaction_status = self.get_transaction_status(signature)?; + let err = match transaction_status { + None => None, + Some((_slot, status)) => status.status.err(), + }; + infos.push(ConfirmedTransactionStatusWithSignature { + signature, + slot, + err, + memo: None, + }); + } + + Ok(infos) + } + pub fn read_rewards(&self, index: Slot) -> Result> { self.rewards_cf.get(index) } @@ -6082,6 +6204,143 @@ pub mod tests { Blockstore::destroy(&blockstore_path).expect("Expected successful database destruction"); } + #[test] + fn test_get_confirmed_signatures_for_address2() { + let blockstore_path = get_tmp_ledger_path!(); + { + let blockstore = Blockstore::open(&blockstore_path).unwrap(); + + fn make_slot_entries_with_transaction_addresses(addresses: &[Pubkey]) -> Vec { + let mut entries: Vec = Vec::new(); + for address in addresses { + let transaction = Transaction::new_with_compiled_instructions( + &[&Keypair::new()], + &[*address], + Hash::default(), + vec![Pubkey::new_rand()], + vec![CompiledInstruction::new(1, &(), vec![0])], + ); + entries.push(next_entry_mut(&mut Hash::default(), 0, vec![transaction])); + let mut tick = create_ticks(1, 0, hash(&serialize(address).unwrap())); + entries.append(&mut tick); + } + entries + } + + let address0 = Pubkey::new_rand(); + let address1 = Pubkey::new_rand(); + + for slot in 2..=4 { + let entries = make_slot_entries_with_transaction_addresses(&[ + address0, address1, address0, address1, + ]); + let shreds = entries_to_test_shreds(entries.clone(), slot, slot - 1, true, 0); + blockstore.insert_shreds(shreds, None, false).unwrap(); + + for entry in &entries { + 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(); + } + } + } + blockstore.set_roots(&[1, 2, 3, 4]).unwrap(); + let highest_confirmed_root = 4; + + // Fetch all signatures for address 0 at once... + let all0 = blockstore + .get_confirmed_signatures_for_address2( + address0, + highest_confirmed_root, + None, + usize::MAX, + ) + .unwrap(); + assert_eq!(all0.len(), 6); + + // Fetch all signatures for address 1 at once... + let all1 = blockstore + .get_confirmed_signatures_for_address2( + address1, + highest_confirmed_root, + None, + usize::MAX, + ) + .unwrap(); + assert_eq!(all1.len(), 6); + + assert!(all0 != all1); + + // Fetch all signatures for address 0 individually + for i in 0..all0.len() { + let results = blockstore + .get_confirmed_signatures_for_address2( + address0, + highest_confirmed_root, + if i == 0 { + None + } else { + Some(all0[i - 1].signature) + }, + 1, + ) + .unwrap(); + assert_eq!(results.len(), 1); + assert_eq!(results[0], all0[i]); + } + + assert!(blockstore + .get_confirmed_signatures_for_address2( + address0, + highest_confirmed_root, + Some(all0[all0.len() - 1].signature), + 1, + ) + .unwrap() + .is_empty()); + + // Fetch all signatures for address 0, three at a time + assert!(all0.len() % 3 == 0); + for i in (0..all0.len()).step_by(3) { + let results = blockstore + .get_confirmed_signatures_for_address2( + address0, + highest_confirmed_root, + if i == 0 { + None + } else { + Some(all0[i - 1].signature) + }, + 3, + ) + .unwrap(); + assert_eq!(results.len(), 3); + assert_eq!(results[0], all0[i]); + assert_eq!(results[1], all0[i + 1]); + assert_eq!(results[2], all0[i + 2]); + } + + // `start_after` signature from address1 should fail + blockstore + .get_confirmed_signatures_for_address2( + address0, + highest_confirmed_root, + Some(all1[0].signature), + usize::MAX, + ) + .unwrap_err(); + } + Blockstore::destroy(&blockstore_path).expect("Expected successful database destruction"); + } + #[test] fn test_get_last_hash() { let mut entries: Vec = vec![]; diff --git a/transaction-status/src/lib.rs b/transaction-status/src/lib.rs index 9086beede9..6044ee1193 100644 --- a/transaction-status/src/lib.rs +++ b/transaction-status/src/lib.rs @@ -17,6 +17,7 @@ use solana_sdk::{ instruction::CompiledInstruction, message::MessageHeader, pubkey::Pubkey, + signature::Signature, transaction::{Result, Transaction, TransactionError}, }; @@ -125,7 +126,7 @@ impl From for UiTransactionStatusMeta { pub struct TransactionStatus { pub slot: Slot, pub confirmations: Option, // None = rooted - pub status: Result<()>, + pub status: Result<()>, // legacy field pub err: Option, } @@ -136,6 +137,15 @@ impl TransactionStatus { } } +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ConfirmedTransactionStatusWithSignature { + pub signature: Signature, + pub slot: Slot, + pub err: Option, + pub memo: Option, +} + #[derive(Debug, PartialEq, Serialize, Deserialize)] pub struct Reward { pub pubkey: String,