From 3ee702a9225f5d049edb6baf93f15e219dbe334d Mon Sep 17 00:00:00 2001 From: Tyera Eulberg Date: Sat, 9 May 2020 12:05:29 -0600 Subject: [PATCH] Rpc: Add getCirculatingSupply endpoint, redux (#9953) * Add Bank.clock() helper * Add non-circulating calculations * Plumb getSupply rpc endpoint * Add docs for getSupply, and remove getTotalSupply from docs * Add pubkeys! procedural macro * Use procedural macro in non_circulating_supply --- client/src/rpc_response.rs | 9 ++ core/src/lib.rs | 1 + core/src/non_circulating_supply.rs | 193 +++++++++++++++++++++++++++++ core/src/rpc.rs | 40 ++++++ docs/src/apps/jsonrpc-api.md | 50 ++++---- runtime/src/bank.rs | 20 +-- sdk/macro/src/lib.rs | 104 +++++++++++++--- sdk/src/lib.rs | 1 + 8 files changed, 371 insertions(+), 47 deletions(-) create mode 100644 core/src/non_circulating_supply.rs diff --git a/client/src/rpc_response.rs b/client/src/rpc_response.rs index 94ea749a6..9a328e0c1 100644 --- a/client/src/rpc_response.rs +++ b/client/src/rpc_response.rs @@ -199,3 +199,12 @@ pub struct RpcAccountBalance { pub address: String, pub lamports: u64, } + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct RpcSupply { + pub total: u64, + pub circulating: u64, + pub non_circulating: u64, + pub non_circulating_accounts: Vec, +} diff --git a/core/src/lib.rs b/core/src/lib.rs index a0b7a6c6d..01afae5f1 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -30,6 +30,7 @@ pub mod gen_keys; pub mod gossip_service; pub mod ledger_cleanup_service; pub mod local_vote_signer_service; +pub mod non_circulating_supply; pub mod poh_recorder; pub mod poh_service; pub mod progress_map; diff --git a/core/src/non_circulating_supply.rs b/core/src/non_circulating_supply.rs new file mode 100644 index 000000000..280cbf5b8 --- /dev/null +++ b/core/src/non_circulating_supply.rs @@ -0,0 +1,193 @@ +use solana_runtime::bank::Bank; +use solana_sdk::pubkey::Pubkey; +use solana_stake_program::stake_state::StakeState; +use std::{collections::HashSet, sync::Arc}; + +pub struct NonCirculatingSupply { + pub lamports: u64, + pub accounts: Vec, +} + +pub fn calculate_non_circulating_supply(bank: Arc) -> NonCirculatingSupply { + debug!("Updating Bank supply, epoch: {}", bank.epoch()); + let mut non_circulating_accounts_set: HashSet = HashSet::new(); + + for key in non_circulating_accounts() { + non_circulating_accounts_set.insert(key); + } + + let clock = bank.clock(); + let stake_accounts = bank.get_program_accounts(Some(&solana_stake_program::id())); + for (pubkey, account) in stake_accounts.iter() { + let stake_account = StakeState::from(&account).unwrap_or_default(); + match stake_account { + StakeState::Initialized(meta) => { + if meta.lockup.is_in_force(&clock, &HashSet::default()) + || meta.authorized.withdrawer == withdraw_authority() + { + non_circulating_accounts_set.insert(*pubkey); + } + } + StakeState::Stake(meta, _stake) => { + if meta.lockup.is_in_force(&clock, &HashSet::default()) + || meta.authorized.withdrawer == withdraw_authority() + { + non_circulating_accounts_set.insert(*pubkey); + } + } + _ => {} + } + } + + let lamports = non_circulating_accounts_set + .iter() + .fold(0, |acc, pubkey| acc + bank.get_balance(&pubkey)); + + NonCirculatingSupply { + lamports, + accounts: non_circulating_accounts_set.into_iter().collect(), + } +} + +// Mainnet-beta accounts that should be considered non-circulating +solana_sdk::pubkeys!( + non_circulating_accounts, + [ + "9huDUZfxoJ7wGMTffUE7vh1xePqef7gyrLJu9NApncqA", + "GK2zqSsXLA2rwVZk347RYhh6jJpRsCA69FjLW93ZGi3B", + "HCV5dGFJXRrJ3jhDYA4DCeb9TEDTwGGYXtT3wHksu2Zr", + "25odAafVXnd63L6Hq5Cx6xGmhKqkhE2y6UrLVuqUfWZj", + "14FUT96s9swbmH7ZjpDvfEDywnAYy9zaNhv4xvezySGu", + "HbZ5FfmKWNHC7uwk6TF1hVi6TCs7dtYfdjEcuPGgzFAg", + "C7C8odR8oashR5Feyrq2tJKaXL18id1dSj2zbkDGL2C2", + "APnSR52EC1eH676m7qTBHUJ1nrGpHYpV7XKPxgRDD8gX", + "9ibqedFVnu5k4wo1mJRbH6KJ5HLBCyjpA9omPYkDeeT5", + "FopBKzQkG9pkyQqjdMFBLMQ995pSkjy83ziR4aism4c6", + "AiUHvJhTbMCcgFE2K26Ea9qCe74y3sFwqUt38iD5sfoR", + "3DndE3W53QdHSfBJiSJgzDKGvKJBoQLVmRHvy5LtqYfG", + "Eyr9P5XsjK2NUKNCnfu39eqpGoiLFgVAv1LSQgMZCwiQ", + "DE1bawNcRJB9rVm3buyMVfr8mBEoyyu73NBovf2oXJsJ", + "CakcnaRDHka2gXyfbEd2d3xsvkJkqsLw2akB3zsN1D2S", + "7Np41oeYqPefeNQEHSv1UDhYrehxin3NStELsSKCT4K2", + "GdnSyH3YtwcxFvQrVVJMm1JhTS4QVX7MFsX56uJLUfiZ", + "Mc5XB47H3DKJHym5RLa9mPzWv5snERsF3KNv5AauXK8", + "7cvkjYAkUYs4W8XcXsca7cBrEGFeSUjeZmKoNBvEwyri", + "AG3m2bAibcY8raMt4oXEGqRHwX4FWKPPJVjZxn1LySDX", + "5XdtyEDREHJXXW1CTtCsVjJRjBapAwK78ZquzvnNVRrV", + "6yKHERk8rsbmJxvMpPuwPs1ct3hRiP7xaJF2tvnGU6nK", + "CHmdL15akDcJgBkY6BP3hzs98Dqr6wbdDC5p8odvtSbq", + "FR84wZQy3Y3j2gWz6pgETUiUoJtreMEuWfbg6573UCj9", + "5q54XjQ7vDx4y6KphPeE97LUNiYGtP55spjvXAWPGBuf", + ] +); + +// Withdraw authority for autostaked accounts on mainnet-beta +solana_sdk::pubkeys!( + withdraw_authority, + "8CUUMKYNGxdgYio5CLHRHyzMEhhVRMcqefgE6dLqnVRK" +); + +#[cfg(test)] +mod tests { + use super::*; + use solana_sdk::{ + account::Account, epoch_schedule::EpochSchedule, genesis_config::GenesisConfig, + }; + use solana_stake_program::stake_state::{Authorized, Lockup, Meta, StakeState}; + use std::{collections::BTreeMap, sync::Arc}; + + fn new_from_parent(parent: &Arc) -> Bank { + Bank::new_from_parent(parent, &Pubkey::default(), parent.slot() + 1) + } + + #[test] + fn test_calculate_non_circulating_supply() { + let mut accounts: BTreeMap = BTreeMap::new(); + let balance = 10; + let num_genesis_accounts = 10; + for _ in 0..num_genesis_accounts { + accounts.insert( + Pubkey::new_rand(), + Account::new(balance, 0, &Pubkey::default()), + ); + } + let non_circulating_accounts = non_circulating_accounts(); + let num_non_circulating_accounts = non_circulating_accounts.len() as u64; + for key in non_circulating_accounts.clone() { + accounts.insert(key, Account::new(balance, 0, &Pubkey::default())); + } + + let num_stake_accounts = 3; + for _ in 0..num_stake_accounts { + let pubkey = Pubkey::new_rand(); + let meta = Meta { + authorized: Authorized::auto(&pubkey), + lockup: Lockup { + epoch: 1, + ..Lockup::default() + }, + ..Meta::default() + }; + let stake_account = Account::new_data_with_space( + balance, + &StakeState::Initialized(meta), + std::mem::size_of::(), + &solana_stake_program::id(), + ) + .unwrap(); + accounts.insert(pubkey, stake_account); + } + + let slots_per_epoch = 32; + let genesis_config = GenesisConfig { + accounts, + epoch_schedule: EpochSchedule::new(slots_per_epoch), + ..GenesisConfig::default() + }; + let mut bank = Arc::new(Bank::new(&genesis_config)); + assert_eq!( + bank.capitalization(), + (num_genesis_accounts + num_non_circulating_accounts + num_stake_accounts) * balance + ); + + let non_circulating_supply = calculate_non_circulating_supply(bank.clone()); + assert_eq!( + non_circulating_supply.lamports, + (num_non_circulating_accounts + num_stake_accounts) * balance + ); + assert_eq!( + non_circulating_supply.accounts.len(), + num_non_circulating_accounts as usize + num_stake_accounts as usize + ); + + bank = Arc::new(new_from_parent(&bank)); + let new_balance = 11; + for key in non_circulating_accounts { + bank.store_account(&key, &Account::new(new_balance, 0, &Pubkey::default())); + } + let non_circulating_supply = calculate_non_circulating_supply(bank.clone()); + assert_eq!( + non_circulating_supply.lamports, + (num_non_circulating_accounts * new_balance) + (num_stake_accounts * balance) + ); + assert_eq!( + non_circulating_supply.accounts.len(), + num_non_circulating_accounts as usize + num_stake_accounts as usize + ); + + // Advance bank an epoch, which should unlock stakes + for _ in 0..slots_per_epoch { + bank = Arc::new(new_from_parent(&bank)); + } + assert_eq!(bank.epoch(), 1); + let non_circulating_supply = calculate_non_circulating_supply(bank.clone()); + assert_eq!( + non_circulating_supply.lamports, + num_non_circulating_accounts * new_balance + ); + assert_eq!( + non_circulating_supply.accounts.len(), + num_non_circulating_accounts as usize + ); + } +} diff --git a/core/src/rpc.rs b/core/src/rpc.rs index 693aed88f..32a524c07 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -4,6 +4,7 @@ use crate::{ cluster_info::ClusterInfo, commitment::{BlockCommitmentArray, BlockCommitmentCache}, contact_info::ContactInfo, + non_circulating_supply::calculate_non_circulating_supply, storage_stage::StorageState, validator::ValidatorExit, }; @@ -302,6 +303,25 @@ impl JsonRpcRequestProcessor { ) } + fn get_supply(&self, commitment: Option) -> RpcResponse { + let bank = self.bank(commitment)?; + let non_circulating_supply = calculate_non_circulating_supply(bank.clone()); + let total_supply = bank.capitalization(); + new_response( + &bank, + RpcSupply { + total: total_supply, + circulating: total_supply - non_circulating_supply.lamports, + non_circulating: non_circulating_supply.lamports, + non_circulating_accounts: non_circulating_supply + .accounts + .iter() + .map(|pubkey| pubkey.to_string()) + .collect(), + }, + ) + } + fn get_vote_accounts( &self, commitment: Option, @@ -786,6 +806,7 @@ pub trait RpcSol { commitment: Option, ) -> Result; + // DEPRECATED #[rpc(meta, name = "getTotalSupply")] fn get_total_supply( &self, @@ -800,6 +821,13 @@ pub trait RpcSol { commitment: Option, ) -> RpcResponse>; + #[rpc(meta, name = "getSupply")] + fn get_supply( + &self, + meta: Self::Metadata, + commitment: Option, + ) -> RpcResponse; + #[rpc(meta, name = "requestAirdrop")] fn request_airdrop( &self, @@ -1213,6 +1241,18 @@ impl RpcSol for RpcSolImpl { .get_largest_accounts(commitment) } + fn get_supply( + &self, + meta: Self::Metadata, + commitment: Option, + ) -> RpcResponse { + debug!("get_supply rpc request received"); + meta.request_processor + .read() + .unwrap() + .get_supply(commitment) + } + fn request_airdrop( &self, meta: Self::Metadata, diff --git a/docs/src/apps/jsonrpc-api.md b/docs/src/apps/jsonrpc-api.md index cc6b4af8b..9b731abc2 100644 --- a/docs/src/apps/jsonrpc-api.md +++ b/docs/src/apps/jsonrpc-api.md @@ -43,8 +43,8 @@ To interact with a Solana node inside a JavaScript application, use the [solana- * [getStoragePubkeysForSlot](jsonrpc-api.md#getstoragepubkeysforslot) * [getStorageTurn](jsonrpc-api.md#getstorageturn) * [getStorageTurnRate](jsonrpc-api.md#getstorageturnrate) +* [getSupply](jsonrpc-api.md#getsupply) * [getTransactionCount](jsonrpc-api.md#gettransactioncount) -* [getTotalSupply](jsonrpc-api.md#gettotalsupply) * [getVersion](jsonrpc-api.md#getversion) * [getVoteAccounts](jsonrpc-api.md#getvoteaccounts) * [minimumLedgerSlot](jsonrpc-api.md#minimumledgerslot) @@ -944,6 +944,32 @@ curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "m {"jsonrpc":"2.0","result":1024,"id":1} ``` +### getSupply + +Returns information about the current supply. + +#### Parameters: + +* `` - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) + +#### Results: + +The result will be an RpcResponse JSON object with `value` equal to a JSON object containing: + +* `total: ` - Total supply in lamports +* `circulating: ` - Circulating supply in lamports +* `nonCirculating: ` - Non-circulating supply in lamports +* `nonCirculatingAccounts: ` - an array of account addresses of non-circulating accounts, as strings + +#### Example: + +```bash +// Request +curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0", "id":1, "method":"getCirculatingSupply"}' http://localhost:8899 +// Result +{"jsonrpc":"2.0","result":{"context":{"slot":1114},"value":{"circulating":16000,"nonCirculating":1000000,"nonCirculatingAccounts":["FEy8pTbP5fEoqMV1GdTz83byuA8EKByqYat1PKDgVAq5","9huDUZfxoJ7wGMTffUE7vh1xePqef7gyrLJu9NApncqA","3mi1GmwEE3zo2jmfDuzvjSX9ovRXsDUKHvsntpkhuLJ9","BYxEJTDerkaRWBem3XgnVcdhppktBXa2HbkHPKj2Ui4Z],total:1016000}},"id":1} +``` + ### getTransactionCount Returns the current Transaction count from the ledger @@ -966,28 +992,6 @@ curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "m {"jsonrpc":"2.0","result":268,"id":1} ``` -### getTotalSupply - -Returns the current total supply in lamports - -#### Parameters: - -* `` - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) - -#### Results: - -* `` - Total supply - -#### Example: - -```bash -// Request -curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getTotalSupply"}' http://localhost:8899 - -// Result -{"jsonrpc":"2.0","result":10126,"id":1} -``` - ### getVersion Returns the current solana versions running on the node diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 6213089aa..b2b1b5059 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -567,16 +567,20 @@ impl Bank { old_account.as_ref().map(|a| a.lamports).unwrap_or(1) } + pub fn clock(&self) -> sysvar::clock::Clock { + sysvar::clock::Clock { + slot: self.slot, + segment: get_segment_from_slot(self.slot, self.slots_per_segment), + epoch: self.epoch_schedule.get_epoch(self.slot), + leader_schedule_epoch: self.epoch_schedule.get_leader_schedule_epoch(self.slot), + unix_timestamp: self.unix_timestamp(), + } + } + fn update_clock(&self) { self.update_sysvar_account(&sysvar::clock::id(), |account| { - sysvar::clock::Clock { - slot: self.slot, - segment: get_segment_from_slot(self.slot, self.slots_per_segment), - epoch: self.epoch_schedule.get_epoch(self.slot), - leader_schedule_epoch: self.epoch_schedule.get_leader_schedule_epoch(self.slot), - unix_timestamp: self.unix_timestamp(), - } - .create_account(self.inherit_sysvar_account_balance(account)) + self.clock() + .create_account(self.inherit_sysvar_account_balance(account)) }); } diff --git a/sdk/macro/src/lib.rs b/sdk/macro/src/lib.rs index 90736556a..d1e71165c 100644 --- a/sdk/macro/src/lib.rs +++ b/sdk/macro/src/lib.rs @@ -9,8 +9,12 @@ use proc_macro2::Span; use quote::{quote, ToTokens}; use std::convert::TryFrom; use syn::{ + bracketed, parse::{Parse, ParseStream, Result}, - parse_macro_input, Expr, LitByte, LitStr, + parse_macro_input, + punctuated::Punctuated, + token::Bracket, + Expr, Ident, LitByte, LitStr, Token, }; struct Id(proc_macro2::TokenStream); @@ -18,21 +22,7 @@ impl Parse for Id { fn parse(input: ParseStream) -> Result { let token_stream = if input.peek(syn::LitStr) { let id_literal: LitStr = input.parse()?; - let id_vec = bs58::decode(id_literal.value()) - .into_vec() - .map_err(|_| syn::Error::new_spanned(&id_literal, "failed to decode base58 id"))?; - let id_array = <[u8; 32]>::try_from(<&[u8]>::clone(&&id_vec[..])).map_err(|_| { - syn::Error::new_spanned( - &id_literal, - format!("id is not 32 bytes long: len={}", id_vec.len()), - ) - })?; - let bytes = id_array.iter().map(|b| LitByte::new(*b, Span::call_site())); - quote! { - ::solana_sdk::pubkey::Pubkey::new_from_array( - [#(#bytes,)*] - ) - } + parse_pubkey(&id_literal)? } else { let expr: Expr = input.parse()?; quote! { #expr } @@ -75,3 +65,85 @@ pub fn declare_id(input: TokenStream) -> TokenStream { let id = parse_macro_input!(input as Id); TokenStream::from(quote! {#id}) } + +fn parse_pubkey(id_literal: &LitStr) -> Result { + let id_vec = bs58::decode(id_literal.value()) + .into_vec() + .map_err(|_| syn::Error::new_spanned(&id_literal, "failed to decode base58 string"))?; + let id_array = <[u8; 32]>::try_from(<&[u8]>::clone(&&id_vec[..])).map_err(|_| { + syn::Error::new_spanned( + &id_literal, + format!("pubkey array is not 32 bytes long: len={}", id_vec.len()), + ) + })?; + let bytes = id_array.iter().map(|b| LitByte::new(*b, Span::call_site())); + Ok(quote! { + ::solana_sdk::pubkey::Pubkey::new_from_array( + [#(#bytes,)*] + ) + }) +} + +struct Pubkeys { + method: Ident, + num: usize, + pubkeys: proc_macro2::TokenStream, +} +impl Parse for Pubkeys { + fn parse(input: ParseStream) -> Result { + let method = input.parse()?; + let _comma: Token![,] = input.parse()?; + let (num, pubkeys) = if input.peek(syn::LitStr) { + let id_literal: LitStr = input.parse()?; + (1, parse_pubkey(&id_literal)?) + } else if input.peek(Bracket) { + let pubkey_strings; + bracketed!(pubkey_strings in input); + let punctuated: Punctuated = + Punctuated::parse_terminated(&pubkey_strings)?; + let mut pubkeys: Punctuated = Punctuated::new(); + for string in punctuated.iter() { + pubkeys.push(parse_pubkey(string)?); + } + (pubkeys.len(), quote! {#pubkeys}) + } else { + let stream: proc_macro2::TokenStream = input.parse()?; + return Err(syn::Error::new_spanned(stream, "unexpected token")); + }; + + Ok(Pubkeys { + method, + num, + pubkeys, + }) + } +} + +impl ToTokens for Pubkeys { + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { + let Pubkeys { + method, + num, + pubkeys, + } = self; + if *num == 1 { + tokens.extend(quote! { + pub fn #method() -> ::solana_sdk::pubkey::Pubkey { + #pubkeys + } + }); + } else { + tokens.extend(quote! { + pub fn #method() -> ::std::vec::Vec<::solana_sdk::pubkey::Pubkey> { + vec![#pubkeys] + } + }); + } + } +} + +#[proc_macro] +pub fn pubkeys(input: TokenStream) -> TokenStream { + let pubkeys = parse_macro_input!(input as Pubkeys); + TokenStream::from(quote! {#pubkeys}) +} diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index 1d1a19579..dedc4ca9d 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -57,6 +57,7 @@ pub mod timing; /// assert_eq!(id(), my_id); /// ``` pub use solana_sdk_macro::declare_id; +pub use solana_sdk_macro::pubkeys; // On-chain program specific modules pub mod account_info;