Compare commits
152 Commits
dependabot
...
master
Author | SHA1 | Date | |
---|---|---|---|
|
41a9eb1bf2 | ||
|
5c7060eaeb | ||
|
d71986cecf | ||
|
5de8061bed | ||
|
6e03e0e987 | ||
|
8274959c50 | ||
|
412c080b04 | ||
|
2dad5df523 | ||
|
65d33b3715 | ||
|
4fda3a3ceb | ||
|
7c45d94ccc | ||
|
a0e3e3c193 | ||
|
75108d8e56 | ||
|
1b7d1f78de | ||
|
43d3f049e9 | ||
|
f33ad34531 | ||
|
34418cb848 | ||
|
b4fd9124bf | ||
|
8c9430359e | ||
|
ba7a2efa66 | ||
|
4ed647d8ec | ||
|
d54ec406df | ||
|
7cf80a3f62 | ||
|
3d0d7dc8fc | ||
|
052c64b01a | ||
|
3cd1aa4a1d | ||
|
47e1c9107d | ||
|
50fba01842 | ||
|
97f2eb8e65 | ||
|
f7d557d5ae | ||
|
e7e7e87c93 | ||
|
8cfc010b84 | ||
|
5e8c12ebdf | ||
|
77182fcdda | ||
|
865e17f3cf | ||
|
a8c695ba52 | ||
|
0e7b0597db | ||
|
2456a7be35 | ||
|
d20b4c9958 | ||
|
a91b0c8ea3 | ||
|
e5fc0d3f76 | ||
|
e570039ee0 | ||
|
e3ef0741be | ||
|
57ff7371b4 | ||
|
b9caa8cdfb | ||
|
255ef66a27 | ||
|
e13efa0883 | ||
|
aea17c35ae | ||
|
e146e860e2 | ||
|
b4b26894cd | ||
|
96e3555e93 | ||
|
26899359d1 | ||
|
2e0bc89ec4 | ||
|
6a474f29cd | ||
|
b6b8783323 | ||
|
096febd593 | ||
|
b8ca1bcb68 | ||
|
47b938e617 | ||
|
c29fca000b | ||
|
e7fcda1424 | ||
|
d8c45a69c3 | ||
|
a43ff3bbcb | ||
|
138f04a49f | ||
|
929753a11f | ||
|
4ac730944e | ||
|
7a4a6597c0 | ||
|
6b611e1c52 | ||
|
69e9ad5571 | ||
|
b035991c35 | ||
|
2d4d639635 | ||
|
8487030ea6 | ||
|
bdbca3362e | ||
|
1bc49d219d | ||
|
605036c117 | ||
|
474080608a | ||
|
f3aa80d3f8 | ||
|
9488a73f52 | ||
|
b6903dab6e | ||
|
865a8307e2 | ||
|
077bc4f407 | ||
|
5a48ef72fd | ||
|
9b8850f99e | ||
|
4fd184c131 | ||
|
6fbe2b936c | ||
|
77f9f7cd60 | ||
|
8a754d45b3 | ||
|
c1687b0604 | ||
|
a2be810dbc | ||
|
552d684bdc | ||
|
8f9554b5b9 | ||
|
a5e740431a | ||
|
f7b00ada1b | ||
|
b38833923d | ||
|
3871c85fd7 | ||
|
c0019edf00 | ||
|
8a73badf3d | ||
|
9ac2245970 | ||
|
60b2155bd3 | ||
|
eb478d72d1 | ||
|
85e5b1e902 | ||
|
b22abbce7d | ||
|
e14933c54d | ||
|
f8628d39e0 | ||
|
ecfa1964ff | ||
|
8eef3d9713 | ||
|
91993d89b0 | ||
|
bf13fb4c4b | ||
|
1882434c69 | ||
|
21a64db140 | ||
|
db50893fa1 | ||
|
35ee38b0f1 | ||
|
ff3b6d2b8b | ||
|
b2d502b461 | ||
|
e98575743e | ||
|
330bdc6580 | ||
|
64abd008ca | ||
|
a058f348a2 | ||
|
2ed29771f2 | ||
|
924b8ea1eb | ||
|
9e07272af8 | ||
|
2e5042d8bd | ||
|
fad9bd0538 | ||
|
c090418f26 | ||
|
6ca84f8a40 | ||
|
d2702201ca | ||
|
689064a4f4 | ||
|
03ed334ebb | ||
|
210f6a6fab | ||
|
cb1507126f | ||
|
1f136de294 | ||
|
b84521d47d | ||
|
1dd63631c0 | ||
|
e105547c14 | ||
|
fbe5e51a16 | ||
|
4dd3987451 | ||
|
781094edb2 | ||
|
c27150b1a3 | ||
|
0c2d9194dd | ||
|
a100b32b37 | ||
|
48d1af01c8 | ||
|
f7b2951c79 | ||
|
42c094739d | ||
|
206c3dd402 | ||
|
4f0e887702 | ||
|
550ca7bf92 | ||
|
fddd162645 | ||
|
fb67ff14de | ||
|
7ee1edddd1 | ||
|
afeb1d3cca | ||
|
efb9cbd8e7 | ||
|
25304ce485 | ||
|
2d1f27ed8e |
20
.mergify.yml
20
.mergify.yml
@ -24,6 +24,7 @@ pull_request_rules:
|
||||
- "#approved-reviews-by=0"
|
||||
- "#commented-reviews-by=0"
|
||||
- "#changes-requested-reviews-by=0"
|
||||
- "#review-requested=0"
|
||||
actions:
|
||||
request_reviews:
|
||||
teams:
|
||||
@ -34,6 +35,7 @@ pull_request_rules:
|
||||
- status-success=buildkite/solana
|
||||
- status-success=ci-gate
|
||||
- label=automerge
|
||||
- label!=no-automerge
|
||||
- author≠@dont-squash-my-commits
|
||||
- or:
|
||||
# only require travis success if docs files changed
|
||||
@ -60,6 +62,7 @@ pull_request_rules:
|
||||
- status-success=Travis CI - Pull Request
|
||||
- status-success=ci-gate
|
||||
- label=automerge
|
||||
- label!=no-automerge
|
||||
- author=@dont-squash-my-commits
|
||||
- or:
|
||||
# only require explorer checks if explorer files changed
|
||||
@ -88,6 +91,17 @@ pull_request_rules:
|
||||
actions:
|
||||
dismiss_reviews:
|
||||
changes_requested: true
|
||||
- name: set automerge label on mergify backport PRs
|
||||
conditions:
|
||||
- author=mergify[bot]
|
||||
- head~=^mergify/bp/
|
||||
- "#status-failure=0"
|
||||
- "-merged"
|
||||
- label!=no-automerge
|
||||
actions:
|
||||
label:
|
||||
add:
|
||||
- automerge
|
||||
- name: v1.9 feature-gate backport
|
||||
conditions:
|
||||
- label=v1.9
|
||||
@ -96,7 +110,6 @@ pull_request_rules:
|
||||
backport:
|
||||
ignore_conflicts: true
|
||||
labels:
|
||||
- automerge
|
||||
- feature-gate
|
||||
branches:
|
||||
- v1.9
|
||||
@ -107,8 +120,6 @@ pull_request_rules:
|
||||
actions:
|
||||
backport:
|
||||
ignore_conflicts: true
|
||||
labels:
|
||||
- automerge
|
||||
branches:
|
||||
- v1.9
|
||||
- name: v1.10 feature-gate backport
|
||||
@ -119,7 +130,6 @@ pull_request_rules:
|
||||
backport:
|
||||
ignore_conflicts: true
|
||||
labels:
|
||||
- automerge
|
||||
- feature-gate
|
||||
branches:
|
||||
- v1.10
|
||||
@ -130,8 +140,6 @@ pull_request_rules:
|
||||
actions:
|
||||
backport:
|
||||
ignore_conflicts: true
|
||||
labels:
|
||||
- automerge
|
||||
branches:
|
||||
- v1.10
|
||||
|
||||
|
544
Cargo.lock
generated
544
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -59,8 +59,6 @@ members = [
|
||||
"rayon-threadlimit",
|
||||
"rbpf-cli",
|
||||
"remote-wallet",
|
||||
"replica-lib",
|
||||
"replica-node",
|
||||
"rpc",
|
||||
"rpc-test",
|
||||
"runtime",
|
||||
@ -88,3 +86,6 @@ members = [
|
||||
exclude = [
|
||||
"programs/bpf",
|
||||
]
|
||||
|
||||
# This prevents a Travis CI error when building for Windows.
|
||||
resolver = "2"
|
||||
|
@ -35,7 +35,7 @@ On Linux systems you may need to install libssl-dev, pkg-config, zlib1g-dev, etc
|
||||
|
||||
```bash
|
||||
$ sudo apt-get update
|
||||
$ sudo apt-get install libssl-dev libudev-dev pkg-config zlib1g-dev llvm clang make
|
||||
$ sudo apt-get install libssl-dev libudev-dev pkg-config zlib1g-dev llvm clang cmake make
|
||||
```
|
||||
|
||||
## **2. Download the source code.**
|
||||
|
@ -11,7 +11,7 @@
|
||||
email to security@solana.com and provide your github username so we can add you
|
||||
to a new draft security advisory for further discussion.
|
||||
|
||||
Expect a response as fast as possible, within one business day at the latest.
|
||||
Expect a response as fast as possible, typically within 72 hours.
|
||||
|
||||
<a name="bounty"></a>
|
||||
## Security Bug Bounties
|
||||
|
@ -6,7 +6,10 @@ use {
|
||||
rayon::prelude::*,
|
||||
solana_measure::measure::Measure,
|
||||
solana_runtime::{
|
||||
accounts::{create_test_accounts, update_accounts_bench, Accounts},
|
||||
accounts::{
|
||||
test_utils::{create_test_accounts, update_accounts_bench},
|
||||
Accounts,
|
||||
},
|
||||
accounts_db::AccountShrinkThreshold,
|
||||
accounts_index::AccountSecondaryIndexes,
|
||||
ancestors::Ancestors,
|
||||
|
@ -15,6 +15,8 @@ log = "0.4.14"
|
||||
rayon = "1.5.1"
|
||||
serde_json = "1.0.79"
|
||||
serde_yaml = "0.8.23"
|
||||
solana-clap-utils = { path = "../clap-utils", version = "=1.11.0" }
|
||||
solana-cli-config = { path = "../cli-config", version = "=1.11.0" }
|
||||
solana-client = { path = "../client", version = "=1.11.0" }
|
||||
solana-core = { path = "../core", version = "=1.11.0" }
|
||||
solana-faucet = { path = "../faucet", version = "=1.11.0" }
|
||||
@ -24,14 +26,17 @@ solana-logger = { path = "../logger", version = "=1.11.0" }
|
||||
solana-measure = { path = "../measure", version = "=1.11.0" }
|
||||
solana-metrics = { path = "../metrics", version = "=1.11.0" }
|
||||
solana-net-utils = { path = "../net-utils", version = "=1.11.0" }
|
||||
solana-rpc = { path = "../rpc", version = "=1.11.0" }
|
||||
solana-runtime = { path = "../runtime", version = "=1.11.0" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.11.0" }
|
||||
solana-streamer = { path = "../streamer", version = "=1.11.0" }
|
||||
solana-version = { path = "../version", version = "=1.11.0" }
|
||||
thiserror = "1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
serial_test = "0.6.0"
|
||||
solana-local-cluster = { path = "../local-cluster", version = "=1.11.0" }
|
||||
solana-test-validator = { path = "../test-validator", version = "=1.11.0" }
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
targets = ["x86_64-unknown-linux-gnu"]
|
||||
|
@ -1,19 +1,21 @@
|
||||
use {
|
||||
crate::cli::Config,
|
||||
crate::{
|
||||
bench_tps_client::*,
|
||||
cli::Config,
|
||||
perf_utils::{sample_txs, SampleStats},
|
||||
},
|
||||
log::*,
|
||||
rayon::prelude::*,
|
||||
solana_client::perf_utils::{sample_txs, SampleStats},
|
||||
solana_core::gen_keys::GenKeys,
|
||||
solana_faucet::faucet::request_airdrop_transaction,
|
||||
solana_measure::measure::Measure,
|
||||
solana_metrics::{self, datapoint_info},
|
||||
solana_sdk::{
|
||||
client::Client,
|
||||
clock::{DEFAULT_MS_PER_SLOT, DEFAULT_S_PER_SLOT, MAX_PROCESSING_AGE},
|
||||
commitment_config::CommitmentConfig,
|
||||
hash::Hash,
|
||||
instruction::{AccountMeta, Instruction},
|
||||
message::Message,
|
||||
native_token::Sol,
|
||||
pubkey::Pubkey,
|
||||
signature::{Keypair, Signer},
|
||||
system_instruction, system_transaction,
|
||||
@ -22,7 +24,6 @@ use {
|
||||
},
|
||||
std::{
|
||||
collections::{HashSet, VecDeque},
|
||||
net::SocketAddr,
|
||||
process::exit,
|
||||
sync::{
|
||||
atomic::{AtomicBool, AtomicIsize, AtomicUsize, Ordering},
|
||||
@ -38,16 +39,9 @@ const MAX_TX_QUEUE_AGE: u64 = (MAX_PROCESSING_AGE as f64 * DEFAULT_S_PER_SLOT) a
|
||||
|
||||
pub const MAX_SPENDS_PER_TX: u64 = 4;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum BenchTpsError {
|
||||
AirdropFailure,
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, BenchTpsError>;
|
||||
|
||||
pub type SharedTransactions = Arc<RwLock<VecDeque<Vec<(Transaction, u64)>>>>;
|
||||
|
||||
fn get_latest_blockhash<T: Client>(client: &T) -> Hash {
|
||||
fn get_latest_blockhash<T: BenchTpsClient>(client: &T) -> Hash {
|
||||
loop {
|
||||
match client.get_latest_blockhash_with_commitment(CommitmentConfig::processed()) {
|
||||
Ok((blockhash, _)) => return blockhash,
|
||||
@ -61,7 +55,7 @@ fn get_latest_blockhash<T: Client>(client: &T) -> Hash {
|
||||
|
||||
fn wait_for_target_slots_per_epoch<T>(target_slots_per_epoch: u64, client: &Arc<T>)
|
||||
where
|
||||
T: 'static + Client + Send + Sync,
|
||||
T: 'static + BenchTpsClient + Send + Sync,
|
||||
{
|
||||
if target_slots_per_epoch != 0 {
|
||||
info!(
|
||||
@ -91,7 +85,7 @@ fn create_sampler_thread<T>(
|
||||
maxes: &Arc<RwLock<Vec<(String, SampleStats)>>>,
|
||||
) -> JoinHandle<()>
|
||||
where
|
||||
T: 'static + Client + Send + Sync,
|
||||
T: 'static + BenchTpsClient + Send + Sync,
|
||||
{
|
||||
info!("Sampling TPS every {} second...", sample_period);
|
||||
let exit_signal = exit_signal.clone();
|
||||
@ -169,7 +163,7 @@ fn create_sender_threads<T>(
|
||||
shared_tx_active_thread_count: &Arc<AtomicIsize>,
|
||||
) -> Vec<JoinHandle<()>>
|
||||
where
|
||||
T: 'static + Client + Send + Sync,
|
||||
T: 'static + BenchTpsClient + Send + Sync,
|
||||
{
|
||||
(0..threads)
|
||||
.map(|_| {
|
||||
@ -197,7 +191,7 @@ where
|
||||
|
||||
pub fn do_bench_tps<T>(client: Arc<T>, config: Config, gen_keypairs: Vec<Keypair>) -> u64
|
||||
where
|
||||
T: 'static + Client + Send + Sync,
|
||||
T: 'static + BenchTpsClient + Send + Sync,
|
||||
{
|
||||
let Config {
|
||||
id,
|
||||
@ -391,7 +385,7 @@ fn generate_txs(
|
||||
}
|
||||
}
|
||||
|
||||
fn get_new_latest_blockhash<T: Client>(client: &Arc<T>, blockhash: &Hash) -> Option<Hash> {
|
||||
fn get_new_latest_blockhash<T: BenchTpsClient>(client: &Arc<T>, blockhash: &Hash) -> Option<Hash> {
|
||||
let start = Instant::now();
|
||||
while start.elapsed().as_secs() < 5 {
|
||||
if let Ok(new_blockhash) = client.get_latest_blockhash() {
|
||||
@ -407,7 +401,7 @@ fn get_new_latest_blockhash<T: Client>(client: &Arc<T>, blockhash: &Hash) -> Opt
|
||||
None
|
||||
}
|
||||
|
||||
fn poll_blockhash<T: Client>(
|
||||
fn poll_blockhash<T: BenchTpsClient>(
|
||||
exit_signal: &Arc<AtomicBool>,
|
||||
blockhash: &Arc<RwLock<Hash>>,
|
||||
client: &Arc<T>,
|
||||
@ -449,7 +443,7 @@ fn poll_blockhash<T: Client>(
|
||||
}
|
||||
}
|
||||
|
||||
fn do_tx_transfers<T: Client>(
|
||||
fn do_tx_transfers<T: BenchTpsClient>(
|
||||
exit_signal: &Arc<AtomicBool>,
|
||||
shared_txs: &SharedTransactions,
|
||||
shared_tx_thread_count: &Arc<AtomicIsize>,
|
||||
@ -467,11 +461,7 @@ fn do_tx_transfers<T: Client>(
|
||||
};
|
||||
if let Some(txs0) = txs {
|
||||
shared_tx_thread_count.fetch_add(1, Ordering::Relaxed);
|
||||
info!(
|
||||
"Transferring 1 unit {} times... to {}",
|
||||
txs0.len(),
|
||||
client.as_ref().tpu_addr(),
|
||||
);
|
||||
info!("Transferring 1 unit {} times...", txs0.len());
|
||||
let tx_len = txs0.len();
|
||||
let transfer_start = Instant::now();
|
||||
let mut old_transactions = false;
|
||||
@ -487,7 +477,7 @@ fn do_tx_transfers<T: Client>(
|
||||
transactions.push(tx.0);
|
||||
}
|
||||
|
||||
if let Err(error) = client.async_send_batch(transactions) {
|
||||
if let Err(error) = client.send_batch(transactions) {
|
||||
warn!("send_batch_sync in do_tx_transfers failed: {}", error);
|
||||
}
|
||||
|
||||
@ -514,7 +504,11 @@ fn do_tx_transfers<T: Client>(
|
||||
}
|
||||
}
|
||||
|
||||
fn verify_funding_transfer<T: Client>(client: &Arc<T>, tx: &Transaction, amount: u64) -> bool {
|
||||
fn verify_funding_transfer<T: BenchTpsClient>(
|
||||
client: &Arc<T>,
|
||||
tx: &Transaction,
|
||||
amount: u64,
|
||||
) -> bool {
|
||||
for a in &tx.message().account_keys[1..] {
|
||||
match client.get_balance_with_commitment(a, CommitmentConfig::processed()) {
|
||||
Ok(balance) => return balance >= amount,
|
||||
@ -525,7 +519,7 @@ fn verify_funding_transfer<T: Client>(client: &Arc<T>, tx: &Transaction, amount:
|
||||
}
|
||||
|
||||
trait FundingTransactions<'a> {
|
||||
fn fund<T: 'static + Client + Send + Sync>(
|
||||
fn fund<T: 'static + BenchTpsClient + Send + Sync>(
|
||||
&mut self,
|
||||
client: &Arc<T>,
|
||||
to_fund: &[(&'a Keypair, Vec<(Pubkey, u64)>)],
|
||||
@ -533,12 +527,16 @@ trait FundingTransactions<'a> {
|
||||
);
|
||||
fn make(&mut self, to_fund: &[(&'a Keypair, Vec<(Pubkey, u64)>)]);
|
||||
fn sign(&mut self, blockhash: Hash);
|
||||
fn send<T: Client>(&self, client: &Arc<T>);
|
||||
fn verify<T: 'static + Client + Send + Sync>(&mut self, client: &Arc<T>, to_lamports: u64);
|
||||
fn send<T: BenchTpsClient>(&self, client: &Arc<T>);
|
||||
fn verify<T: 'static + BenchTpsClient + Send + Sync>(
|
||||
&mut self,
|
||||
client: &Arc<T>,
|
||||
to_lamports: u64,
|
||||
);
|
||||
}
|
||||
|
||||
impl<'a> FundingTransactions<'a> for Vec<(&'a Keypair, Transaction)> {
|
||||
fn fund<T: 'static + Client + Send + Sync>(
|
||||
fn fund<T: 'static + BenchTpsClient + Send + Sync>(
|
||||
&mut self,
|
||||
client: &Arc<T>,
|
||||
to_fund: &[(&'a Keypair, Vec<(Pubkey, u64)>)],
|
||||
@ -607,16 +605,20 @@ impl<'a> FundingTransactions<'a> for Vec<(&'a Keypair, Transaction)> {
|
||||
debug!("sign {} txs: {}us", self.len(), sign_txs.as_us());
|
||||
}
|
||||
|
||||
fn send<T: Client>(&self, client: &Arc<T>) {
|
||||
fn send<T: BenchTpsClient>(&self, client: &Arc<T>) {
|
||||
let mut send_txs = Measure::start("send_txs");
|
||||
self.iter().for_each(|(_, tx)| {
|
||||
client.async_send_transaction(tx.clone()).expect("transfer");
|
||||
client.send_transaction(tx.clone()).expect("transfer");
|
||||
});
|
||||
send_txs.stop();
|
||||
debug!("send {} txs: {}us", self.len(), send_txs.as_us());
|
||||
}
|
||||
|
||||
fn verify<T: 'static + Client + Send + Sync>(&mut self, client: &Arc<T>, to_lamports: u64) {
|
||||
fn verify<T: 'static + BenchTpsClient + Send + Sync>(
|
||||
&mut self,
|
||||
client: &Arc<T>,
|
||||
to_lamports: u64,
|
||||
) {
|
||||
let starting_txs = self.len();
|
||||
let verified_txs = Arc::new(AtomicUsize::new(0));
|
||||
let too_many_failures = Arc::new(AtomicBool::new(false));
|
||||
@ -691,7 +693,7 @@ impl<'a> FundingTransactions<'a> for Vec<(&'a Keypair, Transaction)> {
|
||||
/// fund the dests keys by spending all of the source keys into MAX_SPENDS_PER_TX
|
||||
/// on every iteration. This allows us to replay the transfers because the source is either empty,
|
||||
/// or full
|
||||
pub fn fund_keys<T: 'static + Client + Send + Sync>(
|
||||
pub fn fund_keys<T: 'static + BenchTpsClient + Send + Sync>(
|
||||
client: Arc<T>,
|
||||
source: &Keypair,
|
||||
dests: &[Keypair],
|
||||
@ -733,75 +735,6 @@ pub fn fund_keys<T: 'static + Client + Send + Sync>(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn airdrop_lamports<T: Client>(
|
||||
client: &T,
|
||||
faucet_addr: &SocketAddr,
|
||||
id: &Keypair,
|
||||
desired_balance: u64,
|
||||
) -> Result<()> {
|
||||
let starting_balance = client.get_balance(&id.pubkey()).unwrap_or(0);
|
||||
metrics_submit_lamport_balance(starting_balance);
|
||||
info!("starting balance {}", starting_balance);
|
||||
|
||||
if starting_balance < desired_balance {
|
||||
let airdrop_amount = desired_balance - starting_balance;
|
||||
info!(
|
||||
"Airdropping {:?} lamports from {} for {}",
|
||||
airdrop_amount,
|
||||
faucet_addr,
|
||||
id.pubkey(),
|
||||
);
|
||||
|
||||
let blockhash = get_latest_blockhash(client);
|
||||
match request_airdrop_transaction(faucet_addr, &id.pubkey(), airdrop_amount, blockhash) {
|
||||
Ok(transaction) => {
|
||||
let mut tries = 0;
|
||||
loop {
|
||||
tries += 1;
|
||||
let signature = client.async_send_transaction(transaction.clone()).unwrap();
|
||||
let result = client.poll_for_signature_confirmation(&signature, 1);
|
||||
|
||||
if result.is_ok() {
|
||||
break;
|
||||
}
|
||||
if tries >= 5 {
|
||||
panic!(
|
||||
"Error requesting airdrop: to addr: {:?} amount: {} {:?}",
|
||||
faucet_addr, airdrop_amount, result
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
panic!(
|
||||
"Error requesting airdrop: {:?} to addr: {:?} amount: {}",
|
||||
err, faucet_addr, airdrop_amount
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let current_balance = client
|
||||
.get_balance_with_commitment(&id.pubkey(), CommitmentConfig::processed())
|
||||
.unwrap_or_else(|e| {
|
||||
info!("airdrop error {}", e);
|
||||
starting_balance
|
||||
});
|
||||
info!("current balance {}...", current_balance);
|
||||
|
||||
metrics_submit_lamport_balance(current_balance);
|
||||
if current_balance - starting_balance != airdrop_amount {
|
||||
info!(
|
||||
"Airdrop failed! {} {} {}",
|
||||
id.pubkey(),
|
||||
current_balance,
|
||||
starting_balance
|
||||
);
|
||||
return Err(BenchTpsError::AirdropFailure);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn compute_and_report_stats(
|
||||
maxes: &Arc<RwLock<Vec<(String, SampleStats)>>>,
|
||||
sample_period: u64,
|
||||
@ -885,15 +818,33 @@ pub fn generate_keypairs(seed_keypair: &Keypair, count: u64) -> (Vec<Keypair>, u
|
||||
(rnd.gen_n_keypairs(total_keys), extra)
|
||||
}
|
||||
|
||||
pub fn generate_and_fund_keypairs<T: 'static + Client + Send + Sync>(
|
||||
pub fn generate_and_fund_keypairs<T: 'static + BenchTpsClient + Send + Sync>(
|
||||
client: Arc<T>,
|
||||
faucet_addr: Option<SocketAddr>,
|
||||
funding_key: &Keypair,
|
||||
keypair_count: usize,
|
||||
lamports_per_account: u64,
|
||||
) -> Result<Vec<Keypair>> {
|
||||
let rent = client.get_minimum_balance_for_rent_exemption(0)?;
|
||||
let lamports_per_account = lamports_per_account + rent;
|
||||
|
||||
info!("Creating {} keypairs...", keypair_count);
|
||||
let (mut keypairs, extra) = generate_keypairs(funding_key, keypair_count as u64);
|
||||
fund_keypairs(client, funding_key, &keypairs, extra, lamports_per_account)?;
|
||||
|
||||
// 'generate_keypairs' generates extra keys to be able to have size-aligned funding batches for fund_keys.
|
||||
keypairs.truncate(keypair_count);
|
||||
|
||||
Ok(keypairs)
|
||||
}
|
||||
|
||||
pub fn fund_keypairs<T: 'static + BenchTpsClient + Send + Sync>(
|
||||
client: Arc<T>,
|
||||
funding_key: &Keypair,
|
||||
keypairs: &[Keypair],
|
||||
extra: u64,
|
||||
lamports_per_account: u64,
|
||||
) -> Result<()> {
|
||||
let rent = client.get_minimum_balance_for_rent_exemption(0)?;
|
||||
info!("Get lamports...");
|
||||
|
||||
// Sample the first keypair, to prevent lamport loss on repeated solana-bench-tps executions
|
||||
@ -901,7 +852,7 @@ pub fn generate_and_fund_keypairs<T: 'static + Client + Send + Sync>(
|
||||
let first_keypair_balance = client.get_balance(&first_key).unwrap_or(0);
|
||||
|
||||
// Sample the last keypair, to check if funding was already completed
|
||||
let last_key = keypairs[keypair_count - 1].pubkey();
|
||||
let last_key = keypairs[keypairs.len() - 1].pubkey();
|
||||
let last_keypair_balance = client.get_balance(&last_key).unwrap_or(0);
|
||||
|
||||
// Repeated runs will eat up keypair balances from transaction fees. In order to quickly
|
||||
@ -930,24 +881,35 @@ pub fn generate_and_fund_keypairs<T: 'static + Client + Send + Sync>(
|
||||
funding_key_balance, max_fee, lamports_per_account, extra, total
|
||||
);
|
||||
|
||||
if client.get_balance(&funding_key.pubkey()).unwrap_or(0) < total {
|
||||
airdrop_lamports(client.as_ref(), &faucet_addr.unwrap(), funding_key, total)?;
|
||||
if funding_key_balance < total + rent {
|
||||
error!(
|
||||
"funder has {}, needed {}",
|
||||
Sol(funding_key_balance),
|
||||
Sol(total)
|
||||
);
|
||||
let latest_blockhash = get_latest_blockhash(client.as_ref());
|
||||
if client
|
||||
.request_airdrop_with_blockhash(
|
||||
&funding_key.pubkey(),
|
||||
total + rent - funding_key_balance,
|
||||
&latest_blockhash,
|
||||
)
|
||||
.is_err()
|
||||
{
|
||||
return Err(BenchTpsError::AirdropFailure);
|
||||
}
|
||||
}
|
||||
|
||||
fund_keys(
|
||||
client,
|
||||
funding_key,
|
||||
&keypairs,
|
||||
keypairs,
|
||||
total,
|
||||
max_fee,
|
||||
lamports_per_account,
|
||||
);
|
||||
}
|
||||
|
||||
// 'generate_keypairs' generates extra keys to be able to have size-aligned funding batches for fund_keys.
|
||||
keypairs.truncate(keypair_count);
|
||||
|
||||
Ok(keypairs)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -956,14 +918,14 @@ mod tests {
|
||||
super::*,
|
||||
solana_runtime::{bank::Bank, bank_client::BankClient},
|
||||
solana_sdk::{
|
||||
client::SyncClient, fee_calculator::FeeRateGovernor,
|
||||
genesis_config::create_genesis_config,
|
||||
fee_calculator::FeeRateGovernor, genesis_config::create_genesis_config,
|
||||
native_token::sol_to_lamports,
|
||||
},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_bench_tps_bank_client() {
|
||||
let (genesis_config, id) = create_genesis_config(10_000);
|
||||
let (genesis_config, id) = create_genesis_config(sol_to_lamports(10_000.0));
|
||||
let bank = Bank::new_for_tests(&genesis_config);
|
||||
let client = Arc::new(BankClient::new(bank));
|
||||
|
||||
@ -976,48 +938,49 @@ mod tests {
|
||||
|
||||
let keypair_count = config.tx_count * config.keypair_multiplier;
|
||||
let keypairs =
|
||||
generate_and_fund_keypairs(client.clone(), None, &config.id, keypair_count, 20)
|
||||
.unwrap();
|
||||
generate_and_fund_keypairs(client.clone(), &config.id, keypair_count, 20).unwrap();
|
||||
|
||||
do_bench_tps(client, config, keypairs);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bench_tps_fund_keys() {
|
||||
let (genesis_config, id) = create_genesis_config(10_000);
|
||||
let (genesis_config, id) = create_genesis_config(sol_to_lamports(10_000.0));
|
||||
let bank = Bank::new_for_tests(&genesis_config);
|
||||
let client = Arc::new(BankClient::new(bank));
|
||||
let keypair_count = 20;
|
||||
let lamports = 20;
|
||||
let rent = client.get_minimum_balance_for_rent_exemption(0).unwrap();
|
||||
|
||||
let keypairs =
|
||||
generate_and_fund_keypairs(client.clone(), None, &id, keypair_count, lamports).unwrap();
|
||||
generate_and_fund_keypairs(client.clone(), &id, keypair_count, lamports).unwrap();
|
||||
|
||||
for kp in &keypairs {
|
||||
assert_eq!(
|
||||
client
|
||||
.get_balance_with_commitment(&kp.pubkey(), CommitmentConfig::processed())
|
||||
.unwrap(),
|
||||
lamports
|
||||
lamports + rent
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bench_tps_fund_keys_with_fees() {
|
||||
let (mut genesis_config, id) = create_genesis_config(10_000);
|
||||
let (mut genesis_config, id) = create_genesis_config(sol_to_lamports(10_000.0));
|
||||
let fee_rate_governor = FeeRateGovernor::new(11, 0);
|
||||
genesis_config.fee_rate_governor = fee_rate_governor;
|
||||
let bank = Bank::new_for_tests(&genesis_config);
|
||||
let client = Arc::new(BankClient::new(bank));
|
||||
let keypair_count = 20;
|
||||
let lamports = 20;
|
||||
let rent = client.get_minimum_balance_for_rent_exemption(0).unwrap();
|
||||
|
||||
let keypairs =
|
||||
generate_and_fund_keypairs(client.clone(), None, &id, keypair_count, lamports).unwrap();
|
||||
generate_and_fund_keypairs(client.clone(), &id, keypair_count, lamports).unwrap();
|
||||
|
||||
for kp in &keypairs {
|
||||
assert_eq!(client.get_balance(&kp.pubkey()).unwrap(), lamports);
|
||||
assert_eq!(client.get_balance(&kp.pubkey()).unwrap(), lamports + rent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
87
bench-tps/src/bench_tps_client.rs
Normal file
87
bench-tps/src/bench_tps_client.rs
Normal file
@ -0,0 +1,87 @@
|
||||
use {
|
||||
solana_client::{client_error::ClientError, tpu_client::TpuSenderError},
|
||||
solana_sdk::{
|
||||
commitment_config::CommitmentConfig, epoch_info::EpochInfo, hash::Hash, message::Message,
|
||||
pubkey::Pubkey, signature::Signature, transaction::Transaction, transport::TransportError,
|
||||
},
|
||||
thiserror::Error,
|
||||
};
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum BenchTpsError {
|
||||
#[error("Airdrop failure")]
|
||||
AirdropFailure,
|
||||
#[error("IO error: {0:?}")]
|
||||
IoError(#[from] std::io::Error),
|
||||
#[error("Client error: {0:?}")]
|
||||
ClientError(#[from] ClientError),
|
||||
#[error("TpuClient error: {0:?}")]
|
||||
TpuSenderError(#[from] TpuSenderError),
|
||||
#[error("Transport error: {0:?}")]
|
||||
TransportError(#[from] TransportError),
|
||||
#[error("Custom error: {0}")]
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
pub(crate) type Result<T> = std::result::Result<T, BenchTpsError>;
|
||||
|
||||
pub trait BenchTpsClient {
|
||||
/// Send a signed transaction without confirmation
|
||||
fn send_transaction(&self, transaction: Transaction) -> Result<Signature>;
|
||||
|
||||
/// Send a batch of signed transactions without confirmation.
|
||||
fn send_batch(&self, transactions: Vec<Transaction>) -> Result<()>;
|
||||
|
||||
/// Get latest blockhash
|
||||
fn get_latest_blockhash(&self) -> Result<Hash>;
|
||||
|
||||
/// Get latest blockhash and its last valid block height, using explicit commitment
|
||||
fn get_latest_blockhash_with_commitment(
|
||||
&self,
|
||||
commitment_config: CommitmentConfig,
|
||||
) -> Result<(Hash, u64)>;
|
||||
|
||||
/// Get transaction count
|
||||
fn get_transaction_count(&self) -> Result<u64>;
|
||||
|
||||
/// Get transaction count, using explicit commitment
|
||||
fn get_transaction_count_with_commitment(
|
||||
&self,
|
||||
commitment_config: CommitmentConfig,
|
||||
) -> Result<u64>;
|
||||
|
||||
/// Get epoch info
|
||||
fn get_epoch_info(&self) -> Result<EpochInfo>;
|
||||
|
||||
/// Get account balance
|
||||
fn get_balance(&self, pubkey: &Pubkey) -> Result<u64>;
|
||||
|
||||
/// Get account balance, using explicit commitment
|
||||
fn get_balance_with_commitment(
|
||||
&self,
|
||||
pubkey: &Pubkey,
|
||||
commitment_config: CommitmentConfig,
|
||||
) -> Result<u64>;
|
||||
|
||||
/// Calculate the fee for a `Message`
|
||||
fn get_fee_for_message(&self, message: &Message) -> Result<u64>;
|
||||
|
||||
/// Get the rent-exempt minimum for an account
|
||||
fn get_minimum_balance_for_rent_exemption(&self, data_len: usize) -> Result<u64>;
|
||||
|
||||
/// Return the address of client
|
||||
fn addr(&self) -> String;
|
||||
|
||||
/// Request, submit, and confirm an airdrop transaction
|
||||
fn request_airdrop_with_blockhash(
|
||||
&self,
|
||||
pubkey: &Pubkey,
|
||||
lamports: u64,
|
||||
recent_blockhash: &Hash,
|
||||
) -> Result<Signature>;
|
||||
}
|
||||
|
||||
mod bank_client;
|
||||
mod rpc_client;
|
||||
mod thin_client;
|
||||
mod tpu_client;
|
85
bench-tps/src/bench_tps_client/bank_client.rs
Normal file
85
bench-tps/src/bench_tps_client/bank_client.rs
Normal file
@ -0,0 +1,85 @@
|
||||
use {
|
||||
crate::bench_tps_client::{BenchTpsClient, BenchTpsError, Result},
|
||||
solana_runtime::bank_client::BankClient,
|
||||
solana_sdk::{
|
||||
client::{AsyncClient, SyncClient},
|
||||
commitment_config::CommitmentConfig,
|
||||
epoch_info::EpochInfo,
|
||||
hash::Hash,
|
||||
message::Message,
|
||||
pubkey::Pubkey,
|
||||
signature::Signature,
|
||||
transaction::Transaction,
|
||||
},
|
||||
};
|
||||
|
||||
impl BenchTpsClient for BankClient {
|
||||
fn send_transaction(&self, transaction: Transaction) -> Result<Signature> {
|
||||
AsyncClient::async_send_transaction(self, transaction).map_err(|err| err.into())
|
||||
}
|
||||
fn send_batch(&self, transactions: Vec<Transaction>) -> Result<()> {
|
||||
AsyncClient::async_send_batch(self, transactions).map_err(|err| err.into())
|
||||
}
|
||||
fn get_latest_blockhash(&self) -> Result<Hash> {
|
||||
SyncClient::get_latest_blockhash(self).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_latest_blockhash_with_commitment(
|
||||
&self,
|
||||
commitment_config: CommitmentConfig,
|
||||
) -> Result<(Hash, u64)> {
|
||||
SyncClient::get_latest_blockhash_with_commitment(self, commitment_config)
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_transaction_count(&self) -> Result<u64> {
|
||||
SyncClient::get_transaction_count(self).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_transaction_count_with_commitment(
|
||||
&self,
|
||||
commitment_config: CommitmentConfig,
|
||||
) -> Result<u64> {
|
||||
SyncClient::get_transaction_count_with_commitment(self, commitment_config)
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_epoch_info(&self) -> Result<EpochInfo> {
|
||||
SyncClient::get_epoch_info(self).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_balance(&self, pubkey: &Pubkey) -> Result<u64> {
|
||||
SyncClient::get_balance(self, pubkey).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_balance_with_commitment(
|
||||
&self,
|
||||
pubkey: &Pubkey,
|
||||
commitment_config: CommitmentConfig,
|
||||
) -> Result<u64> {
|
||||
SyncClient::get_balance_with_commitment(self, pubkey, commitment_config)
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_fee_for_message(&self, message: &Message) -> Result<u64> {
|
||||
SyncClient::get_fee_for_message(self, message).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_minimum_balance_for_rent_exemption(&self, data_len: usize) -> Result<u64> {
|
||||
SyncClient::get_minimum_balance_for_rent_exemption(self, data_len).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn addr(&self) -> String {
|
||||
"Local BankClient".to_string()
|
||||
}
|
||||
|
||||
fn request_airdrop_with_blockhash(
|
||||
&self,
|
||||
_pubkey: &Pubkey,
|
||||
_lamports: u64,
|
||||
_recent_blockhash: &Hash,
|
||||
) -> Result<Signature> {
|
||||
// BankClient doesn't support airdrops
|
||||
Err(BenchTpsError::AirdropFailure)
|
||||
}
|
||||
}
|
83
bench-tps/src/bench_tps_client/rpc_client.rs
Normal file
83
bench-tps/src/bench_tps_client/rpc_client.rs
Normal file
@ -0,0 +1,83 @@
|
||||
use {
|
||||
crate::bench_tps_client::{BenchTpsClient, Result},
|
||||
solana_client::rpc_client::RpcClient,
|
||||
solana_sdk::{
|
||||
commitment_config::CommitmentConfig, epoch_info::EpochInfo, hash::Hash, message::Message,
|
||||
pubkey::Pubkey, signature::Signature, transaction::Transaction,
|
||||
},
|
||||
};
|
||||
|
||||
impl BenchTpsClient for RpcClient {
|
||||
fn send_transaction(&self, transaction: Transaction) -> Result<Signature> {
|
||||
RpcClient::send_transaction(self, &transaction).map_err(|err| err.into())
|
||||
}
|
||||
fn send_batch(&self, transactions: Vec<Transaction>) -> Result<()> {
|
||||
for transaction in transactions {
|
||||
BenchTpsClient::send_transaction(self, transaction)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn get_latest_blockhash(&self) -> Result<Hash> {
|
||||
RpcClient::get_latest_blockhash(self).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_latest_blockhash_with_commitment(
|
||||
&self,
|
||||
commitment_config: CommitmentConfig,
|
||||
) -> Result<(Hash, u64)> {
|
||||
RpcClient::get_latest_blockhash_with_commitment(self, commitment_config)
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_transaction_count(&self) -> Result<u64> {
|
||||
RpcClient::get_transaction_count(self).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_transaction_count_with_commitment(
|
||||
&self,
|
||||
commitment_config: CommitmentConfig,
|
||||
) -> Result<u64> {
|
||||
RpcClient::get_transaction_count_with_commitment(self, commitment_config)
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_epoch_info(&self) -> Result<EpochInfo> {
|
||||
RpcClient::get_epoch_info(self).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_balance(&self, pubkey: &Pubkey) -> Result<u64> {
|
||||
RpcClient::get_balance(self, pubkey).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_balance_with_commitment(
|
||||
&self,
|
||||
pubkey: &Pubkey,
|
||||
commitment_config: CommitmentConfig,
|
||||
) -> Result<u64> {
|
||||
RpcClient::get_balance_with_commitment(self, pubkey, commitment_config)
|
||||
.map(|res| res.value)
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_fee_for_message(&self, message: &Message) -> Result<u64> {
|
||||
RpcClient::get_fee_for_message(self, message).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_minimum_balance_for_rent_exemption(&self, data_len: usize) -> Result<u64> {
|
||||
RpcClient::get_minimum_balance_for_rent_exemption(self, data_len).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn addr(&self) -> String {
|
||||
self.url()
|
||||
}
|
||||
|
||||
fn request_airdrop_with_blockhash(
|
||||
&self,
|
||||
pubkey: &Pubkey,
|
||||
lamports: u64,
|
||||
recent_blockhash: &Hash,
|
||||
) -> Result<Signature> {
|
||||
RpcClient::request_airdrop_with_blockhash(self, pubkey, lamports, recent_blockhash)
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
}
|
86
bench-tps/src/bench_tps_client/thin_client.rs
Normal file
86
bench-tps/src/bench_tps_client/thin_client.rs
Normal file
@ -0,0 +1,86 @@
|
||||
use {
|
||||
crate::bench_tps_client::{BenchTpsClient, Result},
|
||||
solana_client::thin_client::ThinClient,
|
||||
solana_sdk::{
|
||||
client::{AsyncClient, Client, SyncClient},
|
||||
commitment_config::CommitmentConfig,
|
||||
epoch_info::EpochInfo,
|
||||
hash::Hash,
|
||||
message::Message,
|
||||
pubkey::Pubkey,
|
||||
signature::Signature,
|
||||
transaction::Transaction,
|
||||
},
|
||||
};
|
||||
|
||||
impl BenchTpsClient for ThinClient {
|
||||
fn send_transaction(&self, transaction: Transaction) -> Result<Signature> {
|
||||
AsyncClient::async_send_transaction(self, transaction).map_err(|err| err.into())
|
||||
}
|
||||
fn send_batch(&self, transactions: Vec<Transaction>) -> Result<()> {
|
||||
AsyncClient::async_send_batch(self, transactions).map_err(|err| err.into())
|
||||
}
|
||||
fn get_latest_blockhash(&self) -> Result<Hash> {
|
||||
SyncClient::get_latest_blockhash(self).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_latest_blockhash_with_commitment(
|
||||
&self,
|
||||
commitment_config: CommitmentConfig,
|
||||
) -> Result<(Hash, u64)> {
|
||||
SyncClient::get_latest_blockhash_with_commitment(self, commitment_config)
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_transaction_count(&self) -> Result<u64> {
|
||||
SyncClient::get_transaction_count(self).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_transaction_count_with_commitment(
|
||||
&self,
|
||||
commitment_config: CommitmentConfig,
|
||||
) -> Result<u64> {
|
||||
SyncClient::get_transaction_count_with_commitment(self, commitment_config)
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_epoch_info(&self) -> Result<EpochInfo> {
|
||||
SyncClient::get_epoch_info(self).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_balance(&self, pubkey: &Pubkey) -> Result<u64> {
|
||||
SyncClient::get_balance(self, pubkey).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_balance_with_commitment(
|
||||
&self,
|
||||
pubkey: &Pubkey,
|
||||
commitment_config: CommitmentConfig,
|
||||
) -> Result<u64> {
|
||||
SyncClient::get_balance_with_commitment(self, pubkey, commitment_config)
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_fee_for_message(&self, message: &Message) -> Result<u64> {
|
||||
SyncClient::get_fee_for_message(self, message).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_minimum_balance_for_rent_exemption(&self, data_len: usize) -> Result<u64> {
|
||||
SyncClient::get_minimum_balance_for_rent_exemption(self, data_len).map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn addr(&self) -> String {
|
||||
Client::tpu_addr(self)
|
||||
}
|
||||
|
||||
fn request_airdrop_with_blockhash(
|
||||
&self,
|
||||
pubkey: &Pubkey,
|
||||
lamports: u64,
|
||||
recent_blockhash: &Hash,
|
||||
) -> Result<Signature> {
|
||||
self.rpc_client()
|
||||
.request_airdrop_with_blockhash(pubkey, lamports, recent_blockhash)
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
}
|
99
bench-tps/src/bench_tps_client/tpu_client.rs
Normal file
99
bench-tps/src/bench_tps_client/tpu_client.rs
Normal file
@ -0,0 +1,99 @@
|
||||
use {
|
||||
crate::bench_tps_client::{BenchTpsClient, Result},
|
||||
solana_client::tpu_client::TpuClient,
|
||||
solana_sdk::{
|
||||
commitment_config::CommitmentConfig, epoch_info::EpochInfo, hash::Hash, message::Message,
|
||||
pubkey::Pubkey, signature::Signature, transaction::Transaction,
|
||||
},
|
||||
};
|
||||
|
||||
impl BenchTpsClient for TpuClient {
|
||||
fn send_transaction(&self, transaction: Transaction) -> Result<Signature> {
|
||||
let signature = transaction.signatures[0];
|
||||
self.try_send_transaction(&transaction)?;
|
||||
Ok(signature)
|
||||
}
|
||||
fn send_batch(&self, transactions: Vec<Transaction>) -> Result<()> {
|
||||
for transaction in transactions {
|
||||
BenchTpsClient::send_transaction(self, transaction)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn get_latest_blockhash(&self) -> Result<Hash> {
|
||||
self.rpc_client()
|
||||
.get_latest_blockhash()
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_latest_blockhash_with_commitment(
|
||||
&self,
|
||||
commitment_config: CommitmentConfig,
|
||||
) -> Result<(Hash, u64)> {
|
||||
self.rpc_client()
|
||||
.get_latest_blockhash_with_commitment(commitment_config)
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_transaction_count(&self) -> Result<u64> {
|
||||
self.rpc_client()
|
||||
.get_transaction_count()
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_transaction_count_with_commitment(
|
||||
&self,
|
||||
commitment_config: CommitmentConfig,
|
||||
) -> Result<u64> {
|
||||
self.rpc_client()
|
||||
.get_transaction_count_with_commitment(commitment_config)
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_epoch_info(&self) -> Result<EpochInfo> {
|
||||
self.rpc_client().get_epoch_info().map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_balance(&self, pubkey: &Pubkey) -> Result<u64> {
|
||||
self.rpc_client()
|
||||
.get_balance(pubkey)
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_balance_with_commitment(
|
||||
&self,
|
||||
pubkey: &Pubkey,
|
||||
commitment_config: CommitmentConfig,
|
||||
) -> Result<u64> {
|
||||
self.rpc_client()
|
||||
.get_balance_with_commitment(pubkey, commitment_config)
|
||||
.map(|res| res.value)
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_fee_for_message(&self, message: &Message) -> Result<u64> {
|
||||
self.rpc_client()
|
||||
.get_fee_for_message(message)
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn get_minimum_balance_for_rent_exemption(&self, data_len: usize) -> Result<u64> {
|
||||
self.rpc_client()
|
||||
.get_minimum_balance_for_rent_exemption(data_len)
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
|
||||
fn addr(&self) -> String {
|
||||
self.rpc_client().url()
|
||||
}
|
||||
|
||||
fn request_airdrop_with_blockhash(
|
||||
&self,
|
||||
pubkey: &Pubkey,
|
||||
lamports: u64,
|
||||
recent_blockhash: &Hash,
|
||||
) -> Result<Signature> {
|
||||
self.rpc_client()
|
||||
.request_airdrop_with_blockhash(pubkey, lamports, recent_blockhash)
|
||||
.map_err(|err| err.into())
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
use {
|
||||
clap::{crate_description, crate_name, App, Arg, ArgMatches},
|
||||
solana_faucet::faucet::FAUCET_PORT,
|
||||
solana_clap_utils::input_validators::{is_url, is_url_or_moniker},
|
||||
solana_cli_config::{ConfigInput, CONFIG_FILE},
|
||||
solana_sdk::{
|
||||
fee_calculator::FeeRateGovernor,
|
||||
pubkey::Pubkey,
|
||||
@ -11,10 +12,28 @@ use {
|
||||
|
||||
const NUM_LAMPORTS_PER_ACCOUNT_DEFAULT: u64 = solana_sdk::native_token::LAMPORTS_PER_SOL;
|
||||
|
||||
pub enum ExternalClientType {
|
||||
// Submits transactions to an Rpc node using an RpcClient
|
||||
RpcClient,
|
||||
// Submits transactions directly to leaders using a ThinClient, broadcasting to multiple
|
||||
// leaders when num_nodes > 1
|
||||
ThinClient,
|
||||
// Submits transactions directly to leaders using a TpuClient, broadcasting to upcoming leaders
|
||||
// via TpuClient default configuration
|
||||
TpuClient,
|
||||
}
|
||||
|
||||
impl Default for ExternalClientType {
|
||||
fn default() -> Self {
|
||||
Self::ThinClient
|
||||
}
|
||||
}
|
||||
|
||||
/// Holds the configuration for a single run of the benchmark
|
||||
pub struct Config {
|
||||
pub entrypoint_addr: SocketAddr,
|
||||
pub faucet_addr: SocketAddr,
|
||||
pub json_rpc_url: String,
|
||||
pub websocket_url: String,
|
||||
pub id: Keypair,
|
||||
pub threads: usize,
|
||||
pub num_nodes: usize,
|
||||
@ -31,13 +50,16 @@ pub struct Config {
|
||||
pub num_lamports_per_account: u64,
|
||||
pub target_slots_per_epoch: u64,
|
||||
pub target_node: Option<Pubkey>,
|
||||
pub external_client_type: ExternalClientType,
|
||||
pub use_quic: bool,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Config {
|
||||
Config {
|
||||
entrypoint_addr: SocketAddr::from(([127, 0, 0, 1], 8001)),
|
||||
faucet_addr: SocketAddr::from(([127, 0, 0, 1], FAUCET_PORT)),
|
||||
json_rpc_url: ConfigInput::default().json_rpc_url,
|
||||
websocket_url: ConfigInput::default().websocket_url,
|
||||
id: Keypair::new(),
|
||||
threads: 4,
|
||||
num_nodes: 1,
|
||||
@ -54,6 +76,8 @@ impl Default for Config {
|
||||
num_lamports_per_account: NUM_LAMPORTS_PER_ACCOUNT_DEFAULT,
|
||||
target_slots_per_epoch: 0,
|
||||
target_node: None,
|
||||
external_client_type: ExternalClientType::default(),
|
||||
use_quic: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -62,6 +86,42 @@ impl Default for Config {
|
||||
pub fn build_args<'a, 'b>(version: &'b str) -> App<'a, 'b> {
|
||||
App::new(crate_name!()).about(crate_description!())
|
||||
.version(version)
|
||||
.arg({
|
||||
let arg = Arg::with_name("config_file")
|
||||
.short("C")
|
||||
.long("config")
|
||||
.value_name("FILEPATH")
|
||||
.takes_value(true)
|
||||
.global(true)
|
||||
.help("Configuration file to use");
|
||||
if let Some(ref config_file) = *CONFIG_FILE {
|
||||
arg.default_value(config_file)
|
||||
} else {
|
||||
arg
|
||||
}
|
||||
})
|
||||
.arg(
|
||||
Arg::with_name("json_rpc_url")
|
||||
.short("u")
|
||||
.long("url")
|
||||
.value_name("URL_OR_MONIKER")
|
||||
.takes_value(true)
|
||||
.global(true)
|
||||
.validator(is_url_or_moniker)
|
||||
.help(
|
||||
"URL for Solana's JSON RPC or moniker (or their first letter): \
|
||||
[mainnet-beta, testnet, devnet, localhost]",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("websocket_url")
|
||||
.long("ws")
|
||||
.value_name("URL")
|
||||
.takes_value(true)
|
||||
.global(true)
|
||||
.validator(is_url)
|
||||
.help("WebSocket URL for the solana cluster"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("entrypoint")
|
||||
.short("n")
|
||||
@ -76,7 +136,8 @@ pub fn build_args<'a, 'b>(version: &'b str) -> App<'a, 'b> {
|
||||
.long("faucet")
|
||||
.value_name("HOST:PORT")
|
||||
.takes_value(true)
|
||||
.help("Location of the faucet; defaults to entrypoint:FAUCET_PORT"),
|
||||
.hidden(true)
|
||||
.help("Deprecated. BenchTps no longer queries the faucet directly"),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("identity")
|
||||
@ -191,6 +252,27 @@ pub fn build_args<'a, 'b>(version: &'b str) -> App<'a, 'b> {
|
||||
"Wait until epochs are this many slots long.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("rpc_client")
|
||||
.long("use-rpc-client")
|
||||
.conflicts_with("tpu_client")
|
||||
.takes_value(false)
|
||||
.help("Submit transactions with a RpcClient")
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("tpu_client")
|
||||
.long("use-tpu-client")
|
||||
.conflicts_with("rpc_client")
|
||||
.takes_value(false)
|
||||
.help("Submit transactions with a TpuClient")
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("tpu_use_quic")
|
||||
.long("tpu-use-quic")
|
||||
.takes_value(false)
|
||||
.help("Submit transactions via QUIC; only affects ThinClient (default) \
|
||||
or TpuClient sends"),
|
||||
)
|
||||
}
|
||||
|
||||
/// Parses a clap `ArgMatches` structure into a `Config`
|
||||
@ -201,6 +283,45 @@ pub fn build_args<'a, 'b>(version: &'b str) -> App<'a, 'b> {
|
||||
pub fn extract_args(matches: &ArgMatches) -> Config {
|
||||
let mut args = Config::default();
|
||||
|
||||
let config = if let Some(config_file) = matches.value_of("config_file") {
|
||||
solana_cli_config::Config::load(config_file).unwrap_or_default()
|
||||
} else {
|
||||
solana_cli_config::Config::default()
|
||||
};
|
||||
let (_, json_rpc_url) = ConfigInput::compute_json_rpc_url_setting(
|
||||
matches.value_of("json_rpc_url").unwrap_or(""),
|
||||
&config.json_rpc_url,
|
||||
);
|
||||
args.json_rpc_url = json_rpc_url;
|
||||
|
||||
let (_, websocket_url) = ConfigInput::compute_websocket_url_setting(
|
||||
matches.value_of("websocket_url").unwrap_or(""),
|
||||
&config.websocket_url,
|
||||
matches.value_of("json_rpc_url").unwrap_or(""),
|
||||
&config.json_rpc_url,
|
||||
);
|
||||
args.websocket_url = websocket_url;
|
||||
|
||||
let (_, id_path) = ConfigInput::compute_keypair_path_setting(
|
||||
matches.value_of("identity").unwrap_or(""),
|
||||
&config.keypair_path,
|
||||
);
|
||||
if let Ok(id) = read_keypair_file(id_path) {
|
||||
args.id = id;
|
||||
} else if matches.is_present("identity") {
|
||||
panic!("could not parse identity path");
|
||||
}
|
||||
|
||||
if matches.is_present("tpu_client") {
|
||||
args.external_client_type = ExternalClientType::TpuClient;
|
||||
} else if matches.is_present("rpc_client") {
|
||||
args.external_client_type = ExternalClientType::RpcClient;
|
||||
}
|
||||
|
||||
if matches.is_present("tpu_use_quic") {
|
||||
args.use_quic = true;
|
||||
}
|
||||
|
||||
if let Some(addr) = matches.value_of("entrypoint") {
|
||||
args.entrypoint_addr = solana_net_utils::parse_host_port(addr).unwrap_or_else(|e| {
|
||||
eprintln!("failed to parse entrypoint address: {}", e);
|
||||
@ -208,18 +329,6 @@ pub fn extract_args(matches: &ArgMatches) -> Config {
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(addr) = matches.value_of("faucet") {
|
||||
args.faucet_addr = solana_net_utils::parse_host_port(addr).unwrap_or_else(|e| {
|
||||
eprintln!("failed to parse faucet address: {}", e);
|
||||
exit(1)
|
||||
});
|
||||
}
|
||||
|
||||
if matches.is_present("identity") {
|
||||
args.id = read_keypair_file(matches.value_of("identity").unwrap())
|
||||
.expect("can't read client identity");
|
||||
}
|
||||
|
||||
if let Some(t) = matches.value_of("threads") {
|
||||
args.threads = t.to_string().parse().expect("can't parse threads");
|
||||
}
|
||||
|
72
bench-tps/src/keypairs.rs
Normal file
72
bench-tps/src/keypairs.rs
Normal file
@ -0,0 +1,72 @@
|
||||
use {
|
||||
crate::{
|
||||
bench::{fund_keypairs, generate_and_fund_keypairs},
|
||||
bench_tps_client::BenchTpsClient,
|
||||
},
|
||||
log::*,
|
||||
solana_genesis::Base64Account,
|
||||
solana_sdk::signature::{Keypair, Signer},
|
||||
std::{collections::HashMap, fs::File, path::Path, process::exit, sync::Arc},
|
||||
};
|
||||
|
||||
pub fn get_keypairs<T>(
|
||||
client: Arc<T>,
|
||||
id: &Keypair,
|
||||
keypair_count: usize,
|
||||
num_lamports_per_account: u64,
|
||||
client_ids_and_stake_file: &str,
|
||||
read_from_client_file: bool,
|
||||
) -> Vec<Keypair>
|
||||
where
|
||||
T: 'static + BenchTpsClient + Send + Sync,
|
||||
{
|
||||
if read_from_client_file {
|
||||
let path = Path::new(client_ids_and_stake_file);
|
||||
let file = File::open(path).unwrap();
|
||||
|
||||
info!("Reading {}", client_ids_and_stake_file);
|
||||
let accounts: HashMap<String, Base64Account> = serde_yaml::from_reader(file).unwrap();
|
||||
let mut keypairs = vec![];
|
||||
let mut last_balance = 0;
|
||||
|
||||
accounts
|
||||
.into_iter()
|
||||
.for_each(|(keypair, primordial_account)| {
|
||||
let bytes: Vec<u8> = serde_json::from_str(keypair.as_str()).unwrap();
|
||||
keypairs.push(Keypair::from_bytes(&bytes).unwrap());
|
||||
last_balance = primordial_account.balance;
|
||||
});
|
||||
|
||||
if keypairs.len() < keypair_count {
|
||||
eprintln!(
|
||||
"Expected {} accounts in {}, only received {} (--tx_count mismatch?)",
|
||||
keypair_count,
|
||||
client_ids_and_stake_file,
|
||||
keypairs.len(),
|
||||
);
|
||||
exit(1);
|
||||
}
|
||||
// Sort keypairs so that do_bench_tps() uses the same subset of accounts for each run.
|
||||
// This prevents the amount of storage needed for bench-tps accounts from creeping up
|
||||
// across multiple runs.
|
||||
keypairs.sort_by_key(|x| x.pubkey().to_string());
|
||||
fund_keypairs(
|
||||
client,
|
||||
id,
|
||||
&keypairs,
|
||||
keypairs.len().saturating_sub(keypair_count) as u64,
|
||||
last_balance,
|
||||
)
|
||||
.unwrap_or_else(|e| {
|
||||
eprintln!("Error could not fund keys: {:?}", e);
|
||||
exit(1);
|
||||
});
|
||||
keypairs
|
||||
} else {
|
||||
generate_and_fund_keypairs(client, id, keypair_count, num_lamports_per_account)
|
||||
.unwrap_or_else(|e| {
|
||||
eprintln!("Error could not fund keys: {:?}", e);
|
||||
exit(1);
|
||||
})
|
||||
}
|
||||
}
|
@ -1,3 +1,6 @@
|
||||
#![allow(clippy::integer_arithmetic)]
|
||||
pub mod bench;
|
||||
pub mod bench_tps_client;
|
||||
pub mod cli;
|
||||
pub mod keypairs;
|
||||
mod perf_utils;
|
||||
|
@ -2,15 +2,19 @@
|
||||
use {
|
||||
log::*,
|
||||
solana_bench_tps::{
|
||||
bench::{do_bench_tps, generate_and_fund_keypairs, generate_keypairs},
|
||||
cli,
|
||||
bench::{do_bench_tps, generate_keypairs},
|
||||
cli::{self, ExternalClientType},
|
||||
keypairs::get_keypairs,
|
||||
},
|
||||
solana_client::{
|
||||
connection_cache,
|
||||
rpc_client::RpcClient,
|
||||
tpu_client::{TpuClient, TpuClientConfig},
|
||||
},
|
||||
solana_genesis::Base64Account,
|
||||
solana_gossip::gossip_service::{discover_cluster, get_client, get_multi_client},
|
||||
solana_sdk::{
|
||||
fee_calculator::FeeRateGovernor,
|
||||
signature::{Keypair, Signer},
|
||||
system_program,
|
||||
commitment_config::CommitmentConfig, fee_calculator::FeeRateGovernor, system_program,
|
||||
},
|
||||
solana_streamer::socket::SocketAddrSpace,
|
||||
std::{collections::HashMap, fs::File, io::prelude::*, path::Path, process::exit, sync::Arc},
|
||||
@ -28,7 +32,8 @@ fn main() {
|
||||
|
||||
let cli::Config {
|
||||
entrypoint_addr,
|
||||
faucet_addr,
|
||||
json_rpc_url,
|
||||
websocket_url,
|
||||
id,
|
||||
num_nodes,
|
||||
tx_count,
|
||||
@ -40,6 +45,8 @@ fn main() {
|
||||
multi_client,
|
||||
num_lamports_per_account,
|
||||
target_node,
|
||||
external_client_type,
|
||||
use_quic,
|
||||
..
|
||||
} = &cli_config;
|
||||
|
||||
@ -75,83 +82,93 @@ fn main() {
|
||||
}
|
||||
|
||||
info!("Connecting to the cluster");
|
||||
let nodes = discover_cluster(entrypoint_addr, *num_nodes, SocketAddrSpace::Unspecified)
|
||||
.unwrap_or_else(|err| {
|
||||
eprintln!("Failed to discover {} nodes: {:?}", num_nodes, err);
|
||||
exit(1);
|
||||
});
|
||||
|
||||
let client = if *multi_client {
|
||||
let (client, num_clients) = get_multi_client(&nodes, &SocketAddrSpace::Unspecified);
|
||||
if nodes.len() < num_clients {
|
||||
eprintln!(
|
||||
"Error: Insufficient nodes discovered. Expecting {} or more",
|
||||
num_nodes
|
||||
);
|
||||
exit(1);
|
||||
}
|
||||
Arc::new(client)
|
||||
} else if let Some(target_node) = target_node {
|
||||
info!("Searching for target_node: {:?}", target_node);
|
||||
let mut target_client = None;
|
||||
for node in nodes {
|
||||
if node.id == *target_node {
|
||||
target_client = Some(Arc::new(get_client(&[node], &SocketAddrSpace::Unspecified)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
target_client.unwrap_or_else(|| {
|
||||
eprintln!("Target node {} not found", target_node);
|
||||
exit(1);
|
||||
})
|
||||
} else {
|
||||
Arc::new(get_client(&nodes, &SocketAddrSpace::Unspecified))
|
||||
};
|
||||
|
||||
let keypairs = if *read_from_client_file {
|
||||
let path = Path::new(&client_ids_and_stake_file);
|
||||
let file = File::open(path).unwrap();
|
||||
|
||||
info!("Reading {}", client_ids_and_stake_file);
|
||||
let accounts: HashMap<String, Base64Account> = serde_yaml::from_reader(file).unwrap();
|
||||
let mut keypairs = vec![];
|
||||
let mut last_balance = 0;
|
||||
|
||||
accounts
|
||||
.into_iter()
|
||||
.for_each(|(keypair, primordial_account)| {
|
||||
let bytes: Vec<u8> = serde_json::from_str(keypair.as_str()).unwrap();
|
||||
keypairs.push(Keypair::from_bytes(&bytes).unwrap());
|
||||
last_balance = primordial_account.balance;
|
||||
});
|
||||
|
||||
if keypairs.len() < keypair_count {
|
||||
eprintln!(
|
||||
"Expected {} accounts in {}, only received {} (--tx_count mismatch?)",
|
||||
match external_client_type {
|
||||
ExternalClientType::RpcClient => {
|
||||
let client = Arc::new(RpcClient::new_with_commitment(
|
||||
json_rpc_url.to_string(),
|
||||
CommitmentConfig::confirmed(),
|
||||
));
|
||||
let keypairs = get_keypairs(
|
||||
client.clone(),
|
||||
id,
|
||||
keypair_count,
|
||||
*num_lamports_per_account,
|
||||
client_ids_and_stake_file,
|
||||
keypairs.len(),
|
||||
*read_from_client_file,
|
||||
);
|
||||
exit(1);
|
||||
do_bench_tps(client, cli_config, keypairs);
|
||||
}
|
||||
// Sort keypairs so that do_bench_tps() uses the same subset of accounts for each run.
|
||||
// This prevents the amount of storage needed for bench-tps accounts from creeping up
|
||||
// across multiple runs.
|
||||
keypairs.sort_by_key(|x| x.pubkey().to_string());
|
||||
keypairs
|
||||
} else {
|
||||
generate_and_fund_keypairs(
|
||||
client.clone(),
|
||||
Some(*faucet_addr),
|
||||
id,
|
||||
keypair_count,
|
||||
*num_lamports_per_account,
|
||||
)
|
||||
.unwrap_or_else(|e| {
|
||||
eprintln!("Error could not fund keys: {:?}", e);
|
||||
exit(1);
|
||||
})
|
||||
};
|
||||
|
||||
do_bench_tps(client, cli_config, keypairs);
|
||||
ExternalClientType::ThinClient => {
|
||||
let nodes = discover_cluster(entrypoint_addr, *num_nodes, SocketAddrSpace::Unspecified)
|
||||
.unwrap_or_else(|err| {
|
||||
eprintln!("Failed to discover {} nodes: {:?}", num_nodes, err);
|
||||
exit(1);
|
||||
});
|
||||
if *use_quic {
|
||||
connection_cache::set_use_quic(true);
|
||||
}
|
||||
let client = if *multi_client {
|
||||
let (client, num_clients) = get_multi_client(&nodes, &SocketAddrSpace::Unspecified);
|
||||
if nodes.len() < num_clients {
|
||||
eprintln!(
|
||||
"Error: Insufficient nodes discovered. Expecting {} or more",
|
||||
num_nodes
|
||||
);
|
||||
exit(1);
|
||||
}
|
||||
Arc::new(client)
|
||||
} else if let Some(target_node) = target_node {
|
||||
info!("Searching for target_node: {:?}", target_node);
|
||||
let mut target_client = None;
|
||||
for node in nodes {
|
||||
if node.id == *target_node {
|
||||
target_client =
|
||||
Some(Arc::new(get_client(&[node], &SocketAddrSpace::Unspecified)));
|
||||
break;
|
||||
}
|
||||
}
|
||||
target_client.unwrap_or_else(|| {
|
||||
eprintln!("Target node {} not found", target_node);
|
||||
exit(1);
|
||||
})
|
||||
} else {
|
||||
Arc::new(get_client(&nodes, &SocketAddrSpace::Unspecified))
|
||||
};
|
||||
let keypairs = get_keypairs(
|
||||
client.clone(),
|
||||
id,
|
||||
keypair_count,
|
||||
*num_lamports_per_account,
|
||||
client_ids_and_stake_file,
|
||||
*read_from_client_file,
|
||||
);
|
||||
do_bench_tps(client, cli_config, keypairs);
|
||||
}
|
||||
ExternalClientType::TpuClient => {
|
||||
let rpc_client = Arc::new(RpcClient::new_with_commitment(
|
||||
json_rpc_url.to_string(),
|
||||
CommitmentConfig::confirmed(),
|
||||
));
|
||||
if *use_quic {
|
||||
connection_cache::set_use_quic(true);
|
||||
}
|
||||
let client = Arc::new(
|
||||
TpuClient::new(rpc_client, websocket_url, TpuClientConfig::default())
|
||||
.unwrap_or_else(|err| {
|
||||
eprintln!("Could not create TpuClient {:?}", err);
|
||||
exit(1);
|
||||
}),
|
||||
);
|
||||
let keypairs = get_keypairs(
|
||||
client.clone(),
|
||||
id,
|
||||
keypair_count,
|
||||
*num_lamports_per_account,
|
||||
client_ids_and_stake_file,
|
||||
*read_from_client_file,
|
||||
);
|
||||
do_bench_tps(client, cli_config, keypairs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
use {
|
||||
crate::bench_tps_client::BenchTpsClient,
|
||||
log::*,
|
||||
solana_sdk::{client::Client, commitment_config::CommitmentConfig, timing::duration_as_s},
|
||||
solana_sdk::{commitment_config::CommitmentConfig, timing::duration_as_s},
|
||||
std::{
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
@ -27,7 +28,7 @@ pub fn sample_txs<T>(
|
||||
sample_period: u64,
|
||||
client: &Arc<T>,
|
||||
) where
|
||||
T: Client,
|
||||
T: BenchTpsClient,
|
||||
{
|
||||
let mut max_tps = 0.0;
|
||||
let mut total_elapsed;
|
||||
@ -81,10 +82,7 @@ pub fn sample_txs<T>(
|
||||
elapsed: total_elapsed,
|
||||
txs: total_txs,
|
||||
};
|
||||
sample_stats
|
||||
.write()
|
||||
.unwrap()
|
||||
.push((client.tpu_addr(), stats));
|
||||
sample_stats.write().unwrap().push((client.addr(), stats));
|
||||
return;
|
||||
}
|
||||
sleep(Duration::from_secs(sample_period));
|
@ -6,15 +6,24 @@ use {
|
||||
bench::{do_bench_tps, generate_and_fund_keypairs},
|
||||
cli::Config,
|
||||
},
|
||||
solana_client::thin_client::create_client,
|
||||
solana_client::{
|
||||
rpc_client::RpcClient,
|
||||
thin_client::create_client,
|
||||
tpu_client::{TpuClient, TpuClientConfig},
|
||||
},
|
||||
solana_core::validator::ValidatorConfig,
|
||||
solana_faucet::faucet::run_local_faucet_with_port,
|
||||
solana_faucet::faucet::{run_local_faucet, run_local_faucet_with_port},
|
||||
solana_local_cluster::{
|
||||
local_cluster::{ClusterConfig, LocalCluster},
|
||||
validator_configs::make_identical_validator_configs,
|
||||
},
|
||||
solana_sdk::signature::{Keypair, Signer},
|
||||
solana_rpc::rpc::JsonRpcConfig,
|
||||
solana_sdk::{
|
||||
commitment_config::CommitmentConfig,
|
||||
signature::{Keypair, Signer},
|
||||
},
|
||||
solana_streamer::socket::SocketAddrSpace,
|
||||
solana_test_validator::TestValidator,
|
||||
std::{sync::Arc, time::Duration},
|
||||
};
|
||||
|
||||
@ -22,13 +31,34 @@ fn test_bench_tps_local_cluster(config: Config) {
|
||||
let native_instruction_processors = vec![];
|
||||
|
||||
solana_logger::setup();
|
||||
|
||||
let faucet_keypair = Keypair::new();
|
||||
let faucet_pubkey = faucet_keypair.pubkey();
|
||||
let (addr_sender, addr_receiver) = unbounded();
|
||||
run_local_faucet_with_port(faucet_keypair, addr_sender, None, 0);
|
||||
let faucet_addr = addr_receiver
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
.expect("run_local_faucet")
|
||||
.expect("faucet_addr");
|
||||
|
||||
const NUM_NODES: usize = 1;
|
||||
let mut validator_config = ValidatorConfig::default_for_test();
|
||||
validator_config.rpc_config = JsonRpcConfig {
|
||||
faucet_addr: Some(faucet_addr),
|
||||
..JsonRpcConfig::default_for_test()
|
||||
};
|
||||
let cluster = LocalCluster::new(
|
||||
&mut ClusterConfig {
|
||||
node_stakes: vec![999_990; NUM_NODES],
|
||||
cluster_lamports: 200_000_000,
|
||||
validator_configs: make_identical_validator_configs(
|
||||
&ValidatorConfig::default_for_test(),
|
||||
&ValidatorConfig {
|
||||
rpc_config: JsonRpcConfig {
|
||||
faucet_addr: Some(faucet_addr),
|
||||
..JsonRpcConfig::default_for_test()
|
||||
},
|
||||
..ValidatorConfig::default_for_test()
|
||||
},
|
||||
NUM_NODES,
|
||||
),
|
||||
native_instruction_processors,
|
||||
@ -37,31 +67,55 @@ fn test_bench_tps_local_cluster(config: Config) {
|
||||
SocketAddrSpace::Unspecified,
|
||||
);
|
||||
|
||||
let faucet_keypair = Keypair::new();
|
||||
cluster.transfer(
|
||||
&cluster.funding_keypair,
|
||||
&faucet_keypair.pubkey(),
|
||||
100_000_000,
|
||||
);
|
||||
cluster.transfer(&cluster.funding_keypair, &faucet_pubkey, 100_000_000);
|
||||
|
||||
let client = Arc::new(create_client(
|
||||
cluster.entry_point_info.rpc,
|
||||
cluster.entry_point_info.tpu,
|
||||
));
|
||||
|
||||
let (addr_sender, addr_receiver) = unbounded();
|
||||
run_local_faucet_with_port(faucet_keypair, addr_sender, None, 0);
|
||||
let faucet_addr = addr_receiver
|
||||
.recv_timeout(Duration::from_secs(2))
|
||||
.expect("run_local_faucet")
|
||||
.expect("faucet_addr");
|
||||
let lamports_per_account = 100;
|
||||
|
||||
let keypair_count = config.tx_count * config.keypair_multiplier;
|
||||
let keypairs = generate_and_fund_keypairs(
|
||||
client.clone(),
|
||||
&config.id,
|
||||
keypair_count,
|
||||
lamports_per_account,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let _total = do_bench_tps(client, config, keypairs);
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
assert!(_total > 100);
|
||||
}
|
||||
|
||||
fn test_bench_tps_test_validator(config: Config) {
|
||||
solana_logger::setup();
|
||||
|
||||
let mint_keypair = Keypair::new();
|
||||
let mint_pubkey = mint_keypair.pubkey();
|
||||
|
||||
let faucet_addr = run_local_faucet(mint_keypair, None);
|
||||
|
||||
let test_validator =
|
||||
TestValidator::with_no_fees(mint_pubkey, Some(faucet_addr), SocketAddrSpace::Unspecified);
|
||||
|
||||
let rpc_client = Arc::new(RpcClient::new_with_commitment(
|
||||
test_validator.rpc_url(),
|
||||
CommitmentConfig::processed(),
|
||||
));
|
||||
let websocket_url = test_validator.rpc_pubsub_url();
|
||||
|
||||
let client =
|
||||
Arc::new(TpuClient::new(rpc_client, &websocket_url, TpuClientConfig::default()).unwrap());
|
||||
|
||||
let lamports_per_account = 100;
|
||||
|
||||
let keypair_count = config.tx_count * config.keypair_multiplier;
|
||||
let keypairs = generate_and_fund_keypairs(
|
||||
client.clone(),
|
||||
Some(faucet_addr),
|
||||
&config.id,
|
||||
keypair_count,
|
||||
lamports_per_account,
|
||||
@ -83,3 +137,13 @@ fn test_bench_tps_local_cluster_solana() {
|
||||
..Config::default()
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[serial]
|
||||
fn test_bench_tps_tpu_client() {
|
||||
test_bench_tps_test_validator(Config {
|
||||
tx_count: 100,
|
||||
duration: Duration::from_secs(10),
|
||||
..Config::default()
|
||||
});
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
#![allow(dead_code)]
|
||||
|
||||
use {
|
||||
crate::{
|
||||
bucket::Bucket,
|
||||
@ -57,8 +58,18 @@ impl IndexEntry {
|
||||
.expect("New storage offset must fit into 7 bytes!")
|
||||
}
|
||||
|
||||
/// return closest bucket index fit for the slot slice.
|
||||
/// Since bucket size is 2^index, the return value is
|
||||
/// min index, such that 2^index >= num_slots
|
||||
/// index = ceiling(log2(num_slots))
|
||||
/// special case, when slot slice empty, return 0th index.
|
||||
pub fn data_bucket_from_num_slots(num_slots: Slot) -> u64 {
|
||||
(num_slots as f64).log2().ceil() as u64 // use int log here?
|
||||
// Compute the ceiling of log2 for integer
|
||||
if num_slots == 0 {
|
||||
0
|
||||
} else {
|
||||
(Slot::BITS - (num_slots - 1).leading_zeros()) as u64
|
||||
}
|
||||
}
|
||||
|
||||
pub fn data_bucket_ix(&self) -> u64 {
|
||||
@ -153,4 +164,23 @@ mod tests {
|
||||
let mut index = IndexEntry::new(Pubkey::new_unique());
|
||||
index.set_storage_offset(too_big);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_data_bucket_from_num_slots() {
|
||||
for n in 0..512 {
|
||||
assert_eq!(
|
||||
IndexEntry::data_bucket_from_num_slots(n),
|
||||
(n as f64).log2().ceil() as u64
|
||||
);
|
||||
}
|
||||
assert_eq!(IndexEntry::data_bucket_from_num_slots(u32::MAX as u64), 32);
|
||||
assert_eq!(
|
||||
IndexEntry::data_bucket_from_num_slots(u32::MAX as u64 + 1),
|
||||
32
|
||||
);
|
||||
assert_eq!(
|
||||
IndexEntry::data_bucket_from_num_slots(u32::MAX as u64 + 2),
|
||||
33
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -148,6 +148,18 @@ all_test_steps() {
|
||||
|
||||
# Full test suite
|
||||
command_step stable ". ci/rust-version.sh; ci/docker-run.sh \$\$rust_stable_docker_image ci/test-stable.sh" 70
|
||||
|
||||
# Docs tests
|
||||
if affects \
|
||||
.rs$ \
|
||||
^ci/rust-version.sh \
|
||||
^ci/test-docs.sh \
|
||||
; then
|
||||
command_step doctest "ci/test-docs.sh" 15
|
||||
else
|
||||
annotate --style info --context test-docs \
|
||||
"Docs skipped as no .rs files were modified"
|
||||
fi
|
||||
wait_step
|
||||
|
||||
# BPF test suite
|
||||
|
@ -1,4 +1,4 @@
|
||||
FROM solanalabs/rust:1.59.0
|
||||
FROM solanalabs/rust:1.60.0
|
||||
ARG date
|
||||
|
||||
RUN set -x \
|
||||
|
@ -3,8 +3,14 @@ set -ex
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
platform=()
|
||||
if [[ $(uname -m) = arm64 ]]; then
|
||||
# Ref: https://blog.jaimyn.dev/how-to-build-multi-architecture-docker-images-on-an-m1-mac/#tldr
|
||||
platform+=(--platform linux/amd64)
|
||||
fi
|
||||
|
||||
nightlyDate=${1:-$(date +%Y-%m-%d)}
|
||||
docker build -t solanalabs/rust-nightly:"$nightlyDate" --build-arg date="$nightlyDate" .
|
||||
docker build "${platform[@]}" -t solanalabs/rust-nightly:"$nightlyDate" --build-arg date="$nightlyDate" .
|
||||
|
||||
maybeEcho=
|
||||
if [[ -z $CI ]]; then
|
||||
|
@ -1,6 +1,6 @@
|
||||
# Note: when the rust version is changed also modify
|
||||
# ci/rust-version.sh to pick up the new image tag
|
||||
FROM rust:1.59.0
|
||||
FROM rust:1.60.0
|
||||
|
||||
# Add Google Protocol Buffers for Libra's metrics library.
|
||||
ENV PROTOC_VERSION 3.8.0
|
||||
|
@ -3,7 +3,14 @@ set -ex
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
docker build -t solanalabs/rust .
|
||||
|
||||
platform=()
|
||||
if [[ $(uname -m) = arm64 ]]; then
|
||||
# Ref: https://blog.jaimyn.dev/how-to-build-multi-architecture-docker-images-on-an-m1-mac/#tldr
|
||||
platform+=(--platform linux/amd64)
|
||||
fi
|
||||
|
||||
docker build "${platform[@]}" -t solanalabs/rust .
|
||||
|
||||
read -r rustc version _ < <(docker run solanalabs/rust rustc --version)
|
||||
[[ $rustc = rustc ]]
|
||||
|
@ -18,13 +18,13 @@
|
||||
if [[ -n $RUST_STABLE_VERSION ]]; then
|
||||
stable_version="$RUST_STABLE_VERSION"
|
||||
else
|
||||
stable_version=1.59.0
|
||||
stable_version=1.60.0
|
||||
fi
|
||||
|
||||
if [[ -n $RUST_NIGHTLY_VERSION ]]; then
|
||||
nightly_version="$RUST_NIGHTLY_VERSION"
|
||||
else
|
||||
nightly_version=2022-02-24
|
||||
nightly_version=2022-04-01
|
||||
fi
|
||||
|
||||
|
||||
|
1
ci/test-docs.sh
Symbolic link
1
ci/test-docs.sh
Symbolic link
@ -0,0 +1 @@
|
||||
test-stable.sh
|
@ -30,7 +30,7 @@ JOBS=$((JOBS>NPROC ? NPROC : JOBS))
|
||||
echo "Executing $testName"
|
||||
case $testName in
|
||||
test-stable)
|
||||
_ "$cargo" stable test --jobs "$JOBS" --all --exclude solana-local-cluster ${V:+--verbose} -- --nocapture
|
||||
_ "$cargo" stable test --jobs "$JOBS" --all --tests --exclude solana-local-cluster ${V:+--verbose} -- --nocapture
|
||||
;;
|
||||
test-stable-bpf)
|
||||
# Clear the C dependency files, if dependency moves these files are not regenerated
|
||||
@ -130,6 +130,10 @@ test-wasm)
|
||||
done
|
||||
exit 0
|
||||
;;
|
||||
test-docs)
|
||||
_ "$cargo" stable test --jobs "$JOBS" --all --doc --exclude solana-local-cluster ${V:+--verbose} -- --nocapture
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Error: Unknown test: $testName"
|
||||
;;
|
||||
|
@ -18,7 +18,7 @@ solana-remote-wallet = { path = "../remote-wallet", version = "=1.11.0", default
|
||||
solana-sdk = { path = "../sdk", version = "=1.11.0" }
|
||||
thiserror = "1.0.30"
|
||||
tiny-bip39 = "0.8.2"
|
||||
uriparse = "0.6.3"
|
||||
uriparse = "0.6.4"
|
||||
url = "2.2.2"
|
||||
|
||||
[dev-dependencies]
|
||||
|
@ -15,6 +15,8 @@ lazy_static = "1.4.0"
|
||||
serde = "1.0.136"
|
||||
serde_derive = "1.0.103"
|
||||
serde_yaml = "0.8.23"
|
||||
solana-clap-utils = { path = "../clap-utils", version = "=1.11.0" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.11.0" }
|
||||
url = "2.2.2"
|
||||
|
||||
[dev-dependencies]
|
||||
|
126
cli-config/src/config_input.rs
Normal file
126
cli-config/src/config_input.rs
Normal file
@ -0,0 +1,126 @@
|
||||
use {
|
||||
crate::Config, solana_clap_utils::input_validators::normalize_to_url_if_moniker,
|
||||
solana_sdk::commitment_config::CommitmentConfig, std::str::FromStr,
|
||||
};
|
||||
|
||||
pub enum SettingType {
|
||||
Explicit,
|
||||
Computed,
|
||||
SystemDefault,
|
||||
}
|
||||
|
||||
pub struct ConfigInput {
|
||||
pub json_rpc_url: String,
|
||||
pub websocket_url: String,
|
||||
pub keypair_path: String,
|
||||
pub commitment: CommitmentConfig,
|
||||
}
|
||||
|
||||
impl ConfigInput {
|
||||
fn default_keypair_path() -> String {
|
||||
Config::default().keypair_path
|
||||
}
|
||||
|
||||
fn default_json_rpc_url() -> String {
|
||||
Config::default().json_rpc_url
|
||||
}
|
||||
|
||||
fn default_websocket_url() -> String {
|
||||
Config::default().websocket_url
|
||||
}
|
||||
|
||||
fn default_commitment() -> CommitmentConfig {
|
||||
CommitmentConfig::confirmed()
|
||||
}
|
||||
|
||||
fn first_nonempty_setting(
|
||||
settings: std::vec::Vec<(SettingType, String)>,
|
||||
) -> (SettingType, String) {
|
||||
settings
|
||||
.into_iter()
|
||||
.find(|(_, value)| !value.is_empty())
|
||||
.expect("no nonempty setting")
|
||||
}
|
||||
|
||||
fn first_setting_is_some<T>(
|
||||
settings: std::vec::Vec<(SettingType, Option<T>)>,
|
||||
) -> (SettingType, T) {
|
||||
let (setting_type, setting_option) = settings
|
||||
.into_iter()
|
||||
.find(|(_, value)| value.is_some())
|
||||
.expect("all settings none");
|
||||
(setting_type, setting_option.unwrap())
|
||||
}
|
||||
|
||||
pub fn compute_websocket_url_setting(
|
||||
websocket_cmd_url: &str,
|
||||
websocket_cfg_url: &str,
|
||||
json_rpc_cmd_url: &str,
|
||||
json_rpc_cfg_url: &str,
|
||||
) -> (SettingType, String) {
|
||||
Self::first_nonempty_setting(vec![
|
||||
(SettingType::Explicit, websocket_cmd_url.to_string()),
|
||||
(SettingType::Explicit, websocket_cfg_url.to_string()),
|
||||
(
|
||||
SettingType::Computed,
|
||||
Config::compute_websocket_url(&normalize_to_url_if_moniker(json_rpc_cmd_url)),
|
||||
),
|
||||
(
|
||||
SettingType::Computed,
|
||||
Config::compute_websocket_url(&normalize_to_url_if_moniker(json_rpc_cfg_url)),
|
||||
),
|
||||
(SettingType::SystemDefault, Self::default_websocket_url()),
|
||||
])
|
||||
}
|
||||
|
||||
pub fn compute_json_rpc_url_setting(
|
||||
json_rpc_cmd_url: &str,
|
||||
json_rpc_cfg_url: &str,
|
||||
) -> (SettingType, String) {
|
||||
let (setting_type, url_or_moniker) = Self::first_nonempty_setting(vec![
|
||||
(SettingType::Explicit, json_rpc_cmd_url.to_string()),
|
||||
(SettingType::Explicit, json_rpc_cfg_url.to_string()),
|
||||
(SettingType::SystemDefault, Self::default_json_rpc_url()),
|
||||
]);
|
||||
(setting_type, normalize_to_url_if_moniker(&url_or_moniker))
|
||||
}
|
||||
|
||||
pub fn compute_keypair_path_setting(
|
||||
keypair_cmd_path: &str,
|
||||
keypair_cfg_path: &str,
|
||||
) -> (SettingType, String) {
|
||||
Self::first_nonempty_setting(vec![
|
||||
(SettingType::Explicit, keypair_cmd_path.to_string()),
|
||||
(SettingType::Explicit, keypair_cfg_path.to_string()),
|
||||
(SettingType::SystemDefault, Self::default_keypair_path()),
|
||||
])
|
||||
}
|
||||
|
||||
pub fn compute_commitment_config(
|
||||
commitment_cmd: &str,
|
||||
commitment_cfg: &str,
|
||||
) -> (SettingType, CommitmentConfig) {
|
||||
Self::first_setting_is_some(vec![
|
||||
(
|
||||
SettingType::Explicit,
|
||||
CommitmentConfig::from_str(commitment_cmd).ok(),
|
||||
),
|
||||
(
|
||||
SettingType::Explicit,
|
||||
CommitmentConfig::from_str(commitment_cfg).ok(),
|
||||
),
|
||||
(SettingType::SystemDefault, Some(Self::default_commitment())),
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ConfigInput {
|
||||
fn default() -> ConfigInput {
|
||||
ConfigInput {
|
||||
json_rpc_url: Self::default_json_rpc_url(),
|
||||
websocket_url: Self::default_websocket_url(),
|
||||
keypair_path: Self::default_keypair_path(),
|
||||
commitment: CommitmentConfig::confirmed(),
|
||||
}
|
||||
}
|
||||
}
|
@ -55,12 +55,16 @@
|
||||
extern crate lazy_static;
|
||||
|
||||
mod config;
|
||||
pub use config::{Config, CONFIG_FILE};
|
||||
mod config_input;
|
||||
use std::{
|
||||
fs::{create_dir_all, File},
|
||||
io::{self, Write},
|
||||
path::Path,
|
||||
};
|
||||
pub use {
|
||||
config::{Config, CONFIG_FILE},
|
||||
config_input::{ConfigInput, SettingType},
|
||||
};
|
||||
|
||||
/// Load a value from a file in YAML format.
|
||||
///
|
||||
|
@ -18,10 +18,12 @@ console = "0.15.0"
|
||||
humantime = "2.0.1"
|
||||
indicatif = "0.16.2"
|
||||
pretty-hex = "0.2.1"
|
||||
semver = "1.0.7"
|
||||
serde = "1.0.136"
|
||||
serde_json = "1.0.79"
|
||||
solana-account-decoder = { path = "../account-decoder", version = "=1.11.0" }
|
||||
solana-clap-utils = { path = "../clap-utils", version = "=1.11.0" }
|
||||
solana-cli-config = { path = "../cli-config", version = "=1.11.0" }
|
||||
solana-client = { path = "../client", version = "=1.11.0" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.11.0" }
|
||||
solana-transaction-status = { path = "../transaction-status", version = "=1.11.0" }
|
||||
|
@ -356,6 +356,7 @@ pub enum CliValidatorsSortOrder {
|
||||
SkipRate,
|
||||
Stake,
|
||||
VoteAccount,
|
||||
Version,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
@ -494,6 +495,22 @@ impl fmt::Display for CliValidators {
|
||||
CliValidatorsSortOrder::Stake => {
|
||||
sorted_validators.sort_by_key(|a| a.activated_stake);
|
||||
}
|
||||
CliValidatorsSortOrder::Version => {
|
||||
sorted_validators.sort_by(|a, b| {
|
||||
use std::cmp::Ordering;
|
||||
let a_version = semver::Version::parse(a.version.as_str()).ok();
|
||||
let b_version = semver::Version::parse(b.version.as_str()).ok();
|
||||
match (a_version, b_version) {
|
||||
(None, None) => a.version.cmp(&b.version),
|
||||
(None, Some(_)) => Ordering::Less,
|
||||
(Some(_), None) => Ordering::Greater,
|
||||
(Some(va), Some(vb)) => match va.cmp(&vb) {
|
||||
Ordering::Equal => a.activated_stake.cmp(&b.activated_stake),
|
||||
ordering => ordering,
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if self.validators_reverse_sort {
|
||||
|
@ -3,6 +3,7 @@ use {
|
||||
chrono::{DateTime, Local, NaiveDateTime, SecondsFormat, TimeZone, Utc},
|
||||
console::style,
|
||||
indicatif::{ProgressBar, ProgressStyle},
|
||||
solana_cli_config::SettingType,
|
||||
solana_sdk::{
|
||||
clock::UnixTimestamp,
|
||||
hash::Hash,
|
||||
@ -104,6 +105,21 @@ pub fn writeln_name_value(f: &mut dyn fmt::Write, name: &str, value: &str) -> fm
|
||||
writeln!(f, "{} {}", style(name).bold(), styled_value)
|
||||
}
|
||||
|
||||
pub fn println_name_value_or(name: &str, value: &str, setting_type: SettingType) {
|
||||
let description = match setting_type {
|
||||
SettingType::Explicit => "",
|
||||
SettingType::Computed => "(computed)",
|
||||
SettingType::SystemDefault => "(default)",
|
||||
};
|
||||
|
||||
println!(
|
||||
"{} {} {}",
|
||||
style(name).bold(),
|
||||
style(value),
|
||||
style(description).italic(),
|
||||
);
|
||||
}
|
||||
|
||||
pub fn format_labeled_address(pubkey: &str, address_labels: &HashMap<String, String>) -> String {
|
||||
let label = address_labels.get(pubkey);
|
||||
match label {
|
||||
|
@ -23,7 +23,7 @@ log = "0.4.14"
|
||||
num-traits = "0.2"
|
||||
pretty-hex = "0.2.1"
|
||||
reqwest = { version = "0.11.10", default-features = false, features = ["blocking", "rustls-tls", "json"] }
|
||||
semver = "1.0.6"
|
||||
semver = "1.0.7"
|
||||
serde = "1.0.136"
|
||||
serde_derive = "1.0.103"
|
||||
serde_json = "1.0.79"
|
||||
@ -42,7 +42,7 @@ solana-sdk = { path = "../sdk", version = "=1.11.0" }
|
||||
solana-transaction-status = { path = "../transaction-status", version = "=1.11.0" }
|
||||
solana-version = { path = "../version", version = "=1.11.0" }
|
||||
solana-vote-program = { path = "../programs/vote", version = "=1.11.0" }
|
||||
solana_rbpf = "=0.2.24"
|
||||
solana_rbpf = "=0.2.25"
|
||||
spl-memo = { version = "=3.0.1", features = ["no-entrypoint"] }
|
||||
thiserror = "1.0.30"
|
||||
tiny-bip39 = "0.8.2"
|
||||
|
125
cli/src/cli.rs
125
cli/src/cli.rs
@ -7,7 +7,8 @@ use {
|
||||
log::*,
|
||||
num_traits::FromPrimitive,
|
||||
serde_json::{self, Value},
|
||||
solana_clap_utils::{self, input_parsers::*, input_validators::*, keypair::*},
|
||||
solana_clap_utils::{self, input_parsers::*, keypair::*},
|
||||
solana_cli_config::ConfigInput,
|
||||
solana_cli_output::{
|
||||
display::println_name_value, CliSignature, CliValidatorsSortOrder, OutputFormat,
|
||||
},
|
||||
@ -187,6 +188,7 @@ pub enum CliCommand {
|
||||
stake_account_pubkey: Pubkey,
|
||||
stake_authority: SignerIndex,
|
||||
sign_only: bool,
|
||||
deactivate_delinquent: bool,
|
||||
dump_transaction_message: bool,
|
||||
blockhash_query: BlockhashQuery,
|
||||
nonce_account: Option<Pubkey>,
|
||||
@ -456,129 +458,23 @@ impl From<nonce_utils::Error> for CliError {
|
||||
}
|
||||
}
|
||||
|
||||
pub enum SettingType {
|
||||
Explicit,
|
||||
Computed,
|
||||
SystemDefault,
|
||||
}
|
||||
|
||||
pub struct CliConfig<'a> {
|
||||
pub command: CliCommand,
|
||||
pub json_rpc_url: String,
|
||||
pub websocket_url: String,
|
||||
pub signers: Vec<&'a dyn Signer>,
|
||||
pub keypair_path: String,
|
||||
pub commitment: CommitmentConfig,
|
||||
pub signers: Vec<&'a dyn Signer>,
|
||||
pub rpc_client: Option<Arc<RpcClient>>,
|
||||
pub rpc_timeout: Duration,
|
||||
pub verbose: bool,
|
||||
pub output_format: OutputFormat,
|
||||
pub commitment: CommitmentConfig,
|
||||
pub send_transaction_config: RpcSendTransactionConfig,
|
||||
pub confirm_transaction_initial_timeout: Duration,
|
||||
pub address_labels: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl CliConfig<'_> {
|
||||
fn default_keypair_path() -> String {
|
||||
solana_cli_config::Config::default().keypair_path
|
||||
}
|
||||
|
||||
fn default_json_rpc_url() -> String {
|
||||
solana_cli_config::Config::default().json_rpc_url
|
||||
}
|
||||
|
||||
fn default_websocket_url() -> String {
|
||||
solana_cli_config::Config::default().websocket_url
|
||||
}
|
||||
|
||||
fn default_commitment() -> CommitmentConfig {
|
||||
CommitmentConfig::confirmed()
|
||||
}
|
||||
|
||||
fn first_nonempty_setting(
|
||||
settings: std::vec::Vec<(SettingType, String)>,
|
||||
) -> (SettingType, String) {
|
||||
settings
|
||||
.into_iter()
|
||||
.find(|(_, value)| !value.is_empty())
|
||||
.expect("no nonempty setting")
|
||||
}
|
||||
|
||||
fn first_setting_is_some<T>(
|
||||
settings: std::vec::Vec<(SettingType, Option<T>)>,
|
||||
) -> (SettingType, T) {
|
||||
let (setting_type, setting_option) = settings
|
||||
.into_iter()
|
||||
.find(|(_, value)| value.is_some())
|
||||
.expect("all settings none");
|
||||
(setting_type, setting_option.unwrap())
|
||||
}
|
||||
|
||||
pub fn compute_websocket_url_setting(
|
||||
websocket_cmd_url: &str,
|
||||
websocket_cfg_url: &str,
|
||||
json_rpc_cmd_url: &str,
|
||||
json_rpc_cfg_url: &str,
|
||||
) -> (SettingType, String) {
|
||||
Self::first_nonempty_setting(vec![
|
||||
(SettingType::Explicit, websocket_cmd_url.to_string()),
|
||||
(SettingType::Explicit, websocket_cfg_url.to_string()),
|
||||
(
|
||||
SettingType::Computed,
|
||||
solana_cli_config::Config::compute_websocket_url(&normalize_to_url_if_moniker(
|
||||
json_rpc_cmd_url,
|
||||
)),
|
||||
),
|
||||
(
|
||||
SettingType::Computed,
|
||||
solana_cli_config::Config::compute_websocket_url(&normalize_to_url_if_moniker(
|
||||
json_rpc_cfg_url,
|
||||
)),
|
||||
),
|
||||
(SettingType::SystemDefault, Self::default_websocket_url()),
|
||||
])
|
||||
}
|
||||
|
||||
pub fn compute_json_rpc_url_setting(
|
||||
json_rpc_cmd_url: &str,
|
||||
json_rpc_cfg_url: &str,
|
||||
) -> (SettingType, String) {
|
||||
let (setting_type, url_or_moniker) = Self::first_nonempty_setting(vec![
|
||||
(SettingType::Explicit, json_rpc_cmd_url.to_string()),
|
||||
(SettingType::Explicit, json_rpc_cfg_url.to_string()),
|
||||
(SettingType::SystemDefault, Self::default_json_rpc_url()),
|
||||
]);
|
||||
(setting_type, normalize_to_url_if_moniker(&url_or_moniker))
|
||||
}
|
||||
|
||||
pub fn compute_keypair_path_setting(
|
||||
keypair_cmd_path: &str,
|
||||
keypair_cfg_path: &str,
|
||||
) -> (SettingType, String) {
|
||||
Self::first_nonempty_setting(vec![
|
||||
(SettingType::Explicit, keypair_cmd_path.to_string()),
|
||||
(SettingType::Explicit, keypair_cfg_path.to_string()),
|
||||
(SettingType::SystemDefault, Self::default_keypair_path()),
|
||||
])
|
||||
}
|
||||
|
||||
pub fn compute_commitment_config(
|
||||
commitment_cmd: &str,
|
||||
commitment_cfg: &str,
|
||||
) -> (SettingType, CommitmentConfig) {
|
||||
Self::first_setting_is_some(vec![
|
||||
(
|
||||
SettingType::Explicit,
|
||||
CommitmentConfig::from_str(commitment_cmd).ok(),
|
||||
),
|
||||
(
|
||||
SettingType::Explicit,
|
||||
CommitmentConfig::from_str(commitment_cfg).ok(),
|
||||
),
|
||||
(SettingType::SystemDefault, Some(Self::default_commitment())),
|
||||
])
|
||||
}
|
||||
|
||||
pub(crate) fn pubkey(&self) -> Result<Pubkey, SignerError> {
|
||||
if !self.signers.is_empty() {
|
||||
self.signers[0].try_pubkey()
|
||||
@ -609,15 +505,15 @@ impl Default for CliConfig<'_> {
|
||||
pubkey: Some(Pubkey::default()),
|
||||
use_lamports_unit: false,
|
||||
},
|
||||
json_rpc_url: Self::default_json_rpc_url(),
|
||||
websocket_url: Self::default_websocket_url(),
|
||||
json_rpc_url: ConfigInput::default().json_rpc_url,
|
||||
websocket_url: ConfigInput::default().websocket_url,
|
||||
keypair_path: ConfigInput::default().keypair_path,
|
||||
commitment: ConfigInput::default().commitment,
|
||||
signers: Vec::new(),
|
||||
keypair_path: Self::default_keypair_path(),
|
||||
rpc_client: None,
|
||||
rpc_timeout: Duration::from_secs(u64::from_str(DEFAULT_RPC_TIMEOUT_SECONDS).unwrap()),
|
||||
verbose: false,
|
||||
output_format: OutputFormat::Display,
|
||||
commitment: CommitmentConfig::confirmed(),
|
||||
send_transaction_config: RpcSendTransactionConfig::default(),
|
||||
confirm_transaction_initial_timeout: Duration::from_secs(
|
||||
u64::from_str(DEFAULT_CONFIRM_TX_TIMEOUT_SECONDS).unwrap(),
|
||||
@ -1188,6 +1084,7 @@ pub fn process_command(config: &CliConfig) -> ProcessResult {
|
||||
stake_account_pubkey,
|
||||
stake_authority,
|
||||
sign_only,
|
||||
deactivate_delinquent,
|
||||
dump_transaction_message,
|
||||
blockhash_query,
|
||||
nonce_account,
|
||||
@ -1201,6 +1098,7 @@ pub fn process_command(config: &CliConfig) -> ProcessResult {
|
||||
stake_account_pubkey,
|
||||
*stake_authority,
|
||||
*sign_only,
|
||||
*deactivate_delinquent,
|
||||
*dump_transaction_message,
|
||||
blockhash_query,
|
||||
*nonce_account,
|
||||
@ -2197,6 +2095,7 @@ mod tests {
|
||||
stake_authority: 0,
|
||||
sign_only: false,
|
||||
dump_transaction_message: false,
|
||||
deactivate_delinquent: false,
|
||||
blockhash_query: BlockhashQuery::default(),
|
||||
nonce_account: None,
|
||||
nonce_authority: 0,
|
||||
|
@ -379,6 +379,7 @@ impl ClusterQuerySubCommands for App<'_, '_> {
|
||||
"root",
|
||||
"skip-rate",
|
||||
"stake",
|
||||
"version",
|
||||
"vote-account",
|
||||
])
|
||||
.default_value("stake")
|
||||
@ -650,6 +651,7 @@ pub fn parse_show_validators(matches: &ArgMatches<'_>) -> Result<CliCommandInfo,
|
||||
"skip-rate" => CliValidatorsSortOrder::SkipRate,
|
||||
"stake" => CliValidatorsSortOrder::Stake,
|
||||
"vote-account" => CliValidatorsSortOrder::VoteAccount,
|
||||
"version" => CliValidatorsSortOrder::Version,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
|
@ -8,30 +8,18 @@ use {
|
||||
},
|
||||
solana_cli::{
|
||||
clap_app::get_clap_app,
|
||||
cli::{parse_command, process_command, CliCommandInfo, CliConfig, SettingType},
|
||||
cli::{parse_command, process_command, CliCommandInfo, CliConfig},
|
||||
},
|
||||
solana_cli_config::{Config, ConfigInput},
|
||||
solana_cli_output::{
|
||||
display::{println_name_value, println_name_value_or},
|
||||
OutputFormat,
|
||||
},
|
||||
solana_cli_config::Config,
|
||||
solana_cli_output::{display::println_name_value, OutputFormat},
|
||||
solana_client::rpc_config::RpcSendTransactionConfig,
|
||||
solana_remote_wallet::remote_wallet::RemoteWalletManager,
|
||||
std::{collections::HashMap, error, path::PathBuf, sync::Arc, time::Duration},
|
||||
};
|
||||
|
||||
pub fn println_name_value_or(name: &str, value: &str, setting_type: SettingType) {
|
||||
let description = match setting_type {
|
||||
SettingType::Explicit => "",
|
||||
SettingType::Computed => "(computed)",
|
||||
SettingType::SystemDefault => "(default)",
|
||||
};
|
||||
|
||||
println!(
|
||||
"{} {} {}",
|
||||
style(name).bold(),
|
||||
style(value),
|
||||
style(description).italic(),
|
||||
);
|
||||
}
|
||||
|
||||
fn parse_settings(matches: &ArgMatches<'_>) -> Result<bool, Box<dyn error::Error>> {
|
||||
let parse_args = match matches.subcommand() {
|
||||
("config", Some(matches)) => {
|
||||
@ -50,17 +38,18 @@ fn parse_settings(matches: &ArgMatches<'_>) -> Result<bool, Box<dyn error::Error
|
||||
match matches.subcommand() {
|
||||
("get", Some(subcommand_matches)) => {
|
||||
let (url_setting_type, json_rpc_url) =
|
||||
CliConfig::compute_json_rpc_url_setting("", &config.json_rpc_url);
|
||||
let (ws_setting_type, websocket_url) = CliConfig::compute_websocket_url_setting(
|
||||
"",
|
||||
&config.websocket_url,
|
||||
"",
|
||||
&config.json_rpc_url,
|
||||
);
|
||||
ConfigInput::compute_json_rpc_url_setting("", &config.json_rpc_url);
|
||||
let (ws_setting_type, websocket_url) =
|
||||
ConfigInput::compute_websocket_url_setting(
|
||||
"",
|
||||
&config.websocket_url,
|
||||
"",
|
||||
&config.json_rpc_url,
|
||||
);
|
||||
let (keypair_setting_type, keypair_path) =
|
||||
CliConfig::compute_keypair_path_setting("", &config.keypair_path);
|
||||
ConfigInput::compute_keypair_path_setting("", &config.keypair_path);
|
||||
let (commitment_setting_type, commitment) =
|
||||
CliConfig::compute_commitment_config("", &config.commitment);
|
||||
ConfigInput::compute_commitment_config("", &config.commitment);
|
||||
|
||||
if let Some(field) = subcommand_matches.value_of("specific_setting") {
|
||||
let (field_name, value, setting_type) = match field {
|
||||
@ -107,17 +96,18 @@ fn parse_settings(matches: &ArgMatches<'_>) -> Result<bool, Box<dyn error::Error
|
||||
config.save(config_file)?;
|
||||
|
||||
let (url_setting_type, json_rpc_url) =
|
||||
CliConfig::compute_json_rpc_url_setting("", &config.json_rpc_url);
|
||||
let (ws_setting_type, websocket_url) = CliConfig::compute_websocket_url_setting(
|
||||
"",
|
||||
&config.websocket_url,
|
||||
"",
|
||||
&config.json_rpc_url,
|
||||
);
|
||||
ConfigInput::compute_json_rpc_url_setting("", &config.json_rpc_url);
|
||||
let (ws_setting_type, websocket_url) =
|
||||
ConfigInput::compute_websocket_url_setting(
|
||||
"",
|
||||
&config.websocket_url,
|
||||
"",
|
||||
&config.json_rpc_url,
|
||||
);
|
||||
let (keypair_setting_type, keypair_path) =
|
||||
CliConfig::compute_keypair_path_setting("", &config.keypair_path);
|
||||
ConfigInput::compute_keypair_path_setting("", &config.keypair_path);
|
||||
let (commitment_setting_type, commitment) =
|
||||
CliConfig::compute_commitment_config("", &config.commitment);
|
||||
ConfigInput::compute_commitment_config("", &config.commitment);
|
||||
|
||||
println_name_value("Config File:", config_file);
|
||||
println_name_value_or("RPC URL:", &json_rpc_url, url_setting_type);
|
||||
@ -158,7 +148,7 @@ pub fn parse_args<'a>(
|
||||
} else {
|
||||
Config::default()
|
||||
};
|
||||
let (_, json_rpc_url) = CliConfig::compute_json_rpc_url_setting(
|
||||
let (_, json_rpc_url) = ConfigInput::compute_json_rpc_url_setting(
|
||||
matches.value_of("json_rpc_url").unwrap_or(""),
|
||||
&config.json_rpc_url,
|
||||
);
|
||||
@ -171,14 +161,14 @@ pub fn parse_args<'a>(
|
||||
let confirm_transaction_initial_timeout =
|
||||
Duration::from_secs(confirm_transaction_initial_timeout);
|
||||
|
||||
let (_, websocket_url) = CliConfig::compute_websocket_url_setting(
|
||||
let (_, websocket_url) = ConfigInput::compute_websocket_url_setting(
|
||||
matches.value_of("websocket_url").unwrap_or(""),
|
||||
&config.websocket_url,
|
||||
matches.value_of("json_rpc_url").unwrap_or(""),
|
||||
&config.json_rpc_url,
|
||||
);
|
||||
let default_signer_arg_name = "keypair".to_string();
|
||||
let (_, default_signer_path) = CliConfig::compute_keypair_path_setting(
|
||||
let (_, default_signer_path) = ConfigInput::compute_keypair_path_setting(
|
||||
matches.value_of(&default_signer_arg_name).unwrap_or(""),
|
||||
&config.keypair_path,
|
||||
);
|
||||
@ -201,7 +191,7 @@ pub fn parse_args<'a>(
|
||||
let verbose = matches.is_present("verbose");
|
||||
let output_format = OutputFormat::from_matches(matches, "output_format", verbose);
|
||||
|
||||
let (_, commitment) = CliConfig::compute_commitment_config(
|
||||
let (_, commitment) = ConfigInput::compute_commitment_config(
|
||||
matches.value_of("commitment").unwrap_or(""),
|
||||
&config.commitment,
|
||||
);
|
||||
|
121
cli/src/stake.rs
121
cli/src/stake.rs
@ -41,6 +41,7 @@ use {
|
||||
self,
|
||||
instruction::{self as stake_instruction, LockupArgs, StakeError},
|
||||
state::{Authorized, Lockup, Meta, StakeActivationStatus, StakeAuthorize, StakeState},
|
||||
tools::{acceptable_reference_epoch_credits, eligible_for_deactivate_delinquent},
|
||||
},
|
||||
stake_history::StakeHistory,
|
||||
system_instruction::SystemError,
|
||||
@ -379,6 +380,13 @@ impl StakeSubCommands for App<'_, '_> {
|
||||
.help("Seed for address generation; if specified, the resulting account \
|
||||
will be at a derived address of STAKE_ACCOUNT_ADDRESS")
|
||||
)
|
||||
.arg(
|
||||
Arg::with_name("delinquent")
|
||||
.long("delinquent")
|
||||
.takes_value(false)
|
||||
.conflicts_with(SIGN_ONLY_ARG.name)
|
||||
.help("Deactivate abandoned stake that is currently delegated to a delinquent vote account")
|
||||
)
|
||||
.arg(stake_authority_arg())
|
||||
.offline_args()
|
||||
.nonce_args(false)
|
||||
@ -995,11 +1003,13 @@ pub fn parse_stake_deactivate_stake(
|
||||
let stake_account_pubkey =
|
||||
pubkey_of_signer(matches, "stake_account_pubkey", wallet_manager)?.unwrap();
|
||||
let sign_only = matches.is_present(SIGN_ONLY_ARG.name);
|
||||
let deactivate_delinquent = matches.is_present("delinquent");
|
||||
let dump_transaction_message = matches.is_present(DUMP_TRANSACTION_MESSAGE.name);
|
||||
let blockhash_query = BlockhashQuery::new_from_matches(matches);
|
||||
let nonce_account = pubkey_of(matches, NONCE_ARG.name);
|
||||
let memo = matches.value_of(MEMO_ARG.name).map(String::from);
|
||||
let seed = value_t!(matches, "seed", String).ok();
|
||||
|
||||
let (stake_authority, stake_authority_pubkey) =
|
||||
signer_of(matches, STAKE_AUTHORITY_ARG.name, wallet_manager)?;
|
||||
let (nonce_authority, nonce_authority_pubkey) =
|
||||
@ -1018,6 +1028,7 @@ pub fn parse_stake_deactivate_stake(
|
||||
stake_account_pubkey,
|
||||
stake_authority: signer_info.index_of(stake_authority_pubkey).unwrap(),
|
||||
sign_only,
|
||||
deactivate_delinquent,
|
||||
dump_transaction_message,
|
||||
blockhash_query,
|
||||
nonce_account,
|
||||
@ -1477,6 +1488,7 @@ pub fn process_deactivate_stake_account(
|
||||
stake_account_pubkey: &Pubkey,
|
||||
stake_authority: SignerIndex,
|
||||
sign_only: bool,
|
||||
deactivate_delinquent: bool,
|
||||
dump_transaction_message: bool,
|
||||
blockhash_query: &BlockhashQuery,
|
||||
nonce_account: Option<Pubkey>,
|
||||
@ -1486,7 +1498,6 @@ pub fn process_deactivate_stake_account(
|
||||
fee_payer: SignerIndex,
|
||||
) -> ProcessResult {
|
||||
let recent_blockhash = blockhash_query.get_blockhash(rpc_client, config.commitment)?;
|
||||
let stake_authority = config.signers[stake_authority];
|
||||
|
||||
let stake_account_address = if let Some(seed) = seed {
|
||||
Pubkey::create_with_seed(stake_account_pubkey, seed, &stake::program::id())?
|
||||
@ -1494,11 +1505,77 @@ pub fn process_deactivate_stake_account(
|
||||
*stake_account_pubkey
|
||||
};
|
||||
|
||||
let ixs = vec![stake_instruction::deactivate_stake(
|
||||
&stake_account_address,
|
||||
&stake_authority.pubkey(),
|
||||
)]
|
||||
let ixs = vec![if deactivate_delinquent {
|
||||
let stake_account = rpc_client.get_account(&stake_account_address)?;
|
||||
if stake_account.owner != stake::program::id() {
|
||||
return Err(CliError::BadParameter(format!(
|
||||
"{} is not a stake account",
|
||||
stake_account_address,
|
||||
))
|
||||
.into());
|
||||
}
|
||||
|
||||
let vote_account_address = match stake_account.state() {
|
||||
Ok(stake_state) => match stake_state {
|
||||
StakeState::Stake(_, stake) => stake.delegation.voter_pubkey,
|
||||
_ => {
|
||||
return Err(CliError::BadParameter(format!(
|
||||
"{} is not a delegated stake account",
|
||||
stake_account_address,
|
||||
))
|
||||
.into())
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
return Err(CliError::RpcRequestError(format!(
|
||||
"Account data could not be deserialized to stake state: {}",
|
||||
err
|
||||
))
|
||||
.into())
|
||||
}
|
||||
};
|
||||
|
||||
let current_epoch = rpc_client.get_epoch_info()?.epoch;
|
||||
|
||||
let (_, vote_state) = crate::vote::get_vote_account(
|
||||
rpc_client,
|
||||
&vote_account_address,
|
||||
rpc_client.commitment(),
|
||||
)?;
|
||||
if !eligible_for_deactivate_delinquent(&vote_state.epoch_credits, current_epoch) {
|
||||
return Err(CliError::BadParameter(format!(
|
||||
"Stake has not been delinquent for {} epochs",
|
||||
stake::MINIMUM_DELINQUENT_EPOCHS_FOR_DEACTIVATION,
|
||||
))
|
||||
.into());
|
||||
}
|
||||
|
||||
// Search for a reference vote account
|
||||
let reference_vote_account_address = rpc_client
|
||||
.get_vote_accounts()?
|
||||
.current
|
||||
.into_iter()
|
||||
.find(|vote_account_info| {
|
||||
acceptable_reference_epoch_credits(&vote_account_info.epoch_credits, current_epoch)
|
||||
});
|
||||
let reference_vote_account_address = reference_vote_account_address
|
||||
.ok_or_else(|| {
|
||||
CliError::RpcRequestError("Unable to find a reference vote account".into())
|
||||
})?
|
||||
.vote_pubkey
|
||||
.parse()?;
|
||||
|
||||
stake_instruction::deactivate_delinquent_stake(
|
||||
&stake_account_address,
|
||||
&vote_account_address,
|
||||
&reference_vote_account_address,
|
||||
)
|
||||
} else {
|
||||
let stake_authority = config.signers[stake_authority];
|
||||
stake_instruction::deactivate_stake(&stake_account_address, &stake_authority.pubkey())
|
||||
}]
|
||||
.with_memo(memo);
|
||||
|
||||
let nonce_authority = config.signers[nonce_authority];
|
||||
let fee_payer = config.signers[fee_payer];
|
||||
|
||||
@ -4174,6 +4251,34 @@ mod tests {
|
||||
stake_account_pubkey,
|
||||
stake_authority: 0,
|
||||
sign_only: false,
|
||||
deactivate_delinquent: false,
|
||||
dump_transaction_message: false,
|
||||
blockhash_query: BlockhashQuery::default(),
|
||||
nonce_account: None,
|
||||
nonce_authority: 0,
|
||||
memo: None,
|
||||
seed: None,
|
||||
fee_payer: 0,
|
||||
},
|
||||
signers: vec![read_keypair_file(&default_keypair_file).unwrap().into()],
|
||||
}
|
||||
);
|
||||
|
||||
// Test DeactivateStake Subcommand with delinquent flag
|
||||
let test_deactivate_stake = test_commands.clone().get_matches_from(vec![
|
||||
"test",
|
||||
"deactivate-stake",
|
||||
&stake_account_string,
|
||||
"--delinquent",
|
||||
]);
|
||||
assert_eq!(
|
||||
parse_command(&test_deactivate_stake, &default_signer, &mut None).unwrap(),
|
||||
CliCommandInfo {
|
||||
command: CliCommand::DeactivateStake {
|
||||
stake_account_pubkey,
|
||||
stake_authority: 0,
|
||||
sign_only: false,
|
||||
deactivate_delinquent: true,
|
||||
dump_transaction_message: false,
|
||||
blockhash_query: BlockhashQuery::default(),
|
||||
nonce_account: None,
|
||||
@ -4201,6 +4306,7 @@ mod tests {
|
||||
stake_account_pubkey,
|
||||
stake_authority: 1,
|
||||
sign_only: false,
|
||||
deactivate_delinquent: false,
|
||||
dump_transaction_message: false,
|
||||
blockhash_query: BlockhashQuery::default(),
|
||||
nonce_account: None,
|
||||
@ -4235,6 +4341,7 @@ mod tests {
|
||||
stake_account_pubkey,
|
||||
stake_authority: 0,
|
||||
sign_only: false,
|
||||
deactivate_delinquent: false,
|
||||
dump_transaction_message: false,
|
||||
blockhash_query: BlockhashQuery::FeeCalculator(
|
||||
blockhash_query::Source::Cluster,
|
||||
@ -4265,6 +4372,7 @@ mod tests {
|
||||
stake_account_pubkey,
|
||||
stake_authority: 0,
|
||||
sign_only: true,
|
||||
deactivate_delinquent: false,
|
||||
dump_transaction_message: false,
|
||||
blockhash_query: BlockhashQuery::None(blockhash),
|
||||
nonce_account: None,
|
||||
@ -4299,6 +4407,7 @@ mod tests {
|
||||
stake_account_pubkey,
|
||||
stake_authority: 0,
|
||||
sign_only: false,
|
||||
deactivate_delinquent: false,
|
||||
dump_transaction_message: false,
|
||||
blockhash_query: BlockhashQuery::FeeCalculator(
|
||||
blockhash_query::Source::Cluster,
|
||||
@ -4345,6 +4454,7 @@ mod tests {
|
||||
stake_account_pubkey,
|
||||
stake_authority: 0,
|
||||
sign_only: false,
|
||||
deactivate_delinquent: false,
|
||||
dump_transaction_message: false,
|
||||
blockhash_query: BlockhashQuery::FeeCalculator(
|
||||
blockhash_query::Source::NonceAccount(nonce_account),
|
||||
@ -4379,6 +4489,7 @@ mod tests {
|
||||
stake_account_pubkey,
|
||||
stake_authority: 0,
|
||||
sign_only: false,
|
||||
deactivate_delinquent: false,
|
||||
dump_transaction_message: false,
|
||||
blockhash_query: BlockhashQuery::All(blockhash_query::Source::Cluster),
|
||||
nonce_account: None,
|
||||
|
@ -1140,7 +1140,7 @@ pub fn process_vote_update_commission(
|
||||
}
|
||||
}
|
||||
|
||||
fn get_vote_account(
|
||||
pub(crate) fn get_vote_account(
|
||||
rpc_client: &RpcClient,
|
||||
vote_account_pubkey: &Pubkey,
|
||||
commitment_config: CommitmentConfig,
|
||||
|
@ -204,6 +204,7 @@ fn test_seed_stake_delegation_and_deactivation() {
|
||||
stake_account_pubkey: stake_address,
|
||||
stake_authority: 0,
|
||||
sign_only: false,
|
||||
deactivate_delinquent: false,
|
||||
dump_transaction_message: false,
|
||||
blockhash_query: BlockhashQuery::default(),
|
||||
nonce_account: None,
|
||||
@ -287,6 +288,7 @@ fn test_stake_delegation_and_deactivation() {
|
||||
stake_account_pubkey: stake_keypair.pubkey(),
|
||||
stake_authority: 0,
|
||||
sign_only: false,
|
||||
deactivate_delinquent: false,
|
||||
dump_transaction_message: false,
|
||||
blockhash_query: BlockhashQuery::default(),
|
||||
nonce_account: None,
|
||||
@ -412,6 +414,7 @@ fn test_offline_stake_delegation_and_deactivation() {
|
||||
stake_account_pubkey: stake_keypair.pubkey(),
|
||||
stake_authority: 0,
|
||||
sign_only: true,
|
||||
deactivate_delinquent: false,
|
||||
dump_transaction_message: false,
|
||||
blockhash_query: BlockhashQuery::None(blockhash),
|
||||
nonce_account: None,
|
||||
@ -431,6 +434,7 @@ fn test_offline_stake_delegation_and_deactivation() {
|
||||
stake_account_pubkey: stake_keypair.pubkey(),
|
||||
stake_authority: 0,
|
||||
sign_only: false,
|
||||
deactivate_delinquent: false,
|
||||
dump_transaction_message: false,
|
||||
blockhash_query: BlockhashQuery::FeeCalculator(blockhash_query::Source::Cluster, blockhash),
|
||||
nonce_account: None,
|
||||
@ -546,6 +550,7 @@ fn test_nonced_stake_delegation_and_deactivation() {
|
||||
stake_account_pubkey: stake_keypair.pubkey(),
|
||||
stake_authority: 0,
|
||||
sign_only: false,
|
||||
deactivate_delinquent: false,
|
||||
dump_transaction_message: false,
|
||||
blockhash_query: BlockhashQuery::FeeCalculator(
|
||||
blockhash_query::Source::NonceAccount(nonce_account.pubkey()),
|
||||
|
@ -11,7 +11,7 @@ edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
async-mutex = "1.4.0"
|
||||
async-trait = "0.1.52"
|
||||
async-trait = "0.1.53"
|
||||
base64 = "0.13.0"
|
||||
bincode = "1.3.3"
|
||||
bs58 = "0.4.0"
|
||||
@ -27,12 +27,13 @@ lazy_static = "1.4.0"
|
||||
log = "0.4.14"
|
||||
lru = "0.7.5"
|
||||
quinn = "0.8.0"
|
||||
quinn-proto = "0.8.0"
|
||||
rand = "0.7.0"
|
||||
rand_chacha = "0.2.2"
|
||||
rayon = "1.5.1"
|
||||
reqwest = { version = "0.11.10", default-features = false, features = ["blocking", "rustls-tls", "json"] }
|
||||
rustls = { version = "0.20.2", features = ["dangerous_configuration"] }
|
||||
semver = "1.0.6"
|
||||
semver = "1.0.7"
|
||||
serde = "1.0.136"
|
||||
serde_derive = "1.0.103"
|
||||
serde_json = "1.0.79"
|
||||
@ -40,6 +41,7 @@ solana-account-decoder = { path = "../account-decoder", version = "=1.11.0" }
|
||||
solana-clap-utils = { path = "../clap-utils", version = "=1.11.0" }
|
||||
solana-faucet = { path = "../faucet", version = "=1.11.0" }
|
||||
solana-measure = { path = "../measure", version = "=1.11.0" }
|
||||
solana-metrics = { path = "../metrics", version = "=1.11.0" }
|
||||
solana-net-utils = { path = "../net-utils", version = "=1.11.0" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.11.0" }
|
||||
solana-streamer = { path = "../streamer", version = "=1.11.0" }
|
||||
|
@ -1,35 +1,135 @@
|
||||
use {
|
||||
crate::{
|
||||
quic_client::QuicTpuConnection, tpu_connection::TpuConnection, udp_client::UdpTpuConnection,
|
||||
quic_client::QuicTpuConnection,
|
||||
tpu_connection::{ClientStats, TpuConnection},
|
||||
udp_client::UdpTpuConnection,
|
||||
},
|
||||
lazy_static::lazy_static,
|
||||
lru::LruCache,
|
||||
solana_net_utils::VALIDATOR_PORT_RANGE,
|
||||
solana_sdk::{transaction::VersionedTransaction, transport::TransportError},
|
||||
solana_sdk::{
|
||||
timing::AtomicInterval, transaction::VersionedTransaction, transport::TransportError,
|
||||
},
|
||||
std::{
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
sync::{Arc, Mutex},
|
||||
sync::{
|
||||
atomic::{AtomicU64, Ordering},
|
||||
Arc, Mutex,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
// Should be non-zero
|
||||
static MAX_CONNECTIONS: usize = 64;
|
||||
static MAX_CONNECTIONS: usize = 1024;
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Connection {
|
||||
pub enum Connection {
|
||||
Udp(Arc<UdpTpuConnection>),
|
||||
Quic(Arc<QuicTpuConnection>),
|
||||
}
|
||||
|
||||
struct ConnMap {
|
||||
#[derive(Default)]
|
||||
struct ConnectionCacheStats {
|
||||
cache_hits: AtomicU64,
|
||||
cache_misses: AtomicU64,
|
||||
sent_packets: AtomicU64,
|
||||
total_batches: AtomicU64,
|
||||
batch_success: AtomicU64,
|
||||
batch_failure: AtomicU64,
|
||||
|
||||
// Need to track these separately per-connection
|
||||
// because we need to track the base stat value from quinn
|
||||
total_client_stats: ClientStats,
|
||||
}
|
||||
|
||||
const CONNECTION_STAT_SUBMISSION_INTERVAL: u64 = 2000;
|
||||
|
||||
impl ConnectionCacheStats {
|
||||
fn add_client_stats(&self, client_stats: &ClientStats, num_packets: usize, is_success: bool) {
|
||||
self.total_client_stats.total_connections.fetch_add(
|
||||
client_stats.total_connections.load(Ordering::Relaxed),
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
self.total_client_stats.connection_reuse.fetch_add(
|
||||
client_stats.connection_reuse.load(Ordering::Relaxed),
|
||||
Ordering::Relaxed,
|
||||
);
|
||||
self.sent_packets
|
||||
.fetch_add(num_packets as u64, Ordering::Relaxed);
|
||||
self.total_batches.fetch_add(1, Ordering::Relaxed);
|
||||
if is_success {
|
||||
self.batch_success.fetch_add(1, Ordering::Relaxed);
|
||||
} else {
|
||||
self.batch_failure.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
fn report(&self) {
|
||||
datapoint_info!(
|
||||
"quic-client-connection-stats",
|
||||
(
|
||||
"cache_hits",
|
||||
self.cache_hits.swap(0, Ordering::Relaxed),
|
||||
i64
|
||||
),
|
||||
(
|
||||
"cache_misses",
|
||||
self.cache_misses.swap(0, Ordering::Relaxed),
|
||||
i64
|
||||
),
|
||||
(
|
||||
"total_connections",
|
||||
self.total_client_stats
|
||||
.total_connections
|
||||
.swap(0, Ordering::Relaxed),
|
||||
i64
|
||||
),
|
||||
(
|
||||
"connection_reuse",
|
||||
self.total_client_stats
|
||||
.connection_reuse
|
||||
.swap(0, Ordering::Relaxed),
|
||||
i64
|
||||
),
|
||||
(
|
||||
"congestion_events",
|
||||
self.total_client_stats.congestion_events.load_and_reset(),
|
||||
i64
|
||||
),
|
||||
(
|
||||
"tx_streams_blocked_uni",
|
||||
self.total_client_stats
|
||||
.tx_streams_blocked_uni
|
||||
.load_and_reset(),
|
||||
i64
|
||||
),
|
||||
(
|
||||
"tx_data_blocked",
|
||||
self.total_client_stats.tx_data_blocked.load_and_reset(),
|
||||
i64
|
||||
),
|
||||
(
|
||||
"tx_acks",
|
||||
self.total_client_stats.tx_acks.load_and_reset(),
|
||||
i64
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
struct ConnectionMap {
|
||||
map: LruCache<SocketAddr, Connection>,
|
||||
stats: Arc<ConnectionCacheStats>,
|
||||
last_stats: AtomicInterval,
|
||||
use_quic: bool,
|
||||
}
|
||||
|
||||
impl ConnMap {
|
||||
impl ConnectionMap {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
map: LruCache::new(MAX_CONNECTIONS),
|
||||
stats: Arc::new(ConnectionCacheStats::default()),
|
||||
last_stats: AtomicInterval::default(),
|
||||
use_quic: false,
|
||||
}
|
||||
}
|
||||
@ -40,7 +140,7 @@ impl ConnMap {
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref CONNECTION_MAP: Mutex<ConnMap> = Mutex::new(ConnMap::new());
|
||||
static ref CONNECTION_MAP: Mutex<ConnectionMap> = Mutex::new(ConnectionMap::new());
|
||||
}
|
||||
|
||||
pub fn set_use_quic(use_quic: bool) {
|
||||
@ -50,11 +150,25 @@ pub fn set_use_quic(use_quic: bool) {
|
||||
|
||||
// TODO: see https://github.com/solana-labs/solana/issues/23661
|
||||
// remove lazy_static and optimize and refactor this
|
||||
fn get_connection(addr: &SocketAddr) -> Connection {
|
||||
fn get_connection(addr: &SocketAddr) -> (Connection, Arc<ConnectionCacheStats>) {
|
||||
let mut map = (*CONNECTION_MAP).lock().unwrap();
|
||||
|
||||
match map.map.get(addr) {
|
||||
Some(connection) => connection.clone(),
|
||||
if map
|
||||
.last_stats
|
||||
.should_update(CONNECTION_STAT_SUBMISSION_INTERVAL)
|
||||
{
|
||||
map.stats.report();
|
||||
}
|
||||
|
||||
let (connection, hit, maybe_stats) = match map.map.get(addr) {
|
||||
Some(connection) => {
|
||||
let mut stats = None;
|
||||
// update connection stats
|
||||
if let Connection::Quic(conn) = connection {
|
||||
stats = conn.stats().map(|s| (conn.base_stats(), s));
|
||||
}
|
||||
(connection.clone(), true, stats)
|
||||
}
|
||||
None => {
|
||||
let (_, send_socket) = solana_net_utils::bind_in_range(
|
||||
IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)),
|
||||
@ -68,9 +182,41 @@ fn get_connection(addr: &SocketAddr) -> Connection {
|
||||
};
|
||||
|
||||
map.map.put(*addr, connection.clone());
|
||||
connection
|
||||
(connection, false, None)
|
||||
}
|
||||
};
|
||||
|
||||
if let Some((connection_stats, new_stats)) = maybe_stats {
|
||||
map.stats.total_client_stats.congestion_events.update_stat(
|
||||
&connection_stats.congestion_events,
|
||||
new_stats.path.congestion_events,
|
||||
);
|
||||
|
||||
map.stats
|
||||
.total_client_stats
|
||||
.tx_streams_blocked_uni
|
||||
.update_stat(
|
||||
&connection_stats.tx_streams_blocked_uni,
|
||||
new_stats.frame_tx.streams_blocked_uni,
|
||||
);
|
||||
|
||||
map.stats.total_client_stats.tx_data_blocked.update_stat(
|
||||
&connection_stats.tx_data_blocked,
|
||||
new_stats.frame_tx.data_blocked,
|
||||
);
|
||||
|
||||
map.stats
|
||||
.total_client_stats
|
||||
.tx_acks
|
||||
.update_stat(&connection_stats.tx_acks, new_stats.frame_tx.acks);
|
||||
}
|
||||
|
||||
if hit {
|
||||
map.stats.cache_hits.fetch_add(1, Ordering::Relaxed);
|
||||
} else {
|
||||
map.stats.cache_misses.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
(connection, map.stats.clone())
|
||||
}
|
||||
|
||||
// TODO: see https://github.com/solana-labs/solana/issues/23851
|
||||
@ -86,44 +232,86 @@ pub fn send_wire_transaction_batch(
|
||||
packets: &[&[u8]],
|
||||
addr: &SocketAddr,
|
||||
) -> Result<(), TransportError> {
|
||||
let conn = get_connection(addr);
|
||||
match conn {
|
||||
Connection::Udp(conn) => conn.send_wire_transaction_batch(packets),
|
||||
Connection::Quic(conn) => conn.send_wire_transaction_batch(packets),
|
||||
}
|
||||
let (conn, stats) = get_connection(addr);
|
||||
let client_stats = ClientStats::default();
|
||||
let r = match conn {
|
||||
Connection::Udp(conn) => conn.send_wire_transaction_batch(packets, &client_stats),
|
||||
Connection::Quic(conn) => conn.send_wire_transaction_batch(packets, &client_stats),
|
||||
};
|
||||
stats.add_client_stats(&client_stats, packets.len(), r.is_ok());
|
||||
r
|
||||
}
|
||||
|
||||
pub fn send_wire_transaction_async(
|
||||
packets: Vec<u8>,
|
||||
addr: &SocketAddr,
|
||||
) -> Result<(), TransportError> {
|
||||
let (conn, stats) = get_connection(addr);
|
||||
let client_stats = Arc::new(ClientStats::default());
|
||||
let r = match conn {
|
||||
Connection::Udp(conn) => conn.send_wire_transaction_async(packets, client_stats.clone()),
|
||||
Connection::Quic(conn) => conn.send_wire_transaction_async(packets, client_stats.clone()),
|
||||
};
|
||||
stats.add_client_stats(&client_stats, 1, r.is_ok());
|
||||
r
|
||||
}
|
||||
|
||||
pub fn send_wire_transaction_batch_async(
|
||||
packets: Vec<Vec<u8>>,
|
||||
addr: &SocketAddr,
|
||||
) -> Result<(), TransportError> {
|
||||
let (conn, stats) = get_connection(addr);
|
||||
let client_stats = Arc::new(ClientStats::default());
|
||||
let len = packets.len();
|
||||
let r = match conn {
|
||||
Connection::Udp(conn) => {
|
||||
conn.send_wire_transaction_batch_async(packets, client_stats.clone())
|
||||
}
|
||||
Connection::Quic(conn) => {
|
||||
conn.send_wire_transaction_batch_async(packets, client_stats.clone())
|
||||
}
|
||||
};
|
||||
stats.add_client_stats(&client_stats, len, r.is_ok());
|
||||
r
|
||||
}
|
||||
|
||||
pub fn send_wire_transaction(
|
||||
wire_transaction: &[u8],
|
||||
addr: &SocketAddr,
|
||||
) -> Result<(), TransportError> {
|
||||
let conn = get_connection(addr);
|
||||
match conn {
|
||||
Connection::Udp(conn) => conn.send_wire_transaction(wire_transaction),
|
||||
Connection::Quic(conn) => conn.send_wire_transaction(wire_transaction),
|
||||
}
|
||||
send_wire_transaction_batch(&[wire_transaction], addr)
|
||||
}
|
||||
|
||||
pub fn serialize_and_send_transaction(
|
||||
transaction: &VersionedTransaction,
|
||||
addr: &SocketAddr,
|
||||
) -> Result<(), TransportError> {
|
||||
let conn = get_connection(addr);
|
||||
match conn {
|
||||
Connection::Udp(conn) => conn.serialize_and_send_transaction(transaction),
|
||||
Connection::Quic(conn) => conn.serialize_and_send_transaction(transaction),
|
||||
}
|
||||
let (conn, stats) = get_connection(addr);
|
||||
let client_stats = ClientStats::default();
|
||||
let r = match conn {
|
||||
Connection::Udp(conn) => conn.serialize_and_send_transaction(transaction, &client_stats),
|
||||
Connection::Quic(conn) => conn.serialize_and_send_transaction(transaction, &client_stats),
|
||||
};
|
||||
stats.add_client_stats(&client_stats, 1, r.is_ok());
|
||||
r
|
||||
}
|
||||
|
||||
pub fn par_serialize_and_send_transaction_batch(
|
||||
transactions: &[VersionedTransaction],
|
||||
addr: &SocketAddr,
|
||||
) -> Result<(), TransportError> {
|
||||
let conn = get_connection(addr);
|
||||
match conn {
|
||||
Connection::Udp(conn) => conn.par_serialize_and_send_transaction_batch(transactions),
|
||||
Connection::Quic(conn) => conn.par_serialize_and_send_transaction_batch(transactions),
|
||||
}
|
||||
let (conn, stats) = get_connection(addr);
|
||||
let client_stats = ClientStats::default();
|
||||
let r = match conn {
|
||||
Connection::Udp(conn) => {
|
||||
conn.par_serialize_and_send_transaction_batch(transactions, &client_stats)
|
||||
}
|
||||
Connection::Quic(conn) => {
|
||||
conn.par_serialize_and_send_transaction_batch(transactions, &client_stats)
|
||||
}
|
||||
};
|
||||
stats.add_client_stats(&client_stats, transactions.len(), r.is_ok());
|
||||
r
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -158,6 +346,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_connection_cache() {
|
||||
solana_logger::setup();
|
||||
// Allow the test to run deterministically
|
||||
// with the same pseudorandom sequence between runs
|
||||
// and on different platforms - the cryptographic security
|
||||
@ -171,7 +360,7 @@ mod tests {
|
||||
// be lazy and not connect until first use or handle connection errors somehow
|
||||
// (without crashing, as would be required in a real practical validator)
|
||||
let first_addr = get_addr(&mut rng);
|
||||
assert!(ip(get_connection(&first_addr)) == first_addr.ip());
|
||||
assert!(ip(get_connection(&first_addr).0) == first_addr.ip());
|
||||
let addrs = (0..MAX_CONNECTIONS)
|
||||
.into_iter()
|
||||
.map(|_| {
|
||||
|
@ -197,6 +197,10 @@ impl RpcSender for HttpSender {
|
||||
return Ok(json["result"].take());
|
||||
}
|
||||
}
|
||||
|
||||
fn url(&self) -> String {
|
||||
self.url.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
@ -2,6 +2,9 @@
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
|
||||
#[macro_use]
|
||||
extern crate solana_metrics;
|
||||
|
||||
pub mod blockhash_query;
|
||||
pub mod client_error;
|
||||
pub mod connection_cache;
|
||||
@ -9,7 +12,6 @@ pub(crate) mod http_sender;
|
||||
pub(crate) mod mock_sender;
|
||||
pub mod nonblocking;
|
||||
pub mod nonce_utils;
|
||||
pub mod perf_utils;
|
||||
pub mod pubsub_client;
|
||||
pub mod quic_client;
|
||||
pub mod rpc_cache;
|
||||
|
@ -470,4 +470,8 @@ impl RpcSender for MockSender {
|
||||
};
|
||||
Ok(val)
|
||||
}
|
||||
|
||||
fn url(&self) -> String {
|
||||
format!("MockSender: {}", self.url)
|
||||
}
|
||||
}
|
||||
|
@ -502,6 +502,11 @@ impl RpcClient {
|
||||
Self::new_with_timeout(url, timeout)
|
||||
}
|
||||
|
||||
/// Get the configured url of the client's sender
|
||||
pub fn url(&self) -> String {
|
||||
self.sender.url()
|
||||
}
|
||||
|
||||
async fn get_node_version(&self) -> Result<semver::Version, RpcError> {
|
||||
let r_node_version = self.node_version.read().await;
|
||||
if let Some(version) = &*r_node_version {
|
||||
|
@ -2,18 +2,29 @@
|
||||
//! an interface for sending transactions which is restricted by the server's flow control.
|
||||
|
||||
use {
|
||||
crate::{client_error::ClientErrorKind, tpu_connection::TpuConnection},
|
||||
crate::{
|
||||
client_error::ClientErrorKind,
|
||||
tpu_connection::{ClientStats, TpuConnection},
|
||||
},
|
||||
async_mutex::Mutex,
|
||||
futures::future::join_all,
|
||||
itertools::Itertools,
|
||||
quinn::{ClientConfig, Endpoint, EndpointConfig, NewConnection, WriteError},
|
||||
lazy_static::lazy_static,
|
||||
log::*,
|
||||
quinn::{
|
||||
ClientConfig, Endpoint, EndpointConfig, IdleTimeout, NewConnection, VarInt, WriteError,
|
||||
},
|
||||
quinn_proto::ConnectionStats,
|
||||
solana_sdk::{
|
||||
quic::{QUIC_MAX_CONCURRENT_STREAMS, QUIC_PORT_OFFSET},
|
||||
quic::{
|
||||
QUIC_KEEP_ALIVE_MS, QUIC_MAX_CONCURRENT_STREAMS, QUIC_MAX_TIMEOUT_MS, QUIC_PORT_OFFSET,
|
||||
},
|
||||
transport::Result as TransportResult,
|
||||
},
|
||||
std::{
|
||||
net::{SocketAddr, UdpSocket},
|
||||
sync::Arc,
|
||||
sync::{atomic::Ordering, Arc},
|
||||
time::Duration,
|
||||
},
|
||||
tokio::runtime::Runtime,
|
||||
};
|
||||
@ -39,18 +50,34 @@ impl rustls::client::ServerCertVerifier for SkipServerVerification {
|
||||
Ok(rustls::client::ServerCertVerified::assertion())
|
||||
}
|
||||
}
|
||||
lazy_static! {
|
||||
static ref RUNTIME: Runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
struct QuicClient {
|
||||
runtime: Runtime,
|
||||
endpoint: Endpoint,
|
||||
connection: Arc<Mutex<Option<Arc<NewConnection>>>>,
|
||||
addr: SocketAddr,
|
||||
stats: Arc<ClientStats>,
|
||||
}
|
||||
|
||||
pub struct QuicTpuConnection {
|
||||
client: Arc<QuicClient>,
|
||||
}
|
||||
|
||||
impl QuicTpuConnection {
|
||||
pub fn stats(&self) -> Option<ConnectionStats> {
|
||||
self.client.stats()
|
||||
}
|
||||
|
||||
pub fn base_stats(&self) -> Arc<ClientStats> {
|
||||
self.client.stats.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl TpuConnection for QuicTpuConnection {
|
||||
fn new(client_socket: UdpSocket, tpu_addr: SocketAddr) -> Self {
|
||||
let tpu_addr = SocketAddr::new(tpu_addr.ip(), tpu_addr.port() + QUIC_PORT_OFFSET);
|
||||
@ -63,35 +90,74 @@ impl TpuConnection for QuicTpuConnection {
|
||||
&self.client.addr
|
||||
}
|
||||
|
||||
fn send_wire_transaction<T>(&self, wire_transaction: T) -> TransportResult<()>
|
||||
fn send_wire_transaction<T>(
|
||||
&self,
|
||||
wire_transaction: T,
|
||||
stats: &ClientStats,
|
||||
) -> TransportResult<()>
|
||||
where
|
||||
T: AsRef<[u8]>,
|
||||
{
|
||||
let _guard = self.client.runtime.enter();
|
||||
let send_buffer = self.client.send_buffer(wire_transaction);
|
||||
self.client.runtime.block_on(send_buffer)?;
|
||||
let _guard = RUNTIME.enter();
|
||||
let send_buffer = self.client.send_buffer(wire_transaction, stats);
|
||||
RUNTIME.block_on(send_buffer)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn send_wire_transaction_batch<T>(&self, buffers: &[T]) -> TransportResult<()>
|
||||
fn send_wire_transaction_batch<T>(
|
||||
&self,
|
||||
buffers: &[T],
|
||||
stats: &ClientStats,
|
||||
) -> TransportResult<()>
|
||||
where
|
||||
T: AsRef<[u8]>,
|
||||
{
|
||||
let _guard = self.client.runtime.enter();
|
||||
let send_batch = self.client.send_batch(buffers);
|
||||
self.client.runtime.block_on(send_batch)?;
|
||||
let _guard = RUNTIME.enter();
|
||||
let send_batch = self.client.send_batch(buffers, stats);
|
||||
RUNTIME.block_on(send_batch)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn send_wire_transaction_async(
|
||||
&self,
|
||||
wire_transaction: Vec<u8>,
|
||||
stats: Arc<ClientStats>,
|
||||
) -> TransportResult<()> {
|
||||
let _guard = RUNTIME.enter();
|
||||
let client = self.client.clone();
|
||||
//drop and detach the task
|
||||
let _ = RUNTIME.spawn(async move {
|
||||
let send_buffer = client.send_buffer(wire_transaction, &stats);
|
||||
if let Err(e) = send_buffer.await {
|
||||
warn!("Failed to send transaction async to {:?}", e);
|
||||
datapoint_warn!("send-wire-async", ("failure", 1, i64),);
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn send_wire_transaction_batch_async(
|
||||
&self,
|
||||
buffers: Vec<Vec<u8>>,
|
||||
stats: Arc<ClientStats>,
|
||||
) -> TransportResult<()> {
|
||||
let _guard = RUNTIME.enter();
|
||||
let client = self.client.clone();
|
||||
//drop and detach the task
|
||||
let _ = RUNTIME.spawn(async move {
|
||||
let send_batch = client.send_batch(&buffers, &stats);
|
||||
if let Err(e) = send_batch.await {
|
||||
warn!("Failed to send transaction batch async to {:?}", e);
|
||||
datapoint_warn!("send-wire-batch-async", ("failure", 1, i64),);
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl QuicClient {
|
||||
pub fn new(client_socket: UdpSocket, addr: SocketAddr) -> Self {
|
||||
let runtime = tokio::runtime::Builder::new_multi_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
let _guard = runtime.enter();
|
||||
let _guard = RUNTIME.enter();
|
||||
|
||||
let crypto = rustls::ClientConfig::builder()
|
||||
.with_safe_defaults()
|
||||
@ -100,18 +166,30 @@ impl QuicClient {
|
||||
|
||||
let create_endpoint = QuicClient::create_endpoint(EndpointConfig::default(), client_socket);
|
||||
|
||||
let mut endpoint = runtime.block_on(create_endpoint);
|
||||
let mut endpoint = RUNTIME.block_on(create_endpoint);
|
||||
|
||||
endpoint.set_default_client_config(ClientConfig::new(Arc::new(crypto)));
|
||||
let mut config = ClientConfig::new(Arc::new(crypto));
|
||||
let transport_config = Arc::get_mut(&mut config.transport).unwrap();
|
||||
let timeout = IdleTimeout::from(VarInt::from_u32(QUIC_MAX_TIMEOUT_MS));
|
||||
transport_config.max_idle_timeout(Some(timeout));
|
||||
transport_config.keep_alive_interval(Some(Duration::from_millis(QUIC_KEEP_ALIVE_MS)));
|
||||
|
||||
endpoint.set_default_client_config(config);
|
||||
|
||||
Self {
|
||||
runtime,
|
||||
endpoint,
|
||||
connection: Arc::new(Mutex::new(None)),
|
||||
addr,
|
||||
stats: Arc::new(ClientStats::default()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stats(&self) -> Option<ConnectionStats> {
|
||||
let conn_guard = self.connection.lock();
|
||||
let x = RUNTIME.block_on(conn_guard);
|
||||
x.as_ref().map(|c| c.connection.stats())
|
||||
}
|
||||
|
||||
// If this function becomes public, it should be changed to
|
||||
// not expose details of the specific Quic implementation we're using
|
||||
async fn create_endpoint(config: EndpointConfig, client_socket: UdpSocket) -> Endpoint {
|
||||
@ -128,18 +206,35 @@ impl QuicClient {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn make_connection(&self, stats: &ClientStats) -> Result<Arc<NewConnection>, WriteError> {
|
||||
let connecting = self.endpoint.connect(self.addr, "connect").unwrap();
|
||||
stats.total_connections.fetch_add(1, Ordering::Relaxed);
|
||||
let connecting_result = connecting.await;
|
||||
if connecting_result.is_err() {
|
||||
stats.connection_errors.fetch_add(1, Ordering::Relaxed);
|
||||
}
|
||||
let connection = connecting_result?;
|
||||
Ok(Arc::new(connection))
|
||||
}
|
||||
|
||||
// Attempts to send data, connecting/reconnecting as necessary
|
||||
// On success, returns the connection used to successfully send the data
|
||||
async fn _send_buffer(&self, data: &[u8]) -> Result<Arc<NewConnection>, WriteError> {
|
||||
async fn _send_buffer(
|
||||
&self,
|
||||
data: &[u8],
|
||||
stats: &ClientStats,
|
||||
) -> Result<Arc<NewConnection>, WriteError> {
|
||||
let connection = {
|
||||
let mut conn_guard = self.connection.lock().await;
|
||||
|
||||
let maybe_conn = (*conn_guard).clone();
|
||||
match maybe_conn {
|
||||
Some(conn) => conn.clone(),
|
||||
Some(conn) => {
|
||||
stats.connection_reuse.fetch_add(1, Ordering::Relaxed);
|
||||
conn.clone()
|
||||
}
|
||||
None => {
|
||||
let connecting = self.endpoint.connect(self.addr, "connect").unwrap();
|
||||
let connection = Arc::new(connecting.await?);
|
||||
let connection = self.make_connection(stats).await?;
|
||||
*conn_guard = Some(connection.clone());
|
||||
connection
|
||||
}
|
||||
@ -149,8 +244,7 @@ impl QuicClient {
|
||||
Ok(()) => Ok(connection),
|
||||
_ => {
|
||||
let connection = {
|
||||
let connecting = self.endpoint.connect(self.addr, "connect").unwrap();
|
||||
let connection = Arc::new(connecting.await?);
|
||||
let connection = self.make_connection(stats).await?;
|
||||
let mut conn_guard = self.connection.lock().await;
|
||||
*conn_guard = Some(connection.clone());
|
||||
connection
|
||||
@ -161,15 +255,19 @@ impl QuicClient {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn send_buffer<T>(&self, data: T) -> Result<(), ClientErrorKind>
|
||||
pub async fn send_buffer<T>(&self, data: T, stats: &ClientStats) -> Result<(), ClientErrorKind>
|
||||
where
|
||||
T: AsRef<[u8]>,
|
||||
{
|
||||
self._send_buffer(data.as_ref()).await?;
|
||||
self._send_buffer(data.as_ref(), stats).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn send_batch<T>(&self, buffers: &[T]) -> Result<(), ClientErrorKind>
|
||||
pub async fn send_batch<T>(
|
||||
&self,
|
||||
buffers: &[T],
|
||||
stats: &ClientStats,
|
||||
) -> Result<(), ClientErrorKind>
|
||||
where
|
||||
T: AsRef<[u8]>,
|
||||
{
|
||||
@ -187,7 +285,7 @@ impl QuicClient {
|
||||
if buffers.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let connection = self._send_buffer(buffers[0].as_ref()).await?;
|
||||
let connection = self._send_buffer(buffers[0].as_ref(), stats).await?;
|
||||
|
||||
// Used to avoid dereferencing the Arc multiple times below
|
||||
// by just getting a reference to the NewConnection once
|
||||
@ -197,13 +295,16 @@ impl QuicClient {
|
||||
.iter()
|
||||
.chunks(QUIC_MAX_CONCURRENT_STREAMS);
|
||||
|
||||
let futures = chunks.into_iter().map(|buffs| {
|
||||
join_all(
|
||||
buffs
|
||||
.into_iter()
|
||||
.map(|buf| Self::_send_buffer_using_conn(buf.as_ref(), connection_ref)),
|
||||
)
|
||||
});
|
||||
let futures: Vec<_> = chunks
|
||||
.into_iter()
|
||||
.map(|buffs| {
|
||||
join_all(
|
||||
buffs
|
||||
.into_iter()
|
||||
.map(|buf| Self::_send_buffer_using_conn(buf.as_ref(), connection_ref)),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
for f in futures {
|
||||
f.await.into_iter().try_for_each(|res| res)?;
|
||||
|
@ -535,6 +535,11 @@ impl RpcClient {
|
||||
Self::new_with_timeout(url, timeout)
|
||||
}
|
||||
|
||||
/// Get the configured url of the client's sender
|
||||
pub fn url(&self) -> String {
|
||||
self.rpc_client.url()
|
||||
}
|
||||
|
||||
/// Get the configured default [commitment level][cl].
|
||||
///
|
||||
/// [cl]: https://docs.solana.com/developing/clients/jsonrpc-api#configuring-state-commitment
|
||||
|
@ -348,7 +348,29 @@ pub struct RpcSimulateTransactionResult {
|
||||
pub logs: Option<Vec<String>>,
|
||||
pub accounts: Option<Vec<Option<UiAccount>>>,
|
||||
pub units_consumed: Option<u64>,
|
||||
pub return_data: Option<TransactionReturnData>,
|
||||
pub return_data: Option<RpcTransactionReturnData>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct RpcTransactionReturnData {
|
||||
pub program_id: String,
|
||||
pub data: (String, ReturnDataEncoding),
|
||||
}
|
||||
|
||||
impl From<TransactionReturnData> for RpcTransactionReturnData {
|
||||
fn from(return_data: TransactionReturnData) -> Self {
|
||||
Self {
|
||||
program_id: return_data.program_id.to_string(),
|
||||
data: (base64::encode(return_data.data), ReturnDataEncoding::Base64),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub enum ReturnDataEncoding {
|
||||
Base64,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
|
@ -32,4 +32,5 @@ pub trait RpcSender {
|
||||
params: serde_json::Value,
|
||||
) -> Result<serde_json::Value>;
|
||||
fn get_transport_stats(&self) -> RpcTransportStats;
|
||||
fn url(&self) -> String;
|
||||
}
|
||||
|
@ -171,7 +171,7 @@ impl ThinClient {
|
||||
&self.tpu_addrs[self.optimizer.best()]
|
||||
}
|
||||
|
||||
fn rpc_client(&self) -> &RpcClient {
|
||||
pub fn rpc_client(&self) -> &RpcClient {
|
||||
&self.rpc_clients[self.optimizer.best()]
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
use {
|
||||
crate::{
|
||||
client_error::ClientError,
|
||||
connection_cache::send_wire_transaction_async,
|
||||
pubsub_client::{PubsubClient, PubsubClientError, PubsubClientSubscription},
|
||||
rpc_client::RpcClient,
|
||||
rpc_request::MAX_GET_SIGNATURE_STATUSES_QUERY_ITEMS,
|
||||
@ -17,6 +18,7 @@ use {
|
||||
signature::SignerError,
|
||||
signers::Signers,
|
||||
transaction::{Transaction, TransactionError},
|
||||
transport::{Result as TransportResult, TransportError},
|
||||
},
|
||||
std::{
|
||||
collections::{HashMap, HashSet, VecDeque},
|
||||
@ -73,7 +75,7 @@ impl Default for TpuClientConfig {
|
||||
/// Client which sends transactions directly to the current leader's TPU port over UDP.
|
||||
/// The client uses RPC to determine the current leader and fetch node contact info
|
||||
pub struct TpuClient {
|
||||
send_socket: UdpSocket,
|
||||
_deprecated: UdpSocket, // TpuClient now uses the connection_cache to choose a send_socket
|
||||
fanout_slots: u64,
|
||||
leader_tpu_service: LeaderTpuService,
|
||||
exit: Arc<AtomicBool>,
|
||||
@ -85,25 +87,48 @@ impl TpuClient {
|
||||
/// size
|
||||
pub fn send_transaction(&self, transaction: &Transaction) -> bool {
|
||||
let wire_transaction = serialize(transaction).expect("serialization should succeed");
|
||||
self.send_wire_transaction(&wire_transaction)
|
||||
self.send_wire_transaction(wire_transaction)
|
||||
}
|
||||
|
||||
/// Send a wire transaction to the current and upcoming leader TPUs according to fanout size
|
||||
pub fn send_wire_transaction(&self, wire_transaction: &[u8]) -> bool {
|
||||
let mut sent = false;
|
||||
pub fn send_wire_transaction(&self, wire_transaction: Vec<u8>) -> bool {
|
||||
self.try_send_wire_transaction(wire_transaction).is_ok()
|
||||
}
|
||||
|
||||
/// Serialize and send transaction to the current and upcoming leader TPUs according to fanout
|
||||
/// size
|
||||
/// Returns the last error if all sends fail
|
||||
pub fn try_send_transaction(&self, transaction: &Transaction) -> TransportResult<()> {
|
||||
let wire_transaction = serialize(transaction).expect("serialization should succeed");
|
||||
self.try_send_wire_transaction(wire_transaction)
|
||||
}
|
||||
|
||||
/// Send a wire transaction to the current and upcoming leader TPUs according to fanout size
|
||||
/// Returns the last error if all sends fail
|
||||
fn try_send_wire_transaction(&self, wire_transaction: Vec<u8>) -> TransportResult<()> {
|
||||
let mut last_error: Option<TransportError> = None;
|
||||
let mut some_success = false;
|
||||
|
||||
for tpu_address in self
|
||||
.leader_tpu_service
|
||||
.leader_tpu_sockets(self.fanout_slots)
|
||||
{
|
||||
if self
|
||||
.send_socket
|
||||
.send_to(wire_transaction, tpu_address)
|
||||
.is_ok()
|
||||
{
|
||||
sent = true;
|
||||
let result = send_wire_transaction_async(wire_transaction.clone(), &tpu_address);
|
||||
if let Err(err) = result {
|
||||
last_error = Some(err);
|
||||
} else {
|
||||
some_success = true;
|
||||
}
|
||||
}
|
||||
sent
|
||||
if !some_success {
|
||||
Err(if let Some(err) = last_error {
|
||||
err
|
||||
} else {
|
||||
std::io::Error::new(std::io::ErrorKind::Other, "No sends attempted").into()
|
||||
})
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new client that disconnects when dropped
|
||||
@ -117,7 +142,7 @@ impl TpuClient {
|
||||
LeaderTpuService::new(rpc_client.clone(), websocket_url, exit.clone())?;
|
||||
|
||||
Ok(Self {
|
||||
send_socket: UdpSocket::bind("0.0.0.0:0").unwrap(),
|
||||
_deprecated: UdpSocket::bind("0.0.0.0:0").unwrap(),
|
||||
fanout_slots: config.fanout_slots.min(MAX_FANOUT_SLOTS).max(1),
|
||||
leader_tpu_service,
|
||||
exit,
|
||||
@ -266,6 +291,10 @@ impl TpuClient {
|
||||
}
|
||||
Err(TpuSenderError::Custom("Max retries exceeded".into()))
|
||||
}
|
||||
|
||||
pub fn rpc_client(&self) -> &RpcClient {
|
||||
&self.rpc_client
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TpuClient {
|
||||
|
@ -1,9 +1,26 @@
|
||||
use {
|
||||
rayon::iter::{IntoParallelIterator, ParallelIterator},
|
||||
solana_metrics::MovingStat,
|
||||
solana_sdk::{transaction::VersionedTransaction, transport::Result as TransportResult},
|
||||
std::net::{SocketAddr, UdpSocket},
|
||||
std::{
|
||||
net::{SocketAddr, UdpSocket},
|
||||
sync::{atomic::AtomicU64, Arc},
|
||||
},
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct ClientStats {
|
||||
pub total_connections: AtomicU64,
|
||||
pub connection_reuse: AtomicU64,
|
||||
pub connection_errors: AtomicU64,
|
||||
|
||||
// these will be the last values of these stats
|
||||
pub congestion_events: MovingStat,
|
||||
pub tx_streams_blocked_uni: MovingStat,
|
||||
pub tx_data_blocked: MovingStat,
|
||||
pub tx_acks: MovingStat,
|
||||
}
|
||||
|
||||
pub trait TpuConnection {
|
||||
fn new(client_socket: UdpSocket, tpu_addr: SocketAddr) -> Self;
|
||||
|
||||
@ -12,29 +29,51 @@ pub trait TpuConnection {
|
||||
fn serialize_and_send_transaction(
|
||||
&self,
|
||||
transaction: &VersionedTransaction,
|
||||
stats: &ClientStats,
|
||||
) -> TransportResult<()> {
|
||||
let wire_transaction =
|
||||
bincode::serialize(transaction).expect("serialize Transaction in send_batch");
|
||||
self.send_wire_transaction(&wire_transaction)
|
||||
self.send_wire_transaction(&wire_transaction, stats)
|
||||
}
|
||||
|
||||
fn send_wire_transaction<T>(&self, wire_transaction: T) -> TransportResult<()>
|
||||
fn send_wire_transaction<T>(
|
||||
&self,
|
||||
wire_transaction: T,
|
||||
stats: &ClientStats,
|
||||
) -> TransportResult<()>
|
||||
where
|
||||
T: AsRef<[u8]>;
|
||||
|
||||
fn send_wire_transaction_async(
|
||||
&self,
|
||||
wire_transaction: Vec<u8>,
|
||||
stats: Arc<ClientStats>,
|
||||
) -> TransportResult<()>;
|
||||
|
||||
fn par_serialize_and_send_transaction_batch(
|
||||
&self,
|
||||
transactions: &[VersionedTransaction],
|
||||
stats: &ClientStats,
|
||||
) -> TransportResult<()> {
|
||||
let buffers = transactions
|
||||
.into_par_iter()
|
||||
.map(|tx| bincode::serialize(&tx).expect("serialize Transaction in send_batch"))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
self.send_wire_transaction_batch(&buffers)
|
||||
self.send_wire_transaction_batch(&buffers, stats)
|
||||
}
|
||||
|
||||
fn send_wire_transaction_batch<T>(&self, buffers: &[T]) -> TransportResult<()>
|
||||
fn send_wire_transaction_batch<T>(
|
||||
&self,
|
||||
buffers: &[T],
|
||||
stats: &ClientStats,
|
||||
) -> TransportResult<()>
|
||||
where
|
||||
T: AsRef<[u8]>;
|
||||
|
||||
fn send_wire_transaction_batch_async(
|
||||
&self,
|
||||
buffers: Vec<Vec<u8>>,
|
||||
stats: Arc<ClientStats>,
|
||||
) -> TransportResult<()>;
|
||||
}
|
||||
|
@ -2,11 +2,14 @@
|
||||
//! an interface for sending transactions
|
||||
|
||||
use {
|
||||
crate::tpu_connection::TpuConnection,
|
||||
crate::tpu_connection::{ClientStats, TpuConnection},
|
||||
core::iter::repeat,
|
||||
solana_sdk::transport::Result as TransportResult,
|
||||
solana_streamer::sendmmsg::batch_send,
|
||||
std::net::{SocketAddr, UdpSocket},
|
||||
std::{
|
||||
net::{SocketAddr, UdpSocket},
|
||||
sync::Arc,
|
||||
},
|
||||
};
|
||||
|
||||
pub struct UdpTpuConnection {
|
||||
@ -26,7 +29,11 @@ impl TpuConnection for UdpTpuConnection {
|
||||
&self.addr
|
||||
}
|
||||
|
||||
fn send_wire_transaction<T>(&self, wire_transaction: T) -> TransportResult<()>
|
||||
fn send_wire_transaction<T>(
|
||||
&self,
|
||||
wire_transaction: T,
|
||||
_stats: &ClientStats,
|
||||
) -> TransportResult<()>
|
||||
where
|
||||
T: AsRef<[u8]>,
|
||||
{
|
||||
@ -34,7 +41,20 @@ impl TpuConnection for UdpTpuConnection {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn send_wire_transaction_batch<T>(&self, buffers: &[T]) -> TransportResult<()>
|
||||
fn send_wire_transaction_async(
|
||||
&self,
|
||||
wire_transaction: Vec<u8>,
|
||||
_stats: Arc<ClientStats>,
|
||||
) -> TransportResult<()> {
|
||||
self.socket.send_to(wire_transaction.as_ref(), self.addr)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn send_wire_transaction_batch<T>(
|
||||
&self,
|
||||
buffers: &[T],
|
||||
_stats: &ClientStats,
|
||||
) -> TransportResult<()>
|
||||
where
|
||||
T: AsRef<[u8]>,
|
||||
{
|
||||
@ -42,4 +62,13 @@ impl TpuConnection for UdpTpuConnection {
|
||||
batch_send(&self.socket, &pkts)?;
|
||||
Ok(())
|
||||
}
|
||||
fn send_wire_transaction_batch_async(
|
||||
&self,
|
||||
buffers: Vec<Vec<u8>>,
|
||||
_stats: Arc<ClientStats>,
|
||||
) -> TransportResult<()> {
|
||||
let pkts: Vec<_> = buffers.into_iter().zip(repeat(self.tpu_addr())).collect();
|
||||
batch_send(&self.socket, &pkts)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -21,12 +21,12 @@ bs58 = "0.4.0"
|
||||
chrono = { version = "0.4.11", features = ["serde"] }
|
||||
crossbeam-channel = "0.5"
|
||||
dashmap = { version = "4.0.2", features = ["rayon", "raw-api"] }
|
||||
etcd-client = { version = "0.8.4", features = ["tls"] }
|
||||
etcd-client = { version = "0.9.0", features = ["tls"] }
|
||||
fs_extra = "1.2.0"
|
||||
histogram = "0.6.9"
|
||||
itertools = "0.10.3"
|
||||
log = "0.4.14"
|
||||
lru = "0.7.3"
|
||||
lru = "0.7.5"
|
||||
rand = "0.7.0"
|
||||
rand_chacha = "0.2.2"
|
||||
rayon = "1.5.1"
|
||||
@ -49,7 +49,6 @@ solana-perf = { path = "../perf", version = "=1.11.0" }
|
||||
solana-poh = { path = "../poh", version = "=1.11.0" }
|
||||
solana-program-runtime = { path = "../program-runtime", version = "=1.11.0" }
|
||||
solana-rayon-threadlimit = { path = "../rayon-threadlimit", version = "=1.11.0" }
|
||||
solana-replica-lib = { path = "../replica-lib", version = "=1.11.0" }
|
||||
solana-rpc = { path = "../rpc", version = "=1.11.0" }
|
||||
solana-runtime = { path = "../runtime", version = "=1.11.0" }
|
||||
solana-sdk = { path = "../sdk", version = "=1.11.0" }
|
||||
|
@ -96,7 +96,7 @@ impl AccountsHashVerifier {
|
||||
fault_injection_rate_slots: u64,
|
||||
snapshot_config: Option<&SnapshotConfig>,
|
||||
) {
|
||||
Self::verify_accounts_package_hash(&accounts_package);
|
||||
let accounts_hash = Self::calculate_and_verify_accounts_hash(&accounts_package);
|
||||
|
||||
Self::push_accounts_hashes_to_cluster(
|
||||
&accounts_package,
|
||||
@ -106,49 +106,63 @@ impl AccountsHashVerifier {
|
||||
hashes,
|
||||
exit,
|
||||
fault_injection_rate_slots,
|
||||
accounts_hash,
|
||||
);
|
||||
|
||||
Self::submit_for_packaging(accounts_package, pending_snapshot_package, snapshot_config);
|
||||
Self::submit_for_packaging(
|
||||
accounts_package,
|
||||
pending_snapshot_package,
|
||||
snapshot_config,
|
||||
accounts_hash,
|
||||
);
|
||||
}
|
||||
|
||||
fn verify_accounts_package_hash(accounts_package: &AccountsPackage) {
|
||||
/// returns calculated accounts hash
|
||||
fn calculate_and_verify_accounts_hash(accounts_package: &AccountsPackage) -> Hash {
|
||||
let mut measure_hash = Measure::start("hash");
|
||||
if let Some(expected_hash) = accounts_package.accounts_hash_for_testing {
|
||||
let mut sort_time = Measure::start("sort_storages");
|
||||
let sorted_storages = SortedStorages::new(&accounts_package.snapshot_storages);
|
||||
sort_time.stop();
|
||||
let mut sort_time = Measure::start("sort_storages");
|
||||
let sorted_storages = SortedStorages::new(&accounts_package.snapshot_storages);
|
||||
sort_time.stop();
|
||||
|
||||
let mut timings = HashStats {
|
||||
storage_sort_us: sort_time.as_us(),
|
||||
..HashStats::default()
|
||||
};
|
||||
timings.calc_storage_size_quartiles(&accounts_package.snapshot_storages);
|
||||
|
||||
let (hash, lamports) = accounts_package
|
||||
.accounts
|
||||
.accounts_db
|
||||
.calculate_accounts_hash_without_index(
|
||||
&CalcAccountsHashConfig {
|
||||
use_bg_thread_pool: true,
|
||||
check_hash: false,
|
||||
ancestors: None,
|
||||
use_write_cache: false,
|
||||
epoch_schedule: &accounts_package.epoch_schedule,
|
||||
rent_collector: &accounts_package.rent_collector,
|
||||
},
|
||||
&sorted_storages,
|
||||
timings,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(accounts_package.expected_capitalization, lamports);
|
||||
assert_eq!(expected_hash, hash);
|
||||
let mut timings = HashStats {
|
||||
storage_sort_us: sort_time.as_us(),
|
||||
..HashStats::default()
|
||||
};
|
||||
timings.calc_storage_size_quartiles(&accounts_package.snapshot_storages);
|
||||
|
||||
let (accounts_hash, lamports) = accounts_package
|
||||
.accounts
|
||||
.accounts_db
|
||||
.calculate_accounts_hash_without_index(
|
||||
&CalcAccountsHashConfig {
|
||||
use_bg_thread_pool: true,
|
||||
check_hash: false,
|
||||
ancestors: None,
|
||||
use_write_cache: false,
|
||||
epoch_schedule: &accounts_package.epoch_schedule,
|
||||
rent_collector: &accounts_package.rent_collector,
|
||||
},
|
||||
&sorted_storages,
|
||||
timings,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(accounts_package.expected_capitalization, lamports);
|
||||
if let Some(expected_hash) = accounts_package.accounts_hash_for_testing {
|
||||
assert_eq!(expected_hash, accounts_hash);
|
||||
};
|
||||
|
||||
measure_hash.stop();
|
||||
solana_runtime::serde_snapshot::reserialize_bank_with_new_accounts_hash(
|
||||
accounts_package.snapshot_links.path(),
|
||||
accounts_package.slot,
|
||||
&accounts_hash,
|
||||
);
|
||||
datapoint_info!(
|
||||
"accounts_hash_verifier",
|
||||
("calculate_hash", measure_hash.as_us(), i64),
|
||||
);
|
||||
accounts_hash
|
||||
}
|
||||
|
||||
fn push_accounts_hashes_to_cluster(
|
||||
@ -159,8 +173,8 @@ impl AccountsHashVerifier {
|
||||
hashes: &mut Vec<(Slot, Hash)>,
|
||||
exit: &Arc<AtomicBool>,
|
||||
fault_injection_rate_slots: u64,
|
||||
accounts_hash: Hash,
|
||||
) {
|
||||
let hash = accounts_package.accounts_hash;
|
||||
if fault_injection_rate_slots != 0
|
||||
&& accounts_package.slot % fault_injection_rate_slots == 0
|
||||
{
|
||||
@ -171,10 +185,10 @@ impl AccountsHashVerifier {
|
||||
};
|
||||
warn!("inserting fault at slot: {}", accounts_package.slot);
|
||||
let rand = thread_rng().gen_range(0, 10);
|
||||
let hash = extend_and_hash(&hash, &[rand]);
|
||||
let hash = extend_and_hash(&accounts_hash, &[rand]);
|
||||
hashes.push((accounts_package.slot, hash));
|
||||
} else {
|
||||
hashes.push((accounts_package.slot, hash));
|
||||
hashes.push((accounts_package.slot, accounts_hash));
|
||||
}
|
||||
|
||||
while hashes.len() > MAX_SNAPSHOT_HASHES {
|
||||
@ -198,6 +212,7 @@ impl AccountsHashVerifier {
|
||||
accounts_package: AccountsPackage,
|
||||
pending_snapshot_package: Option<&PendingSnapshotPackage>,
|
||||
snapshot_config: Option<&SnapshotConfig>,
|
||||
accounts_hash: Hash,
|
||||
) {
|
||||
if accounts_package.snapshot_type.is_none()
|
||||
|| pending_snapshot_package.is_none()
|
||||
@ -206,7 +221,7 @@ impl AccountsHashVerifier {
|
||||
return;
|
||||
};
|
||||
|
||||
let snapshot_package = SnapshotPackage::from(accounts_package);
|
||||
let snapshot_package = SnapshotPackage::new(accounts_package, accounts_hash);
|
||||
let pending_snapshot_package = pending_snapshot_package.unwrap();
|
||||
let _snapshot_config = snapshot_config.unwrap();
|
||||
|
||||
@ -295,6 +310,7 @@ mod tests {
|
||||
sysvar::epoch_schedule::EpochSchedule,
|
||||
},
|
||||
solana_streamer::socket::SocketAddrSpace,
|
||||
std::str::FromStr,
|
||||
};
|
||||
|
||||
fn new_test_cluster_info(contact_info: ContactInfo) -> ClusterInfo {
|
||||
@ -358,6 +374,7 @@ mod tests {
|
||||
..SnapshotConfig::default()
|
||||
};
|
||||
let accounts = Arc::new(solana_runtime::accounts::Accounts::default_for_tests());
|
||||
let expected_hash = Hash::from_str("GKot5hBsd81kMupNCXHaqbhv3huEbxAFMLnpcX2hniwn").unwrap();
|
||||
for i in 0..MAX_SNAPSHOT_HASHES + 1 {
|
||||
let accounts_package = AccountsPackage {
|
||||
slot: full_snapshot_archive_interval_slots + i as u64,
|
||||
@ -365,7 +382,6 @@ mod tests {
|
||||
slot_deltas: vec![],
|
||||
snapshot_links: TempDir::new().unwrap(),
|
||||
snapshot_storages: vec![],
|
||||
accounts_hash: hash(&[i as u8]),
|
||||
archive_format: ArchiveFormat::TarBzip2,
|
||||
snapshot_version: SnapshotVersion::default(),
|
||||
snapshot_archives_dir: PathBuf::default(),
|
||||
@ -403,13 +419,13 @@ mod tests {
|
||||
assert_eq!(cluster_hashes.len(), MAX_SNAPSHOT_HASHES);
|
||||
assert_eq!(
|
||||
cluster_hashes[0],
|
||||
(full_snapshot_archive_interval_slots + 1, hash(&[1]))
|
||||
(full_snapshot_archive_interval_slots + 1, expected_hash)
|
||||
);
|
||||
assert_eq!(
|
||||
cluster_hashes[MAX_SNAPSHOT_HASHES - 1],
|
||||
(
|
||||
full_snapshot_archive_interval_slots + MAX_SNAPSHOT_HASHES as u64,
|
||||
hash(&[MAX_SNAPSHOT_HASHES as u8])
|
||||
expected_hash
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ use {
|
||||
solana_perf::{
|
||||
cuda_runtime::PinnedVec,
|
||||
data_budget::DataBudget,
|
||||
packet::{limited_deserialize, Packet, PacketBatch, PACKETS_PER_BATCH},
|
||||
packet::{Packet, PacketBatch, PACKETS_PER_BATCH},
|
||||
perf_libs,
|
||||
},
|
||||
solana_poh::poh_recorder::{BankStart, PohRecorder, PohRecorderError, TransactionRecorder},
|
||||
@ -45,13 +45,10 @@ use {
|
||||
MAX_TRANSACTION_FORWARDING_DELAY_GPU,
|
||||
},
|
||||
feature_set,
|
||||
message::Message,
|
||||
pubkey::Pubkey,
|
||||
saturating_add_assign,
|
||||
timing::{duration_as_ms, timestamp, AtomicInterval},
|
||||
transaction::{
|
||||
self, AddressLoader, SanitizedTransaction, TransactionError, VersionedTransaction,
|
||||
},
|
||||
transaction::{self, AddressLoader, SanitizedTransaction, TransactionError},
|
||||
transport::TransportError,
|
||||
},
|
||||
solana_transaction_status::token_balances::{
|
||||
@ -558,7 +555,6 @@ impl BankingStage {
|
||||
let mut reached_end_of_slot: Option<EndOfSlot> = None;
|
||||
|
||||
RetainMut::retain_mut(buffered_packet_batches, |deserialized_packet_batch| {
|
||||
let packet_batch = &deserialized_packet_batch.packet_batch;
|
||||
let original_unprocessed_indexes = deserialized_packet_batch
|
||||
.unprocessed_packets
|
||||
.keys()
|
||||
@ -572,8 +568,7 @@ impl BankingStage {
|
||||
let should_retain = if let Some(bank) = &end_of_slot.working_bank {
|
||||
let new_unprocessed_indexes = Self::filter_unprocessed_packets_at_end_of_slot(
|
||||
bank,
|
||||
packet_batch,
|
||||
&original_unprocessed_indexes,
|
||||
&deserialized_packet_batch.unprocessed_packets,
|
||||
my_pubkey,
|
||||
end_of_slot.next_slot_leader,
|
||||
banking_stage_stats,
|
||||
@ -624,8 +619,7 @@ impl BankingStage {
|
||||
&working_bank,
|
||||
&bank_creation_time,
|
||||
recorder,
|
||||
packet_batch,
|
||||
original_unprocessed_indexes.to_owned(),
|
||||
&deserialized_packet_batch.unprocessed_packets,
|
||||
transaction_status_sender.clone(),
|
||||
gossip_vote_sender,
|
||||
banking_stage_stats,
|
||||
@ -1351,38 +1345,28 @@ impl BankingStage {
|
||||
gossip_vote_sender: &ReplayVoteSender,
|
||||
qos_service: &QosService,
|
||||
) -> ProcessTransactionBatchOutput {
|
||||
let ((transactions_qos_results, cost_model_throttled_transactions_count), cost_model_time) =
|
||||
Measure::this(
|
||||
|_| {
|
||||
let tx_costs = qos_service.compute_transaction_costs(txs.iter());
|
||||
let mut cost_model_time = Measure::start("cost_model");
|
||||
|
||||
let (transactions_qos_results, num_included) =
|
||||
qos_service.select_transactions_per_cost(txs.iter(), tx_costs.iter(), bank);
|
||||
let transaction_costs = qos_service.compute_transaction_costs(txs.iter());
|
||||
|
||||
let cost_model_throttled_transactions_count =
|
||||
txs.len().saturating_sub(num_included);
|
||||
let (transactions_qos_results, num_included) =
|
||||
qos_service.select_transactions_per_cost(txs.iter(), transaction_costs.iter(), bank);
|
||||
|
||||
qos_service.accumulate_estimated_transaction_costs(
|
||||
&Self::accumulate_batched_transaction_costs(
|
||||
tx_costs.iter(),
|
||||
transactions_qos_results.iter(),
|
||||
),
|
||||
);
|
||||
(
|
||||
transactions_qos_results,
|
||||
cost_model_throttled_transactions_count,
|
||||
)
|
||||
},
|
||||
(),
|
||||
"cost_model",
|
||||
);
|
||||
let cost_model_throttled_transactions_count = txs.len().saturating_sub(num_included);
|
||||
|
||||
qos_service.accumulate_estimated_transaction_costs(
|
||||
&Self::accumulate_batched_transaction_costs(
|
||||
transaction_costs.iter(),
|
||||
transactions_qos_results.iter(),
|
||||
),
|
||||
);
|
||||
cost_model_time.stop();
|
||||
|
||||
// Only lock accounts for those transactions are selected for the block;
|
||||
// Once accounts are locked, other threads cannot encode transactions that will modify the
|
||||
// same account state
|
||||
let mut lock_time = Measure::start("lock_time");
|
||||
let batch =
|
||||
bank.prepare_sanitized_batch_with_results(txs, transactions_qos_results.into_iter());
|
||||
let batch = bank.prepare_sanitized_batch_with_results(txs, transactions_qos_results.iter());
|
||||
lock_time.stop();
|
||||
|
||||
// retryable_txs includes AccountInUse, WouldExceedMaxBlockCostLimit
|
||||
@ -1397,21 +1381,31 @@ impl BankingStage {
|
||||
gossip_vote_sender,
|
||||
);
|
||||
|
||||
let mut unlock_time = Measure::start("unlock_time");
|
||||
// Once the accounts are new transactions can enter the pipeline to process them
|
||||
drop(batch);
|
||||
unlock_time.stop();
|
||||
|
||||
let ExecuteAndCommitTransactionsOutput {
|
||||
ref mut retryable_transaction_indexes,
|
||||
ref execute_and_commit_timings,
|
||||
..
|
||||
} = execute_and_commit_transactions_output;
|
||||
|
||||
// TODO: This does not revert the cost tracker changes from all unexecuted transactions
|
||||
// yet: For example tx that are too old will not be included in the block, but are not
|
||||
// retryable.
|
||||
QosService::update_or_remove_transaction_costs(
|
||||
transaction_costs.iter(),
|
||||
transactions_qos_results.iter(),
|
||||
retryable_transaction_indexes,
|
||||
bank,
|
||||
);
|
||||
|
||||
retryable_transaction_indexes
|
||||
.iter_mut()
|
||||
.for_each(|x| *x += chunk_offset);
|
||||
|
||||
let mut unlock_time = Measure::start("unlock_time");
|
||||
// Once the accounts are new transactions can enter the pipeline to process them
|
||||
drop(batch);
|
||||
unlock_time.stop();
|
||||
|
||||
let (cu, us) =
|
||||
Self::accumulate_execute_units_and_time(&execute_and_commit_timings.execute_timings);
|
||||
qos_service.accumulate_actual_execute_cu(cu);
|
||||
@ -1685,32 +1679,27 @@ impl BankingStage {
|
||||
// with their packet indexes.
|
||||
#[allow(clippy::needless_collect)]
|
||||
fn transactions_from_packets(
|
||||
packet_batch: &PacketBatch,
|
||||
transaction_indexes: &[usize],
|
||||
deserialized_packet_batch: &HashMap<usize, DeserializedPacket>,
|
||||
feature_set: &Arc<feature_set::FeatureSet>,
|
||||
votes_only: bool,
|
||||
address_loader: impl AddressLoader,
|
||||
) -> (Vec<SanitizedTransaction>, Vec<usize>) {
|
||||
transaction_indexes
|
||||
deserialized_packet_batch
|
||||
.iter()
|
||||
.filter_map(|tx_index| {
|
||||
let p = &packet_batch.packets[*tx_index];
|
||||
if votes_only && !p.meta.is_simple_vote_tx() {
|
||||
.filter_map(|(&tx_index, deserialized_packet)| {
|
||||
if votes_only && !deserialized_packet.is_simple_vote {
|
||||
return None;
|
||||
}
|
||||
|
||||
let tx: VersionedTransaction = limited_deserialize(&p.data[0..p.meta.size]).ok()?;
|
||||
let message_bytes = DeserializedPacketBatch::packet_message(p)?;
|
||||
let message_hash = Message::hash_raw_message(message_bytes);
|
||||
let tx = SanitizedTransaction::try_create(
|
||||
tx,
|
||||
message_hash,
|
||||
Some(p.meta.is_simple_vote_tx()),
|
||||
deserialized_packet.versioned_transaction.clone(),
|
||||
deserialized_packet.message_hash,
|
||||
Some(deserialized_packet.is_simple_vote),
|
||||
address_loader.clone(),
|
||||
)
|
||||
.ok()?;
|
||||
tx.verify_precompiles(feature_set).ok()?;
|
||||
Some((tx, *tx_index))
|
||||
Some((tx, tx_index))
|
||||
})
|
||||
.unzip()
|
||||
}
|
||||
@ -1759,8 +1748,7 @@ impl BankingStage {
|
||||
bank: &Arc<Bank>,
|
||||
bank_creation_time: &Instant,
|
||||
poh: &TransactionRecorder,
|
||||
packet_batch: &PacketBatch,
|
||||
packet_indexes: Vec<usize>,
|
||||
deserialized_packet_batch: &HashMap<usize, DeserializedPacket>,
|
||||
transaction_status_sender: Option<TransactionStatusSender>,
|
||||
gossip_vote_sender: &ReplayVoteSender,
|
||||
banking_stage_stats: &BankingStageStats,
|
||||
@ -1771,8 +1759,7 @@ impl BankingStage {
|
||||
let ((transactions, transaction_to_packet_indexes), packet_conversion_time) = Measure::this(
|
||||
|_| {
|
||||
Self::transactions_from_packets(
|
||||
packet_batch,
|
||||
&packet_indexes,
|
||||
deserialized_packet_batch,
|
||||
&bank.feature_set,
|
||||
bank.vote_only_bank(),
|
||||
bank.as_ref(),
|
||||
@ -1860,8 +1847,7 @@ impl BankingStage {
|
||||
|
||||
fn filter_unprocessed_packets_at_end_of_slot(
|
||||
bank: &Arc<Bank>,
|
||||
packet_batch: &PacketBatch,
|
||||
transaction_indexes: &[usize],
|
||||
deserialized_packet_batch: &HashMap<usize, DeserializedPacket>,
|
||||
my_pubkey: &Pubkey,
|
||||
next_leader: Option<Pubkey>,
|
||||
banking_stage_stats: &BankingStageStats,
|
||||
@ -1871,15 +1857,17 @@ impl BankingStage {
|
||||
// Filtering helps if we were going to forward the packets to some other node
|
||||
if let Some(leader) = next_leader {
|
||||
if leader == *my_pubkey {
|
||||
return transaction_indexes.to_vec();
|
||||
return deserialized_packet_batch
|
||||
.keys()
|
||||
.cloned()
|
||||
.collect::<Vec<usize>>();
|
||||
}
|
||||
}
|
||||
|
||||
let mut unprocessed_packet_conversion_time =
|
||||
Measure::start("unprocessed_packet_conversion");
|
||||
let (transactions, transaction_to_packet_indexes) = Self::transactions_from_packets(
|
||||
packet_batch,
|
||||
transaction_indexes,
|
||||
deserialized_packet_batch,
|
||||
&bank.feature_set,
|
||||
bank.vote_only_bank(),
|
||||
bank.as_ref(),
|
||||
@ -2113,7 +2101,7 @@ mod tests {
|
||||
get_tmp_ledger_path_auto_delete,
|
||||
leader_schedule_cache::LeaderScheduleCache,
|
||||
},
|
||||
solana_perf::packet::{to_packet_batches, PacketFlags},
|
||||
solana_perf::packet::{limited_deserialize, to_packet_batches, PacketFlags},
|
||||
solana_poh::{
|
||||
poh_recorder::{create_test_recorder, Record, WorkingBankEntry},
|
||||
poh_service::PohService,
|
||||
@ -2133,7 +2121,10 @@ mod tests {
|
||||
signature::{Keypair, Signer},
|
||||
system_instruction::SystemError,
|
||||
system_transaction,
|
||||
transaction::{MessageHash, SimpleAddressLoader, Transaction, TransactionError},
|
||||
transaction::{
|
||||
MessageHash, SimpleAddressLoader, Transaction, TransactionError,
|
||||
VersionedTransaction,
|
||||
},
|
||||
},
|
||||
solana_streamer::{recvmmsg::recv_mmsg, socket::SocketAddrSpace},
|
||||
solana_transaction_status::{TransactionStatusMeta, VersionedTransactionWithStatusMeta},
|
||||
@ -2893,6 +2884,131 @@ mod tests {
|
||||
Blockstore::destroy(ledger_path.path()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bank_process_and_record_transactions_cost_tracker() {
|
||||
solana_logger::setup();
|
||||
let GenesisConfigInfo {
|
||||
genesis_config,
|
||||
mint_keypair,
|
||||
..
|
||||
} = create_slow_genesis_config(10_000);
|
||||
let bank = Arc::new(Bank::new_no_wallclock_throttle_for_tests(&genesis_config));
|
||||
let pubkey = solana_sdk::pubkey::new_rand();
|
||||
|
||||
let ledger_path = get_tmp_ledger_path_auto_delete!();
|
||||
{
|
||||
let blockstore = Blockstore::open(ledger_path.path())
|
||||
.expect("Expected to be able to open database ledger");
|
||||
let (poh_recorder, _entry_receiver, record_receiver) = PohRecorder::new(
|
||||
bank.tick_height(),
|
||||
bank.last_blockhash(),
|
||||
bank.clone(),
|
||||
Some((4, 4)),
|
||||
bank.ticks_per_slot(),
|
||||
&pubkey,
|
||||
&Arc::new(blockstore),
|
||||
&Arc::new(LeaderScheduleCache::new_from_bank(&bank)),
|
||||
&Arc::new(PohConfig::default()),
|
||||
Arc::new(AtomicBool::default()),
|
||||
);
|
||||
let recorder = poh_recorder.recorder();
|
||||
let poh_recorder = Arc::new(Mutex::new(poh_recorder));
|
||||
|
||||
let poh_simulator = simulate_poh(record_receiver, &poh_recorder);
|
||||
|
||||
poh_recorder.lock().unwrap().set_bank(&bank);
|
||||
let (gossip_vote_sender, _gossip_vote_receiver) = unbounded();
|
||||
|
||||
let qos_service = QosService::new(Arc::new(RwLock::new(CostModel::default())), 1);
|
||||
|
||||
let get_block_cost = || bank.read_cost_tracker().unwrap().block_cost();
|
||||
let get_tx_count = || bank.read_cost_tracker().unwrap().transaction_count();
|
||||
assert_eq!(get_block_cost(), 0);
|
||||
assert_eq!(get_tx_count(), 0);
|
||||
|
||||
//
|
||||
// TEST: cost tracker's block cost increases when successfully processing a tx
|
||||
//
|
||||
|
||||
let transactions = sanitize_transactions(vec![system_transaction::transfer(
|
||||
&mint_keypair,
|
||||
&pubkey,
|
||||
1,
|
||||
genesis_config.hash(),
|
||||
)]);
|
||||
|
||||
let process_transactions_batch_output = BankingStage::process_and_record_transactions(
|
||||
&bank,
|
||||
&transactions,
|
||||
&recorder,
|
||||
0,
|
||||
None,
|
||||
&gossip_vote_sender,
|
||||
&qos_service,
|
||||
);
|
||||
|
||||
let ExecuteAndCommitTransactionsOutput {
|
||||
executed_with_successful_result_count,
|
||||
commit_transactions_result,
|
||||
..
|
||||
} = process_transactions_batch_output.execute_and_commit_transactions_output;
|
||||
assert_eq!(executed_with_successful_result_count, 1);
|
||||
assert!(commit_transactions_result.is_ok());
|
||||
|
||||
let single_transfer_cost = get_block_cost();
|
||||
assert_ne!(single_transfer_cost, 0);
|
||||
assert_eq!(get_tx_count(), 1);
|
||||
|
||||
//
|
||||
// TEST: When a tx in a batch can't be executed (here because of account
|
||||
// locks), then its cost does not affect the cost tracker.
|
||||
//
|
||||
|
||||
let allocate_keypair = Keypair::new();
|
||||
let transactions = sanitize_transactions(vec![
|
||||
system_transaction::transfer(&mint_keypair, &pubkey, 2, genesis_config.hash()),
|
||||
// intentionally use a tx that has a different cost
|
||||
system_transaction::allocate(
|
||||
&mint_keypair,
|
||||
&allocate_keypair,
|
||||
genesis_config.hash(),
|
||||
1,
|
||||
),
|
||||
]);
|
||||
|
||||
let process_transactions_batch_output = BankingStage::process_and_record_transactions(
|
||||
&bank,
|
||||
&transactions,
|
||||
&recorder,
|
||||
0,
|
||||
None,
|
||||
&gossip_vote_sender,
|
||||
&qos_service,
|
||||
);
|
||||
|
||||
let ExecuteAndCommitTransactionsOutput {
|
||||
executed_with_successful_result_count,
|
||||
commit_transactions_result,
|
||||
retryable_transaction_indexes,
|
||||
..
|
||||
} = process_transactions_batch_output.execute_and_commit_transactions_output;
|
||||
assert_eq!(executed_with_successful_result_count, 1);
|
||||
assert!(commit_transactions_result.is_ok());
|
||||
assert_eq!(retryable_transaction_indexes, vec![1]);
|
||||
|
||||
assert_eq!(get_block_cost(), 2 * single_transfer_cost);
|
||||
assert_eq!(get_tx_count(), 2);
|
||||
|
||||
poh_recorder
|
||||
.lock()
|
||||
.unwrap()
|
||||
.is_exited
|
||||
.store(true, Ordering::Relaxed);
|
||||
let _ = poh_simulator.join();
|
||||
}
|
||||
Blockstore::destroy(ledger_path.path()).unwrap();
|
||||
}
|
||||
|
||||
fn simulate_poh(
|
||||
record_receiver: CrossbeamReceiver<Record>,
|
||||
poh_recorder: &Arc<Mutex<PohRecorder>>,
|
||||
@ -4115,7 +4231,7 @@ mod tests {
|
||||
fn make_test_packets(
|
||||
transactions: Vec<Transaction>,
|
||||
vote_indexes: Vec<usize>,
|
||||
) -> (PacketBatch, Vec<usize>) {
|
||||
) -> DeserializedPacketBatch {
|
||||
let capacity = transactions.len();
|
||||
let mut packet_batch = PacketBatch::with_capacity(capacity);
|
||||
let mut packet_indexes = Vec::with_capacity(capacity);
|
||||
@ -4127,7 +4243,7 @@ mod tests {
|
||||
for index in vote_indexes.iter() {
|
||||
packet_batch.packets[*index].meta.flags |= PacketFlags::SIMPLE_VOTE_TX;
|
||||
}
|
||||
(packet_batch, packet_indexes)
|
||||
DeserializedPacketBatch::new(packet_batch, packet_indexes, false)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -4145,28 +4261,30 @@ mod tests {
|
||||
&keypair,
|
||||
None,
|
||||
);
|
||||
let sorted = |mut v: Vec<usize>| {
|
||||
v.sort_unstable();
|
||||
v
|
||||
};
|
||||
|
||||
// packets with no votes
|
||||
{
|
||||
let vote_indexes = vec![];
|
||||
let (packet_batch, packet_indexes) =
|
||||
let deserialized_packet_batch =
|
||||
make_test_packets(vec![transfer_tx.clone(), transfer_tx.clone()], vote_indexes);
|
||||
|
||||
let mut votes_only = false;
|
||||
let (txs, tx_packet_index) = BankingStage::transactions_from_packets(
|
||||
&packet_batch,
|
||||
&packet_indexes,
|
||||
&deserialized_packet_batch.unprocessed_packets,
|
||||
&Arc::new(FeatureSet::default()),
|
||||
votes_only,
|
||||
SimpleAddressLoader::Disabled,
|
||||
);
|
||||
assert_eq!(2, txs.len());
|
||||
assert_eq!(vec![0, 1], tx_packet_index);
|
||||
assert_eq!(vec![0, 1], sorted(tx_packet_index));
|
||||
|
||||
votes_only = true;
|
||||
let (txs, tx_packet_index) = BankingStage::transactions_from_packets(
|
||||
&packet_batch,
|
||||
&packet_indexes,
|
||||
&deserialized_packet_batch.unprocessed_packets,
|
||||
&Arc::new(FeatureSet::default()),
|
||||
votes_only,
|
||||
SimpleAddressLoader::Disabled,
|
||||
@ -4178,63 +4296,59 @@ mod tests {
|
||||
// packets with some votes
|
||||
{
|
||||
let vote_indexes = vec![0, 2];
|
||||
let (packet_batch, packet_indexes) = make_test_packets(
|
||||
let deserialized_packet_batch = make_test_packets(
|
||||
vec![vote_tx.clone(), transfer_tx, vote_tx.clone()],
|
||||
vote_indexes,
|
||||
);
|
||||
|
||||
let mut votes_only = false;
|
||||
let (txs, tx_packet_index) = BankingStage::transactions_from_packets(
|
||||
&packet_batch,
|
||||
&packet_indexes,
|
||||
&deserialized_packet_batch.unprocessed_packets,
|
||||
&Arc::new(FeatureSet::default()),
|
||||
votes_only,
|
||||
SimpleAddressLoader::Disabled,
|
||||
);
|
||||
assert_eq!(3, txs.len());
|
||||
assert_eq!(vec![0, 1, 2], tx_packet_index);
|
||||
assert_eq!(vec![0, 1, 2], sorted(tx_packet_index));
|
||||
|
||||
votes_only = true;
|
||||
let (txs, tx_packet_index) = BankingStage::transactions_from_packets(
|
||||
&packet_batch,
|
||||
&packet_indexes,
|
||||
&deserialized_packet_batch.unprocessed_packets,
|
||||
&Arc::new(FeatureSet::default()),
|
||||
votes_only,
|
||||
SimpleAddressLoader::Disabled,
|
||||
);
|
||||
assert_eq!(2, txs.len());
|
||||
assert_eq!(vec![0, 2], tx_packet_index);
|
||||
assert_eq!(vec![0, 2], sorted(tx_packet_index));
|
||||
}
|
||||
|
||||
// packets with all votes
|
||||
{
|
||||
let vote_indexes = vec![0, 1, 2];
|
||||
let (packet_batch, packet_indexes) = make_test_packets(
|
||||
let deserialized_packet_batch = make_test_packets(
|
||||
vec![vote_tx.clone(), vote_tx.clone(), vote_tx],
|
||||
vote_indexes,
|
||||
);
|
||||
|
||||
let mut votes_only = false;
|
||||
let (txs, tx_packet_index) = BankingStage::transactions_from_packets(
|
||||
&packet_batch,
|
||||
&packet_indexes,
|
||||
&deserialized_packet_batch.unprocessed_packets,
|
||||
&Arc::new(FeatureSet::default()),
|
||||
votes_only,
|
||||
SimpleAddressLoader::Disabled,
|
||||
);
|
||||
assert_eq!(3, txs.len());
|
||||
assert_eq!(vec![0, 1, 2], tx_packet_index);
|
||||
assert_eq!(vec![0, 1, 2], sorted(tx_packet_index));
|
||||
|
||||
votes_only = true;
|
||||
let (txs, tx_packet_index) = BankingStage::transactions_from_packets(
|
||||
&packet_batch,
|
||||
&packet_indexes,
|
||||
&deserialized_packet_batch.unprocessed_packets,
|
||||
&Arc::new(FeatureSet::default()),
|
||||
votes_only,
|
||||
SimpleAddressLoader::Disabled,
|
||||
);
|
||||
assert_eq!(3, txs.len());
|
||||
assert_eq!(vec![0, 1, 2], tx_packet_index);
|
||||
assert_eq!(vec![0, 1, 2], sorted(tx_packet_index));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -24,7 +24,6 @@ impl LeaderExecuteAndCommitTimings {
|
||||
saturating_add_assign!(self.record_us, other.record_us);
|
||||
saturating_add_assign!(self.commit_us, other.commit_us);
|
||||
saturating_add_assign!(self.find_and_send_votes_us, other.find_and_send_votes_us);
|
||||
saturating_add_assign!(self.commit_us, other.commit_us);
|
||||
self.record_transactions_timings
|
||||
.accumulate(&other.record_transactions_timings);
|
||||
self.execute_timings.accumulate(&other.execute_timings);
|
||||
|
@ -1,4 +1,8 @@
|
||||
//! The `ledger_cleanup_service` drops older ledger data to limit disk space usage
|
||||
//! The `ledger_cleanup_service` drops older ledger data to limit disk space usage.
|
||||
//! The service works by counting the number of live data shreds in the ledger; this
|
||||
//! can be done quickly and should have a fairly stable correlation to actual bytes.
|
||||
//! Once the shred count (and thus roughly the byte count) reaches a threshold,
|
||||
//! the services begins removing data in FIFO order.
|
||||
|
||||
use {
|
||||
crossbeam_channel::{Receiver, RecvTimeoutError},
|
||||
|
@ -59,6 +59,7 @@ pub mod sigverify;
|
||||
pub mod sigverify_shreds;
|
||||
pub mod sigverify_stage;
|
||||
pub mod snapshot_packager_service;
|
||||
pub mod staked_nodes_updater_service;
|
||||
pub mod stats_reporter_service;
|
||||
pub mod system_monitor_service;
|
||||
mod tower1_7_14;
|
||||
@ -73,6 +74,7 @@ pub mod verified_vote_packets;
|
||||
pub mod vote_simulator;
|
||||
pub mod vote_stake_tracker;
|
||||
pub mod voting_service;
|
||||
pub mod warm_quic_cache_service;
|
||||
pub mod window_service;
|
||||
|
||||
#[macro_use]
|
||||
|
@ -133,7 +133,7 @@ impl QosService {
|
||||
let mut num_included = 0;
|
||||
let select_results = transactions
|
||||
.zip(transactions_costs)
|
||||
.map(|(tx, cost)| match cost_tracker.try_add(tx, cost) {
|
||||
.map(|(tx, cost)| match cost_tracker.try_add(cost) {
|
||||
Ok(current_block_cost) => {
|
||||
debug!("slot {:?}, transaction {:?}, cost {:?}, fit into current block, current block cost {}", bank.slot(), tx, cost, current_block_cost);
|
||||
self.metrics.stats.selected_txs_count.fetch_add(1, Ordering::Relaxed);
|
||||
@ -170,6 +170,35 @@ impl QosService {
|
||||
(select_results, num_included)
|
||||
}
|
||||
|
||||
/// Update the transaction cost in the cost_tracker with the real cost for
|
||||
/// transactions that were executed successfully;
|
||||
/// Otherwise remove the cost from the cost tracker, therefore preventing cost_tracker
|
||||
/// being inflated with unsuccessfully executed transactions.
|
||||
pub fn update_or_remove_transaction_costs<'a>(
|
||||
transaction_costs: impl Iterator<Item = &'a TransactionCost>,
|
||||
transaction_qos_results: impl Iterator<Item = &'a transaction::Result<()>>,
|
||||
retryable_transaction_indexes: &[usize],
|
||||
bank: &Arc<Bank>,
|
||||
) {
|
||||
let mut cost_tracker = bank.write_cost_tracker().unwrap();
|
||||
transaction_costs
|
||||
.zip(transaction_qos_results)
|
||||
.enumerate()
|
||||
.for_each(|(index, (tx_cost, qos_inclusion_result))| {
|
||||
// Only transactions that the qos service incuded have been added to the
|
||||
// cost tracker.
|
||||
if qos_inclusion_result.is_ok() && retryable_transaction_indexes.contains(&index) {
|
||||
cost_tracker.remove(tx_cost);
|
||||
} else {
|
||||
// TODO: Update the cost tracker with the actual execution compute units.
|
||||
// Will have to plumb it in next; For now, keep estimated costs.
|
||||
//
|
||||
// let actual_execution_cost = 0;
|
||||
// cost_tracker.update_execution_cost(tx_cost, actual_execution_cost);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// metrics are reported by bank slot
|
||||
pub fn report_metrics(&self, bank: Arc<Bank>) {
|
||||
self.report_sender
|
||||
|
@ -349,6 +349,7 @@ mod tests {
|
||||
snapshots_dir,
|
||||
accounts_dir,
|
||||
archive_format,
|
||||
snapshot_utils::VerifyBank::Deterministic,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
76
core/src/staked_nodes_updater_service.rs
Normal file
76
core/src/staked_nodes_updater_service.rs
Normal file
@ -0,0 +1,76 @@
|
||||
use {
|
||||
solana_gossip::cluster_info::ClusterInfo,
|
||||
solana_runtime::bank_forks::BankForks,
|
||||
std::{
|
||||
collections::HashMap,
|
||||
net::IpAddr,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, RwLock,
|
||||
},
|
||||
thread::{self, sleep, Builder, JoinHandle},
|
||||
time::{Duration, Instant},
|
||||
},
|
||||
};
|
||||
|
||||
const IP_TO_STAKE_REFRESH_DURATION: Duration = Duration::from_secs(5);
|
||||
|
||||
pub struct StakedNodesUpdaterService {
|
||||
thread_hdl: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl StakedNodesUpdaterService {
|
||||
pub fn new(
|
||||
exit: Arc<AtomicBool>,
|
||||
cluster_info: Arc<ClusterInfo>,
|
||||
bank_forks: Arc<RwLock<BankForks>>,
|
||||
shared_staked_nodes: Arc<RwLock<HashMap<IpAddr, u64>>>,
|
||||
) -> Self {
|
||||
let thread_hdl = Builder::new()
|
||||
.name("sol-sn-updater".to_string())
|
||||
.spawn(move || {
|
||||
let mut last_stakes = Instant::now();
|
||||
while !exit.load(Ordering::Relaxed) {
|
||||
let mut new_ip_to_stake = HashMap::new();
|
||||
Self::try_refresh_ip_to_stake(
|
||||
&mut last_stakes,
|
||||
&mut new_ip_to_stake,
|
||||
&bank_forks,
|
||||
&cluster_info,
|
||||
);
|
||||
let mut shared = shared_staked_nodes.write().unwrap();
|
||||
*shared = new_ip_to_stake;
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
Self { thread_hdl }
|
||||
}
|
||||
|
||||
fn try_refresh_ip_to_stake(
|
||||
last_stakes: &mut Instant,
|
||||
ip_to_stake: &mut HashMap<IpAddr, u64>,
|
||||
bank_forks: &RwLock<BankForks>,
|
||||
cluster_info: &ClusterInfo,
|
||||
) {
|
||||
if last_stakes.elapsed() > IP_TO_STAKE_REFRESH_DURATION {
|
||||
let root_bank = bank_forks.read().unwrap().root_bank();
|
||||
let staked_nodes = root_bank.staked_nodes();
|
||||
*ip_to_stake = cluster_info
|
||||
.tvu_peers()
|
||||
.into_iter()
|
||||
.filter_map(|node| {
|
||||
let stake = staked_nodes.get(&node.id)?;
|
||||
Some((node.tvu.ip(), *stake))
|
||||
})
|
||||
.collect();
|
||||
*last_stakes = Instant::now();
|
||||
} else {
|
||||
sleep(Duration::from_millis(1));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn join(self) -> thread::Result<()> {
|
||||
self.thread_hdl.join()
|
||||
}
|
||||
}
|
@ -11,7 +11,6 @@ use {
|
||||
fs::{self, File},
|
||||
io::{self, BufReader},
|
||||
path::PathBuf,
|
||||
sync::RwLock,
|
||||
},
|
||||
};
|
||||
|
||||
@ -214,7 +213,7 @@ impl TowerStorage for FileTowerStorage {
|
||||
}
|
||||
|
||||
pub struct EtcdTowerStorage {
|
||||
client: RwLock<etcd_client::Client>,
|
||||
client: tokio::sync::Mutex<etcd_client::Client>,
|
||||
instance_id: [u8; 8],
|
||||
runtime: tokio::runtime::Runtime,
|
||||
}
|
||||
@ -260,7 +259,7 @@ impl EtcdTowerStorage {
|
||||
.map_err(Self::etdc_to_tower_error)?;
|
||||
|
||||
Ok(Self {
|
||||
client: RwLock::new(client),
|
||||
client: tokio::sync::Mutex::new(client),
|
||||
instance_id: solana_sdk::timing::timestamp().to_le_bytes(),
|
||||
runtime,
|
||||
})
|
||||
@ -280,7 +279,6 @@ impl EtcdTowerStorage {
|
||||
impl TowerStorage for EtcdTowerStorage {
|
||||
fn load(&self, node_pubkey: &Pubkey) -> Result<Tower> {
|
||||
let (instance_key, tower_key) = Self::get_keys(node_pubkey);
|
||||
let mut client = self.client.write().unwrap();
|
||||
|
||||
let txn = etcd_client::Txn::new().and_then(vec![etcd_client::TxnOp::put(
|
||||
instance_key.clone(),
|
||||
@ -288,7 +286,7 @@ impl TowerStorage for EtcdTowerStorage {
|
||||
None,
|
||||
)]);
|
||||
self.runtime
|
||||
.block_on(async { client.txn(txn).await })
|
||||
.block_on(async { self.client.lock().await.txn(txn).await })
|
||||
.map_err(|err| {
|
||||
error!("Failed to acquire etcd instance lock: {}", err);
|
||||
Self::etdc_to_tower_error(err)
|
||||
@ -304,7 +302,7 @@ impl TowerStorage for EtcdTowerStorage {
|
||||
|
||||
let response = self
|
||||
.runtime
|
||||
.block_on(async { client.txn(txn).await })
|
||||
.block_on(async { self.client.lock().await.txn(txn).await })
|
||||
.map_err(|err| {
|
||||
error!("Failed to read etcd saved tower: {}", err);
|
||||
Self::etdc_to_tower_error(err)
|
||||
@ -336,7 +334,6 @@ impl TowerStorage for EtcdTowerStorage {
|
||||
|
||||
fn store(&self, saved_tower: &SavedTowerVersions) -> Result<()> {
|
||||
let (instance_key, tower_key) = Self::get_keys(&saved_tower.pubkey());
|
||||
let mut client = self.client.write().unwrap();
|
||||
|
||||
let txn = etcd_client::Txn::new()
|
||||
.when(vec![etcd_client::Compare::value(
|
||||
@ -352,7 +349,7 @@ impl TowerStorage for EtcdTowerStorage {
|
||||
|
||||
let response = self
|
||||
.runtime
|
||||
.block_on(async { client.txn(txn).await })
|
||||
.block_on(async { self.client.lock().await.txn(txn).await })
|
||||
.map_err(|err| {
|
||||
error!("Failed to write etcd saved tower: {}", err);
|
||||
err
|
||||
|
@ -13,8 +13,9 @@ use {
|
||||
find_packet_sender_stake_stage::FindPacketSenderStakeStage,
|
||||
sigverify::TransactionSigVerifier,
|
||||
sigverify_stage::SigVerifyStage,
|
||||
staked_nodes_updater_service::StakedNodesUpdaterService,
|
||||
},
|
||||
crossbeam_channel::{unbounded, Receiver},
|
||||
crossbeam_channel::{bounded, unbounded, Receiver, RecvTimeoutError},
|
||||
solana_gossip::cluster_info::ClusterInfo,
|
||||
solana_ledger::{blockstore::Blockstore, blockstore_processor::TransactionStatusSender},
|
||||
solana_poh::poh_recorder::{PohRecorder, WorkingBankEntry},
|
||||
@ -28,15 +29,21 @@ use {
|
||||
vote_sender_types::{ReplayVoteReceiver, ReplayVoteSender},
|
||||
},
|
||||
solana_sdk::signature::Keypair,
|
||||
solana_streamer::quic::{spawn_server, MAX_STAKED_CONNECTIONS, MAX_UNSTAKED_CONNECTIONS},
|
||||
std::{
|
||||
collections::HashMap,
|
||||
net::UdpSocket,
|
||||
sync::{atomic::AtomicBool, Arc, Mutex, RwLock},
|
||||
thread,
|
||||
time::Duration,
|
||||
},
|
||||
};
|
||||
|
||||
pub const DEFAULT_TPU_COALESCE_MS: u64 = 5;
|
||||
|
||||
/// Timeout interval when joining threads during TPU close
|
||||
const TPU_THREADS_JOIN_TIMEOUT_SECONDS: u64 = 10;
|
||||
|
||||
// allow multiple connections for NAT and any open/close overlap
|
||||
pub const MAX_QUIC_CONNECTIONS_PER_IP: usize = 8;
|
||||
|
||||
@ -58,6 +65,7 @@ pub struct Tpu {
|
||||
tpu_quic_t: thread::JoinHandle<()>,
|
||||
find_packet_sender_stake_stage: FindPacketSenderStakeStage,
|
||||
vote_find_packet_sender_stake_stage: FindPacketSenderStakeStage,
|
||||
staked_nodes_updater_service: StakedNodesUpdaterService,
|
||||
}
|
||||
|
||||
impl Tpu {
|
||||
@ -128,13 +136,23 @@ impl Tpu {
|
||||
|
||||
let (verified_sender, verified_receiver) = unbounded();
|
||||
|
||||
let tpu_quic_t = solana_streamer::quic::spawn_server(
|
||||
let staked_nodes = Arc::new(RwLock::new(HashMap::new()));
|
||||
let staked_nodes_updater_service = StakedNodesUpdaterService::new(
|
||||
exit.clone(),
|
||||
cluster_info.clone(),
|
||||
bank_forks.clone(),
|
||||
staked_nodes.clone(),
|
||||
);
|
||||
let tpu_quic_t = spawn_server(
|
||||
transactions_quic_sockets,
|
||||
keypair,
|
||||
cluster_info.my_contact_info().tpu.ip(),
|
||||
packet_sender,
|
||||
exit.clone(),
|
||||
MAX_QUIC_CONNECTIONS_PER_IP,
|
||||
staked_nodes,
|
||||
MAX_STAKED_CONNECTIONS,
|
||||
MAX_UNSTAKED_CONNECTIONS,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@ -204,10 +222,27 @@ impl Tpu {
|
||||
tpu_quic_t,
|
||||
find_packet_sender_stake_stage,
|
||||
vote_find_packet_sender_stake_stage,
|
||||
staked_nodes_updater_service,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn join(self) -> thread::Result<()> {
|
||||
// spawn a new thread to wait for tpu close
|
||||
let (sender, receiver) = bounded(0);
|
||||
let _ = thread::spawn(move || {
|
||||
let _ = self.do_join();
|
||||
sender.send(()).unwrap();
|
||||
});
|
||||
|
||||
// exit can deadlock. put an upper-bound on how long we wait for it
|
||||
let timeout = Duration::from_secs(TPU_THREADS_JOIN_TIMEOUT_SECONDS);
|
||||
if let Err(RecvTimeoutError::Timeout) = receiver.recv_timeout(timeout) {
|
||||
error!("timeout for closing tvu");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn do_join(self) -> thread::Result<()> {
|
||||
let results = vec![
|
||||
self.fetch_stage.join(),
|
||||
self.sigverify_stage.join(),
|
||||
@ -216,6 +251,7 @@ impl Tpu {
|
||||
self.banking_stage.join(),
|
||||
self.find_packet_sender_stake_stage.join(),
|
||||
self.vote_find_packet_sender_stake_stage.join(),
|
||||
self.staked_nodes_updater_service.join(),
|
||||
];
|
||||
self.tpu_quic_t.join()?;
|
||||
let broadcast_result = self.broadcast_stage.join();
|
||||
|
@ -25,8 +25,9 @@ use {
|
||||
sigverify_stage::SigVerifyStage,
|
||||
tower_storage::TowerStorage,
|
||||
voting_service::VotingService,
|
||||
warm_quic_cache_service::WarmQuicCacheService,
|
||||
},
|
||||
crossbeam_channel::{unbounded, Receiver},
|
||||
crossbeam_channel::{bounded, unbounded, Receiver, RecvTimeoutError},
|
||||
solana_geyser_plugin_manager::block_metadata_notifier_interface::BlockMetadataNotifierLock,
|
||||
solana_gossip::cluster_info::ClusterInfo,
|
||||
solana_ledger::{
|
||||
@ -60,9 +61,13 @@ use {
|
||||
net::UdpSocket,
|
||||
sync::{atomic::AtomicBool, Arc, Mutex, RwLock},
|
||||
thread,
|
||||
time::Duration,
|
||||
},
|
||||
};
|
||||
|
||||
/// Timeout interval when joining threads during TVU close
|
||||
const TVU_THREADS_JOIN_TIMEOUT_SECONDS: u64 = 10;
|
||||
|
||||
pub struct Tvu {
|
||||
fetch_stage: ShredFetchStage,
|
||||
sigverify_stage: SigVerifyStage,
|
||||
@ -74,6 +79,7 @@ pub struct Tvu {
|
||||
accounts_hash_verifier: AccountsHashVerifier,
|
||||
cost_update_service: CostUpdateService,
|
||||
voting_service: VotingService,
|
||||
warm_quic_cache_service: WarmQuicCacheService,
|
||||
drop_bank_service: DropBankService,
|
||||
transaction_cost_metrics_service: TransactionCostMetricsService,
|
||||
}
|
||||
@ -279,6 +285,9 @@ impl Tvu {
|
||||
bank_forks.clone(),
|
||||
);
|
||||
|
||||
let warm_quic_cache_service =
|
||||
WarmQuicCacheService::new(cluster_info.clone(), poh_recorder.clone(), exit.clone());
|
||||
|
||||
let (cost_update_sender, cost_update_receiver) = unbounded();
|
||||
let cost_update_service =
|
||||
CostUpdateService::new(blockstore.clone(), cost_model.clone(), cost_update_receiver);
|
||||
@ -352,12 +361,29 @@ impl Tvu {
|
||||
accounts_hash_verifier,
|
||||
cost_update_service,
|
||||
voting_service,
|
||||
warm_quic_cache_service,
|
||||
drop_bank_service,
|
||||
transaction_cost_metrics_service,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn join(self) -> thread::Result<()> {
|
||||
// spawn a new thread to wait for tvu close
|
||||
let (sender, receiver) = bounded(0);
|
||||
let _ = thread::spawn(move || {
|
||||
let _ = self.do_join();
|
||||
sender.send(()).unwrap();
|
||||
});
|
||||
|
||||
// exit can deadlock. put an upper-bound on how long we wait for it
|
||||
let timeout = Duration::from_secs(TVU_THREADS_JOIN_TIMEOUT_SECONDS);
|
||||
if let Err(RecvTimeoutError::Timeout) = receiver.recv_timeout(timeout) {
|
||||
error!("timeout for closing tvu");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn do_join(self) -> thread::Result<()> {
|
||||
self.retransmit_stage.join()?;
|
||||
self.fetch_stage.join()?;
|
||||
self.sigverify_stage.join()?;
|
||||
@ -370,6 +396,7 @@ impl Tvu {
|
||||
self.accounts_hash_verifier.join()?;
|
||||
self.cost_update_service.join()?;
|
||||
self.voting_service.join()?;
|
||||
self.warm_quic_cache_service.join()?;
|
||||
self.drop_bank_service.join()?;
|
||||
self.transaction_cost_metrics_service.join()?;
|
||||
Ok(())
|
||||
|
@ -15,14 +15,9 @@ use {
|
||||
/// SanitizedTransaction
|
||||
#[derive(Debug, Default)]
|
||||
pub struct DeserializedPacket {
|
||||
#[allow(dead_code)]
|
||||
versioned_transaction: VersionedTransaction,
|
||||
|
||||
#[allow(dead_code)]
|
||||
message_hash: Hash,
|
||||
|
||||
#[allow(dead_code)]
|
||||
is_simple_vote: bool,
|
||||
pub versioned_transaction: VersionedTransaction,
|
||||
pub message_hash: Hash,
|
||||
pub is_simple_vote: bool,
|
||||
}
|
||||
|
||||
/// Defines the type of entry in `UnprocessedPacketBatches`, it holds original packet_batch
|
||||
|
@ -50,10 +50,6 @@ use {
|
||||
poh_recorder::{PohRecorder, GRACE_TICKS_FACTOR, MAX_GRACE_SLOTS},
|
||||
poh_service::{self, PohService},
|
||||
},
|
||||
solana_replica_lib::{
|
||||
accountsdb_repl_server::{AccountsDbReplService, AccountsDbReplServiceConfig},
|
||||
accountsdb_repl_server_factory,
|
||||
},
|
||||
solana_rpc::{
|
||||
max_slots::MaxSlots,
|
||||
optimistically_confirmed_bank_tracker::{
|
||||
@ -77,6 +73,7 @@ use {
|
||||
commitment::BlockCommitmentCache,
|
||||
cost_model::CostModel,
|
||||
hardened_unpack::{open_genesis_config, MAX_GENESIS_ARCHIVE_UNPACKED_SIZE},
|
||||
runtime_config::RuntimeConfig,
|
||||
snapshot_archive_info::SnapshotArchiveInfoGetter,
|
||||
snapshot_config::SnapshotConfig,
|
||||
snapshot_hash::StartingSnapshotHashes,
|
||||
@ -114,7 +111,6 @@ const MAX_COMPLETED_DATA_SETS_IN_CHANNEL: usize = 100_000;
|
||||
const WAIT_FOR_SUPERMAJORITY_THRESHOLD_PERCENT: u64 = 80;
|
||||
|
||||
pub struct ValidatorConfig {
|
||||
pub dev_halt_at_slot: Option<Slot>,
|
||||
pub expected_genesis_hash: Option<Hash>,
|
||||
pub expected_bank_hash: Option<Hash>,
|
||||
pub expected_shred_version: Option<u16>,
|
||||
@ -122,7 +118,6 @@ pub struct ValidatorConfig {
|
||||
pub account_paths: Vec<PathBuf>,
|
||||
pub account_shrink_paths: Option<Vec<PathBuf>>,
|
||||
pub rpc_config: JsonRpcConfig,
|
||||
pub accountsdb_repl_service_config: Option<AccountsDbReplServiceConfig>,
|
||||
pub geyser_plugin_config_files: Option<Vec<PathBuf>>,
|
||||
pub rpc_addrs: Option<(SocketAddr, SocketAddr)>, // (JsonRpc, JsonRpcPubSub)
|
||||
pub pubsub_config: PubSubConfig,
|
||||
@ -151,7 +146,6 @@ pub struct ValidatorConfig {
|
||||
pub debug_keys: Option<Arc<HashSet<Pubkey>>>,
|
||||
pub contact_debug_interval: u64,
|
||||
pub contact_save_interval: u64,
|
||||
pub bpf_jit: bool,
|
||||
pub send_transaction_service_config: send_transaction_service::Config,
|
||||
pub no_poh_speed_test: bool,
|
||||
pub no_os_memory_stats_reporting: bool,
|
||||
@ -170,12 +164,12 @@ pub struct ValidatorConfig {
|
||||
pub accounts_shrink_ratio: AccountShrinkThreshold,
|
||||
pub wait_to_vote_slot: Option<Slot>,
|
||||
pub ledger_column_options: LedgerColumnOptions,
|
||||
pub runtime_config: RuntimeConfig,
|
||||
}
|
||||
|
||||
impl Default for ValidatorConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
dev_halt_at_slot: None,
|
||||
expected_genesis_hash: None,
|
||||
expected_bank_hash: None,
|
||||
expected_shred_version: None,
|
||||
@ -184,7 +178,6 @@ impl Default for ValidatorConfig {
|
||||
account_paths: Vec::new(),
|
||||
account_shrink_paths: None,
|
||||
rpc_config: JsonRpcConfig::default(),
|
||||
accountsdb_repl_service_config: None,
|
||||
geyser_plugin_config_files: None,
|
||||
rpc_addrs: None,
|
||||
pubsub_config: PubSubConfig::default(),
|
||||
@ -212,7 +205,6 @@ impl Default for ValidatorConfig {
|
||||
debug_keys: None,
|
||||
contact_debug_interval: DEFAULT_CONTACT_DEBUG_INTERVAL_MILLIS,
|
||||
contact_save_interval: DEFAULT_CONTACT_SAVE_INTERVAL_MILLIS,
|
||||
bpf_jit: false,
|
||||
send_transaction_service_config: send_transaction_service::Config::default(),
|
||||
no_poh_speed_test: true,
|
||||
no_os_memory_stats_reporting: true,
|
||||
@ -231,6 +223,7 @@ impl Default for ValidatorConfig {
|
||||
accounts_db_config: None,
|
||||
wait_to_vote_slot: None,
|
||||
ledger_column_options: LedgerColumnOptions::default(),
|
||||
runtime_config: RuntimeConfig::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -238,6 +231,7 @@ impl Default for ValidatorConfig {
|
||||
impl ValidatorConfig {
|
||||
pub fn default_for_test() -> Self {
|
||||
Self {
|
||||
enforce_ulimit_nofile: false,
|
||||
rpc_config: JsonRpcConfig::default_for_test(),
|
||||
..Self::default()
|
||||
}
|
||||
@ -339,7 +333,6 @@ pub struct Validator {
|
||||
pub cluster_info: Arc<ClusterInfo>,
|
||||
pub bank_forks: Arc<RwLock<BankForks>>,
|
||||
pub blockstore: Arc<Blockstore>,
|
||||
accountsdb_repl_service: Option<AccountsDbReplService>,
|
||||
geyser_plugin_service: Option<GeyserPluginService>,
|
||||
}
|
||||
|
||||
@ -673,7 +666,6 @@ impl Validator {
|
||||
pubsub_service,
|
||||
optimistically_confirmed_bank_tracker,
|
||||
bank_notification_sender,
|
||||
accountsdb_repl_service,
|
||||
) = if let Some((rpc_addr, rpc_pubsub_addr)) = config.rpc_addrs {
|
||||
if ContactInfo::is_valid_address(&node.info.rpc, &socket_addr_space) {
|
||||
assert!(ContactInfo::is_valid_address(
|
||||
@ -687,13 +679,6 @@ impl Validator {
|
||||
));
|
||||
}
|
||||
|
||||
let accountsdb_repl_service = config.accountsdb_repl_service_config.as_ref().map(|accountsdb_repl_service_config| {
|
||||
let (bank_notification_sender, bank_notification_receiver) = unbounded();
|
||||
bank_notification_senders.push(bank_notification_sender);
|
||||
accountsdb_repl_server_factory::AccountsDbReplServerFactory::build_accountsdb_repl_server(
|
||||
accountsdb_repl_service_config.clone(), bank_notification_receiver, bank_forks.clone())
|
||||
});
|
||||
|
||||
let (bank_notification_sender, bank_notification_receiver) = unbounded();
|
||||
let confirmed_bank_subscribers = if !bank_notification_senders.is_empty() {
|
||||
Some(Arc::new(RwLock::new(bank_notification_senders)))
|
||||
@ -746,13 +731,12 @@ impl Validator {
|
||||
confirmed_bank_subscribers,
|
||||
)),
|
||||
Some(bank_notification_sender),
|
||||
accountsdb_repl_service,
|
||||
)
|
||||
} else {
|
||||
(None, None, None, None, None)
|
||||
(None, None, None, None)
|
||||
};
|
||||
|
||||
if config.dev_halt_at_slot.is_some() {
|
||||
if config.runtime_config.dev_halt_at_slot.is_some() {
|
||||
// Simulate a confirmed root to avoid RPC errors with CommitmentConfig::finalized() and
|
||||
// to ensure RPC endpoints like getConfirmedBlock, which require a confirmed root, work
|
||||
block_commitment_cache
|
||||
@ -814,8 +798,7 @@ impl Validator {
|
||||
let enable_gossip_push = config
|
||||
.accounts_db_config
|
||||
.as_ref()
|
||||
.and_then(|config| config.filler_account_count)
|
||||
.map(|count| count == 0)
|
||||
.map(|config| config.filler_accounts_config.count == 0)
|
||||
.unwrap_or(true);
|
||||
|
||||
let snapshot_packager_service = SnapshotPackagerService::new(
|
||||
@ -999,7 +982,6 @@ impl Validator {
|
||||
cluster_info,
|
||||
bank_forks,
|
||||
blockstore: blockstore.clone(),
|
||||
accountsdb_repl_service,
|
||||
geyser_plugin_service,
|
||||
}
|
||||
}
|
||||
@ -1119,12 +1101,6 @@ impl Validator {
|
||||
ip_echo_server.shutdown_background();
|
||||
}
|
||||
|
||||
if let Some(accountsdb_repl_service) = self.accountsdb_repl_service {
|
||||
accountsdb_repl_service
|
||||
.join()
|
||||
.expect("accountsdb_repl_service");
|
||||
}
|
||||
|
||||
if let Some(geyser_plugin_service) = self.geyser_plugin_service {
|
||||
geyser_plugin_service.join().expect("geyser_plugin_service");
|
||||
}
|
||||
@ -1332,9 +1308,7 @@ fn load_blockstore(
|
||||
let blockstore_root_scan = BlockstoreRootScan::new(config, &blockstore, exit);
|
||||
|
||||
let process_options = blockstore_processor::ProcessOptions {
|
||||
bpf_jit: config.bpf_jit,
|
||||
poh_verify: config.poh_verify,
|
||||
dev_halt_at_slot: config.dev_halt_at_slot,
|
||||
new_hard_forks: config.new_hard_forks.clone(),
|
||||
debug_keys: config.debug_keys.clone(),
|
||||
account_indexes: config.account_indexes.clone(),
|
||||
@ -1343,6 +1317,7 @@ fn load_blockstore(
|
||||
shrink_ratio: config.accounts_shrink_ratio,
|
||||
accounts_db_test_hash_calculation: config.accounts_db_test_hash_calculation,
|
||||
accounts_db_skip_shrink: config.accounts_db_skip_shrink,
|
||||
runtime_config: config.runtime_config.clone(),
|
||||
..blockstore_processor::ProcessOptions::default()
|
||||
};
|
||||
|
||||
@ -1867,7 +1842,20 @@ mod tests {
|
||||
*start_progress.read().unwrap(),
|
||||
ValidatorStartProgress::Running
|
||||
);
|
||||
validator.close();
|
||||
|
||||
// spawn a new thread to wait for validator close
|
||||
let (sender, receiver) = bounded(0);
|
||||
let _ = thread::spawn(move || {
|
||||
validator.close();
|
||||
sender.send(()).unwrap();
|
||||
});
|
||||
|
||||
// exit can deadlock. put an upper-bound on how long we wait for it
|
||||
let timeout = Duration::from_secs(30);
|
||||
if let Err(RecvTimeoutError::Timeout) = receiver.recv_timeout(timeout) {
|
||||
panic!("timeout for closing validator");
|
||||
}
|
||||
|
||||
remove_dir_all(validator_ledger_path).unwrap();
|
||||
}
|
||||
|
||||
|
70
core/src/warm_quic_cache_service.rs
Normal file
70
core/src/warm_quic_cache_service.rs
Normal file
@ -0,0 +1,70 @@
|
||||
// Connect to future leaders with some jitter so the quic connection is warm
|
||||
// by the time we need it.
|
||||
|
||||
use {
|
||||
rand::{thread_rng, Rng},
|
||||
solana_client::connection_cache::send_wire_transaction,
|
||||
solana_gossip::cluster_info::ClusterInfo,
|
||||
solana_poh::poh_recorder::PohRecorder,
|
||||
std::{
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, Mutex,
|
||||
},
|
||||
thread::{self, sleep, Builder, JoinHandle},
|
||||
time::Duration,
|
||||
},
|
||||
};
|
||||
|
||||
pub struct WarmQuicCacheService {
|
||||
thread_hdl: JoinHandle<()>,
|
||||
}
|
||||
|
||||
// ~50 seconds
|
||||
const CACHE_OFFSET_SLOT: i64 = 100;
|
||||
const CACHE_JITTER_SLOT: i64 = 20;
|
||||
|
||||
impl WarmQuicCacheService {
|
||||
pub fn new(
|
||||
cluster_info: Arc<ClusterInfo>,
|
||||
poh_recorder: Arc<Mutex<PohRecorder>>,
|
||||
exit: Arc<AtomicBool>,
|
||||
) -> Self {
|
||||
let thread_hdl = Builder::new()
|
||||
.name("sol-warm-quic-service".to_string())
|
||||
.spawn(move || {
|
||||
let slot_jitter = thread_rng().gen_range(-CACHE_JITTER_SLOT, CACHE_JITTER_SLOT);
|
||||
let mut maybe_last_leader = None;
|
||||
while !exit.load(Ordering::Relaxed) {
|
||||
if let Some(leader_pubkey) = poh_recorder
|
||||
.lock()
|
||||
.unwrap()
|
||||
.leader_after_n_slots((CACHE_OFFSET_SLOT + slot_jitter) as u64)
|
||||
{
|
||||
if maybe_last_leader
|
||||
.map_or(true, |last_leader| last_leader != leader_pubkey)
|
||||
{
|
||||
maybe_last_leader = Some(leader_pubkey);
|
||||
if let Some(addr) = cluster_info
|
||||
.lookup_contact_info(&leader_pubkey, |leader| leader.tpu)
|
||||
{
|
||||
if let Err(err) = send_wire_transaction(&[0u8], &addr) {
|
||||
warn!(
|
||||
"Failed to warmup QUIC connection to the leader {:?}, Error {:?}",
|
||||
leader_pubkey, err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sleep(Duration::from_millis(200));
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
Self { thread_hdl }
|
||||
}
|
||||
|
||||
pub fn join(self) -> thread::Result<()> {
|
||||
self.thread_hdl.join()
|
||||
}
|
||||
}
|
@ -166,10 +166,10 @@ mod tests {
|
||||
old_genesis_config: &GenesisConfig,
|
||||
account_paths: &[PathBuf],
|
||||
) {
|
||||
let (snapshot_path, snapshot_archives_dir) = old_bank_forks
|
||||
let snapshot_archives_dir = old_bank_forks
|
||||
.snapshot_config
|
||||
.as_ref()
|
||||
.map(|c| (&c.bank_snapshots_dir, &c.snapshot_archives_dir))
|
||||
.map(|c| &c.snapshot_archives_dir)
|
||||
.unwrap();
|
||||
|
||||
let old_last_bank = old_bank_forks.get(old_last_slot).unwrap();
|
||||
@ -213,12 +213,6 @@ mod tests {
|
||||
.unwrap()
|
||||
.clone();
|
||||
assert_eq!(*bank, deserialized_bank);
|
||||
|
||||
let bank_snapshots = snapshot_utils::get_bank_snapshots(&snapshot_path);
|
||||
|
||||
for p in bank_snapshots {
|
||||
snapshot_utils::remove_bank_snapshot(p.slot, &snapshot_path).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
// creates banks up to "last_slot" and runs the input function `f` on each bank created
|
||||
@ -274,7 +268,7 @@ mod tests {
|
||||
let snapshot_config = &snapshot_test_config.snapshot_config;
|
||||
let bank_snapshots_dir = &snapshot_config.bank_snapshots_dir;
|
||||
let last_bank_snapshot_info =
|
||||
snapshot_utils::get_highest_bank_snapshot_info(bank_snapshots_dir)
|
||||
snapshot_utils::get_highest_bank_snapshot_pre(bank_snapshots_dir)
|
||||
.expect("no bank snapshots found in path");
|
||||
let accounts_package = AccountsPackage::new(
|
||||
last_bank,
|
||||
@ -289,7 +283,13 @@ mod tests {
|
||||
Some(SnapshotType::FullSnapshot),
|
||||
)
|
||||
.unwrap();
|
||||
let snapshot_package = SnapshotPackage::from(accounts_package);
|
||||
solana_runtime::serde_snapshot::reserialize_bank_with_new_accounts_hash(
|
||||
accounts_package.snapshot_links.path(),
|
||||
accounts_package.slot,
|
||||
&last_bank.get_accounts_hash(),
|
||||
);
|
||||
let snapshot_package =
|
||||
SnapshotPackage::new(accounts_package, last_bank.get_accounts_hash());
|
||||
snapshot_utils::archive_snapshot_package(
|
||||
&snapshot_package,
|
||||
snapshot_config.maximum_full_snapshot_archives_to_retain,
|
||||
@ -392,7 +392,6 @@ mod tests {
|
||||
let tx = system_transaction::transfer(mint_keypair, &key1, 1, genesis_config.hash());
|
||||
assert_eq!(bank.process_transaction(&tx), Ok(()));
|
||||
bank.squash();
|
||||
let accounts_hash = bank.update_accounts_hash();
|
||||
|
||||
let pending_accounts_package = {
|
||||
if slot == saved_slot as u64 {
|
||||
@ -462,7 +461,9 @@ mod tests {
|
||||
saved_archive_path = Some(snapshot_utils::build_full_snapshot_archive_path(
|
||||
snapshot_archives_dir,
|
||||
slot,
|
||||
&accounts_hash,
|
||||
// this needs to match the hash value that we reserialize with later. It is complicated, so just use default.
|
||||
// This hash value is just used to build the file name. Since this is mocked up test code, it is sufficient to pass default here.
|
||||
&Hash::default(),
|
||||
ArchiveFormat::TarBzip2,
|
||||
));
|
||||
}
|
||||
@ -472,7 +473,7 @@ mod tests {
|
||||
// currently sitting in the channel
|
||||
snapshot_utils::purge_old_bank_snapshots(bank_snapshots_dir);
|
||||
|
||||
let mut bank_snapshots = snapshot_utils::get_bank_snapshots(&bank_snapshots_dir);
|
||||
let mut bank_snapshots = snapshot_utils::get_bank_snapshots_pre(&bank_snapshots_dir);
|
||||
bank_snapshots.sort_unstable();
|
||||
assert!(bank_snapshots
|
||||
.into_iter()
|
||||
@ -511,7 +512,12 @@ mod tests {
|
||||
.unwrap()
|
||||
.take()
|
||||
.unwrap();
|
||||
let snapshot_package = SnapshotPackage::from(accounts_package);
|
||||
solana_runtime::serde_snapshot::reserialize_bank_with_new_accounts_hash(
|
||||
accounts_package.snapshot_links.path(),
|
||||
accounts_package.slot,
|
||||
&Hash::default(),
|
||||
);
|
||||
let snapshot_package = SnapshotPackage::new(accounts_package, Hash::default());
|
||||
pending_snapshot_package
|
||||
.lock()
|
||||
.unwrap()
|
||||
@ -548,11 +554,19 @@ mod tests {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// files were saved off before we reserialized the bank in the hacked up accounts_hash_verifier stand-in.
|
||||
solana_runtime::serde_snapshot::reserialize_bank_with_new_accounts_hash(
|
||||
saved_snapshots_dir.path(),
|
||||
saved_slot,
|
||||
&Hash::default(),
|
||||
);
|
||||
|
||||
snapshot_utils::verify_snapshot_archive(
|
||||
saved_archive_path.unwrap(),
|
||||
saved_snapshots_dir.path(),
|
||||
saved_accounts_dir.path(),
|
||||
ArchiveFormat::TarBzip2,
|
||||
snapshot_utils::VerifyBank::NonDeterministic(saved_slot),
|
||||
);
|
||||
}
|
||||
|
||||
@ -751,7 +765,7 @@ mod tests {
|
||||
let slot = bank.slot();
|
||||
info!("Making full snapshot archive from bank at slot: {}", slot);
|
||||
let bank_snapshot_info =
|
||||
snapshot_utils::get_bank_snapshots(&snapshot_config.bank_snapshots_dir)
|
||||
snapshot_utils::get_bank_snapshots_pre(&snapshot_config.bank_snapshots_dir)
|
||||
.into_iter()
|
||||
.find(|elem| elem.slot == slot)
|
||||
.ok_or_else(|| {
|
||||
@ -786,7 +800,7 @@ mod tests {
|
||||
slot, incremental_snapshot_base_slot,
|
||||
);
|
||||
let bank_snapshot_info =
|
||||
snapshot_utils::get_bank_snapshots(&snapshot_config.bank_snapshots_dir)
|
||||
snapshot_utils::get_bank_snapshots_pre(&snapshot_config.bank_snapshots_dir)
|
||||
.into_iter()
|
||||
.find(|elem| elem.slot == slot)
|
||||
.ok_or_else(|| {
|
||||
|
@ -179,6 +179,7 @@ module.exports = {
|
||||
"proposals/block-confirmation",
|
||||
"proposals/cluster-test-framework",
|
||||
"proposals/embedding-move",
|
||||
"proposals/handle-duplicate-block",
|
||||
"proposals/interchain-transaction-verification",
|
||||
"proposals/ledger-replication-to-implement",
|
||||
"proposals/optimistic-confirmation-and-slashing",
|
||||
|
@ -33,6 +33,14 @@ solana airdrop 1 <RECIPIENT_ACCOUNT_ADDRESS> --url https://api.devnet.solana.com
|
||||
where you replace the text `<RECIPIENT_ACCOUNT_ADDRESS>` with your base58-encoded
|
||||
public key/wallet address.
|
||||
|
||||
A response with the signature of the transaction will be returned. If the balance
|
||||
of the address does not change by the expected amount, run the following command
|
||||
for more information on what potentially went wrong:
|
||||
|
||||
```bash
|
||||
solana confirm -v <TRANSACTION_SIGNATURE>
|
||||
```
|
||||
|
||||
#### Check your balance
|
||||
|
||||
Confirm the airdrop was successful by checking the account's balance.
|
||||
|
@ -3384,7 +3384,7 @@ The result will be an RpcResponse JSON object with `value` set to a JSON object
|
||||
|
||||
- `err: <object | string | null>` - Error if transaction failed, null if transaction succeeded. [TransactionError definitions](https://github.com/solana-labs/solana/blob/c0c60386544ec9a9ec7119229f37386d9f070523/sdk/src/transaction/error.rs#L13)
|
||||
- `logs: <array | null>` - Array of log messages the transaction instructions output during execution, null if simulation failed before the transaction was able to execute (for example due to an invalid blockhash or signature verification failure)
|
||||
- `accounts: <array> | null>` - array of accounts with the same length as the `accounts.addresses` array in the request
|
||||
- `accounts: <array | null>` - array of accounts with the same length as the `accounts.addresses` array in the request
|
||||
- `<null>` - if the account doesn't exist or if `err` is not null
|
||||
- `<object>` - otherwise, a JSON object containing:
|
||||
- `lamports: <u64>`, number of lamports assigned to this account, as a u64
|
||||
@ -3393,6 +3393,9 @@ The result will be an RpcResponse JSON object with `value` set to a JSON object
|
||||
- `executable: <bool>`, boolean indicating if the account contains a program \(and is strictly read-only\)
|
||||
- `rentEpoch: <u64>`, the epoch at which this account will next owe rent, as u64
|
||||
- `unitsConsumed: <u64 | undefined>`, The number of compute budget units consumed during the processing of this transaction
|
||||
- `returnData: <object | null>` - the most-recent return data generated by an instruction in the transaction, with the following fields:
|
||||
- `programId: <string>`, the program that generated the return data, as base-58 encoded Pubkey
|
||||
- `data: <[string, encoding]>`, the return data itself, as base-64 encoded binary data
|
||||
|
||||
#### Example:
|
||||
|
||||
@ -3403,7 +3406,10 @@ curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d '
|
||||
"id": 1,
|
||||
"method": "simulateTransaction",
|
||||
"params": [
|
||||
"4hXTCkRzt9WyecNzV1XPgCDfGAZzQKNxLXgynz5QDuWWPSAZBZSHptvWRL3BjCvzUXRdKvHL2b7yGrRQcWyaqsaBCncVG7BFggS8w9snUts67BSh3EqKpXLUm5UMHfD7ZBe9GhARjbNQMLJ1QD3Spr6oMTBU6EhdB4RD8CP2xUxr2u3d6fos36PD98XS6oX8TQjLpsMwncs5DAMiD4nNnR8NBfyghGCWvCVifVwvA8B8TJxE1aiyiv2L429BCWfyzAme5sZW8rDb14NeCQHhZbtNqfXhcp2tAnaAT"
|
||||
"AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEDArczbMia1tLmq7zz4DinMNN0pJ1JtLdqIJPUw3YrGCzYAMHBsgN27lcgB6H2WQvFgyZuJYHa46puOQo9yQ8CVQbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCp20C7Wj2aiuk5TReAXo+VTVg8QTHjs0UjNMMKCvpzZ+ABAgEBARU=",
|
||||
{
|
||||
"encoding":"base64",
|
||||
}
|
||||
]
|
||||
}
|
||||
'
|
||||
@ -3422,8 +3428,19 @@ Result:
|
||||
"err": null,
|
||||
"accounts": null,
|
||||
"logs": [
|
||||
"BPF program 83astBRguLMdt2h5U1Tpdq5tjFoJ6noeGwaY3mDLVcri success"
|
||||
]
|
||||
"Program 83astBRguLMdt2h5U1Tpdq5tjFoJ6noeGwaY3mDLVcri invoke [1]",
|
||||
"Program 83astBRguLMdt2h5U1Tpdq5tjFoJ6noeGwaY3mDLVcri consumed 2366 of 1400000 compute units",
|
||||
"Program return: 83astBRguLMdt2h5U1Tpdq5tjFoJ6noeGwaY3mDLVcri KgAAAAAAAAA=",
|
||||
"Program 83astBRguLMdt2h5U1Tpdq5tjFoJ6noeGwaY3mDLVcri success"
|
||||
],
|
||||
"returnData": {
|
||||
"data": [
|
||||
"Kg==",
|
||||
"base64"
|
||||
],
|
||||
"programId": "83astBRguLMdt2h5U1Tpdq5tjFoJ6noeGwaY3mDLVcri"
|
||||
},
|
||||
"unitsConsumed": 2366
|
||||
}
|
||||
},
|
||||
"id": 1
|
||||
|
@ -1,3 +1,7 @@
|
||||
---
|
||||
title: Handle Duplicate Block
|
||||
---
|
||||
|
||||
# Leader Duplicate Block Slashing
|
||||
|
||||
This design describes how the cluster slashes leaders that produce duplicate
|
2140
explorer/package-lock.json
generated
2140
explorer/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -5,7 +5,7 @@
|
||||
"dependencies": {
|
||||
"@blockworks-foundation/mango-client": "^3.2.16",
|
||||
"@bonfida/bot": "^0.5.3",
|
||||
"@bonfida/spl-name-service": "^0.1.22",
|
||||
"@bonfida/spl-name-service": "^0.1.30",
|
||||
"@cloudflare/stream-react": "^1.2.0",
|
||||
"@metamask/jazzicon": "^2.0.0",
|
||||
"@metaplex/js": "4.12.0",
|
||||
|
@ -3,6 +3,7 @@ import { Cluster } from "providers/cluster";
|
||||
import { TableCardBody } from "components/common/TableCardBody";
|
||||
import { InstructionLogs } from "utils/program-logs";
|
||||
import { ProgramName } from "utils/anchor";
|
||||
import React from "react";
|
||||
|
||||
export function ProgramLogsCardBody({
|
||||
message,
|
||||
|
@ -13,14 +13,30 @@ import {
|
||||
import { Cluster, useCluster } from "providers/cluster";
|
||||
import { useTokenRegistry } from "providers/mints/token-registry";
|
||||
import { TokenInfoMap } from "@solana/spl-token-registry";
|
||||
import { Connection } from "@solana/web3.js";
|
||||
import { getDomainInfo, hasDomainSyntax } from "utils/name-service";
|
||||
|
||||
interface SearchOptions {
|
||||
label: string;
|
||||
options: {
|
||||
label: string;
|
||||
value: string[];
|
||||
pathname: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export function SearchBar() {
|
||||
const [search, setSearch] = React.useState("");
|
||||
const searchRef = React.useRef("");
|
||||
const [searchOptions, setSearchOptions] = React.useState<SearchOptions[]>([]);
|
||||
const [loadingSearch, setLoadingSearch] = React.useState<boolean>(false);
|
||||
const [loadingSearchMessage, setLoadingSearchMessage] =
|
||||
React.useState<string>("loading...");
|
||||
const selectRef = React.useRef<StateManager<any> | null>(null);
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const { tokenRegistry } = useTokenRegistry();
|
||||
const { cluster, clusterInfo } = useCluster();
|
||||
const { url, cluster, clusterInfo } = useCluster();
|
||||
|
||||
const onChange = (
|
||||
{ pathname }: ValueType<any, false>,
|
||||
@ -33,7 +49,54 @@ export function SearchBar() {
|
||||
};
|
||||
|
||||
const onInputChange = (value: string, { action }: InputActionMeta) => {
|
||||
if (action === "input-change") setSearch(value);
|
||||
if (action === "input-change") {
|
||||
setSearch(value);
|
||||
}
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
searchRef.current = search;
|
||||
setLoadingSearchMessage("Loading...");
|
||||
setLoadingSearch(true);
|
||||
|
||||
// builds and sets local search output
|
||||
const options = buildOptions(
|
||||
search,
|
||||
cluster,
|
||||
tokenRegistry,
|
||||
clusterInfo?.epochInfo.epoch
|
||||
);
|
||||
|
||||
setSearchOptions(options);
|
||||
|
||||
// checking for non local search output
|
||||
if (hasDomainSyntax(search)) {
|
||||
// if search input is a potential domain we continue the loading state
|
||||
domainSearch(options);
|
||||
} else {
|
||||
// if search input is not a potential domain we can conclude the search has finished
|
||||
setLoadingSearch(false);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [search]);
|
||||
|
||||
// appends domain lookup results to the local search state
|
||||
const domainSearch = async (options: SearchOptions[]) => {
|
||||
setLoadingSearchMessage("Looking up domain...");
|
||||
const connection = new Connection(url);
|
||||
const searchTerm = search;
|
||||
const updatedOptions = await buildDomainOptions(
|
||||
connection,
|
||||
search,
|
||||
options
|
||||
);
|
||||
if (searchRef.current === searchTerm) {
|
||||
setSearchOptions(updatedOptions);
|
||||
// after attempting to fetch the domain name we can conclude the loading state
|
||||
setLoadingSearch(false);
|
||||
setLoadingSearchMessage("Loading...");
|
||||
}
|
||||
};
|
||||
|
||||
const resetValue = "" as any;
|
||||
@ -44,13 +107,9 @@ export function SearchBar() {
|
||||
<Select
|
||||
autoFocus
|
||||
ref={(ref) => (selectRef.current = ref)}
|
||||
options={buildOptions(
|
||||
search,
|
||||
cluster,
|
||||
tokenRegistry,
|
||||
clusterInfo?.epochInfo.epoch
|
||||
)}
|
||||
options={searchOptions}
|
||||
noOptionsMessage={() => "No Results"}
|
||||
loadingMessage={() => loadingSearchMessage}
|
||||
placeholder="Search for blocks, accounts, transactions, programs, and tokens"
|
||||
value={resetValue}
|
||||
inputValue={search}
|
||||
@ -65,6 +124,7 @@ export function SearchBar() {
|
||||
onInputChange={onInputChange}
|
||||
components={{ DropdownIndicator }}
|
||||
classNamePrefix="search-bar"
|
||||
isLoading={loadingSearch}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -187,7 +247,7 @@ function buildTokenOptions(
|
||||
if (matchedTokens.length > 0) {
|
||||
return {
|
||||
label: "Tokens",
|
||||
options: matchedTokens.map(([id, details]) => ({
|
||||
options: matchedTokens.slice(0, 10).map(([id, details]) => ({
|
||||
label: details.name,
|
||||
value: [details.name, details.symbol, id],
|
||||
pathname: "/address/" + id,
|
||||
@ -196,6 +256,39 @@ function buildTokenOptions(
|
||||
}
|
||||
}
|
||||
|
||||
async function buildDomainOptions(
|
||||
connection: Connection,
|
||||
search: string,
|
||||
options: SearchOptions[]
|
||||
) {
|
||||
const domainInfo = await getDomainInfo(search, connection);
|
||||
const updatedOptions: SearchOptions[] = [...options];
|
||||
if (domainInfo && domainInfo.owner && domainInfo.address) {
|
||||
updatedOptions.push({
|
||||
label: "Domain Owner",
|
||||
options: [
|
||||
{
|
||||
label: domainInfo.owner,
|
||||
value: [search],
|
||||
pathname: "/address/" + domainInfo.owner,
|
||||
},
|
||||
],
|
||||
});
|
||||
updatedOptions.push({
|
||||
label: "Name Service Account",
|
||||
options: [
|
||||
{
|
||||
label: search,
|
||||
value: [search],
|
||||
pathname: "/address/" + domainInfo.address,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
return updatedOptions;
|
||||
}
|
||||
|
||||
// builds local search options
|
||||
function buildOptions(
|
||||
rawSearch: string,
|
||||
cluster: Cluster,
|
||||
@ -287,6 +380,7 @@ function buildOptions(
|
||||
});
|
||||
}
|
||||
} catch (err) {}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
|
@ -1,63 +1,62 @@
|
||||
import React, { useMemo } from "react";
|
||||
|
||||
import { Account } from "providers/accounts";
|
||||
import { Address } from "components/common/Address";
|
||||
import { BorshAccountsCoder } from "@project-serum/anchor";
|
||||
import { capitalizeFirstLetter } from "utils/anchor";
|
||||
import { ErrorCard } from "components/common/ErrorCard";
|
||||
import { PublicKey } from "@solana/web3.js";
|
||||
import BN from "bn.js";
|
||||
|
||||
import ReactJson from "react-json-view";
|
||||
import { useCluster } from "providers/cluster";
|
||||
import { BorshAccountsCoder } from "@project-serum/anchor";
|
||||
import { IdlTypeDef } from "@project-serum/anchor/dist/cjs/idl";
|
||||
import { getProgramName, mapAccountToRows } from "utils/anchor";
|
||||
import { ErrorCard } from "components/common/ErrorCard";
|
||||
import { useAnchorProgram } from "providers/anchor";
|
||||
|
||||
export function AnchorAccountCard({ account }: { account: Account }) {
|
||||
const { lamports } = account;
|
||||
const { url } = useCluster();
|
||||
const program = useAnchorProgram(
|
||||
account.details?.owner.toString() ?? "",
|
||||
const anchorProgram = useAnchorProgram(
|
||||
account.details?.owner.toString() || "",
|
||||
url
|
||||
);
|
||||
const rawData = account?.details?.rawData;
|
||||
const programName = getProgramName(anchorProgram) || "Unknown Program";
|
||||
|
||||
const { foundAccountLayoutName, decodedAnchorAccountData } = useMemo(() => {
|
||||
let foundAccountLayoutName: string | undefined;
|
||||
let decodedAnchorAccountData: { [key: string]: any } | undefined;
|
||||
if (program && account.details && account.details.rawData) {
|
||||
const accountBuffer = account.details.rawData;
|
||||
const discriminator = accountBuffer.slice(0, 8);
|
||||
|
||||
// Iterate all the structs, see if any of the name-hashes match
|
||||
Object.keys(program.account).forEach((accountType) => {
|
||||
const layoutName = capitalizeFirstLetter(accountType);
|
||||
const discriminatorToCheck =
|
||||
BorshAccountsCoder.accountDiscriminator(layoutName);
|
||||
|
||||
if (discriminatorToCheck.equals(discriminator)) {
|
||||
foundAccountLayoutName = layoutName;
|
||||
const accountDecoder = program.account[accountType];
|
||||
decodedAnchorAccountData = accountDecoder.coder.accounts.decode(
|
||||
layoutName,
|
||||
accountBuffer
|
||||
);
|
||||
}
|
||||
});
|
||||
const { decodedAccountData, accountDef } = useMemo(() => {
|
||||
let decodedAccountData: any | null = null;
|
||||
let accountDef: IdlTypeDef | undefined = undefined;
|
||||
if (anchorProgram && rawData) {
|
||||
const coder = new BorshAccountsCoder(anchorProgram.idl);
|
||||
const accountDefTmp = anchorProgram.idl.accounts?.find(
|
||||
(accountType: any) =>
|
||||
(rawData as Buffer)
|
||||
.slice(0, 8)
|
||||
.equals(BorshAccountsCoder.accountDiscriminator(accountType.name))
|
||||
);
|
||||
if (accountDefTmp) {
|
||||
accountDef = accountDefTmp;
|
||||
decodedAccountData = coder.decode(accountDef.name, rawData);
|
||||
}
|
||||
}
|
||||
return { foundAccountLayoutName, decodedAnchorAccountData };
|
||||
}, [program, account.details]);
|
||||
|
||||
if (!foundAccountLayoutName || !decodedAnchorAccountData) {
|
||||
return {
|
||||
decodedAccountData,
|
||||
accountDef,
|
||||
};
|
||||
}, [anchorProgram, rawData]);
|
||||
|
||||
if (lamports === undefined) return null;
|
||||
if (!anchorProgram) return <ErrorCard text="No Anchor IDL found" />;
|
||||
if (!decodedAccountData || !accountDef) {
|
||||
return (
|
||||
<ErrorCard text="Failed to decode account data according to its public anchor interface" />
|
||||
<ErrorCard text="Failed to decode account data according to the public Anchor interface" />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<div className="card">
|
||||
<div className="card-header">
|
||||
<div className="row align-items-center">
|
||||
<div className="col">
|
||||
<h3 className="card-header-title">{foundAccountLayoutName}</h3>
|
||||
<h3 className="card-header-title">
|
||||
{programName}: {accountDef.name}
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -66,92 +65,21 @@ export function AnchorAccountCard({ account }: { account: Account }) {
|
||||
<table className="table table-sm table-nowrap card-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="w-1 text-muted">Key</th>
|
||||
<th className="text-muted">Value</th>
|
||||
<th className="w-1">Field</th>
|
||||
<th className="w-1">Type</th>
|
||||
<th className="w-1">Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="list">
|
||||
{decodedAnchorAccountData &&
|
||||
Object.keys(decodedAnchorAccountData).map((key) => (
|
||||
<AccountRow
|
||||
key={key}
|
||||
valueName={key}
|
||||
value={decodedAnchorAccountData[key]}
|
||||
/>
|
||||
))}
|
||||
<tbody>
|
||||
{mapAccountToRows(
|
||||
decodedAccountData,
|
||||
accountDef as IdlTypeDef,
|
||||
anchorProgram.idl
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="card-footer">
|
||||
<div className="text-muted text-center">
|
||||
{decodedAnchorAccountData &&
|
||||
Object.keys(decodedAnchorAccountData).length > 0
|
||||
? `Decoded ${Object.keys(decodedAnchorAccountData).length} Items`
|
||||
: "No decoded data"}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function AccountRow({ valueName, value }: { valueName: string; value: any }) {
|
||||
let displayValue: JSX.Element;
|
||||
if (value instanceof PublicKey) {
|
||||
displayValue = <Address pubkey={value} link />;
|
||||
} else if (value instanceof BN) {
|
||||
displayValue = <>{value.toString()}</>;
|
||||
} else if (!(value instanceof Object)) {
|
||||
displayValue = <>{String(value)}</>;
|
||||
} else if (value) {
|
||||
const displayObject = stringifyPubkeyAndBigNums(value);
|
||||
displayValue = (
|
||||
<ReactJson
|
||||
src={JSON.parse(JSON.stringify(displayObject))}
|
||||
collapsed={1}
|
||||
theme="solarized"
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
displayValue = <>null</>;
|
||||
}
|
||||
return (
|
||||
<tr>
|
||||
<td className="w-1 text-monospace">{camelToUnderscore(valueName)}</td>
|
||||
<td className="text-monospace">{displayValue}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
function camelToUnderscore(key: string) {
|
||||
var result = key.replace(/([A-Z])/g, " $1");
|
||||
return result.split(" ").join("_").toLowerCase();
|
||||
}
|
||||
|
||||
function stringifyPubkeyAndBigNums(object: Object): Object {
|
||||
if (!Array.isArray(object)) {
|
||||
if (object instanceof PublicKey) {
|
||||
return object.toString();
|
||||
} else if (object instanceof BN) {
|
||||
return object.toString();
|
||||
} else if (!(object instanceof Object)) {
|
||||
return object;
|
||||
} else {
|
||||
const parsedObject: { [key: string]: Object } = {};
|
||||
Object.keys(object).map((key) => {
|
||||
let value = (object as { [key: string]: any })[key];
|
||||
if (value instanceof Object) {
|
||||
value = stringifyPubkeyAndBigNums(value);
|
||||
}
|
||||
parsedObject[key] = value;
|
||||
return null;
|
||||
});
|
||||
return parsedObject;
|
||||
}
|
||||
}
|
||||
return object.map((innerObject) =>
|
||||
innerObject instanceof Object
|
||||
? stringifyPubkeyAndBigNums(innerObject)
|
||||
: innerObject
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -21,15 +21,14 @@ export function DomainsCard({ pubkey }: { pubkey: PublicKey }) {
|
||||
return (
|
||||
<div className="card">
|
||||
<div className="card-header align-items-center">
|
||||
<h3 className="card-header-title">Domain Names Owned</h3>
|
||||
<h3 className="card-header-title">Owned Domain Names</h3>
|
||||
</div>
|
||||
<div className="table-responsive mb-0">
|
||||
<table className="table table-sm table-nowrap card-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th className="text-muted">Domain name</th>
|
||||
<th className="text-muted">Domain Address</th>
|
||||
<th className="text-muted">Domain Class Address</th>
|
||||
<th className="text-muted">Domain Name</th>
|
||||
<th className="text-muted">Name Service Account</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="list">
|
||||
@ -53,9 +52,6 @@ function RenderDomainRow({ domainInfo }: { domainInfo: DomainInfo }) {
|
||||
<td>
|
||||
<Address pubkey={domainInfo.address} link />
|
||||
</td>
|
||||
<td>
|
||||
<Address pubkey={domainInfo.class} link />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
@ -343,7 +343,7 @@ function NonFungibleTokenMintAccountCard({
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
{nftData?.metadata.collection?.verified && (
|
||||
{!!nftData?.metadata.collection?.verified && (
|
||||
<tr>
|
||||
<td>Verified Collection Address</td>
|
||||
<td className="text-lg-end">
|
||||
|
@ -18,6 +18,7 @@ type Props = {
|
||||
truncateUnknown?: boolean;
|
||||
truncateChars?: number;
|
||||
useMetadata?: boolean;
|
||||
overrideText?: string;
|
||||
};
|
||||
|
||||
export function Address({
|
||||
@ -29,6 +30,7 @@ export function Address({
|
||||
truncateUnknown,
|
||||
truncateChars,
|
||||
useMetadata,
|
||||
overrideText,
|
||||
}: Props) {
|
||||
const address = pubkey.toBase58();
|
||||
const { tokenRegistry } = useTokenRegistry();
|
||||
@ -52,6 +54,10 @@ export function Address({
|
||||
addressLabel = addressLabel.slice(0, truncateChars) + "…";
|
||||
}
|
||||
|
||||
if (overrideText) {
|
||||
addressLabel = overrideText;
|
||||
}
|
||||
|
||||
const content = (
|
||||
<Copyable text={address} replaceText={!alignRight}>
|
||||
<span className="font-monospace">
|
||||
|
@ -1,15 +1,21 @@
|
||||
import { SignatureResult, TransactionInstruction } from "@solana/web3.js";
|
||||
import { InstructionCard } from "./InstructionCard";
|
||||
import { Idl, Program, BorshInstructionCoder } from "@project-serum/anchor";
|
||||
import {
|
||||
Idl,
|
||||
Program,
|
||||
BorshInstructionCoder,
|
||||
Instruction,
|
||||
} from "@project-serum/anchor";
|
||||
import {
|
||||
getAnchorNameForInstruction,
|
||||
getProgramName,
|
||||
capitalizeFirstLetter,
|
||||
getAnchorAccountsFromInstruction,
|
||||
mapIxArgsToRows,
|
||||
} from "utils/anchor";
|
||||
import { HexData } from "components/common/HexData";
|
||||
import { Address } from "components/common/Address";
|
||||
import ReactJson from "react-json-view";
|
||||
import { camelToTitleCase } from "utils";
|
||||
import { IdlInstruction } from "@project-serum/anchor/dist/cjs/idl";
|
||||
import { useMemo } from "react";
|
||||
|
||||
export default function AnchorDetailsCard(props: {
|
||||
key: string;
|
||||
@ -26,46 +32,99 @@ export default function AnchorDetailsCard(props: {
|
||||
|
||||
const ixName =
|
||||
getAnchorNameForInstruction(ix, anchorProgram) ?? "Unknown Instruction";
|
||||
const cardTitle = `${programName}: ${ixName}`;
|
||||
const cardTitle = `${camelToTitleCase(programName)}: ${camelToTitleCase(
|
||||
ixName
|
||||
)}`;
|
||||
|
||||
return (
|
||||
<InstructionCard title={cardTitle} {...props}>
|
||||
<RawAnchorDetails ix={ix} anchorProgram={anchorProgram} />
|
||||
<AnchorDetails ix={ix} anchorProgram={anchorProgram} />
|
||||
</InstructionCard>
|
||||
);
|
||||
}
|
||||
|
||||
function RawAnchorDetails({
|
||||
function AnchorDetails({
|
||||
ix,
|
||||
anchorProgram,
|
||||
}: {
|
||||
ix: TransactionInstruction;
|
||||
anchorProgram: Program;
|
||||
}) {
|
||||
let ixAccounts:
|
||||
| {
|
||||
name: string;
|
||||
isMut: boolean;
|
||||
isSigner: boolean;
|
||||
pda?: Object;
|
||||
}[]
|
||||
| null = null;
|
||||
var decodedIxData = null;
|
||||
if (anchorProgram) {
|
||||
const decoder = new BorshInstructionCoder(anchorProgram.idl);
|
||||
decodedIxData = decoder.decode(ix.data);
|
||||
ixAccounts = getAnchorAccountsFromInstruction(decodedIxData, anchorProgram);
|
||||
const { ixAccounts, decodedIxData, ixDef } = useMemo(() => {
|
||||
let ixAccounts:
|
||||
| {
|
||||
name: string;
|
||||
isMut: boolean;
|
||||
isSigner: boolean;
|
||||
pda?: Object;
|
||||
}[]
|
||||
| null = null;
|
||||
let decodedIxData: Instruction | null = null;
|
||||
let ixDef: IdlInstruction | undefined;
|
||||
if (anchorProgram) {
|
||||
const coder = new BorshInstructionCoder(anchorProgram.idl);
|
||||
decodedIxData = coder.decode(ix.data);
|
||||
if (decodedIxData) {
|
||||
ixDef = anchorProgram.idl.instructions.find(
|
||||
(ixDef) => ixDef.name === decodedIxData?.name
|
||||
);
|
||||
if (ixDef) {
|
||||
ixAccounts = getAnchorAccountsFromInstruction(
|
||||
decodedIxData,
|
||||
anchorProgram
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
ixAccounts,
|
||||
decodedIxData,
|
||||
ixDef,
|
||||
};
|
||||
}, [anchorProgram, ix.data]);
|
||||
|
||||
if (!ixAccounts || !decodedIxData || !ixDef) {
|
||||
return (
|
||||
<tr>
|
||||
<td colSpan={3} className="text-lg-center">
|
||||
Failed to decode account data according to the public Anchor interface
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
const programName = getProgramName(anchorProgram) ?? "Unknown Program";
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr>
|
||||
<td>Program</td>
|
||||
<td className="text-lg-end" colSpan={2}>
|
||||
<Address
|
||||
pubkey={ix.programId}
|
||||
alignRight
|
||||
link
|
||||
raw
|
||||
overrideText={programName}
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
<tr className="table-sep">
|
||||
<td>Account Name</td>
|
||||
<td className="text-lg-end" colSpan={2}>
|
||||
Address
|
||||
</td>
|
||||
</tr>
|
||||
{ix.keys.map(({ pubkey, isSigner, isWritable }, keyIndex) => {
|
||||
return (
|
||||
<tr key={keyIndex}>
|
||||
<td>
|
||||
<div className="me-2 d-md-inline">
|
||||
{ixAccounts && keyIndex < ixAccounts.length
|
||||
? `${capitalizeFirstLetter(ixAccounts[keyIndex].name)}`
|
||||
{ixAccounts
|
||||
? keyIndex < ixAccounts.length
|
||||
? `${camelToTitleCase(ixAccounts[keyIndex].name)}`
|
||||
: `Remaining Account #${keyIndex + 1 - ixAccounts.length}`
|
||||
: `Account #${keyIndex + 1}`}
|
||||
</div>
|
||||
{isWritable && (
|
||||
@ -75,27 +134,23 @@ function RawAnchorDetails({
|
||||
<span className="badge bg-info-soft me-1">Signer</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="text-lg-end">
|
||||
<td className="text-lg-end" colSpan={2}>
|
||||
<Address pubkey={pubkey} alignRight link />
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
Instruction Data <span className="text-muted">(Hex)</span>
|
||||
</td>
|
||||
{decodedIxData ? (
|
||||
<td className="metadata-json-viewer m-4">
|
||||
<ReactJson src={decodedIxData} theme="solarized" />
|
||||
</td>
|
||||
) : (
|
||||
<td className="text-lg-end">
|
||||
<HexData raw={ix.data} />
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
{decodedIxData && ixDef && ixDef.args.length > 0 && (
|
||||
<>
|
||||
<tr className="table-sep">
|
||||
<td>Argument Name</td>
|
||||
<td>Type</td>
|
||||
<td className="text-lg-end">Value</td>
|
||||
</tr>
|
||||
{mapIxArgsToRows(decodedIxData.data, ixDef, anchorProgram.idl)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -100,12 +100,16 @@ export function InstructionCard({
|
||||
children
|
||||
)}
|
||||
{innerCards && innerCards.length > 0 && (
|
||||
<tr>
|
||||
<td colSpan={2}>
|
||||
Inner Instructions
|
||||
<div className="inner-cards">{innerCards}</div>
|
||||
</td>
|
||||
</tr>
|
||||
<>
|
||||
<tr className="table-sep">
|
||||
<td colSpan={3}>Inner Instructions</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colSpan={3}>
|
||||
<div className="inner-cards">{innerCards}</div>
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
|
@ -271,7 +271,7 @@ function DetailsSections({
|
||||
account. Please be cautious sending SOL to this account.
|
||||
</div>
|
||||
)}
|
||||
{<InfoSection account={account} />}
|
||||
<InfoSection account={account} />
|
||||
<MoreSection
|
||||
account={account}
|
||||
tab={moreTab}
|
||||
@ -517,17 +517,17 @@ function getAnchorTabs(pubkey: PublicKey, account: Account) {
|
||||
),
|
||||
});
|
||||
|
||||
const anchorAccountTab: Tab = {
|
||||
const accountDataTab: Tab = {
|
||||
slug: "anchor-account",
|
||||
title: "Anchor Account",
|
||||
title: "Anchor Data",
|
||||
path: "/anchor-account",
|
||||
};
|
||||
tabComponents.push({
|
||||
tab: anchorAccountTab,
|
||||
tab: accountDataTab,
|
||||
component: (
|
||||
<React.Suspense key={anchorAccountTab.slug} fallback={<></>}>
|
||||
<AnchorAccountLink
|
||||
tab={anchorAccountTab}
|
||||
<React.Suspense key={accountDataTab.slug} fallback={<></>}>
|
||||
<AccountDataLink
|
||||
tab={accountDataTab}
|
||||
address={pubkey.toString()}
|
||||
programId={account.details?.owner}
|
||||
/>
|
||||
@ -567,7 +567,7 @@ function AnchorProgramLink({
|
||||
);
|
||||
}
|
||||
|
||||
function AnchorAccountLink({
|
||||
function AccountDataLink({
|
||||
address,
|
||||
tab,
|
||||
programId,
|
||||
|
@ -317,6 +317,8 @@ async function fetchAccountInfo(
|
||||
});
|
||||
}
|
||||
|
||||
const IMAGE_MIME_TYPE_REGEX = /data:image\/(svg\+xml|png|jpeg|gif)/g;
|
||||
|
||||
const getMetaDataJSON = async (
|
||||
id: string,
|
||||
metadata: programs.metadata.MetadataData
|
||||
@ -331,9 +333,11 @@ const getMetaDataJSON = async (
|
||||
}
|
||||
|
||||
if (extended?.image) {
|
||||
extended.image = extended.image.startsWith("http")
|
||||
? extended.image
|
||||
: `${metadata.data.uri}/${extended.image}`;
|
||||
extended.image =
|
||||
extended.image.startsWith("http") ||
|
||||
IMAGE_MIME_TYPE_REGEX.test(extended.image)
|
||||
? extended.image
|
||||
: `${metadata.data.uri}/${extended.image}`;
|
||||
}
|
||||
|
||||
return extended;
|
||||
|
@ -1,9 +1,9 @@
|
||||
//
|
||||
//
|
||||
// tables.scss
|
||||
// Extended from Bootstrap
|
||||
//
|
||||
|
||||
//
|
||||
//
|
||||
// Bootstrap Overrides =====================================
|
||||
//
|
||||
|
||||
@ -25,6 +25,15 @@
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.table-sep {
|
||||
background-color: $table-head-bg;
|
||||
text-transform: uppercase;
|
||||
font-size: $font-size-xs;
|
||||
font-weight: $font-weight-bold;
|
||||
letter-spacing: .08em;
|
||||
color: $table-head-color;
|
||||
}
|
||||
|
||||
|
||||
// Sizing
|
||||
|
||||
|
@ -1,43 +1,33 @@
|
||||
import React from "react";
|
||||
import React, { Fragment, ReactNode, useState } from "react";
|
||||
import { Cluster } from "providers/cluster";
|
||||
import { PublicKey, TransactionInstruction } from "@solana/web3.js";
|
||||
import { BorshInstructionCoder, Program } from "@project-serum/anchor";
|
||||
import { BorshInstructionCoder, Program, Idl } from "@project-serum/anchor";
|
||||
import { useAnchorProgram } from "providers/anchor";
|
||||
import { programLabel } from "utils/tx";
|
||||
import { ErrorBoundary } from "@sentry/react";
|
||||
|
||||
function snakeToPascal(string: string) {
|
||||
return string
|
||||
.split("/")
|
||||
.map((snake) =>
|
||||
snake
|
||||
.split("_")
|
||||
.map((substr) => substr.charAt(0).toUpperCase() + substr.slice(1))
|
||||
.join("")
|
||||
)
|
||||
.join("/");
|
||||
}
|
||||
import { snakeToTitleCase, camelToTitleCase, numberWithSeparator } from "utils";
|
||||
import {
|
||||
IdlInstruction,
|
||||
IdlType,
|
||||
IdlTypeDef,
|
||||
} from "@project-serum/anchor/dist/cjs/idl";
|
||||
import { Address } from "components/common/Address";
|
||||
import ReactJson from "react-json-view";
|
||||
|
||||
export function getProgramName(program: Program | null): string | undefined {
|
||||
return program ? snakeToPascal(program.idl.name) : undefined;
|
||||
return program ? snakeToTitleCase(program.idl.name) : undefined;
|
||||
}
|
||||
|
||||
export function capitalizeFirstLetter(input: string) {
|
||||
return input.charAt(0).toUpperCase() + input.slice(1);
|
||||
}
|
||||
|
||||
function AnchorProgramName({
|
||||
export function AnchorProgramName({
|
||||
programId,
|
||||
url,
|
||||
defaultName = "Unknown Program",
|
||||
}: {
|
||||
programId: PublicKey;
|
||||
url: string;
|
||||
defaultName?: string;
|
||||
}) {
|
||||
const program = useAnchorProgram(programId.toString(), url);
|
||||
if (!program) {
|
||||
throw new Error("No anchor program name found for given programId");
|
||||
}
|
||||
const programName = getProgramName(program);
|
||||
const programName = getProgramName(program) || defaultName;
|
||||
return <>{programName}</>;
|
||||
}
|
||||
|
||||
@ -52,12 +42,13 @@ export function ProgramName({
|
||||
}) {
|
||||
const defaultProgramName =
|
||||
programLabel(programId.toBase58(), cluster) || "Unknown Program";
|
||||
|
||||
return (
|
||||
<React.Suspense fallback={defaultProgramName}>
|
||||
<ErrorBoundary fallback={<>{defaultProgramName}</>}>
|
||||
<AnchorProgramName programId={programId} url={url} />
|
||||
</ErrorBoundary>
|
||||
<React.Suspense fallback={<>{defaultProgramName}</>}>
|
||||
<AnchorProgramName
|
||||
programId={programId}
|
||||
url={url}
|
||||
defaultName={defaultProgramName}
|
||||
/>
|
||||
</React.Suspense>
|
||||
);
|
||||
}
|
||||
@ -107,3 +98,387 @@ export function getAnchorAccountsFromInstruction(
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function mapIxArgsToRows(ixArgs: any, ixType: IdlInstruction, idl: Idl) {
|
||||
return Object.entries(ixArgs).map(([key, value]) => {
|
||||
try {
|
||||
const fieldDef = ixType.args.find((ixDefArg) => ixDefArg.name === key);
|
||||
if (!fieldDef) {
|
||||
throw Error(
|
||||
`Could not find expected ${key} field on account type definition for ${ixType.name}`
|
||||
);
|
||||
}
|
||||
return mapField(key, value, fieldDef.type, idl);
|
||||
} catch (error: any) {
|
||||
console.log("Error while displaying IDL-based account data", error);
|
||||
return (
|
||||
<tr key={key}>
|
||||
<td>{key}</td>
|
||||
<td className="text-lg-end">
|
||||
<td className="metadata-json-viewer m-4">
|
||||
<ReactJson src={ixArgs} theme="solarized" />
|
||||
</td>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function mapAccountToRows(
|
||||
accountData: any,
|
||||
accountType: IdlTypeDef,
|
||||
idl: Idl
|
||||
) {
|
||||
return Object.entries(accountData).map(([key, value]) => {
|
||||
try {
|
||||
if (accountType.type.kind !== "struct") {
|
||||
throw Error(
|
||||
`Account ${accountType.name} is of type ${accountType.type.kind} (expected: 'struct')`
|
||||
);
|
||||
}
|
||||
const fieldDef = accountType.type.fields.find(
|
||||
(ixDefArg) => ixDefArg.name === key
|
||||
);
|
||||
if (!fieldDef) {
|
||||
throw Error(
|
||||
`Could not find expected ${key} field on account type definition for ${accountType.name}`
|
||||
);
|
||||
}
|
||||
return mapField(key, value as any, fieldDef.type, idl);
|
||||
} catch (error: any) {
|
||||
console.log("Error while displaying IDL-based account data", error);
|
||||
return (
|
||||
<tr key={key}>
|
||||
<td>{key}</td>
|
||||
<td className="text-lg-end">
|
||||
<td className="metadata-json-viewer m-4">
|
||||
<ReactJson src={accountData} theme="solarized" />
|
||||
</td>
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function mapField(
|
||||
key: string,
|
||||
value: any,
|
||||
type: IdlType,
|
||||
idl: Idl,
|
||||
keySuffix?: any,
|
||||
nestingLevel: number = 0
|
||||
): ReactNode {
|
||||
let itemKey = key;
|
||||
if (/^-?\d+$/.test(keySuffix)) {
|
||||
itemKey = `#${keySuffix}`;
|
||||
}
|
||||
itemKey = camelToTitleCase(itemKey);
|
||||
|
||||
if (value === undefined) {
|
||||
return (
|
||||
<SimpleRow
|
||||
key={keySuffix ? `${key}-${keySuffix}` : key}
|
||||
rawKey={key}
|
||||
type={type}
|
||||
keySuffix={keySuffix}
|
||||
nestingLevel={nestingLevel}
|
||||
>
|
||||
<div>null</div>
|
||||
</SimpleRow>
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
type === "u8" ||
|
||||
type === "i8" ||
|
||||
type === "u16" ||
|
||||
type === "i16" ||
|
||||
type === "u32" ||
|
||||
type === "i32" ||
|
||||
type === "f32" ||
|
||||
type === "u64" ||
|
||||
type === "i64" ||
|
||||
type === "f64" ||
|
||||
type === "u128" ||
|
||||
type === "i128"
|
||||
) {
|
||||
return (
|
||||
<SimpleRow
|
||||
key={keySuffix ? `${key}-${keySuffix}` : key}
|
||||
rawKey={key}
|
||||
type={type}
|
||||
keySuffix={keySuffix}
|
||||
nestingLevel={nestingLevel}
|
||||
>
|
||||
<div>{numberWithSeparator(value.toString())}</div>
|
||||
</SimpleRow>
|
||||
);
|
||||
} else if (type === "bool" || type === "bytes" || type === "string") {
|
||||
return (
|
||||
<SimpleRow
|
||||
key={keySuffix ? `${key}-${keySuffix}` : key}
|
||||
rawKey={key}
|
||||
type={type}
|
||||
keySuffix={keySuffix}
|
||||
nestingLevel={nestingLevel}
|
||||
>
|
||||
<div>{value.toString()}</div>
|
||||
</SimpleRow>
|
||||
);
|
||||
} else if (type === "publicKey") {
|
||||
return (
|
||||
<SimpleRow
|
||||
key={keySuffix ? `${key}-${keySuffix}` : key}
|
||||
rawKey={key}
|
||||
type={type}
|
||||
keySuffix={keySuffix}
|
||||
nestingLevel={nestingLevel}
|
||||
>
|
||||
<Address pubkey={value} link alignRight />
|
||||
</SimpleRow>
|
||||
);
|
||||
} else if ("defined" in type) {
|
||||
const fieldType = idl.types?.find((t) => t.name === type.defined);
|
||||
if (!fieldType) {
|
||||
throw Error(`Could not type definition for ${type.defined} field in IDL`);
|
||||
}
|
||||
if (fieldType.type.kind === "struct") {
|
||||
const structFields = fieldType.type.fields;
|
||||
return (
|
||||
<ExpandableRow
|
||||
fieldName={itemKey}
|
||||
fieldType={typeDisplayName(type)}
|
||||
nestingLevel={nestingLevel}
|
||||
key={keySuffix ? `${key}-${keySuffix}` : key}
|
||||
>
|
||||
<Fragment key={keySuffix ? `${key}-${keySuffix}` : key}>
|
||||
{Object.entries(value).map(
|
||||
([innerKey, innerValue]: [string, any]) => {
|
||||
const innerFieldType = structFields.find(
|
||||
(t) => t.name === innerKey
|
||||
);
|
||||
if (!innerFieldType) {
|
||||
throw Error(
|
||||
`Could not type definition for ${innerKey} field in user-defined struct ${fieldType.name}`
|
||||
);
|
||||
}
|
||||
return mapField(
|
||||
innerKey,
|
||||
innerValue,
|
||||
innerFieldType?.type,
|
||||
idl,
|
||||
key,
|
||||
nestingLevel + 1
|
||||
);
|
||||
}
|
||||
)}
|
||||
</Fragment>
|
||||
</ExpandableRow>
|
||||
);
|
||||
} else {
|
||||
const enumValue = Object.keys(value)[0];
|
||||
return (
|
||||
<SimpleRow
|
||||
key={keySuffix ? `${key}-${keySuffix}` : key}
|
||||
rawKey={key}
|
||||
type={{ enum: type.defined }}
|
||||
keySuffix={keySuffix}
|
||||
nestingLevel={nestingLevel}
|
||||
>
|
||||
{camelToTitleCase(enumValue)}
|
||||
</SimpleRow>
|
||||
);
|
||||
}
|
||||
} else if ("option" in type) {
|
||||
if (value === null) {
|
||||
return (
|
||||
<SimpleRow
|
||||
key={keySuffix ? `${key}-${keySuffix}` : key}
|
||||
rawKey={key}
|
||||
type={type}
|
||||
keySuffix={keySuffix}
|
||||
nestingLevel={nestingLevel}
|
||||
>
|
||||
Not provided
|
||||
</SimpleRow>
|
||||
);
|
||||
}
|
||||
return mapField(key, value, type.option, idl, key, nestingLevel);
|
||||
} else if ("vec" in type) {
|
||||
const itemType = type.vec;
|
||||
return (
|
||||
<ExpandableRow
|
||||
fieldName={itemKey}
|
||||
fieldType={typeDisplayName(type)}
|
||||
nestingLevel={nestingLevel}
|
||||
key={keySuffix ? `${key}-${keySuffix}` : key}
|
||||
>
|
||||
<Fragment key={keySuffix ? `${key}-${keySuffix}` : key}>
|
||||
{(value as any[]).map((item, i) =>
|
||||
mapField(key, item, itemType, idl, i, nestingLevel + 1)
|
||||
)}
|
||||
</Fragment>
|
||||
</ExpandableRow>
|
||||
);
|
||||
} else if ("array" in type) {
|
||||
const [itemType] = type.array;
|
||||
return (
|
||||
<ExpandableRow
|
||||
fieldName={itemKey}
|
||||
fieldType={typeDisplayName(type)}
|
||||
nestingLevel={nestingLevel}
|
||||
key={keySuffix ? `${key}-${keySuffix}` : key}
|
||||
>
|
||||
<Fragment key={keySuffix ? `${key}-${keySuffix}` : key}>
|
||||
{(value as any[]).map((item, i) =>
|
||||
mapField(key, item, itemType, idl, i, nestingLevel + 1)
|
||||
)}
|
||||
</Fragment>
|
||||
</ExpandableRow>
|
||||
);
|
||||
} else {
|
||||
console.log("Impossible type:", type);
|
||||
return (
|
||||
<tr key={keySuffix ? `${key}-${keySuffix}` : key}>
|
||||
<td>{camelToTitleCase(key)}</td>
|
||||
<td></td>
|
||||
<td className="text-lg-end">???</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function SimpleRow({
|
||||
rawKey,
|
||||
type,
|
||||
keySuffix,
|
||||
nestingLevel = 0,
|
||||
children,
|
||||
}: {
|
||||
rawKey: string;
|
||||
type: IdlType | { enum: string };
|
||||
keySuffix?: any;
|
||||
nestingLevel: number;
|
||||
children?: ReactNode;
|
||||
}) {
|
||||
let itemKey = rawKey;
|
||||
if (/^-?\d+$/.test(keySuffix)) {
|
||||
itemKey = `#${keySuffix}`;
|
||||
}
|
||||
itemKey = camelToTitleCase(itemKey);
|
||||
return (
|
||||
<tr
|
||||
style={{
|
||||
...(nestingLevel === 0 ? {} : { backgroundColor: "#141816" }),
|
||||
}}
|
||||
>
|
||||
<td className="d-flex flex-row">
|
||||
{nestingLevel > 0 && (
|
||||
<span
|
||||
className="text-info fe fe-corner-down-right me-2"
|
||||
style={{
|
||||
paddingLeft: `${15 * nestingLevel}px`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div>{itemKey}</div>
|
||||
</td>
|
||||
<td>{typeDisplayName(type)}</td>
|
||||
<td className="text-lg-end">{children}</td>
|
||||
</tr>
|
||||
);
|
||||
}
|
||||
|
||||
export function ExpandableRow({
|
||||
fieldName,
|
||||
fieldType,
|
||||
nestingLevel,
|
||||
children,
|
||||
}: {
|
||||
fieldName: string;
|
||||
fieldType: string;
|
||||
nestingLevel: number;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<tr
|
||||
style={{
|
||||
...(nestingLevel === 0 ? {} : { backgroundColor: "#141816" }),
|
||||
}}
|
||||
>
|
||||
<td className="d-flex flex-row">
|
||||
{nestingLevel > 0 && (
|
||||
<div
|
||||
className="text-info fe fe-corner-down-right me-2"
|
||||
style={{
|
||||
paddingLeft: `${15 * nestingLevel}px`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<div>{fieldName}</div>
|
||||
</td>
|
||||
<td>{fieldType}</td>
|
||||
<td
|
||||
className="text-lg-end"
|
||||
onClick={() => setExpanded((current) => !current)}
|
||||
>
|
||||
<div className="c-pointer">
|
||||
{expanded ? (
|
||||
<>
|
||||
<span className="text-info me-2">Collapse</span>
|
||||
<span className="fe fe-chevron-up" />
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<span className="text-info me-2">Expand</span>
|
||||
<span className="fe fe-chevron-down" />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{expanded && <>{children}</>}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function typeDisplayName(
|
||||
type:
|
||||
| IdlType
|
||||
| {
|
||||
enum: string;
|
||||
}
|
||||
): string {
|
||||
switch (type) {
|
||||
case "bool":
|
||||
case "u8":
|
||||
case "i8":
|
||||
case "u16":
|
||||
case "i16":
|
||||
case "u32":
|
||||
case "i32":
|
||||
case "f32":
|
||||
case "u64":
|
||||
case "i64":
|
||||
case "f64":
|
||||
case "u128":
|
||||
case "i128":
|
||||
case "bytes":
|
||||
case "string":
|
||||
return type.toString();
|
||||
case "publicKey":
|
||||
return "PublicKey";
|
||||
default:
|
||||
if ("enum" in type) return `${type.enum} (enum)`;
|
||||
if ("defined" in type) return type.defined;
|
||||
if ("option" in type) return `${typeDisplayName(type.option)} (optional)`;
|
||||
if ("vec" in type) return `${typeDisplayName(type.vec)}[]`;
|
||||
if ("array" in type)
|
||||
return `${typeDisplayName(type.array[0])}[${type.array[1]}]`;
|
||||
return "unkonwn";
|
||||
}
|
||||
}
|
||||
|
@ -56,6 +56,10 @@ export function lamportsToSolString(
|
||||
return new Intl.NumberFormat("en-US", { maximumFractionDigits }).format(sol);
|
||||
}
|
||||
|
||||
export function numberWithSeparator(s: string) {
|
||||
return s.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
|
||||
}
|
||||
|
||||
export function SolBalance({
|
||||
lamports,
|
||||
maximumFractionDigits = 9,
|
||||
@ -126,6 +130,27 @@ export function camelToTitleCase(str: string): string {
|
||||
return result.charAt(0).toUpperCase() + result.slice(1);
|
||||
}
|
||||
|
||||
export function snakeToTitleCase(str: string): string {
|
||||
const result = str.replace(/([-_]\w)/g, (g) => ` ${g[1].toUpperCase()}`);
|
||||
return result.charAt(0).toUpperCase() + result.slice(1);
|
||||
}
|
||||
|
||||
export function snakeToPascal(string: string) {
|
||||
return string
|
||||
.split("/")
|
||||
.map((snake) =>
|
||||
snake
|
||||
.split("_")
|
||||
.map((substr) => substr.charAt(0).toUpperCase() + substr.slice(1))
|
||||
.join("")
|
||||
)
|
||||
.join("/");
|
||||
}
|
||||
|
||||
export function capitalizeFirstLetter(input: string) {
|
||||
return input.charAt(0).toUpperCase() + input.slice(1);
|
||||
}
|
||||
|
||||
export function abbreviatedNumber(value: number, fixed = 1) {
|
||||
if (value < 1e3) return value;
|
||||
if (value >= 1e3 && value < 1e6) return +(value / 1e3).toFixed(fixed) + "K";
|
||||
|
@ -1,25 +1,27 @@
|
||||
import { PublicKey, Connection } from "@solana/web3.js";
|
||||
import {
|
||||
getFilteredProgramAccounts,
|
||||
getHashedName,
|
||||
getNameAccountKey,
|
||||
NameRegistryState,
|
||||
getFilteredProgramAccounts,
|
||||
getNameOwner,
|
||||
NAME_PROGRAM_ID,
|
||||
performReverseLookup,
|
||||
} from "@bonfida/spl-name-service";
|
||||
import BN from "bn.js";
|
||||
import { useState, useEffect } from "react";
|
||||
import { Cluster, useCluster } from "providers/cluster";
|
||||
|
||||
// Name auctionning Program ID
|
||||
export const PROGRAM_ID = new PublicKey(
|
||||
"jCebN34bUfdeUYJT13J1yG16XWQpt5PDx6Mse9GUqhR"
|
||||
// Address of the SOL TLD
|
||||
const SOL_TLD_AUTHORITY = new PublicKey(
|
||||
"58PwtjSDuFHuUkYjH9BYnnQKHfwo9reZhC2zMJv9JPkx"
|
||||
);
|
||||
|
||||
export interface DomainInfo {
|
||||
name: string;
|
||||
address: PublicKey;
|
||||
class: PublicKey;
|
||||
}
|
||||
export const hasDomainSyntax = (value: string) => {
|
||||
return value.length > 4 && value.substring(value.length - 4) === ".sol";
|
||||
};
|
||||
|
||||
async function getDomainKey(
|
||||
name: string,
|
||||
@ -35,15 +37,43 @@ async function getDomainKey(
|
||||
return nameKey;
|
||||
}
|
||||
|
||||
export async function findOwnedNameAccountsForUser(
|
||||
// returns non empty wallet string if a given .sol domain is owned by a wallet
|
||||
export async function getDomainInfo(domain: string, connection: Connection) {
|
||||
const domainKey = await getDomainKey(
|
||||
domain.slice(0, -4), // remove .sol
|
||||
undefined,
|
||||
SOL_TLD_AUTHORITY
|
||||
);
|
||||
try {
|
||||
const registry = await getNameOwner(connection, domainKey);
|
||||
return registry && registry.registry.owner
|
||||
? {
|
||||
owner: registry.registry.owner.toString(),
|
||||
address: domainKey.toString(),
|
||||
}
|
||||
: null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function getUserDomainAddresses(
|
||||
connection: Connection,
|
||||
userAccount: PublicKey
|
||||
userAddress: PublicKey
|
||||
): Promise<PublicKey[]> {
|
||||
const filters = [
|
||||
// parent
|
||||
{
|
||||
memcmp: {
|
||||
offset: 0,
|
||||
bytes: SOL_TLD_AUTHORITY.toBase58(),
|
||||
},
|
||||
},
|
||||
// owner
|
||||
{
|
||||
memcmp: {
|
||||
offset: 32,
|
||||
bytes: userAccount.toBase58(),
|
||||
bytes: userAddress.toBase58(),
|
||||
},
|
||||
},
|
||||
];
|
||||
@ -55,41 +85,8 @@ export async function findOwnedNameAccountsForUser(
|
||||
return accounts.map((a) => a.publicKey);
|
||||
}
|
||||
|
||||
export async function performReverseLookup(
|
||||
connection: Connection,
|
||||
nameAccounts: PublicKey[]
|
||||
): Promise<DomainInfo[]> {
|
||||
let [centralState] = await PublicKey.findProgramAddress(
|
||||
[PROGRAM_ID.toBuffer()],
|
||||
PROGRAM_ID
|
||||
);
|
||||
|
||||
const reverseLookupAccounts = await Promise.all(
|
||||
nameAccounts.map((name) => getDomainKey(name.toBase58(), centralState))
|
||||
);
|
||||
|
||||
let names = await NameRegistryState.retrieveBatch(
|
||||
connection,
|
||||
reverseLookupAccounts
|
||||
);
|
||||
|
||||
return names
|
||||
.map((name) => {
|
||||
if (!name?.data) {
|
||||
return undefined;
|
||||
}
|
||||
const nameLength = new BN(name!.data.slice(0, 4), "le").toNumber();
|
||||
return {
|
||||
name: name.data.slice(4, 4 + nameLength).toString() + ".sol",
|
||||
address: name.address,
|
||||
class: name.class,
|
||||
};
|
||||
})
|
||||
.filter((e) => !!e) as DomainInfo[];
|
||||
}
|
||||
|
||||
export const useUserDomains = (
|
||||
address: PublicKey
|
||||
userAddress: PublicKey
|
||||
): [DomainInfo[] | null, boolean] => {
|
||||
const { url, cluster } = useCluster();
|
||||
const [result, setResult] = useState<DomainInfo[] | null>(null);
|
||||
@ -102,12 +99,21 @@ export const useUserDomains = (
|
||||
const connection = new Connection(url, "confirmed");
|
||||
try {
|
||||
setLoading(true);
|
||||
const domains = await findOwnedNameAccountsForUser(connection, address);
|
||||
let names = await performReverseLookup(connection, domains);
|
||||
names.sort((a, b) => {
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
setResult(names);
|
||||
const userDomainAddresses = await getUserDomainAddresses(
|
||||
connection,
|
||||
userAddress
|
||||
);
|
||||
const userDomains = await Promise.all(
|
||||
userDomainAddresses.map(async (address) => {
|
||||
const domainName = await performReverseLookup(connection, address);
|
||||
return {
|
||||
name: `${domainName}.sol`,
|
||||
address,
|
||||
};
|
||||
})
|
||||
);
|
||||
userDomains.sort((a, b) => a.name.localeCompare(b.name));
|
||||
setResult(userDomains);
|
||||
} catch (err) {
|
||||
console.log(`Error fetching user domains ${err}`);
|
||||
} finally {
|
||||
@ -115,7 +121,7 @@ export const useUserDomains = (
|
||||
}
|
||||
};
|
||||
resolve();
|
||||
}, [address, url]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
}, [userAddress, url]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return [result, loading];
|
||||
};
|
||||
|
@ -107,6 +107,10 @@ pub enum GeyserPluginError {
|
||||
/// Any custom error defined by the plugin.
|
||||
#[error("Plugin-defined custom error. Error message: ({0})")]
|
||||
Custom(Box<dyn error::Error + Send + Sync>),
|
||||
|
||||
/// Error when updating the transaction.
|
||||
#[error("Error updating transaction. Error message: ({msg})")]
|
||||
TransactionUpdateError { msg: String },
|
||||
}
|
||||
|
||||
/// The current status of a slot
|
||||
|
@ -18,7 +18,7 @@ flate2 = "1.0"
|
||||
indexmap = { version = "1.8", features = ["rayon"] }
|
||||
itertools = "0.10.3"
|
||||
log = "0.4.14"
|
||||
lru = "0.7.3"
|
||||
lru = "0.7.5"
|
||||
matches = "0.1.9"
|
||||
num-traits = "0.2"
|
||||
rand = "0.7.0"
|
||||
|
@ -536,14 +536,7 @@ pub(crate) fn submit_gossip_stats(
|
||||
.pull
|
||||
.votes
|
||||
.into_iter()
|
||||
.map(|(slot, num_votes)| (slot, num_votes))
|
||||
.chain(
|
||||
crds_stats
|
||||
.push
|
||||
.votes
|
||||
.into_iter()
|
||||
.map(|(slot, num_votes)| (slot, num_votes)),
|
||||
)
|
||||
.chain(crds_stats.push.votes.into_iter())
|
||||
.into_grouping_map()
|
||||
.aggregate(|acc, _slot, num_votes| Some(acc.unwrap_or_default() + num_votes));
|
||||
submit_vote_stats("cluster_info_crds_stats_votes", &votes);
|
||||
|
@ -23,7 +23,7 @@ indicatif = "0.16.2"
|
||||
lazy_static = "1.4.0"
|
||||
nix = "0.23.1"
|
||||
reqwest = { version = "0.11.10", default-features = false, features = ["blocking", "rustls-tls", "json"] }
|
||||
semver = "1.0.6"
|
||||
semver = "1.0.7"
|
||||
serde = { version = "1.0.136", features = ["derive"] }
|
||||
serde_yaml = "0.8.23"
|
||||
solana-clap-utils = { path = "../clap-utils", version = "=1.11.0" }
|
||||
|
@ -16,6 +16,7 @@ use {
|
||||
},
|
||||
solana_ledger::{blockstore::Blockstore, blockstore_db::AccessType},
|
||||
solana_sdk::{clock::Slot, pubkey::Pubkey, signature::Signature},
|
||||
solana_storage_bigtable::CredentialType,
|
||||
solana_transaction_status::{
|
||||
BlockEncodingOptions, ConfirmedBlock, EncodeError, TransactionDetails,
|
||||
UiTransactionEncoding,
|
||||
@ -642,16 +643,18 @@ pub fn bigtable_process_command(ledger_path: &Path, matches: &ArgMatches<'_>) {
|
||||
instance_name,
|
||||
..solana_storage_bigtable::LedgerStorageConfig::default()
|
||||
};
|
||||
|
||||
let credential_path = Some(value_t_or_exit!(
|
||||
arg_matches,
|
||||
"reference_credential",
|
||||
String
|
||||
));
|
||||
|
||||
let ref_instance_name =
|
||||
value_t_or_exit!(arg_matches, "reference_instance_name", String);
|
||||
let ref_config = solana_storage_bigtable::LedgerStorageConfig {
|
||||
read_only: false,
|
||||
credential_path,
|
||||
credential_type: CredentialType::Filepath(credential_path),
|
||||
instance_name: ref_instance_name,
|
||||
..solana_storage_bigtable::LedgerStorageConfig::default()
|
||||
};
|
||||
|
@ -32,13 +32,14 @@ use {
|
||||
},
|
||||
solana_measure::measure::Measure,
|
||||
solana_runtime::{
|
||||
accounts_db::AccountsDbConfig,
|
||||
accounts_index::{AccountsIndexConfig, ScanConfig},
|
||||
accounts_db::{AccountsDbConfig, FillerAccountsConfig},
|
||||
accounts_index::{AccountsIndexConfig, IndexLimitMb, ScanConfig},
|
||||
bank::{Bank, RewardCalculationEvent},
|
||||
bank_forks::BankForks,
|
||||
cost_model::CostModel,
|
||||
cost_tracker::CostTracker,
|
||||
hardened_unpack::{open_genesis_config, MAX_GENESIS_ARCHIVE_UNPACKED_SIZE},
|
||||
runtime_config::RuntimeConfig,
|
||||
snapshot_archive_info::SnapshotArchiveInfoGetter,
|
||||
snapshot_config::SnapshotConfig,
|
||||
snapshot_hash::StartingSnapshotHashes,
|
||||
@ -822,7 +823,7 @@ fn compute_slot_cost(blockstore: &Blockstore, slot: Slot) -> Result<(), String>
|
||||
num_programs += transaction.message().instructions().len();
|
||||
|
||||
let tx_cost = cost_model.calculate_cost(&transaction);
|
||||
let result = cost_tracker.try_add(&transaction, &tx_cost);
|
||||
let result = cost_tracker.try_add(&tx_cost);
|
||||
if result.is_err() {
|
||||
println!(
|
||||
"Slot: {}, CostModel rejected transaction {:?}, reason {:?}",
|
||||
@ -880,7 +881,7 @@ fn main() {
|
||||
|
||||
let starting_slot_arg = Arg::with_name("starting_slot")
|
||||
.long("starting-slot")
|
||||
.value_name("NUM")
|
||||
.value_name("SLOT")
|
||||
.takes_value(true)
|
||||
.default_value("0")
|
||||
.help("Start at this slot");
|
||||
@ -928,7 +929,16 @@ fn main() {
|
||||
.value_name("COUNT")
|
||||
.validator(is_parsable::<usize>)
|
||||
.takes_value(true)
|
||||
.default_value("0")
|
||||
.help("How many accounts to add to stress the system. Accounts are ignored in operations related to correctness.");
|
||||
let accounts_filler_size = Arg::with_name("accounts_filler_size")
|
||||
.long("accounts-filler-size")
|
||||
.value_name("BYTES")
|
||||
.validator(is_parsable::<usize>)
|
||||
.takes_value(true)
|
||||
.default_value("0")
|
||||
.requires("accounts_filler_count")
|
||||
.help("Size per filler account in bytes.");
|
||||
let account_paths_arg = Arg::with_name("account_paths")
|
||||
.long("accounts")
|
||||
.value_name("PATHS")
|
||||
@ -1119,7 +1129,7 @@ fn main() {
|
||||
.arg(
|
||||
Arg::with_name("target_db")
|
||||
.long("target-db")
|
||||
.value_name("PATH")
|
||||
.value_name("DIR")
|
||||
.takes_value(true)
|
||||
.help("Target db"),
|
||||
)
|
||||
@ -1283,6 +1293,7 @@ fn main() {
|
||||
.arg(&disable_disk_index)
|
||||
.arg(&accountsdb_skip_shrink)
|
||||
.arg(&accounts_filler_count)
|
||||
.arg(&accounts_filler_size)
|
||||
.arg(&verify_index_arg)
|
||||
.arg(&hard_forks_arg)
|
||||
.arg(&no_accounts_db_caching_arg)
|
||||
@ -1775,9 +1786,12 @@ fn main() {
|
||||
}
|
||||
("shred-version", Some(arg_matches)) => {
|
||||
let process_options = ProcessOptions {
|
||||
dev_halt_at_slot: Some(0),
|
||||
new_hard_forks: hardforks_of(arg_matches, "hard_forks"),
|
||||
poh_verify: false,
|
||||
runtime_config: RuntimeConfig {
|
||||
dev_halt_at_slot: Some(0),
|
||||
..RuntimeConfig::default()
|
||||
},
|
||||
..ProcessOptions::default()
|
||||
};
|
||||
let genesis_config = open_genesis_config_by(&ledger_path, arg_matches);
|
||||
@ -1852,9 +1866,12 @@ fn main() {
|
||||
}
|
||||
("bank-hash", Some(arg_matches)) => {
|
||||
let process_options = ProcessOptions {
|
||||
dev_halt_at_slot: Some(0),
|
||||
new_hard_forks: hardforks_of(arg_matches, "hard_forks"),
|
||||
poh_verify: false,
|
||||
runtime_config: RuntimeConfig {
|
||||
dev_halt_at_slot: Some(0),
|
||||
..RuntimeConfig::default()
|
||||
},
|
||||
..ProcessOptions::default()
|
||||
};
|
||||
let genesis_config = open_genesis_config_by(&ledger_path, arg_matches);
|
||||
@ -2042,13 +2059,15 @@ fn main() {
|
||||
let system_monitor_service =
|
||||
SystemMonitorService::new(Arc::clone(&exit_signal), true, false);
|
||||
|
||||
if let Some(limit) =
|
||||
accounts_index_config.index_limit_mb = if let Some(limit) =
|
||||
value_t!(arg_matches, "accounts_index_memory_limit_mb", usize).ok()
|
||||
{
|
||||
accounts_index_config.index_limit_mb = Some(limit);
|
||||
IndexLimitMb::Limit(limit)
|
||||
} else if arg_matches.is_present("disable_accounts_disk_index") {
|
||||
accounts_index_config.index_limit_mb = None;
|
||||
}
|
||||
IndexLimitMb::InMemOnly
|
||||
} else {
|
||||
IndexLimitMb::Unspecified
|
||||
};
|
||||
|
||||
{
|
||||
let mut accounts_index_paths: Vec<PathBuf> =
|
||||
@ -2066,21 +2085,21 @@ fn main() {
|
||||
accounts_index_config.drives = Some(accounts_index_paths);
|
||||
}
|
||||
|
||||
let filler_account_count =
|
||||
value_t!(arg_matches, "accounts_filler_count", usize).ok();
|
||||
let filler_accounts_config = FillerAccountsConfig {
|
||||
count: value_t_or_exit!(arg_matches, "accounts_filler_count", usize),
|
||||
size: value_t_or_exit!(arg_matches, "accounts_filler_size", usize),
|
||||
};
|
||||
|
||||
let accounts_db_config = Some(AccountsDbConfig {
|
||||
index: Some(accounts_index_config),
|
||||
accounts_hash_cache_path: Some(ledger_path.clone()),
|
||||
filler_account_count,
|
||||
filler_accounts_config,
|
||||
..AccountsDbConfig::default()
|
||||
});
|
||||
|
||||
let process_options = ProcessOptions {
|
||||
dev_halt_at_slot: value_t!(arg_matches, "halt_at_slot", Slot).ok(),
|
||||
new_hard_forks: hardforks_of(arg_matches, "hard_forks"),
|
||||
poh_verify: !arg_matches.is_present("skip_poh_verify"),
|
||||
bpf_jit: !matches.is_present("no_bpf_jit"),
|
||||
accounts_db_caching_enabled: !arg_matches.is_present("no_accounts_db_caching"),
|
||||
limit_load_slot_count_from_snapshot: value_t!(
|
||||
arg_matches,
|
||||
@ -2094,6 +2113,11 @@ fn main() {
|
||||
accounts_db_test_hash_calculation: arg_matches
|
||||
.is_present("accounts_db_test_hash_calculation"),
|
||||
accounts_db_skip_shrink: arg_matches.is_present("accounts_db_skip_shrink"),
|
||||
runtime_config: RuntimeConfig {
|
||||
dev_halt_at_slot: value_t!(arg_matches, "halt_at_slot", Slot).ok(),
|
||||
bpf_jit: !matches.is_present("no_bpf_jit"),
|
||||
..RuntimeConfig::default()
|
||||
},
|
||||
..ProcessOptions::default()
|
||||
};
|
||||
let print_accounts_stats = arg_matches.is_present("print_accounts_stats");
|
||||
@ -2130,9 +2154,12 @@ fn main() {
|
||||
let output_file = value_t_or_exit!(arg_matches, "graph_filename", String);
|
||||
|
||||
let process_options = ProcessOptions {
|
||||
dev_halt_at_slot: value_t!(arg_matches, "halt_at_slot", Slot).ok(),
|
||||
new_hard_forks: hardforks_of(arg_matches, "hard_forks"),
|
||||
poh_verify: false,
|
||||
runtime_config: RuntimeConfig {
|
||||
dev_halt_at_slot: value_t!(arg_matches, "halt_at_slot", Slot).ok(),
|
||||
..RuntimeConfig::default()
|
||||
},
|
||||
..ProcessOptions::default()
|
||||
};
|
||||
|
||||
@ -2256,9 +2283,12 @@ fn main() {
|
||||
&genesis_config,
|
||||
&blockstore,
|
||||
ProcessOptions {
|
||||
dev_halt_at_slot: Some(snapshot_slot),
|
||||
new_hard_forks,
|
||||
poh_verify: false,
|
||||
runtime_config: RuntimeConfig {
|
||||
dev_halt_at_slot: Some(snapshot_slot),
|
||||
..RuntimeConfig::default()
|
||||
},
|
||||
..ProcessOptions::default()
|
||||
},
|
||||
snapshot_archive_path,
|
||||
@ -2553,9 +2583,12 @@ fn main() {
|
||||
("accounts", Some(arg_matches)) => {
|
||||
let dev_halt_at_slot = value_t!(arg_matches, "halt_at_slot", Slot).ok();
|
||||
let process_options = ProcessOptions {
|
||||
dev_halt_at_slot,
|
||||
new_hard_forks: hardforks_of(arg_matches, "hard_forks"),
|
||||
poh_verify: false,
|
||||
runtime_config: RuntimeConfig {
|
||||
dev_halt_at_slot,
|
||||
..RuntimeConfig::default()
|
||||
},
|
||||
..ProcessOptions::default()
|
||||
};
|
||||
let genesis_config = open_genesis_config_by(&ledger_path, arg_matches);
|
||||
@ -2616,9 +2649,12 @@ fn main() {
|
||||
("capitalization", Some(arg_matches)) => {
|
||||
let dev_halt_at_slot = value_t!(arg_matches, "halt_at_slot", Slot).ok();
|
||||
let process_options = ProcessOptions {
|
||||
dev_halt_at_slot,
|
||||
new_hard_forks: hardforks_of(arg_matches, "hard_forks"),
|
||||
poh_verify: false,
|
||||
runtime_config: RuntimeConfig {
|
||||
dev_halt_at_slot,
|
||||
..RuntimeConfig::default()
|
||||
},
|
||||
..ProcessOptions::default()
|
||||
};
|
||||
let genesis_config = open_genesis_config_by(&ledger_path, arg_matches);
|
||||
|
@ -22,11 +22,11 @@ itertools = "0.10.3"
|
||||
lazy_static = "1.4.0"
|
||||
libc = "0.2.120"
|
||||
log = { version = "0.4.14" }
|
||||
lru = "0.7.3"
|
||||
lru = "0.7.5"
|
||||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
num_cpus = "1.13.1"
|
||||
prost = "0.9.0"
|
||||
prost = "0.10.0"
|
||||
rand = "0.7.0"
|
||||
rand_chacha = "0.2.2"
|
||||
rayon = "1.5.1"
|
||||
|
@ -125,13 +125,12 @@ pub fn load_bank_forks(
|
||||
accounts_update_notifier,
|
||||
)
|
||||
} else {
|
||||
if process_options
|
||||
let maybe_filler_accounts = process_options
|
||||
.accounts_db_config
|
||||
.as_ref()
|
||||
.and_then(|config| config.filler_account_count)
|
||||
.unwrap_or_default()
|
||||
> 0
|
||||
{
|
||||
.map(|config| config.filler_accounts_config.count > 0);
|
||||
|
||||
if let Some(true) = maybe_filler_accounts {
|
||||
panic!("filler accounts specified, but not loading from snapshot");
|
||||
}
|
||||
|
||||
@ -212,14 +211,16 @@ fn bank_forks_from_snapshot(
|
||||
process::exit(1);
|
||||
}
|
||||
|
||||
let (deserialized_bank, full_snapshot_archive_info, incremental_snapshot_archive_info) =
|
||||
let (mut deserialized_bank, full_snapshot_archive_info, incremental_snapshot_archive_info) =
|
||||
snapshot_utils::bank_from_latest_snapshot_archives(
|
||||
&snapshot_config.bank_snapshots_dir,
|
||||
&snapshot_config.snapshot_archives_dir,
|
||||
&account_paths,
|
||||
genesis_config,
|
||||
process_options.debug_keys.clone(),
|
||||
Some(&crate::builtins::get(process_options.bpf_jit)),
|
||||
Some(&crate::builtins::get(
|
||||
process_options.runtime_config.bpf_jit,
|
||||
)),
|
||||
process_options.account_indexes.clone(),
|
||||
process_options.accounts_db_caching_enabled,
|
||||
process_options.limit_load_slot_count_from_snapshot,
|
||||
@ -236,6 +237,8 @@ fn bank_forks_from_snapshot(
|
||||
deserialized_bank.set_shrink_paths(shrink_paths);
|
||||
}
|
||||
|
||||
deserialized_bank.set_compute_budget(process_options.runtime_config.compute_budget);
|
||||
|
||||
let full_snapshot_hash = FullSnapshotHash {
|
||||
hash: (
|
||||
full_snapshot_archive_info.slot(),
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user