blueprint_eigenlayer_testing_utils/
harness.rs

1use crate::Error;
2use crate::env::{EigenlayerTestEnvironment, setup_eigenlayer_test_environment};
3use alloy_primitives::Address;
4use alloy_provider::RootProvider;
5use blueprint_auth::db::RocksDb;
6use blueprint_chain_setup::anvil::keys::{ANVIL_PRIVATE_KEYS, inject_anvil_key};
7use blueprint_chain_setup::anvil::{Container, start_empty_anvil_testnet};
8use blueprint_core::{error, info};
9use blueprint_evm_extra::util::get_provider_http;
10use blueprint_manager_bridge::server::{Bridge, BridgeHandle};
11use blueprint_runner::config::{BlueprintEnvironment, ContextConfig, SupportedChains};
12use blueprint_runner::eigenlayer::config::EigenlayerProtocolSettings;
13use std::future::Future;
14use std::marker::PhantomData;
15use std::net::Ipv4Addr;
16use std::path::PathBuf;
17use tempfile::TempDir;
18use tokio::task::JoinHandle;
19use url::Url;
20
21/// Configuration for the Eigenlayer test harness
22#[derive(Default)]
23pub struct EigenlayerTestConfig {
24    pub http_endpoint: Option<Url>,
25    pub ws_endpoint: Option<Url>,
26    pub eigenlayer_contract_addresses: Option<EigenlayerProtocolSettings>,
27}
28
29/// Test harness for Eigenlayer network tests
30pub struct EigenlayerTestHarness<Ctx> {
31    env: BlueprintEnvironment,
32    config: EigenlayerTestConfig,
33    pub http_endpoint: Url,
34    pub ws_endpoint: Url,
35    pub accounts: Vec<Address>,
36    pub eigenlayer_contract_addresses: EigenlayerProtocolSettings,
37    _temp_dir: TempDir,
38    _container: Container,
39    _phantom: PhantomData<Ctx>,
40    _auth_proxy: JoinHandle<Result<(), Error>>,
41    _bridge: BridgeHandle,
42}
43
44impl EigenlayerTestHarness<()> {
45    /// Create a new `EigenlayerTestHarness`
46    ///
47    /// NOTE: The resulting harness will have a context of `()`. This is not valid for jobs that require
48    ///       a context. See [`Self::setup_with_context()`] and [`Self::set_context()`].
49    ///
50    /// # Errors
51    ///
52    /// * See [`Self::setup_with_context()`]
53    pub async fn setup(test_dir: TempDir) -> Result<Self, Error> {
54        Self::setup_with_context(test_dir, ()).await
55    }
56}
57
58impl<Ctx> EigenlayerTestHarness<Ctx>
59where
60    Ctx: Clone + Send + Sync + 'static,
61{
62    /// Create a new `EigenlayerTestHarness` with a predefined context
63    ///
64    /// NOTE: If your context type depends on [`Self::env()`], see [`Self::setup()`]
65    ///
66    /// # Errors
67    ///
68    /// * TODO
69    pub async fn setup_with_context(test_dir: TempDir, _context: Ctx) -> Result<Self, Error> {
70        // Start local Anvil testnet
71        let testnet = start_empty_anvil_testnet(true).await;
72
73        // Setup Eigenlayer test environment
74        let EigenlayerTestEnvironment {
75            accounts,
76            http_endpoint,
77            ws_endpoint,
78            eigenlayer_contract_addresses,
79        } = setup_eigenlayer_test_environment(testnet.http_endpoint, testnet.ws_endpoint).await;
80
81        // Setup temporary testing keystore
82        let keystore_path = test_dir.path().join("keystore");
83        inject_anvil_key(&keystore_path, ANVIL_PRIVATE_KEYS[0])?;
84
85        let data_dir = test_dir.path().join("data");
86        tokio::fs::create_dir_all(&data_dir).await?;
87
88        // Setup auth proxy
89        const DEFAULT_AUTH_PROXY_PORT: u16 = 50051;
90        let (auth_proxy_db, auth_proxy_task) =
91            run_auth_proxy(test_dir.path().to_path_buf(), DEFAULT_AUTH_PROXY_PORT).await?;
92
93        let auth_proxy = tokio::spawn(auth_proxy_task);
94
95        // Setup bridge
96        let runtime_dir = test_dir.path().join("runtime");
97        tokio::fs::create_dir_all(&runtime_dir).await?;
98
99        let bridge = Bridge::new(runtime_dir, String::from("service"), auth_proxy_db, true);
100        let bridge_socket_path = bridge.base_socket_path();
101
102        let (bridge_handle, _alive_rx) = bridge.spawn()?;
103
104        // Create context config
105        let context_config = ContextConfig::create_eigenlayer_config(
106            Url::parse(&http_endpoint)?,
107            Url::parse(&ws_endpoint)?,
108            keystore_path.to_string_lossy().into_owned(),
109            None,
110            data_dir,
111            None,
112            SupportedChains::LocalTestnet,
113            eigenlayer_contract_addresses,
114        );
115
116        // Load environment with bridge configuration
117        let mut env = BlueprintEnvironment::load_with_config(context_config)
118            .map_err(|e| Error::Setup(e.to_string()))?;
119
120        env.bridge_socket_path = Some(bridge_socket_path);
121        env.test_mode = true;
122
123        // Create config
124        let config = EigenlayerTestConfig {
125            http_endpoint: Some(Url::parse(&http_endpoint)?),
126            ws_endpoint: Some(Url::parse(&ws_endpoint)?),
127            eigenlayer_contract_addresses: Some(eigenlayer_contract_addresses),
128        };
129
130        Ok(Self {
131            env,
132            config,
133            http_endpoint: Url::parse(&http_endpoint)?,
134            ws_endpoint: Url::parse(&ws_endpoint)?,
135            accounts,
136            eigenlayer_contract_addresses,
137            _temp_dir: test_dir,
138            _container: testnet.container,
139            _phantom: core::marker::PhantomData,
140            _auth_proxy: auth_proxy,
141            _bridge: bridge_handle,
142        })
143    }
144
145    #[must_use]
146    #[allow(clippy::used_underscore_binding)]
147    pub fn set_context<Ctx2: Clone + Send + Sync + 'static>(
148        self,
149        _context: Ctx2,
150    ) -> EigenlayerTestHarness<Ctx2> {
151        EigenlayerTestHarness {
152            env: self.env,
153            config: self.config,
154            http_endpoint: self.http_endpoint,
155            ws_endpoint: self.ws_endpoint,
156            accounts: self.accounts,
157            eigenlayer_contract_addresses: self.eigenlayer_contract_addresses,
158            _temp_dir: self._temp_dir,
159            _container: self._container,
160            _phantom: PhantomData::<Ctx2>,
161            _auth_proxy: self._auth_proxy,
162            _bridge: self._bridge,
163        }
164    }
165
166    #[must_use]
167    pub fn env(&self) -> &BlueprintEnvironment {
168        &self.env
169    }
170}
171
172impl<Ctx> EigenlayerTestHarness<Ctx> {
173    /// Gets a provider for the HTTP endpoint
174    #[must_use]
175    pub fn provider(&self) -> RootProvider {
176        get_provider_http(self.http_endpoint.as_str())
177    }
178
179    /// Gets the list of accounts
180    #[must_use]
181    pub fn accounts(&self) -> &[Address] {
182        &self.accounts
183    }
184
185    /// Gets the owner account (first account)
186    #[must_use]
187    pub fn owner_account(&self) -> Address {
188        self.accounts[0]
189    }
190
191    /// Gets the aggregator account (ninth account)
192    #[must_use]
193    pub fn aggregator_account(&self) -> Address {
194        self.accounts[9]
195    }
196
197    /// Gets the task generator account (fourth account)
198    #[must_use]
199    pub fn task_generator_account(&self) -> Address {
200        self.accounts[4]
201    }
202}
203
204/// Runs the authentication proxy server.
205///
206/// This function sets up and runs an authenticated proxy server that listens on the configured host and port.
207/// It creates necessary directories for the proxy's database and then starts the server.
208///
209/// # Arguments
210///
211/// * `data_dir` - The path to the data directory where the proxy's database will be stored.
212/// * `auth_proxy_port` - The port on which the proxy server will listen.
213///
214/// # Errors
215///
216/// This function will return an error if:
217/// - It fails to create the necessary directories for the database.
218/// - It fails to bind to the specified host and port.
219/// - The Axum server encounters an error during operation.
220async fn run_auth_proxy(
221    data_dir: PathBuf,
222    auth_proxy_port: u16,
223) -> Result<(RocksDb, impl Future<Output = Result<(), Error>>), Error> {
224    let db_path = data_dir.join("private").join("auth-proxy").join("db");
225    tokio::fs::create_dir_all(&db_path).await?;
226
227    let proxy = blueprint_auth::proxy::AuthenticatedProxy::new(&db_path)?;
228    let db = proxy.db();
229
230    let task = async move {
231        let router = proxy.router();
232        let listener =
233            tokio::net::TcpListener::bind((Ipv4Addr::LOCALHOST, auth_proxy_port)).await?;
234        info!(
235            "Auth proxy listening on {}:{}",
236            Ipv4Addr::LOCALHOST,
237            auth_proxy_port
238        );
239        let result = axum::serve(listener, router).await;
240        if let Err(err) = result {
241            error!("Auth proxy error: {err}");
242        }
243
244        Ok(())
245    };
246
247    Ok((db, task))
248}