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#[derive(Parser, Debug)]
88pub struct MaxTpsArgs {
89 #[arg(short, long)]
91 tps: u64,
92
93 #[arg(short, long, default_value_t = 30)]
95 duration: u64,
96
97 #[arg(short, long, default_value_t = NonZeroU64::new(100).unwrap())]
99 accounts: NonZeroU64,
100
101 #[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 #[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 #[arg(long, default_value_t = 100)]
118 max_concurrent_requests: usize,
119
120 #[arg(long, default_value_t = 10000)]
125 max_concurrent_transactions: usize,
126
127 #[arg(long)]
129 fd_limit: Option<u64>,
130
131 #[arg(long)]
133 node_commit_sha: Option<String>,
134
135 #[arg(long)]
137 build_profile: Option<String>,
138
139 #[arg(long)]
141 benchmark_mode: Option<String>,
142
143 #[arg(long, default_value_t = 1.0)]
145 tip20_weight: f64,
146
147 #[arg(long, default_value_t = 0.0)]
156 tip20_virtual_weight: f64,
157
158 #[arg(long, default_value_t = 0.0)]
160 place_order_weight: f64,
161
162 #[arg(long, default_value_t = 0.0)]
164 swap_weight: f64,
165
166 #[arg(long, default_value_t = 0.0)]
168 erc20_weight: f64,
169
170 #[arg(long, default_value_t = 0.0)]
173 mpp_weight: f64,
174
175 #[arg(long, default_value = "0x33b901018174ddabe4841042ab76ba85d4e24f25")]
177 mpp_contract_address: Option<Address>,
178
179 #[arg(long, default_value_t = true, default_missing_value = "true", num_args = 0..=1, action = clap::ArgAction::Set)]
185 existing_recipients: bool,
186
187 #[arg(long, default_value_t = 100)]
189 sample_size: usize,
190
191 #[arg(long)]
195 faucet: bool,
196
197 #[arg(long)]
199 faucet_url: Option<Url>,
200
201 #[arg(long)]
205 clear_txpool: bool,
206
207 #[arg(long)]
215 use_2d_nonces: bool,
216
217 #[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 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 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 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 let provider = signer_providers[0].1.clone();
338
339 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 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 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 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 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(target_count)
546 .take_until(sleep(Duration::from_secs(self.duration)))
548 .buffer_unordered(self.tps as usize)
550 .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 .zip(stream::repeat_with(|| {
564 signer_provider_manager.random_unsigned_provider()
565 }))
566 .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 .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 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 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 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 recipients: Option<Vec<Address>>,
728 expiry_secs: Option<u64>,
731 virtual_master_ids: Vec<MasterId>,
733}
734
735fn 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 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 let gas_estimates: [Arc<OnceLock<(u128, u128, u64)>>; TX_TYPES] = Default::default();
770 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 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 token
809 .transfer(recipient, U256::ONE)
810 .into_transaction_request()
811 }
812 1 => {
813 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 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 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 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 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 tx.inner.set_from(signer.address());
885
886 let gas = &gas_estimates[tx_index];
887 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 let filled = provider.fill(tx).await?;
900
901 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 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 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 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
958async 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)?; 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 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 while benchmarked_blocks.first().is_some_and(|b| b.gas_used == 0) {
1109 benchmarked_blocks.remove(0);
1110 }
1111
1112 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 let pending_txs = stream::iter(chunk)
1194 .buffer_unordered(max_concurrent_requests)
1195 .try_collect::<Vec<_>>()
1196 .await?;
1197
1198 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}