Skip to main content

tempo_sidecar/synthetic_load/
mod.rs

1use 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}