tempo_sidecar/synthetic_load/
mod.rs1use alloy::{
2 network::EthereumWallet,
3 primitives::{
4 Address, U256,
5 private::{
6 rand,
7 rand::{RngCore, SeedableRng, rngs::StdRng},
8 },
9 },
10 providers::ProviderBuilder,
11 signers::local::MnemonicBuilder,
12};
13use eyre::Context;
14use rand_distr::{Distribution, Exp, Zipf};
15use reqwest::Url;
16use tempo_precompiles::{TIP_FEE_MANAGER_ADDRESS, tip_fee_manager::IFeeManager, tip20::ITIP20};
17use tempo_telemetry_util::error_field;
18use tracing::{debug, info, warn};
19
20pub struct SyntheticLoadGenerator {
21 mnemonic: String,
22 rpc_url: Url,
23 wallet_count: usize,
24 average_tps: usize,
25 fee_token_addresses: Vec<Address>,
26 seed: Option<u64>,
27}
28
29impl SyntheticLoadGenerator {
30 pub fn new(
31 mnemonic: String,
32 rpc_url: Url,
33 wallet_count: usize,
34 average_tps: usize,
35 fee_token_addresses: Vec<Address>,
36 seed: Option<u64>,
37 ) -> Self {
38 Self {
39 rpc_url,
40 wallet_count,
41 average_tps,
42 fee_token_addresses,
43 mnemonic,
44 seed,
45 }
46 }
47
48 pub async fn worker(&self) -> eyre::Result<()> {
49 info!("starting synthetic load generator");
50
51 let mut rng = match self.seed {
52 Some(seed) => StdRng::seed_from_u64(seed),
53 None => StdRng::seed_from_u64(rand::rng().next_u64()),
54 };
55
56 let mut wallet = EthereumWallet::default();
57 let mut addresses = Vec::new();
58 for index in 0..self.wallet_count {
59 let signer = MnemonicBuilder::from_phrase_nth(&self.mnemonic, index as u32);
60 addresses.push(signer.address());
61 wallet.register_signer(signer);
62 }
63
64 let provider = ProviderBuilder::new()
65 .wallet(wallet.clone())
66 .connect_http(self.rpc_url.clone());
67
68 let fee_token_zipf = Zipf::new(self.fee_token_addresses.len() as f64, 1.4)?;
69
70 info!("setting fee tokens for load generating wallets");
71
72 for address in &addresses {
73 let fee_token_address =
74 zipf_vec_sample(&mut rng, fee_token_zipf, &self.fee_token_addresses)?;
75 let fee_manager = IFeeManager::new(TIP_FEE_MANAGER_ADDRESS, provider.clone());
76 _ = fee_manager
77 .setUserToken(*fee_token_address)
78 .from(*address)
79 .send()
80 .await
81 .wrap_err_with(|| {
82 format!("failed to set fee token {address} for address {fee_token_address}",)
83 })?;
84 }
85
86 let exp = Exp::new(self.average_tps as f64)?;
87 let zipf = Zipf::new(self.wallet_count as f64, 1.4)?;
88
89 loop {
90 let sender = zipf_vec_sample(&mut rng, zipf, &addresses)?;
91 let recipient = zipf_vec_sample(&mut rng, zipf, &addresses)?;
92 let token = zipf_vec_sample(&mut rng, fee_token_zipf, &self.fee_token_addresses)?;
93
94 info!(
95 %sender,
96 %recipient,
97 "sending tip20 tokens"
98 );
99
100 let token = ITIP20::new(*token, provider.clone());
101 if let Err(e) = token
102 .transfer(*recipient, U256::from(10))
103 .from(*sender)
104 .send()
105 .await
106 {
107 warn!(
108 %sender,
109 %recipient,
110 err = error_field(&e),
111 "failed to transfer tip20 token"
112 );
113 }
114
115 let delay = exp.sample(&mut rng);
116
117 debug!(%delay, "sleeping until next round");
118
119 tokio::time::sleep(std::time::Duration::from_secs_f64(delay)).await;
120 }
121 }
122}
123
124fn zipf_vec_sample<'a, T>(
125 rng: &mut StdRng,
126 zipf: Zipf<f64>,
127 items: &'a [T],
128) -> eyre::Result<&'a T> {
129 let index = zipf.sample(rng) as u32 - 1;
130 items
131 .get(index as usize)
132 .ok_or_else(|| eyre::eyre!("zipf out of bounds"))
133}