diff --git a/client/src/rpc_response.rs b/client/src/rpc_response.rs index 95cef7e01..94ea749a6 100644 --- a/client/src/rpc_response.rs +++ b/client/src/rpc_response.rs @@ -192,3 +192,10 @@ pub struct RpcStorageTurn { pub blockhash: String, pub slot: Slot, } + +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +#[serde(rename_all = "camelCase")] +pub struct RpcAccountBalance { + pub address: String, + pub lamports: u64, +} diff --git a/core/src/rpc.rs b/core/src/rpc.rs index 4b9e466a7..94201d736 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -19,7 +19,7 @@ use solana_client::{ use solana_faucet::faucet::request_airdrop_transaction; use solana_ledger::{bank_forks::BankForks, blockstore::Blockstore}; use solana_perf::packet::PACKET_DATA_SIZE; -use solana_runtime::bank::Bank; +use solana_runtime::{accounts::AccountAddressFilter, bank::Bank}; use solana_sdk::{ clock::{Slot, UnixTimestamp}, commitment_config::{CommitmentConfig, CommitmentLevel}, @@ -37,7 +37,7 @@ use solana_transaction_status::{ use solana_vote_program::vote_state::{VoteState, MAX_LOCKOUT_HISTORY}; use std::{ cmp::max, - collections::HashMap, + collections::{HashMap, HashSet}, net::{SocketAddr, UdpSocket}, str::FromStr, sync::{Arc, RwLock}, @@ -46,6 +46,7 @@ use std::{ }; const JSON_RPC_SERVER_ERROR_0: i64 = -32000; +const NUM_LARGEST_ACCOUNTS: usize = 20; type RpcResponse = Result>; @@ -280,6 +281,27 @@ impl JsonRpcRequestProcessor { Ok(self.bank(commitment)?.capitalization()) } + fn get_largest_accounts( + &self, + commitment: Option, + ) -> RpcResponse> { + let bank = self.bank(commitment)?; + new_response( + &bank, + bank.get_largest_accounts( + NUM_LARGEST_ACCOUNTS, + &HashSet::new(), + AccountAddressFilter::Exclude, + ) + .into_iter() + .map(|(address, lamports)| RpcAccountBalance { + address: address.to_string(), + lamports, + }) + .collect(), + ) + } + fn get_vote_accounts( &self, commitment: Option, @@ -730,6 +752,13 @@ pub trait RpcSol { commitment: Option, ) -> Result; + #[rpc(meta, name = "getLargestAccounts")] + fn get_largest_accounts( + &self, + meta: Self::Metadata, + commitment: Option, + ) -> RpcResponse>; + #[rpc(meta, name = "requestAirdrop")] fn request_airdrop( &self, @@ -1131,6 +1160,18 @@ impl RpcSol for RpcSolImpl { .get_total_supply(commitment) } + fn get_largest_accounts( + &self, + meta: Self::Metadata, + commitment: Option, + ) -> RpcResponse> { + debug!("get_largest_accounts rpc request received"); + meta.request_processor + .read() + .unwrap() + .get_largest_accounts(commitment) + } + fn request_airdrop( &self, meta: Self::Metadata, @@ -1771,6 +1812,49 @@ pub mod tests { assert!(supply >= TEST_MINT_LAMPORTS); } + #[test] + fn test_get_largest_accounts() { + let bob_pubkey = Pubkey::new_rand(); + let RpcHandler { + io, meta, alice, .. + } = start_rpc_handler_with_tx(&bob_pubkey); + let req = format!(r#"{{"jsonrpc":"2.0","id":1,"method":"getLargestAccounts"}}"#); + let res = io.handle_request_sync(&req, meta.clone()); + let json: Value = serde_json::from_str(&res.unwrap()).unwrap(); + let largest_accounts: Vec = + serde_json::from_value(json["result"]["value"].clone()) + .expect("actual response deserialization"); + assert_eq!(largest_accounts.len(), 18); + + // Get Alice balance + let req = format!( + r#"{{"jsonrpc":"2.0","id":1,"method":"getBalance","params":["{}"]}}"#, + alice.pubkey() + ); + let res = io.handle_request_sync(&req, meta.clone()); + let json: Value = serde_json::from_str(&res.unwrap()).unwrap(); + let alice_balance: u64 = serde_json::from_value(json["result"]["value"].clone()) + .expect("actual response deserialization"); + assert!(largest_accounts.contains(&RpcAccountBalance { + address: alice.pubkey().to_string(), + lamports: alice_balance, + })); + + // Get Bob balance + let req = format!( + r#"{{"jsonrpc":"2.0","id":1,"method":"getBalance","params":["{}"]}}"#, + bob_pubkey + ); + let res = io.handle_request_sync(&req, meta); + let json: Value = serde_json::from_str(&res.unwrap()).unwrap(); + let bob_balance: u64 = serde_json::from_value(json["result"]["value"].clone()) + .expect("actual response deserialization"); + assert!(largest_accounts.contains(&RpcAccountBalance { + address: bob_pubkey.to_string(), + lamports: bob_balance, + })); + } + #[test] fn test_rpc_get_minimum_balance_for_rent_exemption() { let bob_pubkey = Pubkey::new_rand(); diff --git a/docs/src/apps/jsonrpc-api.md b/docs/src/apps/jsonrpc-api.md index b1ffc5228..b4b60a5c7 100644 --- a/docs/src/apps/jsonrpc-api.md +++ b/docs/src/apps/jsonrpc-api.md @@ -31,6 +31,7 @@ To interact with a Solana node inside a JavaScript application, use the [solana- * [getGenesisHash](jsonrpc-api.md#getgenesishash) * [getIdentity](jsonrpc-api.md#getidentity) * [getInflation](jsonrpc-api.md#getinflation) +* [getLargestAccounts](jsonrpc-api.md#getlargestaccounts) * [getLeaderSchedule](jsonrpc-api.md#getleaderschedule) * [getMinimumBalanceForRentExemption](jsonrpc-api.md#getminimumbalanceforrentexemption) * [getProgramAccounts](jsonrpc-api.md#getprogramaccounts) @@ -634,6 +635,32 @@ curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "m {"jsonrpc":"2.0","result":{"foundation":0.05,"foundationTerm":7.0,"initial":0.15,"storage":0.1,"taper":0.15,"terminal":0.015},"id":1} ``` +### getLargestAccounts + +Returns the 20 largest accounts, by lamport balance + +#### Parameters: + +* `` - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) + +#### Results: + +The result will be an RpcResponse JSON object with `value` equal to an array of: + +* `` - otherwise, a JSON object containing: + * `address: `, base-58 encoded address of the account + * `lamports: `, number of lamports in the account, as a u64 + +#### Example: + +```bash +// Request +curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getInflation"}' http://localhost:8899 + +// Result +{"jsonrpc":"2.0","result":{"context":{"slot":54},"value":[{"lamports":999974,"address":"99P8ZgtJYe1buSK8JXkvpLh8xPsCFuLYhz9hQFNw93WJ"},{"lamports":42,"address":"uPwWLo16MVehpyWqsLkK3Ka8nLowWvAHbBChqv2FZeL"},{"lamports":42,"address":"aYJCgU7REfu3XF8b3QhkqgqQvLizx8zxuLBHA25PzDS"},{"lamports":42,"address":"CTvHVtQ4gd4gUcw3bdVgZJJqApXE9nCbbbP4VTS5wE1D"},{"lamports":20,"address":"4fq3xJ6kfrh9RkJQsmVd5gNMvJbuSHfErywvEjNQDPxu"},{"lamports":4,"address":"AXJADheGVp9cruP8WYu46oNkRbeASngN5fPCMVGQqNHa"},{"lamports":2,"address":"8NT8yS6LiwNprgW4yM1jPPow7CwRUotddBVkrkWgYp24"},{"lamports":1,"address":"SysvarEpochSchedu1e111111111111111111111111"},{"lamports":1,"address":"11111111111111111111111111111111"},{"lamports":1,"address":"Stake11111111111111111111111111111111111111"},{"lamports":1,"address":"SysvarC1ock11111111111111111111111111111111"},{"lamports":1,"address":"StakeConfig11111111111111111111111111111111"},{"lamports":1,"address":"SysvarRent111111111111111111111111111111111"},{"lamports":1,"address":"Config1111111111111111111111111111111111111"},{"lamports":1,"address":"SysvarStakeHistory1111111111111111111111111"},{"lamports":1,"address":"SysvarRecentB1ockHashes11111111111111111111"},{"lamports":1,"address":"SysvarFees111111111111111111111111111111111"},{"lamports":1,"address":"Vote111111111111111111111111111111111111111"}]},"id":1} +``` + ### getLeaderSchedule Returns the leader schedule for an epoch diff --git a/runtime/src/accounts.rs b/runtime/src/accounts.rs index 0b95a7328..c156b3418 100644 --- a/runtime/src/accounts.rs +++ b/runtime/src/accounts.rs @@ -59,6 +59,11 @@ pub type TransactionLoaders = Vec>; pub type TransactionLoadResult = (TransactionAccounts, TransactionLoaders, TransactionRent); +pub enum AccountAddressFilter { + Exclude, // exclude all addresses matching the fiter + Include, // only include addresses matching the filter +} + impl Accounts { pub fn new(paths: Vec) -> Self { Self::new_with_frozen_accounts(paths, &HashMap::default(), &[]) @@ -407,6 +412,39 @@ impl Accounts { }) } + pub fn load_largest_accounts( + &self, + ancestors: &Ancestors, + num: usize, + filter_by_address: &HashSet, + filter: AccountAddressFilter, + ) -> Vec<(Pubkey, u64)> { + let mut accounts_balances = self.accounts_db.scan_accounts( + ancestors, + |collector: &mut Vec<(Pubkey, u64)>, option| { + if let Some(data) = option + .filter(|(pubkey, account, _)| { + let should_include_pubkey = match filter { + AccountAddressFilter::Exclude => !filter_by_address.contains(&pubkey), + AccountAddressFilter::Include => filter_by_address.contains(&pubkey), + }; + should_include_pubkey + && account.lamports != 0 + && !(account.lamports == std::u64::MAX + && account.owner == solana_storage_program::id()) + }) + .map(|(pubkey, account, _slot)| (*pubkey, account.lamports)) + { + collector.push(data) + } + }, + ); + + accounts_balances.sort_by(|a, b| a.1.cmp(&b.1).reverse()); + accounts_balances.truncate(num); + accounts_balances + } + #[must_use] pub fn verify_bank_hash(&self, slot: Slot, ancestors: &Ancestors) -> bool { if let Err(err) = self.accounts_db.verify_bank_hash(slot, ancestors) { diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 833b1284a..db2036770 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -3,7 +3,10 @@ //! on behalf of the caller, and a low-level API for when they have //! already been signed and verified. use crate::{ - accounts::{Accounts, TransactionAccounts, TransactionLoadResult, TransactionLoaders}, + accounts::{ + AccountAddressFilter, Accounts, TransactionAccounts, TransactionLoadResult, + TransactionLoaders, + }, accounts_db::{AccountsDBSerialize, ErrorCounters, SnapshotStorage, SnapshotStorages}, accounts_index::Ancestors, blockhash_queue::BlockhashQueue, @@ -58,7 +61,7 @@ use solana_stake_program::stake_state::{self, Delegation}; use solana_vote_program::vote_state::VoteState; use std::{ cell::RefCell, - collections::HashMap, + collections::{HashMap, HashSet}, io::{BufReader, Cursor, Error as IOError, Read}, path::{Path, PathBuf}, rc::Rc, @@ -1844,6 +1847,17 @@ impl Bank { None } + pub fn get_largest_accounts( + &self, + num: usize, + filter_by_address: &HashSet, + filter: AccountAddressFilter, + ) -> Vec<(Pubkey, u64)> { + self.rc + .accounts + .load_largest_accounts(&self.ancestors, num, filter_by_address, filter) + } + pub fn transaction_count(&self) -> u64 { self.transaction_count.load(Ordering::Relaxed) }