tempo_e2e/
execution_runtime.rs

1//! The environment to launch tempo execution nodes in.
2use std::{
3    net::SocketAddr,
4    path::{Path, PathBuf},
5    sync::Arc,
6    time::Duration,
7};
8
9use alloy::{
10    providers::ProviderBuilder,
11    rpc::types::TransactionReceipt,
12    signers::{local::MnemonicBuilder, utils::secret_key_to_address},
13    transports::http::reqwest::Url,
14};
15use alloy_evm::{EvmFactory as _, EvmInternals, revm::inspector::JournalExt as _};
16use alloy_genesis::{Genesis, GenesisAccount};
17use alloy_primitives::{Address, B256};
18use commonware_codec::Encode;
19use commonware_cryptography::{
20    bls12381::primitives::{poly::Public, variant::MinSig},
21    ed25519::PublicKey,
22};
23use commonware_utils::set::OrderedAssociated;
24use eyre::{OptionExt as _, WrapErr as _};
25use futures::StreamExt;
26use reth_db::mdbx::DatabaseEnv;
27use reth_ethereum::{
28    evm::{
29        primitives::EvmEnv,
30        revm::db::{CacheDB, EmptyDB},
31    },
32    network::{
33        Peers as _,
34        api::{
35            NetworkEventListenerProvider, PeersInfo,
36            events::{NetworkEvent, PeerEvent},
37        },
38    },
39    tasks::TaskManager,
40};
41use reth_network_peers::{NodeRecord, TrustedPeer};
42use reth_node_builder::{NodeBuilder, NodeConfig};
43use reth_node_core::{
44    args::{DatadirArgs, PayloadBuilderArgs, RpcServerArgs},
45    exit::NodeExitFuture,
46};
47use reth_rpc_builder::RpcModuleSelection;
48use secp256k1::SecretKey;
49use std::net::TcpListener;
50use tempfile::TempDir;
51use tempo_chainspec::TempoChainSpec;
52use tempo_commonware_node_config::{Peers, PublicPolynomial};
53use tempo_dkg_onchain_artifacts::PublicOutcome;
54use tempo_node::{
55    TempoFullNode,
56    evm::{TempoEvmFactory, evm::TempoEvm},
57    node::TempoNode,
58};
59use tempo_precompiles::{
60    VALIDATOR_CONFIG_ADDRESS,
61    storage::evm::EvmPrecompileStorageProvider,
62    validator_config::{IValidatorConfig, ValidatorConfig},
63};
64
65const ADMIN_INDEX: u32 = 0;
66const VALIDATOR_START_INDEX: u32 = 1;
67
68/// Same mnemonic as used in the imported test-genesis and in the `tempo-node` integration tests.
69pub const TEST_MNEMONIC: &str = "test test test test test test test test test test test junk";
70
71#[derive(Default, Debug)]
72pub struct Builder {
73    allegretto_time: Option<u64>,
74    epoch_length: Option<u64>,
75    public_polynomial: Option<PublicPolynomial>,
76    validators: Option<Peers>,
77    write_validators_into_genesis: bool,
78}
79
80impl Builder {
81    pub fn new() -> Self {
82        Self {
83            allegretto_time: None,
84            epoch_length: None,
85            public_polynomial: None,
86            validators: None,
87            write_validators_into_genesis: true,
88        }
89    }
90
91    pub fn set_allegretto_time(self, allegretto_time: Option<u64>) -> Self {
92        Self {
93            allegretto_time,
94            ..self
95        }
96    }
97
98    pub fn set_write_validators_into_genesis(self, write_validators_into_genesis: bool) -> Self {
99        Self {
100            write_validators_into_genesis,
101            ..self
102        }
103    }
104
105    pub fn with_allegretto_time(self, allegretto_time: u64) -> Self {
106        Self {
107            allegretto_time: Some(allegretto_time),
108            ..self
109        }
110    }
111
112    pub fn with_epoch_length(self, epoch_length: u64) -> Self {
113        Self {
114            epoch_length: Some(epoch_length),
115            ..self
116        }
117    }
118
119    pub fn with_public_polynomial(self, public_polynomial: Public<MinSig>) -> Self {
120        Self {
121            public_polynomial: Some(public_polynomial.into()),
122            ..self
123        }
124    }
125
126    pub fn with_validators(self, validators: OrderedAssociated<PublicKey, SocketAddr>) -> Self {
127        Self {
128            validators: Some(validators.into()),
129            ..self
130        }
131    }
132
133    pub fn launch(self) -> eyre::Result<ExecutionRuntime> {
134        let Self {
135            allegretto_time,
136            epoch_length,
137            public_polynomial,
138            validators,
139            write_validators_into_genesis,
140        } = self;
141
142        let epoch_length = epoch_length.ok_or_eyre("must specify epoch length")?;
143        let public_polynomial = public_polynomial.ok_or_eyre("must specify a public polynomial")?;
144        let validators = validators.ok_or_eyre("must specify validators")?;
145
146        let mut genesis = genesis();
147        genesis
148            .config
149            .extra_fields
150            .insert_value("epochLength".to_string(), epoch_length)
151            .wrap_err("failed to insert epoch length into genesis")?;
152        genesis
153            .config
154            .extra_fields
155            .insert_value("publicPolynomial".to_string(), public_polynomial.clone())
156            .wrap_err("failed to insert public polynomial into genesis")?;
157        genesis
158            .config
159            .extra_fields
160            .insert_value("validators".to_string(), validators.clone())
161            .wrap_err("failed to insert validators into genesis")?;
162
163        if let Some(allegretto_time) = allegretto_time {
164            genesis
165                .config
166                .extra_fields
167                .insert_value("allegrettoTime".to_string(), allegretto_time)
168                .wrap_err("failed to insert allegretto timestamp into genesis")?;
169
170            genesis.extra_data = PublicOutcome {
171                epoch: 0,
172                participants: validators.public_keys().clone(),
173                public: public_polynomial.into_inner(),
174            }
175            .encode()
176            .freeze()
177            .to_vec()
178            .into();
179
180            if write_validators_into_genesis {
181                let mut evm = setup_tempo_evm();
182
183                {
184                    let ctx = evm.ctx_mut();
185                    let evm_internals = EvmInternals::new(&mut ctx.journaled_state, &ctx.block);
186                    let mut provider =
187                        EvmPrecompileStorageProvider::new_max_gas(evm_internals, &ctx.cfg);
188
189                    // TODO(janis): figure out the owner of the test-genesis.json
190                    let mut validator_config = ValidatorConfig::new(&mut provider);
191                    validator_config
192                        .initialize(admin())
193                        .wrap_err("Failed to initialize validator config")
194                        .unwrap();
195
196                    for (i, (peer, addr)) in validators.into_inner().iter_pairs().enumerate() {
197                        validator_config
198                            .add_validator(
199                                admin(),
200                                IValidatorConfig::addValidatorCall {
201                                    newValidatorAddress: validator(i as u32),
202                                    publicKey: peer.encode().freeze().as_ref().try_into().unwrap(),
203                                    active: true,
204                                    inboundAddress: addr.to_string(),
205                                    outboundAddress: addr.to_string(),
206                                },
207                            )
208                            .unwrap();
209                    }
210                }
211
212                let evm_state = evm.ctx_mut().journaled_state.evm_state();
213                for (address, account) in evm_state.iter() {
214                    let storage = if !account.storage.is_empty() {
215                        Some(
216                            account
217                                .storage
218                                .iter()
219                                .map(|(key, val)| ((*key).into(), val.present_value.into()))
220                                .collect(),
221                        )
222                    } else {
223                        None
224                    };
225                    genesis.alloc.insert(
226                        *address,
227                        GenesisAccount {
228                            nonce: Some(account.info.nonce),
229                            code: account.info.code.as_ref().map(|c| c.original_bytes()),
230                            storage,
231                            ..Default::default()
232                        },
233                    );
234                }
235            }
236        }
237
238        Ok(ExecutionRuntime::with_chain_spec(
239            TempoChainSpec::from_genesis(genesis),
240        ))
241    }
242}
243
244/// Configuration for launching an execution node.
245#[derive(Clone, Debug)]
246pub struct ExecutionNodeConfig {
247    /// Network secret key for the node's identity.
248    pub secret_key: B256,
249    /// List of trusted peer enode URLs to connect to.
250    pub trusted_peers: Vec<String>,
251    /// Port for the network service.
252    pub port: u16,
253}
254
255impl ExecutionNodeConfig {
256    /// Create a default generator for building multiple execution node configs.
257    pub fn generator() -> ExecutionNodeConfigGenerator {
258        ExecutionNodeConfigGenerator::default()
259    }
260}
261
262/// Generator for creating multiple execution node configurations.
263#[derive(Default)]
264pub struct ExecutionNodeConfigGenerator {
265    count: u32,
266    connect_peers: bool,
267}
268
269impl ExecutionNodeConfigGenerator {
270    /// Set the number of nodes to generate.
271    pub fn with_count(mut self, count: u32) -> Self {
272        self.count = count;
273        self
274    }
275
276    /// Set whether to enable peer connections between all generated nodes.
277    pub fn with_peers(mut self, connect: bool) -> Self {
278        self.connect_peers = connect;
279        self
280    }
281
282    /// Generate the execution node configurations.
283    pub fn generate(self) -> Vec<ExecutionNodeConfig> {
284        if !self.connect_peers {
285            // No peer connections needed, use port 0 (OS will assign)
286            return (0..self.count)
287                .map(|_| ExecutionNodeConfig {
288                    secret_key: B256::random(),
289                    trusted_peers: vec![],
290                    port: 0,
291                })
292                .collect();
293        }
294
295        // Reserve ports by binding to them for peer connections
296        let ports: Vec<u16> = (0..self.count)
297            .map(|_| {
298                // This should work, but there's a chance that it results in flaky tests
299                let listener = TcpListener::bind("127.0.0.1:0").unwrap();
300                let port = listener
301                    .local_addr()
302                    .expect("failed to get local addr")
303                    .port();
304                drop(listener);
305                port
306            })
307            .collect();
308
309        let mut configs: Vec<ExecutionNodeConfig> = ports
310            .into_iter()
311            .map(|port| ExecutionNodeConfig {
312                secret_key: B256::random(),
313                trusted_peers: vec![],
314                port,
315            })
316            .collect();
317
318        let enode_urls: Vec<String> = configs
319            .iter()
320            .map(|config| {
321                let secret_key =
322                    SecretKey::from_slice(config.secret_key.as_slice()).expect("valid secret key");
323                let addr = SocketAddr::from(([127, 0, 0, 1], config.port));
324                NodeRecord::from_secret_key(addr, &secret_key).to_string()
325            })
326            .collect();
327
328        for (i, config) in configs.iter_mut().enumerate() {
329            for (j, enode_url) in enode_urls.iter().enumerate() {
330                if i != j {
331                    config.trusted_peers.push(enode_url.clone());
332                }
333            }
334        }
335
336        configs
337    }
338}
339
340/// An execution runtime wrapping a thread running a [`tokio::runtime::Runtime`].
341///
342/// This is needed to spawn tempo execution nodes, which require a tokio runtime.
343///
344/// The commonware itself is launched in their
345/// [`commonware_runtime::deterministic`] and so this extra effort is necessary.
346pub struct ExecutionRuntime {
347    // The tokio runtime launched on a different thread.
348    rt: std::thread::JoinHandle<()>,
349
350    // Base directory where all reth databases will be initialized.
351    _tempdir: TempDir,
352
353    // Channel to request the runtime to launch new execution nodes.
354    to_runtime: tokio::sync::mpsc::UnboundedSender<Message>,
355}
356
357impl ExecutionRuntime {
358    pub fn builder() -> Builder {
359        Builder::new()
360    }
361
362    /// Constructs a new execution runtime to launch execution nodes.
363    pub fn with_chain_spec(chain_spec: TempoChainSpec) -> Self {
364        let tempdir = tempfile::Builder::new()
365            // TODO(janis): cargo manifest prefix?
366            .prefix("tempo_e2e_test")
367            .disable_cleanup(true)
368            .tempdir()
369            .expect("must be able to create a temp directory run tun tests");
370
371        let (to_runtime, mut from_handle) = tokio::sync::mpsc::unbounded_channel();
372
373        let datadir = tempdir.path().to_path_buf();
374        let rt = std::thread::spawn(move || {
375            let rt = tokio::runtime::Runtime::new()
376                .expect("must be able to initialize a runtime to run execution/reth nodes");
377            let wallet = MnemonicBuilder::from_phrase(crate::execution_runtime::TEST_MNEMONIC)
378                .build()
379                .unwrap();
380            rt.block_on(async move {
381                while let Some(msg) = from_handle.recv().await {
382                    // create a new task manager for the new node instance
383                    let task_manager = TaskManager::current();
384                    match msg {
385                        Message::AddValidator(add_validator) => {
386                            let AddValidator {
387                                http_url,
388                                address,
389                                public_key,
390                                addr,
391                                response,
392                            } = *add_validator;
393                            let provider = ProviderBuilder::new()
394                                .wallet(wallet.clone())
395                                .connect_http(http_url);
396                            let validator_config =
397                                IValidatorConfig::new(VALIDATOR_CONFIG_ADDRESS, provider);
398                            let receipt = validator_config
399                                .addValidator(
400                                    address,
401                                    public_key.encode().as_ref().try_into().unwrap(),
402                                    true,
403                                    addr.to_string(),
404                                    addr.to_string(),
405                                )
406                                .send()
407                                .await
408                                .unwrap()
409                                .get_receipt()
410                                .await
411                                .unwrap();
412                            let _ = response.send(receipt);
413                        }
414                        Message::ChangeValidatorStatus(change_validator_status) => {
415                            let ChangeValidatorStatus {
416                                http_url,
417                                active,
418                                address,
419                                response,
420                            } = *change_validator_status;
421                            let provider = ProviderBuilder::new()
422                                .wallet(wallet.clone())
423                                .connect_http(http_url);
424                            let validator_config =
425                                IValidatorConfig::new(VALIDATOR_CONFIG_ADDRESS, provider);
426                            let receipt = validator_config
427                                .changeValidatorStatus(address, active)
428                                .send()
429                                .await
430                                .unwrap()
431                                .get_receipt()
432                                .await
433                                .unwrap();
434                            let _ = response.send(receipt);
435                        }
436                        Message::SpawnNode {
437                            name,
438                            config,
439                            database,
440                            response,
441                        } => {
442                            let node = launch_execution_node(
443                                task_manager,
444                                chain_spec.clone(),
445                                datadir.join(name),
446                                config,
447                                database,
448                            )
449                            .await
450                            .expect("must be able to launch execution nodes");
451                            response.send(node).expect(
452                                "receiver must hold the return channel until the node is returned",
453                            );
454                        }
455                        Message::Stop => {
456                            break;
457                        }
458                    }
459                }
460            })
461        });
462
463        Self {
464            rt,
465            _tempdir: tempdir,
466            to_runtime,
467        }
468    }
469
470    /// Returns a handle to this runtime.
471    ///
472    /// Can be used to spawn nodes.
473    pub fn handle(&self) -> ExecutionRuntimeHandle {
474        ExecutionRuntimeHandle {
475            to_runtime: self.to_runtime.clone(),
476            nodes_dir: self._tempdir.path().to_path_buf(),
477        }
478    }
479
480    pub async fn add_validator(
481        &self,
482        http_url: Url,
483        address: Address,
484        public_key: PublicKey,
485        addr: SocketAddr,
486    ) -> eyre::Result<TransactionReceipt> {
487        let (tx, rx) = tokio::sync::oneshot::channel();
488        self.to_runtime
489            .send(
490                AddValidator {
491                    http_url,
492                    address,
493                    public_key,
494                    addr,
495                    response: tx,
496                }
497                .into(),
498            )
499            .wrap_err("the execution runtime went away")?;
500        rx.await
501            .wrap_err("the execution runtime dropped the response channel before sending a receipt")
502    }
503
504    pub async fn change_validator_status(
505        &self,
506        http_url: Url,
507        address: Address,
508        active: bool,
509    ) -> eyre::Result<TransactionReceipt> {
510        let (tx, rx) = tokio::sync::oneshot::channel();
511        self.to_runtime
512            .send(
513                ChangeValidatorStatus {
514                    address,
515                    active,
516                    http_url,
517                    response: tx,
518                }
519                .into(),
520            )
521            .wrap_err("the execution runtime went away")?;
522        rx.await
523            .wrap_err("the execution runtime dropped the response channel before sending a receipt")
524    }
525
526    pub async fn remove_validator(
527        &self,
528        http_url: Url,
529        address: Address,
530        public_key: PublicKey,
531        addr: SocketAddr,
532    ) -> eyre::Result<TransactionReceipt> {
533        let (tx, rx) = tokio::sync::oneshot::channel();
534        self.to_runtime
535            .send(
536                AddValidator {
537                    http_url,
538                    address,
539                    public_key,
540                    addr,
541                    response: tx,
542                }
543                .into(),
544            )
545            .wrap_err("the execution runtime went away")?;
546        rx.await
547            .wrap_err("the execution runtime dropped the response channel before sending a receipt")
548    }
549
550    /// Instructs the runtime to stop and exit.
551    pub fn stop(self) -> eyre::Result<()> {
552        self.to_runtime
553            .send(Message::Stop)
554            .wrap_err("the execution runtime went away")?;
555        match self.rt.join() {
556            Ok(()) => Ok(()),
557            Err(e) => std::panic::resume_unwind(e),
558        }
559    }
560}
561
562/// A handle to the execution runtime.
563///
564/// Can be used to spawn nodes.
565#[derive(Clone)]
566pub struct ExecutionRuntimeHandle {
567    to_runtime: tokio::sync::mpsc::UnboundedSender<Message>,
568    nodes_dir: PathBuf,
569}
570
571impl ExecutionRuntimeHandle {
572    /// Returns the base directory where execution node data is stored.
573    pub fn nodes_dir(&self) -> &Path {
574        &self.nodes_dir
575    }
576
577    /// Requests a new execution node and blocks until its returned.
578    pub async fn spawn_node(
579        &self,
580        name: &str,
581        config: ExecutionNodeConfig,
582        database: Arc<DatabaseEnv>,
583    ) -> eyre::Result<ExecutionNode> {
584        let (tx, rx) = tokio::sync::oneshot::channel();
585        self.to_runtime
586            .send(Message::SpawnNode {
587                name: name.to_string(),
588                config,
589                database,
590                response: tx,
591            })
592            .wrap_err("the execution runtime went away")?;
593        rx.await.wrap_err(
594            "the execution runtime dropped the response channel before sending an execution node",
595        )
596    }
597}
598
599/// An execution node spawned by the execution runtime.
600///
601/// This is essentially the same as [`reth_node_builder::NodeHandle`], but
602/// avoids the type parameters.
603pub struct ExecutionNode {
604    /// All handles to interact with the launched node instances and services.
605    pub node: TempoFullNode,
606    /// The [`TaskManager`] that drives the node's services.
607    pub task_manager: TaskManager,
608    /// The exist future that resolves when the node's engine future resolves.
609    pub exit_fut: NodeExitFuture,
610}
611
612impl ExecutionNode {
613    /// Connect peers bidirectionally.
614    pub async fn connect_peer(&self, other: &Self) {
615        let self_record = self.node.network.local_node_record();
616        let other_record = other.node.network.local_node_record();
617        let mut events = self.node.network.event_listener();
618
619        self.node
620            .network
621            .add_trusted_peer(other_record.id, other_record.tcp_addr());
622
623        match events.next().await {
624            Some(NetworkEvent::Peer(PeerEvent::PeerAdded(_))) => (),
625            ev => panic!("Expected a peer added event, got: {ev:?}"),
626        }
627
628        match events.next().await {
629            Some(NetworkEvent::ActivePeerSession { .. }) => (),
630            ev => panic!("Expected an active peer session event, got: {ev:?}"),
631        }
632
633        tracing::debug!(
634            "Connected peers: {:?} -> {:?}",
635            self_record.id,
636            other_record.id
637        );
638    }
639
640    /// Shuts down the node and awaits until the node is terminated.
641    pub async fn shutdown(self) {
642        let _ = self.node.rpc_server_handle().clone().stop();
643        self.task_manager
644            .graceful_shutdown_with_timeout(Duration::from_secs(10));
645        let _ = self.exit_fut.await;
646    }
647}
648
649/// Returns the chainspec used for e2e tests.
650///
651/// TODO(janis): allow configuring this.
652pub fn chainspec() -> TempoChainSpec {
653    TempoChainSpec::from_genesis(genesis())
654}
655
656/// Generate execution node name from public key.
657pub fn execution_node_name(public_key: &PublicKey) -> String {
658    format!("{}-{}", crate::EXECUTION_NODE_PREFIX, public_key)
659}
660
661// TODO(janis): would be nicer if we could identify the node somehow?
662impl std::fmt::Debug for ExecutionNode {
663    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
664        f.debug_struct("ExecutionNode")
665            .field("node", &"<TempoFullNode>")
666            .field("exit_fut", &"<NodeExitFuture>")
667            .finish()
668    }
669}
670
671pub fn genesis() -> Genesis {
672    serde_json::from_str(include_str!(
673        "../../node/tests/assets/test-genesis-moderato.json"
674    ))
675    .unwrap()
676}
677
678/// Launches a tempo execution node.
679///
680/// Difference compared to starting the node through the binary:
681///
682/// 1. faucet is always disabled
683/// 2. components are not provided (looking at the node command, the components
684///    are not passed to it).
685/// 3. consensus config is not necessary
686pub async fn launch_execution_node<P: AsRef<Path>>(
687    task_manager: TaskManager,
688    chain_spec: TempoChainSpec,
689    datadir: P,
690    config: ExecutionNodeConfig,
691    database: Arc<DatabaseEnv>,
692) -> eyre::Result<ExecutionNode> {
693    println!("launching node at {}", datadir.as_ref().display());
694    let node_config = NodeConfig::new(Arc::new(chain_spec))
695        .with_rpc(
696            RpcServerArgs::default()
697                .with_unused_ports()
698                .with_http()
699                .with_http_api(RpcModuleSelection::All),
700        )
701        .with_datadir_args(DatadirArgs {
702            datadir: datadir.as_ref().to_path_buf().into(),
703            ..DatadirArgs::default()
704        })
705        .with_payload_builder(PayloadBuilderArgs {
706            interval: Duration::from_millis(100),
707            ..Default::default()
708        })
709        .apply(|mut c| {
710            c.network.discovery.disable_discovery = true;
711            c.network.trusted_peers = config
712                .trusted_peers
713                .into_iter()
714                .map(|s| {
715                    s.parse::<TrustedPeer>()
716                        .expect("invalid trusted peer enode")
717                })
718                .collect();
719            c.network.port = config.port;
720            c.network.p2p_secret_key_hex = Some(config.secret_key);
721            c
722        });
723
724    let node_handle = NodeBuilder::new(node_config)
725        .with_database(database)
726        .with_launch_context(task_manager.executor())
727        .node(TempoNode::default())
728        .launch()
729        .await
730        .wrap_err_with(|| {
731            format!(
732                "failed launching node; databasedir: `{}`",
733                datadir.as_ref().display()
734            )
735        })?;
736
737    Ok(ExecutionNode {
738        node: node_handle.node,
739        task_manager,
740        exit_fut: node_handle.node_exit_future,
741    })
742}
743
744#[derive(Debug)]
745enum Message {
746    AddValidator(Box<AddValidator>),
747    ChangeValidatorStatus(Box<ChangeValidatorStatus>),
748    SpawnNode {
749        name: String,
750        config: ExecutionNodeConfig,
751        database: Arc<DatabaseEnv>,
752        response: tokio::sync::oneshot::Sender<ExecutionNode>,
753    },
754    Stop,
755}
756
757impl From<AddValidator> for Message {
758    fn from(value: AddValidator) -> Self {
759        Self::AddValidator(value.into())
760    }
761}
762
763impl From<ChangeValidatorStatus> for Message {
764    fn from(value: ChangeValidatorStatus) -> Self {
765        Self::ChangeValidatorStatus(value.into())
766    }
767}
768
769#[derive(Debug)]
770struct AddValidator {
771    /// URL of the node to send this to.
772    http_url: Url,
773    address: Address,
774    public_key: PublicKey,
775    addr: SocketAddr,
776    response: tokio::sync::oneshot::Sender<TransactionReceipt>,
777}
778
779#[derive(Debug)]
780struct ChangeValidatorStatus {
781    /// URL of the node to send this to.
782    http_url: Url,
783    address: Address,
784    active: bool,
785    response: tokio::sync::oneshot::Sender<TransactionReceipt>,
786}
787
788pub fn admin() -> Address {
789    address(ADMIN_INDEX)
790}
791
792pub fn validator(idx: u32) -> Address {
793    address(VALIDATOR_START_INDEX + idx)
794}
795
796pub fn address(index: u32) -> Address {
797    secret_key_to_address(MnemonicBuilder::from_phrase_nth(TEST_MNEMONIC, index).credential())
798}
799
800fn setup_tempo_evm() -> TempoEvm<CacheDB<EmptyDB>> {
801    let db = CacheDB::default();
802    let env = EvmEnv::default();
803    let factory = TempoEvmFactory::default();
804    factory.create_evm(db, env)
805}