1use 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
68pub 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 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#[derive(Clone, Debug)]
246pub struct ExecutionNodeConfig {
247 pub secret_key: B256,
249 pub trusted_peers: Vec<String>,
251 pub port: u16,
253}
254
255impl ExecutionNodeConfig {
256 pub fn generator() -> ExecutionNodeConfigGenerator {
258 ExecutionNodeConfigGenerator::default()
259 }
260}
261
262#[derive(Default)]
264pub struct ExecutionNodeConfigGenerator {
265 count: u32,
266 connect_peers: bool,
267}
268
269impl ExecutionNodeConfigGenerator {
270 pub fn with_count(mut self, count: u32) -> Self {
272 self.count = count;
273 self
274 }
275
276 pub fn with_peers(mut self, connect: bool) -> Self {
278 self.connect_peers = connect;
279 self
280 }
281
282 pub fn generate(self) -> Vec<ExecutionNodeConfig> {
284 if !self.connect_peers {
285 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 let ports: Vec<u16> = (0..self.count)
297 .map(|_| {
298 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
340pub struct ExecutionRuntime {
347 rt: std::thread::JoinHandle<()>,
349
350 _tempdir: TempDir,
352
353 to_runtime: tokio::sync::mpsc::UnboundedSender<Message>,
355}
356
357impl ExecutionRuntime {
358 pub fn builder() -> Builder {
359 Builder::new()
360 }
361
362 pub fn with_chain_spec(chain_spec: TempoChainSpec) -> Self {
364 let tempdir = tempfile::Builder::new()
365 .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 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 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 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#[derive(Clone)]
566pub struct ExecutionRuntimeHandle {
567 to_runtime: tokio::sync::mpsc::UnboundedSender<Message>,
568 nodes_dir: PathBuf,
569}
570
571impl ExecutionRuntimeHandle {
572 pub fn nodes_dir(&self) -> &Path {
574 &self.nodes_dir
575 }
576
577 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
599pub struct ExecutionNode {
604 pub node: TempoFullNode,
606 pub task_manager: TaskManager,
608 pub exit_fut: NodeExitFuture,
610}
611
612impl ExecutionNode {
613 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 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
649pub fn chainspec() -> TempoChainSpec {
653 TempoChainSpec::from_genesis(genesis())
654}
655
656pub fn execution_node_name(public_key: &PublicKey) -> String {
658 format!("{}-{}", crate::EXECUTION_NODE_PREFIX, public_key)
659}
660
661impl 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
678pub 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 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 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}