Skip to main content

tempo_bench/cmd/
max_tps.rs

1mod dex;
2mod erc20;
3mod mpp;
4mod virtual_address;
5
6use alloy_consensus::Transaction;
7use itertools::Itertools;
8use reth_tracing::{
9    RethTracer, Tracer,
10    tracing::{debug, error, info},
11};
12use tempo_alloy::{
13    TempoNetwork, fillers::ExpiringNonceFiller, provider::ext::TempoProviderBuilderExt,
14};
15
16use alloy::{
17    consensus::BlockHeader,
18    eips::Encodable2718,
19    network::{
20        EthereumWallet, NetworkTransactionBuilder, ReceiptResponse, TransactionBuilder,
21        TxSignerSync,
22    },
23    primitives::{Address, B256, BlockNumber, U256},
24    providers::{
25        DynProvider, PendingTransactionBuilder, PendingTransactionError, Provider, ProviderBuilder,
26        SendableTx, WatchTxError, fillers::TxFiller,
27    },
28    rpc::client::NoParams,
29    signers::local::{
30        Secp256k1Signer,
31        coins_bip39::{English, Mnemonic, MnemonicError},
32    },
33    sol_types::SolEvent,
34    transports::http::reqwest::Url,
35};
36use clap::Parser;
37use eyre::{Context, OptionExt};
38use futures::{
39    FutureExt, Stream, StreamExt, TryStreamExt,
40    future::BoxFuture,
41    stream::{self},
42};
43use governor::{Quota, RateLimiter, state::StreamRateLimitExt};
44use indicatif::{ProgressBar, ProgressIterator};
45use rand::{random_range, seq::IndexedRandom};
46use rlimit::Resource;
47use serde::Serialize;
48use std::{
49    collections::VecDeque,
50    fs::File,
51    io::BufWriter,
52    num::{NonZeroU32, NonZeroU64},
53    str::FromStr,
54    sync::{
55        Arc, OnceLock,
56        atomic::{AtomicUsize, Ordering},
57    },
58    time::Duration,
59};
60use tempo_contracts::precompiles::{
61    ADDRESS_REGISTRY_ADDRESS,
62    IAddressRegistry::{self, IAddressRegistryInstance},
63    IFeeManager::IFeeManagerInstance,
64    IRolesAuth,
65    IStablecoinDEX::IStablecoinDEXInstance,
66    ITIP20::{self, ITIP20Instance},
67    ITIP20Factory, STABLECOIN_DEX_ADDRESS, TIP20_FACTORY_ADDRESS,
68};
69use tempo_precompiles::{
70    TIP_FEE_MANAGER_ADDRESS,
71    address_registry::MasterId,
72    stablecoin_dex::{MAX_TICK, MIN_ORDER_AMOUNT, MIN_TICK, TICK_SPACING},
73    tip_fee_manager::DEFAULT_FEE_TOKEN,
74    tip20::ISSUER_ROLE,
75};
76use tokio::{
77    select,
78    time::{interval, sleep},
79};
80use tokio_util::sync::CancellationToken;
81
82use crate::cmd::signer_providers::SignerProviderManager;
83
84use self::virtual_address::{ANVIL_VIRTUAL_SALTS, make_virtual_address};
85
86/// Run maximum TPS throughput benchmarking
87#[derive(Parser, Debug)]
88pub struct MaxTpsArgs {
89    /// Target transactions per second
90    #[arg(short, long)]
91    tps: u64,
92
93    /// Test duration in seconds
94    #[arg(short, long, default_value_t = 30)]
95    duration: u64,
96
97    /// Number of accounts for pre-generation
98    #[arg(short, long, default_value_t = NonZeroU64::new(100).unwrap())]
99    accounts: NonZeroU64,
100
101    /// Mnemonic for generating accounts
102    #[arg(short, long, default_value = "random")]
103    mnemonic: MnemonicArg,
104
105    #[arg(short, long, default_value_t = 0)]
106    from_mnemonic_index: u32,
107
108    #[arg(long, default_value_t = DEFAULT_FEE_TOKEN)]
109    fee_token: Address,
110
111    /// Target URLs for network connections (comma-separated or multiple --target-urls)
112    #[arg(long, value_delimiter = ',', action = clap::ArgAction::Append, default_values_t = vec!["http://localhost:8545".parse::<Url>().unwrap()])]
113    target_urls: Vec<Url>,
114
115    /// A limit of the maximum number of concurrent requests, prevents issues with too many
116    /// connections open at once.
117    #[arg(long, default_value_t = 100)]
118    max_concurrent_requests: usize,
119
120    /// A number of transaction to send, before waiting for their receipts, that should be likely
121    /// safe.
122    ///
123    /// Large amount of transactions in a block will result in system transaction OutOfGas error.
124    #[arg(long, default_value_t = 10000)]
125    max_concurrent_transactions: usize,
126
127    /// File descriptor limit to set
128    #[arg(long)]
129    fd_limit: Option<u64>,
130
131    /// Node commit SHA for metadata
132    #[arg(long)]
133    node_commit_sha: Option<String>,
134
135    /// Build profile for metadata (e.g., "release", "debug", "maxperf")
136    #[arg(long)]
137    build_profile: Option<String>,
138
139    /// Benchmark mode for metadata (e.g., "max_tps", "stress_test")
140    #[arg(long)]
141    benchmark_mode: Option<String>,
142
143    /// A weight that determines the likelihood of generating a TIP-20 transfer transaction.
144    #[arg(long, default_value_t = 1.0)]
145    tip20_weight: f64,
146
147    /// A weight that determines the likelihood of generating a TIP-20 transfer
148    /// to a virtual address. Treated as a separate transaction type alongside
149    /// `--tip20-weight`, not a fraction of it.
150    ///
151    /// Requires the anvil mnemonic (`-m "test test test ... junk"`) with
152    /// `--from-mnemonic-index` covering indices 0–5, since virtual-address
153    /// registration needs pre-mined TIP-1022 PoW salts that are only embedded
154    /// for the first 6 anvil accounts.
155    #[arg(long, default_value_t = 0.0)]
156    tip20_virtual_weight: f64,
157
158    /// A weight that determines the likelihood of generating a DEX place transaction.
159    #[arg(long, default_value_t = 0.0)]
160    place_order_weight: f64,
161
162    /// A weight that determines the likelihood of generating a DEX swapExactAmountIn transaction.
163    #[arg(long, default_value_t = 0.0)]
164    swap_weight: f64,
165
166    /// A weight that determines the likelihood of generating an ERC-20 transfer transaction.
167    #[arg(long, default_value_t = 0.0)]
168    erc20_weight: f64,
169
170    /// A weight that determines the likelihood of generating an MPP channel open+close
171    /// transaction. Requires `--mpp-contract-address`.
172    #[arg(long, default_value_t = 0.0)]
173    mpp_weight: f64,
174
175    /// Address of a deployed TempoStreamChannel contract. Required when `--mpp-weight` > 0.
176    #[arg(long, default_value = "0x33b901018174ddabe4841042ab76ba85d4e24f25")]
177    mpp_contract_address: Option<Address>,
178
179    /// Send transfers to existing signer accounts instead of random new addresses.
180    ///
181    /// When enabled, TIP-20 and ERC-20 transfers are sent to the bench's own signer addresses
182    /// (which already exist on-chain), avoiding cold SSTORE for account creation. This tests
183    /// pure transfer throughput without state growth.
184    #[arg(long, default_value_t = true, default_missing_value = "true", num_args = 0..=1, action = clap::ArgAction::Set)]
185    existing_recipients: bool,
186
187    /// An amount of receipts to wait for after sending all the transactions.
188    #[arg(long, default_value_t = 100)]
189    sample_size: usize,
190
191    /// Fund accounts from the faucet before running the benchmark.
192    ///
193    /// Calls tempo_fundAddress for each account.
194    #[arg(long)]
195    faucet: bool,
196
197    /// URL for the faucet service. If not provided, uses the first target URL.
198    #[arg(long)]
199    faucet_url: Option<Url>,
200
201    /// Clear the transaction pool before running the benchmark.
202    ///
203    /// Calls admin_clearTxpool.
204    #[arg(long)]
205    clear_txpool: bool,
206
207    /// Use 2D nonces instead of expiring nonces.
208    ///
209    /// By default, tempo-bench uses expiring nonces ([TIP-1009]) which use a circular buffer
210    /// for replay protection, avoiding state bloat. Use this flag to switch to 2D nonces
211    /// which store nonce state per (address, nonce_key) pair.
212    ///
213    /// [TIP-1009]: <https://docs.tempo.xyz/protocol/tips/tip-1009>
214    #[arg(long)]
215    use_2d_nonces: bool,
216
217    /// Use standard sequential nonces instead of expiring nonces.
218    ///
219    /// This disables both expiring nonces and 2D nonces, using traditional sequential
220    /// nonce management.
221    #[arg(long)]
222    use_standard_nonces: bool,
223}
224
225impl MaxTpsArgs {
226    const WEIGHT_PRECISION: f64 = 1000.0;
227
228    pub async fn run(self) -> eyre::Result<()> {
229        RethTracer::new().init()?;
230
231        let accounts = self.accounts.get();
232
233        // Set file descriptor limit if provided
234        if let Some(fd_limit) = self.fd_limit {
235            increase_nofile_limit(fd_limit).context("Failed to increase nofile limit")?;
236        }
237
238        let signer_provider_factory = Box::new(|signer, target_url, cached_nonce_manager| {
239            ProviderBuilder::default()
240                .fetch_chain_id()
241                .with_gas_estimation()
242                .with_nonce_management(cached_nonce_manager)
243                .wallet(EthereumWallet::from(signer))
244                .connect_http(target_url)
245                .erased()
246        });
247
248        if self.use_2d_nonces {
249            info!(
250                accounts = self.accounts,
251                "Creating signers (with 2D nonces)"
252            );
253            let signer_provider_manager = SignerProviderManager::new(
254                self.mnemonic.resolve(),
255                self.from_mnemonic_index,
256                accounts,
257                self.target_urls.clone(),
258                Box::new(|target_url, _cached_nonce_manager| {
259                    ProviderBuilder::new_with_network::<TempoNetwork>()
260                        .with_random_2d_nonces()
261                        .connect_http(target_url)
262                }),
263                signer_provider_factory,
264            );
265            self.run_with_manager(signer_provider_manager).await
266        } else if self.use_standard_nonces {
267            info!(
268                accounts = self.accounts,
269                "Creating signers (with standard nonces)"
270            );
271            let signer_provider_manager = SignerProviderManager::new(
272                self.mnemonic.resolve(),
273                self.from_mnemonic_index,
274                accounts,
275                self.target_urls.clone(),
276                Box::new(|target_url, cached_nonce_manager| {
277                    ProviderBuilder::default()
278                        .fetch_chain_id()
279                        .with_gas_estimation()
280                        .with_nonce_management(cached_nonce_manager)
281                        .connect_http(target_url)
282                }),
283                signer_provider_factory,
284            );
285            self.run_with_manager(signer_provider_manager).await
286        } else {
287            // Default: Use expiring nonces (TIP-1009)
288            // Use the default 25-second expiry window (protocol max is 30s).
289            let expiry_secs = ExpiringNonceFiller::DEFAULT_EXPIRY_SECS;
290            info!(
291                accounts = self.accounts,
292                expiry_secs, "Creating signers (with expiring nonces - TIP-1009)"
293            );
294            let signer_provider_manager = SignerProviderManager::new(
295                self.mnemonic.resolve(),
296                self.from_mnemonic_index,
297                accounts,
298                self.target_urls.clone(),
299                Box::new(move |target_url, _cached_nonce_manager| {
300                    ProviderBuilder::default()
301                        .filler(ExpiringNonceFiller::with_expiry_secs(expiry_secs))
302                        .with_gas_estimation()
303                        .fetch_chain_id()
304                        .connect_http(target_url)
305                }),
306                signer_provider_factory,
307            );
308            self.run_with_manager(signer_provider_manager).await
309        }
310    }
311
312    async fn run_with_manager<F: TxFiller<TempoNetwork> + 'static>(
313        self,
314        signer_provider_manager: SignerProviderManager<F>,
315    ) -> eyre::Result<()> {
316        // Validate required addresses before sending any transactions
317        eyre::ensure!(
318            self.mpp_weight == 0.0 || self.mpp_contract_address.is_some(),
319            "--mpp-contract-address is required when --mpp-weight > 0"
320        );
321
322        let signer_providers = signer_provider_manager.signer_providers();
323
324        if self.clear_txpool {
325            for (target_url, provider) in signer_provider_manager.target_url_providers() {
326                let transactions: u64 = provider
327                    .raw_request("admin_clearTxpool".into(), NoParams::default())
328                    .await
329                    .context(
330                        format!("Failed to clear transaction pool for {target_url}. Is `admin_clearTxpool` RPC method available?"),
331                    )?;
332                info!(%target_url, transactions, "Cleared transaction pool");
333            }
334        }
335
336        // Grab first provider to call some RPC methods
337        let provider = signer_providers[0].1.clone();
338
339        // Fund accounts from faucet if requested
340        if self.faucet {
341            let faucet_provider: DynProvider<TempoNetwork> =
342                if let Some(ref faucet_url) = self.faucet_url {
343                    info!(%faucet_url, "Using custom faucet URL");
344                    ProviderBuilder::new_with_network::<TempoNetwork>()
345                        .connect_http(faucet_url.clone())
346                        .erased()
347                } else {
348                    provider.clone()
349                };
350            fund_accounts(
351                &faucet_provider,
352                &signer_providers
353                    .iter()
354                    .map(|(signer, _)| signer.address())
355                    .collect::<Vec<_>>(),
356                self.max_concurrent_requests,
357                self.max_concurrent_transactions,
358            )
359            .await
360            .context("Failed to fund accounts from faucet")?;
361        }
362
363        info!(fee_token = %self.fee_token, "Setting default fee token");
364        join_all(
365            signer_providers
366                .iter()
367                .map(async |(_, provider)| {
368                    IFeeManagerInstance::new(TIP_FEE_MANAGER_ADDRESS, provider.clone())
369                        .setUserToken(self.fee_token)
370                        .send()
371                        .await
372                })
373                .progress(),
374            self.max_concurrent_requests,
375            self.max_concurrent_transactions,
376        )
377        .await
378        .context("Failed to set default fee token")?;
379
380        // Register virtual-address masters using pre-mined anvil salts.
381        // Only signers with a known salt are registered; each master supports
382        // unlimited virtual addresses via random userTags.
383        let virtual_master_ids = if self.tip20_virtual_weight > 0.0 {
384            let mut master_ids = Vec::new();
385            for (signer, signer_provider) in signer_providers.iter() {
386                let Some(&(_, salt)) = ANVIL_VIRTUAL_SALTS
387                    .iter()
388                    .find(|(a, _)| *a == signer.address())
389                else {
390                    continue;
391                };
392                let registry = IAddressRegistryInstance::new(
393                    ADDRESS_REGISTRY_ADDRESS,
394                    signer_provider.clone(),
395                );
396                let receipt = registry
397                    .registerVirtualMaster(salt)
398                    .send()
399                    .await?
400                    .get_receipt()
401                    .await?;
402                eyre::ensure!(receipt.status(), "registerVirtualMaster failed");
403                let event = receipt
404                    .logs()
405                    .iter()
406                    .filter_map(|log| {
407                        IAddressRegistry::MasterRegistered::decode_log(&log.inner).ok()
408                    })
409                    .next()
410                    .ok_or_eyre("MasterRegistered event not found")?;
411                master_ids.push(event.masterId);
412            }
413            eyre::ensure!(
414                !master_ids.is_empty(),
415                "no signers matched the pre-mined anvil salts — use the anvil mnemonic"
416            );
417            info!(
418                masters = master_ids.len(),
419                weight = self.tip20_virtual_weight,
420                "Registered virtual-address masters"
421            );
422            master_ids
423        } else {
424            Vec::new()
425        };
426
427        // Setup DEX tokens, pairs, and liquidity only if any DEX transaction type has non-zero
428        // weight. Otherwise, use the fee token for TIP-20 transfers directly.
429        let (quote_token, user_tokens) = if self.place_order_weight > 0.0 || self.swap_weight > 0.0
430        {
431            let user_tokens = 2;
432            info!(user_tokens, "Setting up DEX");
433            let (quote_token, user_tokens) = dex::setup(
434                signer_providers,
435                user_tokens,
436                self.max_concurrent_requests,
437                self.max_concurrent_transactions,
438            )
439            .await?;
440            (Some(quote_token), user_tokens)
441        } else {
442            info!(fee_token = %self.fee_token, "Using fee token for TIP-20 transfers");
443            (None, vec![self.fee_token])
444        };
445
446        let erc20_tokens = if self.erc20_weight > 0.0 {
447            let num_erc20_tokens = 1;
448            info!(num_erc20_tokens, "Setting up ERC-20 tokens");
449            erc20::setup(
450                signer_providers,
451                num_erc20_tokens,
452                self.max_concurrent_requests,
453                self.max_concurrent_transactions,
454            )
455            .await?
456        } else {
457            Vec::new()
458        };
459
460        let mpp_contract_address = if self.mpp_weight > 0.0 {
461            let addr = self.mpp_contract_address.expect("validated above");
462            mpp::setup(
463                signer_providers,
464                addr,
465                self.fee_token,
466                self.max_concurrent_requests,
467                self.max_concurrent_transactions,
468            )
469            .await?;
470            Some(addr)
471        } else {
472            None
473        };
474
475        // Generate and send transactions
476        let total_txs = self.tps * self.duration;
477        let tip20_weight = (self.tip20_weight * Self::WEIGHT_PRECISION).trunc() as u64;
478        let tip20_virtual_weight =
479            (self.tip20_virtual_weight * Self::WEIGHT_PRECISION).trunc() as u64;
480        let place_order_weight = (self.place_order_weight * Self::WEIGHT_PRECISION).trunc() as u64;
481        let swap_weight = (self.swap_weight * Self::WEIGHT_PRECISION).trunc() as u64;
482        let erc20_weight = (self.erc20_weight * Self::WEIGHT_PRECISION).trunc() as u64;
483        let mpp_weight = (self.mpp_weight * Self::WEIGHT_PRECISION).trunc() as u64;
484
485        let recipients = if self.existing_recipients {
486            let addrs: Vec<Address> = signer_providers
487                .iter()
488                .map(|(signer, _)| signer.address())
489                .collect();
490            info!(
491                recipients = addrs.len(),
492                "Using existing signer addresses as recipients"
493            );
494            Some(addrs)
495        } else {
496            None
497        };
498
499        let use_expiring_nonces = !self.use_2d_nonces && !self.use_standard_nonces;
500        let expiry_secs = if use_expiring_nonces {
501            Some(ExpiringNonceFiller::DEFAULT_EXPIRY_SECS)
502        } else {
503            None
504        };
505
506        let chain_id = provider.get_chain_id().await?;
507
508        let gen_input = GenerateTransactionsInput {
509            tip20_weight,
510            tip20_virtual_weight,
511            place_order_weight,
512            swap_weight,
513            erc20_weight,
514            mpp_weight,
515            quote_token,
516            user_tokens,
517            erc20_tokens,
518            mpp_contract_address,
519            chain_id,
520            recipients,
521            expiry_secs,
522            virtual_master_ids,
523        };
524
525        info!(total_txs, "Generating and sending transactions");
526
527        let counters = TransactionCounters::default();
528        let target_count = total_txs as usize;
529        let cancel_token = CancellationToken::new();
530
531        // Start TPS monitor
532        tokio::spawn(monitor_tps(
533            counters.clone(),
534            target_count,
535            cancel_token.clone(),
536        ));
537
538        let rate_limiter =
539            RateLimiter::direct(Quota::per_second(NonZeroU32::new(self.tps as u32).unwrap()));
540        let start_block_number = provider.get_block_number().await?;
541
542        let mut pending_txs =
543            generate_transactions(signer_provider_manager.clone(), gen_input, counters.clone())
544                // Take exactly the required number of transactions to not over-send
545                .take(target_count)
546                // Stop when duration exceeded, no matter if we sent all transactions or not
547                .take_until(sleep(Duration::from_secs(self.duration)))
548                // Keep a buffer of pre-generated transactions to send as fast as possible
549                .buffer_unordered(self.tps as usize)
550                // Filter only successfully generated transactions
551                .filter_map(|result| async {
552                    match result {
553                        Ok(bytes) => Some(bytes),
554                        Err(e) => {
555                            debug!(?e, "Transaction generation failed");
556                            None
557                        }
558                    }
559                })
560                .boxed()
561                .ratelimit_stream(&rate_limiter)
562                // Pair each transaction with a random provider to send it
563                .zip(stream::repeat_with(|| {
564                    signer_provider_manager.random_unsigned_provider()
565                }))
566                // Prepare transaction sending futures
567                .map(|(bytes, provider)| async move {
568                    tokio::time::timeout(
569                        Duration::from_secs(1),
570                        provider.send_raw_transaction(&bytes),
571                    )
572                    .await
573                })
574                // Send transactions in parallel with up to the specified concurrency limit
575                .buffer_unordered(self.max_concurrent_requests)
576                .filter_map({
577                    let counters = counters.clone();
578                    move |result| {
579                        let counters = counters.clone();
580                        async move {
581                            match result {
582                                Ok(Ok(pending_tx)) => {
583                                    counters.sent.fetch_add(1, Ordering::Relaxed);
584                                    counters.success.fetch_add(1, Ordering::Relaxed);
585                                    Some(pending_tx)
586                                }
587                                Ok(Err(err)) => {
588                                    counters.sent.fetch_add(1, Ordering::Relaxed);
589                                    counters.failed.fetch_add(1, Ordering::Relaxed);
590                                    debug!(?err, "Failed to send transaction");
591                                    None
592                                }
593                                Err(_) => {
594                                    counters.sent.fetch_add(1, Ordering::Relaxed);
595                                    counters.timed_out.fetch_add(1, Ordering::Relaxed);
596                                    debug!("Transaction sending timed out");
597                                    None
598                                }
599                            }
600                        }
601                    }
602                })
603                .collect::<VecDeque<_>>()
604                .await;
605
606        cancel_token.cancel();
607
608        info!(
609            tip20_transfers = counters.tip20_transfers.load(Ordering::Relaxed),
610            tip20_virtual_transfers = counters.tip20_virtual_transfers.load(Ordering::Relaxed),
611            swaps = counters.swaps.load(Ordering::Relaxed),
612            orders = counters.orders.load(Ordering::Relaxed),
613            erc20_transfers = counters.erc20_transfers.load(Ordering::Relaxed),
614            mpp_open_close = counters.mpp_open_close.load(Ordering::Relaxed),
615            success = counters.success.load(Ordering::Relaxed),
616            failed = counters.failed.load(Ordering::Relaxed),
617            timed_out = counters.timed_out.load(Ordering::Relaxed),
618            "Finished sending transactions"
619        );
620
621        let end_block_number = provider.get_block_number().await?;
622
623        // Collect a sample of receipts and print the stats
624        let sample_size = pending_txs.len().min(self.sample_size);
625        let successful = Arc::new(AtomicUsize::new(0));
626        let timeout = Arc::new(AtomicUsize::new(0));
627        let failed = Arc::new(AtomicUsize::new(0));
628        info!(sample_size, "Collecting a sample of receipts");
629        stream::iter(0..sample_size)
630            .map(|_| {
631                let idx = random_range(0..pending_txs.len());
632                pending_txs.remove(idx).expect("index is in range")
633            })
634            .map(|pending_tx| {
635                let hash = *pending_tx.tx_hash();
636                pending_tx
637                    .with_timeout(Some(Duration::from_secs(5)))
638                    .get_receipt()
639                    .map(move |result| (hash, result))
640            })
641            .buffered(self.max_concurrent_requests)
642            .for_each(async |(hash, result)| match result {
643                Ok(_) => {
644                    successful.fetch_add(1, Ordering::Relaxed);
645                }
646                Err(PendingTransactionError::TxWatcher(WatchTxError::Timeout)) => {
647                    timeout.fetch_add(1, Ordering::Relaxed);
648                    error!(?hash, "Transaction receipt retrieval timed out");
649                }
650                Err(err) => {
651                    failed.fetch_add(1, Ordering::Relaxed);
652                    error!(?hash, "Transaction receipt retrieval failed: {}", err);
653                }
654            })
655            .await;
656        info!(
657            successful = successful.load(Ordering::Relaxed),
658            timeout = timeout.load(Ordering::Relaxed),
659            failed = failed.load(Ordering::Relaxed),
660            "Collected a sample of receipts"
661        );
662
663        generate_report(provider, start_block_number, end_block_number, &self).await?;
664
665        Ok(())
666    }
667}
668
669#[derive(Debug, Clone)]
670enum MnemonicArg {
671    Mnemonic(String),
672    Random,
673}
674
675impl FromStr for MnemonicArg {
676    type Err = MnemonicError;
677
678    fn from_str(s: &str) -> Result<Self, Self::Err> {
679        match s {
680            "random" => Ok(MnemonicArg::Random),
681            mnemonic => Ok(MnemonicArg::Mnemonic(
682                Mnemonic::<English>::from_str(mnemonic)?.to_phrase(),
683            )),
684        }
685    }
686}
687
688impl MnemonicArg {
689    fn resolve(&self) -> String {
690        match self {
691            MnemonicArg::Mnemonic(mnemonic) => mnemonic.clone(),
692            MnemonicArg::Random => Mnemonic::<English>::new(&mut rand_08::thread_rng()).to_phrase(),
693        }
694    }
695}
696
697#[derive(Clone, Default)]
698struct TransactionCounters {
699    /// Per-type generation counters
700    tip20_transfers: Arc<AtomicUsize>,
701    tip20_virtual_transfers: Arc<AtomicUsize>,
702    swaps: Arc<AtomicUsize>,
703    orders: Arc<AtomicUsize>,
704    erc20_transfers: Arc<AtomicUsize>,
705    mpp_open_close: Arc<AtomicUsize>,
706    /// Sending counters
707    sent: Arc<AtomicUsize>,
708    success: Arc<AtomicUsize>,
709    failed: Arc<AtomicUsize>,
710    timed_out: Arc<AtomicUsize>,
711}
712
713#[derive(Clone)]
714struct GenerateTransactionsInput {
715    tip20_weight: u64,
716    tip20_virtual_weight: u64,
717    place_order_weight: u64,
718    swap_weight: u64,
719    erc20_weight: u64,
720    mpp_weight: u64,
721    quote_token: Option<Address>,
722    user_tokens: Vec<Address>,
723    erc20_tokens: Vec<Address>,
724    mpp_contract_address: Option<Address>,
725    chain_id: u64,
726    /// When set, transfers go to these existing addresses instead of `Address::random()`.
727    recipients: Option<Vec<Address>>,
728    /// When `Some`, sets `valid_before` on each transaction right before signing
729    /// to keep it fresh for expiring nonces.
730    expiry_secs: Option<u64>,
731    /// Registered master IDs for constructing virtual address recipients.
732    virtual_master_ids: Vec<MasterId>,
733}
734
735/// Returns an infinite stream of futures, each generating, signing, and encoding one transaction.
736fn generate_transactions<F: TxFiller<TempoNetwork> + 'static>(
737    signer_provider_manager: SignerProviderManager<F>,
738    input: GenerateTransactionsInput,
739    counters: TransactionCounters,
740) -> impl Stream<Item = impl Future<Output = eyre::Result<Vec<u8>>>> {
741    let GenerateTransactionsInput {
742        tip20_weight,
743        tip20_virtual_weight,
744        place_order_weight,
745        swap_weight,
746        erc20_weight,
747        mpp_weight,
748        quote_token,
749        user_tokens,
750        erc20_tokens,
751        mpp_contract_address,
752        chain_id,
753        recipients,
754        expiry_secs,
755        virtual_master_ids,
756    } = input;
757
758    const TX_TYPES: usize = 6;
759    // Weights for random sampling for each transaction type
760    let tx_weights: [u64; TX_TYPES] = [
761        tip20_weight,
762        tip20_virtual_weight,
763        swap_weight,
764        place_order_weight,
765        erc20_weight,
766        mpp_weight,
767    ];
768    // Cached gas estimates for each transaction type
769    let gas_estimates: [Arc<OnceLock<(u128, u128, u64)>>; TX_TYPES] = Default::default();
770    // Global tx counter used to bump priority fee, ensuring unique tx hashes
771    // when using expiring nonces (which share nonce=0).
772    let tx_id = Arc::new(AtomicUsize::new(0));
773
774    stream::repeat_with(move || {
775        let signer_provider_manager = signer_provider_manager.clone();
776        let gas_estimates = gas_estimates.clone();
777        let tx_id = tx_id.clone();
778        let recipients = recipients.clone();
779        let user_tokens = user_tokens.clone();
780        let erc20_tokens = erc20_tokens.clone();
781        let virtual_master_ids = virtual_master_ids.clone();
782        let counters = counters.clone();
783
784        async move {
785            let provider = signer_provider_manager.random_unsigned_provider();
786            let signer = signer_provider_manager.random_signer();
787            let token = user_tokens.choose(&mut rand::rng()).copied().unwrap();
788
789            // TODO: can be improved with an enum per transaction type
790            let tx_index = tx_weights
791                .iter()
792                .enumerate()
793                .collect::<Vec<_>>()
794                .choose_weighted(&mut rand::rng(), |(_, weight)| *weight)?
795                .0;
796
797            let recipient = match &recipients {
798                Some(addrs) => *addrs.choose(&mut rand::rng()).unwrap(),
799                None => Address::random(),
800            };
801
802            let mut tx = match tx_index {
803                0 => {
804                    counters.tip20_transfers.fetch_add(1, Ordering::Relaxed);
805                    let token = ITIP20Instance::new(token, provider.clone());
806
807                    // Transfer minimum possible amount
808                    token
809                        .transfer(recipient, U256::ONE)
810                        .into_transaction_request()
811                }
812                1 => {
813                    // TIP-20 transfer to a virtual address
814                    counters
815                        .tip20_virtual_transfers
816                        .fetch_add(1, Ordering::Relaxed);
817                    let token = ITIP20Instance::new(token, provider.clone());
818                    let master_id = virtual_master_ids
819                        .choose(&mut rand::rng())
820                        .copied()
821                        .unwrap();
822                    let to = make_virtual_address(master_id);
823
824                    token.transfer(to, U256::ONE).into_transaction_request()
825                }
826                2 => {
827                    counters.swaps.fetch_add(1, Ordering::Relaxed);
828                    let exchange =
829                        IStablecoinDEXInstance::new(STABLECOIN_DEX_ADDRESS, provider.clone());
830
831                    // Swap minimum possible amount
832                    let quote_token =
833                        quote_token.expect("quote_token must be set when swap_weight > 0");
834                    exchange
835                        .quoteSwapExactAmountIn(token, quote_token, 1)
836                        .into_transaction_request()
837                }
838                3 => {
839                    counters.orders.fetch_add(1, Ordering::Relaxed);
840                    let exchange =
841                        IStablecoinDEXInstance::new(STABLECOIN_DEX_ADDRESS, provider.clone());
842
843                    // Place an order at a random tick that's a multiple of `TICK_SPACING`
844                    let tick = random_range(MIN_TICK / TICK_SPACING..=MAX_TICK / TICK_SPACING)
845                        * TICK_SPACING;
846                    exchange
847                        .place(token, MIN_ORDER_AMOUNT, true, tick)
848                        .into_transaction_request()
849                }
850                4 => {
851                    counters.erc20_transfers.fetch_add(1, Ordering::Relaxed);
852                    let token_address = erc20_tokens.choose(&mut rand::rng()).copied().unwrap();
853                    let token = erc20::MockERC20::new(token_address, provider.clone());
854
855                    // Transfer minimum possible amount
856                    token
857                        .transfer(recipient, U256::ONE)
858                        .into_transaction_request()
859                }
860                5 => {
861                    counters.mpp_open_close.fetch_add(1, Ordering::Relaxed);
862                    let contract = mpp_contract_address
863                        .expect("mpp_channel_address must be set when mpp_weight > 0");
864
865                    // Single Tempo tx with two calls: open a channel, then close it.
866                    // Signer is both payer and payee, so close succeeds immediately.
867                    let salt = B256::random();
868                    let payer = signer.address();
869                    let channel_id = mpp::compute_channel_id(
870                        payer,
871                        payer,
872                        token,
873                        salt,
874                        Address::ZERO,
875                        contract,
876                        chain_id,
877                    );
878                    mpp::build_open_and_close(contract, payer, token, salt, channel_id)
879                }
880                _ => unreachable!("Only {TX_TYPES} transaction types are supported"),
881            };
882
883            // Get a random signer and set it as the sender of the transaction.
884            tx.inner.set_from(signer.address());
885
886            let gas = &gas_estimates[tx_index];
887            // If we already filled the gas fields once for that transaction type, use it.
888            // This will skip the gas filler.
889            if let Some((max_fee_per_gas, max_priority_fee_per_gas, gas_limit)) = gas.get() {
890                tx.inner.set_max_fee_per_gas(*max_fee_per_gas);
891                tx.inner
892                    .set_max_priority_fee_per_gas(*max_priority_fee_per_gas);
893                tx.inner.set_gas_limit(*gas_limit);
894            }
895
896            // Fill the rest of transaction. In case we already filled the gas fields,
897            // it will only fill the chain ID and nonce that are efficiently cached inside
898            // the fillers.
899            let filled = provider.fill(tx).await?;
900
901            // If we never filled the gas fields for that transaction type, cache the estimated
902            // gas.
903            if gas.get().is_none() {
904                let _ = gas.set(match &filled {
905                    SendableTx::Builder(builder) => (
906                        builder
907                            .max_fee_per_gas()
908                            .ok_or_eyre("max fee per gas should be filled")?,
909                        builder
910                            .max_priority_fee_per_gas()
911                            .ok_or_eyre("max priority fee per gas should be filled")?,
912                        builder
913                            .gas_limit()
914                            .ok_or_eyre("gas limit should be filled")?,
915                    ),
916                    SendableTx::Envelope(envelope) => (
917                        envelope.max_fee_per_gas(),
918                        envelope
919                            .max_priority_fee_per_gas()
920                            .ok_or_eyre("max priority fee per gas should be filled")?,
921                        envelope.gas_limit(),
922                    ),
923                });
924            }
925
926            let mut req = filled.try_into_request()?;
927
928            // Bump priority fee by a unique counter to ensure unique tx hashes
929            // when using expiring nonces (which share nonce=0).
930            let id = tx_id.fetch_add(1, Ordering::Relaxed) as u128;
931            if let Some(fee) = req.max_priority_fee_per_gas() {
932                req.inner.set_max_priority_fee_per_gas(fee + id);
933            }
934            if let Some(fee) = req.max_fee_per_gas() {
935                req.inner.set_max_fee_per_gas(fee + id);
936            }
937
938            // Set valid_before right before signing to keep it fresh (expiring nonces only)
939            if let Some(expiry_secs) = expiry_secs {
940                let now = std::time::SystemTime::now()
941                    .duration_since(std::time::UNIX_EPOCH)
942                    .map(|d| d.as_secs())
943                    .unwrap_or(0);
944                req.set_valid_before(
945                    NonZeroU64::new(now + expiry_secs)
946                        .expect("current time plus expiry should be non-zero"),
947                );
948            }
949
950            // Sign and encode
951            let mut unsigned = req.build_unsigned()?;
952            let sig = signer.sign_transaction_sync(unsigned.as_dyn_signable_mut())?;
953            eyre::Ok(unsigned.into_envelope(sig).encoded_2718())
954        }
955    })
956}
957
958/// Funds accounts from the faucet using `temp_fundAddress` RPC.
959async fn fund_accounts(
960    provider: &DynProvider<TempoNetwork>,
961    addresses: &[Address],
962    max_concurrent_requests: usize,
963    max_concurrent_transactions: usize,
964) -> eyre::Result<()> {
965    info!(accounts = addresses.len(), "Funding accounts from faucet");
966    let progress = ProgressBar::new(addresses.len() as u64);
967
968    let chunks = addresses
969        .iter()
970        .map(|address| {
971            let address = *address;
972            provider.raw_request::<_, Vec<B256>>("tempo_fundAddress".into(), (address,))
973        })
974        .chunks(max_concurrent_transactions);
975
976    for chunk in chunks.into_iter() {
977        let tx_hashes = stream::iter(chunk)
978            .buffer_unordered(max_concurrent_requests)
979            .try_collect::<Vec<_>>()
980            .await?
981            .into_iter()
982            .inspect(|_| progress.inc(1))
983            .flatten()
984            .map(async |hash| {
985                Ok(
986                    PendingTransactionBuilder::new(provider.root().clone(), hash)
987                        .get_receipt()
988                        .await?,
989                )
990            });
991        assert_receipts(tx_hashes, max_concurrent_requests)
992            .await
993            .expect("Failed to fund accounts");
994    }
995    Ok(())
996}
997
998pub fn increase_nofile_limit(min_limit: u64) -> eyre::Result<u64> {
999    let (soft, hard) = Resource::NOFILE.get()?;
1000    info!(soft, hard, "File descriptor limit at startup");
1001
1002    if hard < min_limit {
1003        panic!(
1004            "File descriptor hard limit is too low. Please increase it to at least {min_limit}."
1005        );
1006    }
1007
1008    if soft != hard {
1009        Resource::NOFILE.set(hard, hard)?; // Just max things out to give us plenty of overhead.
1010        let (soft, hard) = Resource::NOFILE.get()?;
1011        info!(soft, hard, "After increasing file descriptor limit");
1012    }
1013
1014    Ok(soft)
1015}
1016
1017#[derive(Serialize)]
1018struct BenchmarkedBlock {
1019    number: BlockNumber,
1020    tx_count: usize,
1021    ok_count: usize,
1022    err_count: usize,
1023    gas_used: u64,
1024    timestamp: u64,
1025    latency_ms: Option<u64>,
1026}
1027
1028#[derive(Serialize)]
1029struct BenchmarkMetadata {
1030    target_tps: u64,
1031    run_duration_secs: u64,
1032    accounts: u64,
1033    chain_id: u64,
1034    total_connections: usize,
1035    start_block: BlockNumber,
1036    end_block: BlockNumber,
1037    #[serde(skip_serializing_if = "Option::is_none")]
1038    node_commit_sha: Option<String>,
1039    #[serde(skip_serializing_if = "Option::is_none")]
1040    build_profile: Option<String>,
1041    #[serde(skip_serializing_if = "Option::is_none")]
1042    mode: Option<String>,
1043    tip20_weight: f64,
1044    tip20_virtual_weight: f64,
1045    place_order_weight: f64,
1046    swap_weight: f64,
1047    erc20_weight: f64,
1048    mpp_weight: f64,
1049}
1050
1051#[derive(Serialize)]
1052struct BenchmarkReport {
1053    metadata: BenchmarkMetadata,
1054    blocks: Vec<BenchmarkedBlock>,
1055}
1056
1057pub async fn generate_report(
1058    provider: DynProvider<TempoNetwork>,
1059    start_block: BlockNumber,
1060    end_block: BlockNumber,
1061    args: &MaxTpsArgs,
1062) -> eyre::Result<()> {
1063    info!(start_block, end_block, "Generating report");
1064
1065    let mut last_block_timestamp: Option<u64> = None;
1066
1067    let mut benchmarked_blocks = Vec::new();
1068
1069    // Skip start_block — it was the chain head before any transactions were sent
1070    for number in (start_block + 1)..=end_block {
1071        let block = provider
1072            .get_block(number.into())
1073            .await?
1074            .ok_or_eyre("Block {number} not found")?;
1075        let receipts = provider
1076            .get_block_receipts(number.into())
1077            .await?
1078            .ok_or_eyre("Receipts for block {number} not found")?;
1079
1080        let timestamp = block.header.timestamp_millis();
1081
1082        let latency_ms = last_block_timestamp.map(|last| timestamp - last);
1083        let (ok_count, err_count) =
1084            receipts
1085                .iter()
1086                .fold((0, 0), |(successes, failures), receipt| {
1087                    if receipt.status() {
1088                        (successes + 1, failures)
1089                    } else {
1090                        (successes, failures + 1)
1091                    }
1092                });
1093
1094        benchmarked_blocks.push(BenchmarkedBlock {
1095            number,
1096            tx_count: receipts.len(),
1097            ok_count,
1098            err_count,
1099            gas_used: block.header.gas_used(),
1100            timestamp: block.header.timestamp_millis(),
1101            latency_ms,
1102        });
1103
1104        last_block_timestamp = Some(timestamp);
1105    }
1106
1107    // Remove leading ramp-up blocks (system-only, no gas used)
1108    while benchmarked_blocks.first().is_some_and(|b| b.gas_used == 0) {
1109        benchmarked_blocks.remove(0);
1110    }
1111
1112    // Reset latency_ms for the new first block (its latency was relative to a stripped block)
1113    if let Some(first) = benchmarked_blocks.first_mut() {
1114        first.latency_ms = None;
1115    }
1116
1117    let metadata = BenchmarkMetadata {
1118        target_tps: args.tps,
1119        run_duration_secs: args.duration,
1120        accounts: args.accounts.get(),
1121        chain_id: provider.get_chain_id().await?,
1122        total_connections: args.max_concurrent_requests,
1123        start_block,
1124        end_block,
1125        node_commit_sha: args.node_commit_sha.clone(),
1126        build_profile: args.build_profile.clone(),
1127        mode: args.benchmark_mode.clone(),
1128        tip20_weight: args.tip20_weight,
1129        tip20_virtual_weight: args.tip20_virtual_weight,
1130        place_order_weight: args.place_order_weight,
1131        swap_weight: args.swap_weight,
1132        erc20_weight: args.erc20_weight,
1133        mpp_weight: args.mpp_weight,
1134    };
1135
1136    let report = BenchmarkReport {
1137        metadata,
1138        blocks: benchmarked_blocks,
1139    };
1140
1141    let path = "report.json";
1142    let file = File::create(path)?;
1143    let writer = BufWriter::new(file);
1144    serde_json::to_writer_pretty(writer, &report)?;
1145
1146    info!(path, "Generated report");
1147
1148    Ok(())
1149}
1150
1151async fn monitor_tps(counters: TransactionCounters, target_count: usize, token: CancellationToken) {
1152    let mut last_count = 0;
1153    let mut ticker = interval(Duration::from_secs(1));
1154
1155    loop {
1156        select! {
1157            _ = ticker.tick() => {
1158                let current_count = counters.sent.load(Ordering::Relaxed);
1159                let tps = current_count - last_count;
1160                last_count = current_count;
1161
1162                info!(
1163                    tps,
1164                    total = current_count,
1165                    failed = counters.failed.load(Ordering::Relaxed),
1166                    timed_out = counters.timed_out.load(Ordering::Relaxed),
1167                    "Status"
1168                );
1169                tokio::time::sleep(Duration::from_secs(1)).await;
1170
1171                if current_count == target_count {
1172                    break;
1173                }
1174            }
1175            _ = token.cancelled() => {
1176                break;
1177            }
1178        }
1179    }
1180}
1181
1182async fn join_all<
1183    T: Future<Output = alloy::contract::Result<PendingTransactionBuilder<TempoNetwork>>>,
1184>(
1185    futures: impl IntoIterator<Item = T>,
1186    max_concurrent_requests: usize,
1187    max_concurrent_transactions: usize,
1188) -> eyre::Result<()> {
1189    let chunks = futures.into_iter().chunks(max_concurrent_transactions);
1190
1191    for chunk in chunks.into_iter() {
1192        // Send transactions and collect pending builders
1193        let pending_txs = stream::iter(chunk)
1194            .buffer_unordered(max_concurrent_requests)
1195            .try_collect::<Vec<_>>()
1196            .await?;
1197
1198        // Fetch receipts and assert status
1199        assert_receipts(
1200            pending_txs
1201                .into_iter()
1202                .map(|tx| async move { Ok(tx.get_receipt().await?) }),
1203            max_concurrent_requests,
1204        )
1205        .await?;
1206    }
1207
1208    Ok(())
1209}
1210
1211async fn assert_receipts<R: ReceiptResponse, F: Future<Output = eyre::Result<R>>>(
1212    receipts: impl IntoIterator<Item = F>,
1213    max_concurrent_requests: usize,
1214) -> eyre::Result<()> {
1215    stream::iter(receipts)
1216        .buffer_unordered(max_concurrent_requests)
1217        .try_for_each(|receipt| assert_receipt(receipt))
1218        .await
1219}
1220
1221async fn assert_receipt<R: ReceiptResponse>(receipt: R) -> eyre::Result<()> {
1222    eyre::ensure!(
1223        receipt.status(),
1224        "Transaction {} failed",
1225        receipt.transaction_hash()
1226    );
1227    Ok(())
1228}