diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..6f338d5 --- /dev/null +++ b/Makefile @@ -0,0 +1,14 @@ +images: + bash ./scripts/build_local_images.sh + +module: + bash ./scripts/build_local_modules.sh + +docker: images module + +start: docker + cargo run --bin commit-boost -- init --config config.example.toml + cargo run --bin commit-boost -- start --docker "./cb.docker-compose.yml" --env "./.cb.env" + +stop: + cargo run --bin commit-boost -- stop \ No newline at end of file diff --git a/bin/src/lib.rs b/bin/src/lib.rs index 9aab7bc..efb76a4 100644 --- a/bin/src/lib.rs +++ b/bin/src/lib.rs @@ -2,8 +2,12 @@ pub mod prelude { pub use cb_common::{ commit, commit::request::SignRequest, - config::{load_builder_module_config, load_commit_module_config, StartCommitModuleConfig}, - pbs::{BuilderEvent, BuilderEventClient, OnBuilderApiEvent}, + config::{ + load_builder_module_config, load_commit_module_config, load_preconf_module_config, + StartCommitModuleConfig, StartPreconfModuleConfig, + }, + pbs::{BuilderEvent, BuilderEventClient, OnBuilderApiEvent, RelayEntry}, + types::Chain, utils::{initialize_tracing_log, utcnow_ms, utcnow_ns, utcnow_sec, utcnow_us}, }; pub use cb_metrics::provider::MetricsProvider; diff --git a/config.example.toml b/config.example.toml index d3ed18e..4f21280 100644 --- a/config.example.toml +++ b/config.example.toml @@ -1,4 +1,4 @@ -chain = "Holesky" +chain = "Custom" [pbs] port = 18550 @@ -12,9 +12,8 @@ min_bid_eth = 0.0 late_in_slot_time_ms = 2000 [[relays]] -id = "example-relay" -url = "http://0xa1cec75a3f0661e99299274182938151e8433c61a19222347ea1313d839229cb4ce4e3e5aa2bdeb71c8fcf1b084963c2@abc.xyz" -headers = { X-MyCustomHeader = "MyCustomValue" } +id = "preconf-relay" +url = "http://0xa9e9cff900de07e295a044789fd4bdb6785eb0651ad282f9e76d12afd87e75180bdd64caf2e315b815d7322bd31ab48a@host.docker.internal:4040" enable_timing_games = false target_first_request_ms = 200 frequency_get_header_ms = 300 @@ -22,20 +21,14 @@ frequency_get_header_ms = 300 [signer] [signer.loader] key_path = "./keys.example.json" -# keys_path = "" -# secrets_path = "" [metrics] prometheus_config = "./docker/prometheus.yml" use_grafana = true [[modules]] -id = "DA_COMMIT" -type = "commit" -docker_image = "test_da_commit" -sleep_secs = 5 - -[[modules]] -id = "BUILDER_LOG" -type = "events" -docker_image = "test_builder_log" +id = "PRECONF" +type = "preconf" +docker_image = "test_preconf" +beacon_nodes = ["http://host.docker.internal:33001"] +chain_id = 3151908 diff --git a/crates/cli/src/docker_init.rs b/crates/cli/src/docker_init.rs index adc98ad..57e6d39 100644 --- a/crates/cli/src/docker_init.rs +++ b/crates/cli/src/docker_init.rs @@ -3,9 +3,9 @@ use std::{path::Path, vec}; use cb_common::{ config::{ CommitBoostConfig, ModuleKind, BUILDER_SERVER_ENV, CB_CONFIG_ENV, CB_CONFIG_NAME, JWTS_ENV, - METRICS_SERVER_ENV, MODULE_ID_ENV, MODULE_JWT_ENV, SIGNER_DIR_KEYS, SIGNER_DIR_KEYS_ENV, - SIGNER_DIR_SECRETS, SIGNER_DIR_SECRETS_ENV, SIGNER_KEYS, SIGNER_KEYS_ENV, - SIGNER_SERVER_ENV, + METRICS_SERVER_ENV, MODULE_ID_ENV, MODULE_JWT_ENV, PRECONF_SERVER_ENV, SIGNER_DIR_KEYS, + SIGNER_DIR_KEYS_ENV, SIGNER_DIR_SECRETS, SIGNER_DIR_SECRETS_ENV, SIGNER_KEYS, + SIGNER_KEYS_ENV, SIGNER_SERVER_ENV, }, loader::SignerLoader, utils::random_jwt, @@ -18,7 +18,7 @@ use eyre::Result; use indexmap::IndexMap; use serde::Serialize; -pub(super) const CB_CONFIG_FILE: &str = "cb-config.toml"; +pub(super) const CB_CONFIG_FILE: &str = "config.example.toml"; pub(super) const CB_COMPOSE_FILE: &str = "cb.docker-compose.yml"; pub(super) const CB_ENV_FILE: &str = ".cb.env"; pub(super) const CB_TARGETS_FILE: &str = "targets.json"; // needs to match prometheus.yml @@ -57,6 +57,8 @@ pub fn handle_docker_init(config_path: String, output_dir: String) -> Result<()> let builder_events_port = 30000; let mut builder_events_modules = Vec::new(); + let preconf_server_port = 40000; + // setup pbs service targets.push(PrometheusTargetConfig { targets: vec![format!("cb_pbs:{metrics_port}")], @@ -137,6 +139,42 @@ pub fn handle_docker_init(config_path: String, output_dir: String) -> Result<()> ..Service::default() } } + ModuleKind::Preconf => { + needs_signer_module = true; + + let jwt = random_jwt(); + let jwt_name = format!("CB_JWT_{}", module.id.to_uppercase()); + + // module ids are assumed unique, so envs dont override each other + let module_envs = IndexMap::from([ + get_env_val(MODULE_ID_ENV, &module.id), + get_env_same(CB_CONFIG_ENV), + get_env_interp(MODULE_JWT_ENV, &jwt_name), + get_env_val(METRICS_SERVER_ENV, &metrics_port.to_string()), + get_env_val(SIGNER_SERVER_ENV, &signer_server), + get_env_val(PRECONF_SERVER_ENV, &preconf_server_port.to_string()), + ]); + + envs.insert(jwt_name.clone(), jwt.clone()); + jwts.insert(module.id.clone(), jwt); + + Service { + container_name: Some(module_cid.clone()), + image: Some(module.docker_image), + ports: Ports::Short(vec![format!( + "{}:{}", + preconf_server_port, preconf_server_port + )]), + networks: Networks::Simple(vec![ + METRICS_NETWORK.to_owned(), + SIGNER_NETWORK.to_owned(), + ]), + volumes: vec![config_volume.clone()], + environment: Environment::KvPair(module_envs), + depends_on: DependsOnOptions::Simple(vec!["cb_signer".to_owned()]), + ..Service::default() + } + } }; services.insert(module_cid, Some(module_service)); @@ -288,7 +326,14 @@ pub fn handle_docker_init(config_path: String, output_dir: String) -> Result<()> networks: Networks::Simple(vec![METRICS_NETWORK.to_owned()]), depends_on: DependsOnOptions::Simple(vec!["cb_prometheus".to_owned()]), environment: Environment::List(vec!["GF_SECURITY_ADMIN_PASSWORD=admin".to_owned()]), - volumes: vec![Volumes::Simple("./grafana/dashboards:/etc/grafana/provisioning/dashboards".to_owned()), Volumes::Simple("./grafana/datasources:/etc/grafana/provisioning/datasources".to_owned())], + volumes: vec![ + Volumes::Simple( + "./grafana/dashboards:/etc/grafana/provisioning/dashboards".to_owned(), + ), + Volumes::Simple( + "./grafana/datasources:/etc/grafana/provisioning/datasources".to_owned(), + ), + ], // TODO: re-enable logging here once we move away from docker logs logging: Some(LoggingParameters { driver: Some("none".to_owned()), options: None }), ..Service::default() diff --git a/crates/common/src/config/constants.rs b/crates/common/src/config/constants.rs index 4d96d80..ad815e4 100644 --- a/crates/common/src/config/constants.rs +++ b/crates/common/src/config/constants.rs @@ -3,9 +3,10 @@ pub const MODULE_JWT_ENV: &str = "CB_SIGNER_JWT"; pub const METRICS_SERVER_ENV: &str = "METRICS_SERVER"; pub const SIGNER_SERVER_ENV: &str = "SIGNER_SERVER"; pub const BUILDER_SERVER_ENV: &str = "BUILDER_SERVER"; +pub const PRECONF_SERVER_ENV: &str = "PRECONF_SERVER"; pub const CB_CONFIG_ENV: &str = "CB_CONFIG"; -pub const CB_CONFIG_NAME: &str = "/cb-config.toml"; +pub const CB_CONFIG_NAME: &str = "/config.example.toml"; pub const SIGNER_KEYS_ENV: &str = "CB_SIGNER_FILE"; pub const SIGNER_KEYS: &str = "/keys.json"; diff --git a/crates/common/src/config/module.rs b/crates/common/src/config/module.rs index 508c6b5..2d5c964 100644 --- a/crates/common/src/config/module.rs +++ b/crates/common/src/config/module.rs @@ -5,11 +5,14 @@ use toml::Table; use crate::{ commit::client::SignerClient, config::{ - constants::{CB_CONFIG_ENV, MODULE_ID_ENV, MODULE_JWT_ENV, SIGNER_SERVER_ENV}, + constants::{ + CB_CONFIG_ENV, MODULE_ID_ENV, MODULE_JWT_ENV, PRECONF_SERVER_ENV, SIGNER_SERVER_ENV, + }, load_env_var, utils::load_file_from_env, - BUILDER_SERVER_ENV, + CommitBoostConfig, BUILDER_SERVER_ENV, }, + pbs::RelayEntry, types::Chain, }; @@ -17,6 +20,8 @@ use crate::{ pub enum ModuleKind { #[serde(alias = "commit")] Commit, + #[serde(alias = "preconf")] + Preconf, #[serde(alias = "events")] Events, } @@ -174,3 +179,77 @@ pub fn load_builder_module_config() -> eyre::Result { + pub id: String, + pub chain: Chain, + pub signer_client: SignerClient, + pub server_port: u16, + pub relays: Vec, + pub extra: T, +} + +pub fn load_preconf_module_config() -> Result> { + let module_id = load_env_var(MODULE_ID_ENV)?; + let module_jwt = load_env_var(MODULE_JWT_ENV)?; + let signer_server_address = load_env_var(SIGNER_SERVER_ENV)?; + let preconf_requests_port: u16 = load_env_var(PRECONF_SERVER_ENV)?.parse()?; + + #[derive(Debug, Deserialize)] + struct ThisModuleConfig { + #[serde(flatten)] + static_config: StaticModuleConfig, + #[serde(flatten)] + extra: U, + } + + #[derive(Debug, Deserialize)] + #[serde(untagged)] + enum ThisModule { + Target(ThisModuleConfig), + #[allow(dead_code)] + Other(Table), + } + + #[derive(Deserialize, Debug)] + struct StubConfig { + chain: Chain, + modules: Vec>, + } + + // load module config including the extra data (if any) + let config: StubConfig = load_file_from_env(CB_CONFIG_ENV)?; + + // find all matching modules config + let matches: Vec> = config + .modules + .into_iter() + .filter_map(|m| match m { + ThisModule::Target(config) => Some(config), + _ => None, + }) + .collect(); + + eyre::ensure!(!matches.is_empty(), "Failed to find matching config type"); + + let module_config = matches + .into_iter() + .find(|m| m.static_config.id == module_id) + .wrap_err(format!("failed to find module for {module_id}"))?; + + let signer_client = SignerClient::new(signer_server_address, &module_jwt)?; + + let pbs_config = CommitBoostConfig::from_env_path()?; + let relays = pbs_config.relays.into_iter().map(|r| r.entry).collect::>(); + + Ok(StartPreconfModuleConfig { + id: module_config.static_config.id, + chain: config.chain, + signer_client, + server_port: preconf_requests_port, + extra: module_config.extra, + relays, + }) +} diff --git a/crates/common/src/constants.rs b/crates/common/src/constants.rs index 036cd71..5665ca4 100644 --- a/crates/common/src/constants.rs +++ b/crates/common/src/constants.rs @@ -34,3 +34,11 @@ pub const HELDER_BUILDER_DOMAIN: [u8; 32] = [ 15, 75, 232, 122, 96, 208, 102, 76, 201, 209, 255, ]; pub const HELDER_GENESIS_TIME_SECONDS: u64 = 1718967660; + +// CUSTOM (LimeChain) +pub const CUSTOM_FORK_VERSION: [u8; 4] = [80, 0, 0, 56]; + +pub const CUSTOM_BUILDER_DOMAIN: [u8; 32] = [0, 0, 0, 1, 11, 65, 190, 76, 219, 52, 209, 131, 221, + 220, 165, 57, 131, 55, 98, 109, 205, 207, 175, 23, 32, 193, 32, 45, 59, 149, 248, 78 +]; +pub const CUSTOM_GENESIS_TIME_SECONDS: u64 = 1724410824; diff --git a/crates/common/src/types.rs b/crates/common/src/types.rs index 6767164..d8a144a 100644 --- a/crates/common/src/types.rs +++ b/crates/common/src/types.rs @@ -5,6 +5,7 @@ use crate::constants::{ HOLESKY_BUILDER_DOMAIN, HOLESKY_FORK_VERSION, HOLESKY_GENESIS_TIME_SECONDS, MAINNET_BUILDER_DOMAIN, MAINNET_FORK_VERSION, MAINNET_GENESIS_TIME_SECONDS, RHEA_BUILDER_DOMAIN, RHEA_FORK_VERSION, RHEA_GENESIS_TIME_SECONDS, + CUSTOM_BUILDER_DOMAIN, CUSTOM_FORK_VERSION, CUSTOM_GENESIS_TIME_SECONDS, }; #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)] @@ -13,6 +14,7 @@ pub enum Chain { Holesky, Rhea, Helder, + Custom, } impl Chain { @@ -22,6 +24,7 @@ impl Chain { Chain::Holesky => HOLESKY_BUILDER_DOMAIN, Chain::Rhea => RHEA_BUILDER_DOMAIN, Chain::Helder => HELDER_BUILDER_DOMAIN, + Chain::Custom => CUSTOM_BUILDER_DOMAIN, } } @@ -31,6 +34,7 @@ impl Chain { Chain::Holesky => HOLESKY_FORK_VERSION, Chain::Rhea => RHEA_FORK_VERSION, Chain::Helder => HELDER_FORK_VERSION, + Chain::Custom => CUSTOM_FORK_VERSION, } } @@ -40,6 +44,7 @@ impl Chain { Chain::Holesky => HOLESKY_GENESIS_TIME_SECONDS, Chain::Rhea => RHEA_GENESIS_TIME_SECONDS, Chain::Helder => HELDER_GENESIS_TIME_SECONDS, + Chain::Custom => CUSTOM_GENESIS_TIME_SECONDS, } } } diff --git a/examples/preconf/Cargo.toml b/examples/preconf/Cargo.toml index 5cba7ac..6bdffc1 100644 --- a/examples/preconf/Cargo.toml +++ b/examples/preconf/Cargo.toml @@ -7,21 +7,43 @@ rust-version.workspace = true [dependencies] commit-boost = { path = "../../bin" } +# ethereum +alloy = { workspace = true, features = ["ssz"] } +ethereum-types.workspace = true +ethereum_serde_utils.workspace = true +ethereum_ssz_derive.workspace = true +ethereum_ssz.workspace = true +ssz_rs = "0.9.0" +ssz_types.workspace = true +ethereum-consensus = { git = "https://github.com/ralexstokes/ethereum-consensus", rev = "cf3c404043230559660810bc0c9d6d5a8498d819" } + # async / threads tokio.workspace = true +futures.workspace = true # networking axum.workspace = true +reqwest.workspace = true +reqwest-eventsource = "0.6.0" # serialization serde_json.workspace = true serde.workspace = true +serde_with = "3.3.0" # telemetry tracing.workspace = true prometheus.workspace = true +# crypto +tree_hash.workspace = true +tree_hash_derive.workspace = true + # misc +thiserror.workspace = true eyre.workspace = true +rand.workspace = true +url.workspace = true color-eyre.workspace = true lazy_static.workspace = true +bincode = "1.3.3" diff --git a/examples/preconf/Dockerfile b/examples/preconf/Dockerfile index 2c3d5fe..7c40cf8 100644 --- a/examples/preconf/Dockerfile +++ b/examples/preconf/Dockerfile @@ -14,11 +14,11 @@ COPY . . RUN cargo build --release --bin preconf -FROM ubuntu AS runtime +FROM ubuntu:22.04 AS runtime WORKDIR /app RUN apt-get update -RUN apt-get install -y openssl ca-certificates libssl3 libssl-dev +RUN apt-get install -y openssl ca-certificates libssl3 libssl-dev && rm -rf /var/lib/apt/lists/* COPY --from=builder /app/target/release/preconf /usr/local/bin ENTRYPOINT ["/usr/local/bin/preconf"] diff --git a/examples/preconf/src/api.rs b/examples/preconf/src/api.rs new file mode 100644 index 0000000..6b27499 --- /dev/null +++ b/examples/preconf/src/api.rs @@ -0,0 +1,203 @@ +use std::sync::Arc; + +use alloy::rpc::types::beacon::BlsPublicKey; +use ethereum_consensus::ssz::prelude::{HashTreeRoot, List}; +use axum::{ + extract::State, + http::StatusCode, + response::IntoResponse, + routing::{get, post}, + Json, Router, +}; +use commit_boost::prelude::*; +use eyre::Result; +use futures::future::{join_all, select_ok}; +use reqwest::Client; +use tokio::sync::RwLock; +use tracing::{error, info}; + +use crate::{ + config::ExtraConfig, + constants::{ + GET_NEXT_ACTIVE_SLOT, MAX_TRANSACTIONS_PER_BLOCK, SET_CONSTRAINTS_PATH + }, + error::PreconfError, + types::{Constraint, ConstraintsMessage, ProposerConstraintsV1, SignedConstraints}, + AppState, VAL_RECEIVED_COUNTER, +}; + +pub fn create_router(app_state: AppState) -> Router { + Router::new() + .route( + "/v1/validators", + get({ + let app_state = app_state.clone(); + move || { + let app_state = app_state.clone(); + async move { + let service = app_state.service.read().await; + service.get_pubkeys().await + } + } + }), + ) + .route("/v1/constraints", post(set_constraints)) + .with_state(app_state) +} + +async fn set_constraints( + State(app_state): State, + Json(payload): Json, +) -> Result { + let service = app_state.service.read().await; + + match service.set_constraints(payload).await { + Ok(signed_constraints) => Ok((StatusCode::OK, Json(signed_constraints))), + Err(status_code) => Err(status_code), + } +} + +pub struct PreconfService { + config: StartPreconfModuleConfig, + latest_signed_constraints: Arc>>, +} + +impl PreconfService { + pub async fn new(config: StartPreconfModuleConfig) -> Self { + PreconfService { config, latest_signed_constraints: Arc::new(RwLock::new(None)) } + } + + pub async fn get_pubkeys(&self) -> Result { + match self.config.signer_client.get_pubkeys().await { + Ok(pubkeys_response) => { + let response = serde_json::json!(pubkeys_response); + VAL_RECEIVED_COUNTER.inc(); + Ok((StatusCode::OK, Json(response))) + } + Err(err) => { + error!(?err, "Failed to get pubkeys"); + Err(StatusCode::INTERNAL_SERVER_ERROR) + } + } + } + + pub async fn get_next_active_slot( + config: &StartPreconfModuleConfig, + pubkey: &BlsPublicKey, + ) -> Result { + let mut handles = Vec::with_capacity(config.relays.len()); + let client = Client::new(); + + for relay in &config.relays { + let url = format!( + "{}{}", + relay.url, + GET_NEXT_ACTIVE_SLOT.replace(":pubkey", &pubkey.to_string()) + ); + handles.push(client.get(&url).send()); + } + + let results = select_ok(handles).await; + match results { + Ok((response, _remaining)) => { + let code = response.status(); + if !code.is_success() { + let response_bytes = response.bytes().await?; + error!(?code, "Failed to fetch slot"); + return Err(PreconfError::RelayResponse { + error_msg: String::from_utf8_lossy(&response_bytes).into_owned(), + code: code.as_u16(), + }); + } + Ok(response) + } + Err(e) => Err(PreconfError::Reqwest(e)), + } + } + + pub async fn set_constraints(&self, payload: ProposerConstraintsV1) -> Result<(), StatusCode> { + let pubkeys = self.config.signer_client.get_pubkeys().await.map_err(|err| { + error!(?err, "Failed to get pubkeys"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + let pubkey = pubkeys.consensus.first().ok_or_else(|| { + error!("No key available"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + let next_active_slot = match Self::get_next_active_slot(&self.config, pubkey).await { + Ok(response) => response.json::().await.map_err(|err| { + error!(?err, "Failed to parse slot"); + StatusCode::INTERNAL_SERVER_ERROR + })?, + Err(err) => { + error!(?err, "Failed to fetch slot"); + return Err(StatusCode::INTERNAL_SERVER_ERROR); + } + }; + + info!("Next Active slot: {}", next_active_slot); + + let mut constraints_inner: List = List::default(); + for tx in payload.top.iter() { + let constraint = Constraint { tx: tx.clone() }; + constraints_inner.push(constraint); + } + + let mut constraints: List, MAX_TRANSACTIONS_PER_BLOCK> = List::default(); + constraints.push(constraints_inner); + + let message = ConstraintsMessage { slot: next_active_slot, constraints }; + let tree_hash_root_result = message.hash_tree_root(); + let tree_hash_root = tree_hash_root_result.as_deref().unwrap(); + + let request = SignRequest::builder(&self.config.id, *pubkey).with_root(*tree_hash_root); + let signature = + self.config.signer_client.request_signature(&request).await.map_err(|err| { + error!(?err, "Failed to request signature"); + StatusCode::INTERNAL_SERVER_ERROR + })?; + + let signed_constraints = SignedConstraints { message, signature }; + + let mut handles = Vec::new(); + + info!("Received constraints signature: {signature}"); + info!("Sending constraints {}", serde_json::to_string(&signed_constraints).unwrap()); + + for relay in &self.config.relays { + let client = Client::new(); + handles.push( + client + .post(format!("{}{SET_CONSTRAINTS_PATH}", relay.url)) + .json(&signed_constraints) + .send(), + ); + } + + let results = join_all(handles).await; + + for res in results { + match res { + Ok(response) => { + let status = response.status(); + let response_bytes = response.bytes().await.expect("failed to get bytes"); + let ans = String::from_utf8_lossy(&response_bytes).into_owned(); + if !status.is_success() { + error!(err = ans, ?status, "failed sending set constraints request"); + continue; + } + + // Store the latest signed constraints in memory + let mut latest_constraints = self.latest_signed_constraints.write().await; + *latest_constraints = Some(signed_constraints.clone()); + + info!("Successful set constraints: {ans:?}") + } + Err(err) => error!("Failed set constraints: {err}"), + } + } + + Ok(()) + } +} diff --git a/examples/preconf/src/beacon_client/client.rs b/examples/preconf/src/beacon_client/client.rs new file mode 100644 index 0000000..100c003 --- /dev/null +++ b/examples/preconf/src/beacon_client/client.rs @@ -0,0 +1,333 @@ +#![allow(dead_code)] +use std::{ + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, + time::Duration, +}; + +use alloy::{ + primitives::B256, + rpc::types::beacon::events::{HeadEvent, PayloadAttributesEvent}, +}; +use futures::{future::join_all, StreamExt}; +use reqwest_eventsource::EventSource; +use tokio::{ + sync::{ + broadcast::{self, Sender}, + mpsc::UnboundedSender, + }, + task::JoinError, + time::sleep, +}; +use tracing::{debug, error, warn}; +use url::Url; + +use crate::beacon_client::{ + error::BeaconClientError, + types::{ApiResult, BeaconResponse, ProposerDuty, SyncStatus}, +}; + +const EPOCH_SLOTS: u64 = 32; +const BEACON_CLIENT_REQUEST_TIMEOUT: Duration = Duration::from_secs(5); +const PROPOSER_DUTIES_REFRESH_FREQ: u64 = EPOCH_SLOTS / 4; + +/// Handles communication with multiple `BeaconClient` instances. +/// Load balances requests. +#[derive(Clone)] +pub struct MultiBeaconClient { + /// Vec of all beacon clients with a fixed usize ID used when + /// fetching: `beacon_clients_by_last_response` + pub beacon_clients: Vec<(usize, Arc)>, + /// The ID of the beacon client with the most recent successful response. + pub best_beacon_instance: Arc, +} + +impl MultiBeaconClient { + pub fn new(beacon_clients: Vec>) -> Self { + let beacon_clients_with_index = beacon_clients.into_iter().enumerate().collect(); + + Self { + beacon_clients: beacon_clients_with_index, + best_beacon_instance: Arc::new(AtomicUsize::new(0)), + } + } + + pub fn from_endpoint_strs(endpoints: &[String]) -> Self { + let clients = endpoints + .iter() + .map(|endpoint| Arc::new(BeaconClient::from_endpoint_str(endpoint))) + .collect(); + Self::new(clients) + } + + /// Retrieves the sync status from multiple beacon clients and selects the + /// best one. + /// + /// The function spawns async tasks to fetch the sync status from each + /// beacon client. It then selects the sync status with the highest + /// `head_slot`. + pub async fn best_sync_status(&self) -> Result { + let clients = self.beacon_clients_by_last_response(); + + let handles = clients + .into_iter() + .map(|(_, client)| tokio::spawn(async move { client.sync_status().await })) + .collect::>(); + + let results: Vec, JoinError>> = + join_all(handles).await; + + let mut best_sync_status: Option = None; + for join_result in results { + match join_result { + Ok(sync_status_result) => match sync_status_result { + Ok(sync_status) => { + if best_sync_status.as_ref().map_or(true, |current_best| { + current_best.head_slot < sync_status.head_slot + }) { + best_sync_status = Some(sync_status); + } + } + Err(err) => warn!("Failed to get sync status: {err:?}"), + }, + Err(join_err) => { + error!("Tokio join error for best_sync_status: {join_err:?}") + } + } + } + + best_sync_status.ok_or(BeaconClientError::BeaconNodeUnavailable) + } + + pub async fn get_proposer_duties( + &self, + epoch: u64, + ) -> Result<(B256, Vec), BeaconClientError> { + let clients = self.beacon_clients_by_last_response(); + let mut last_error = None; + + for (i, client) in clients.into_iter() { + match client.get_proposer_duties(epoch).await { + Ok(proposer_duties) => { + self.best_beacon_instance.store(i, Ordering::Relaxed); + return Ok(proposer_duties); + } + Err(err) => { + last_error = Some(err); + } + } + } + + Err(last_error.unwrap_or(BeaconClientError::BeaconNodeUnavailable)) + } + + /// `subscribe_to_payload_attributes_events` subscribes to payload + /// attributes events from all beacon nodes. + /// + /// This function swaps async tasks for all beacon clients. Therefore, + /// a single payload event will be received multiple times, likely once for + /// every beacon node. + pub async fn subscribe_to_payload_attributes_events( + &self, + chan: Sender, + ) { + let clients = self.beacon_clients_by_last_response(); + + for (_, client) in clients { + let chan = chan.clone(); + tokio::spawn(async move { + client.subscribe_to_payload_attributes_events(chan).await; + }); + } + } + + /// `subscribe_to_head_events` subscribes to head events from all beacon + /// nodes. + /// + /// This function swaps async tasks for all beacon clients. Therefore, + /// a single head event will be received multiple times, likely once for + /// every beacon node. + pub async fn subscribe_to_head_events(&self, chan: Sender) { + let clients = self.beacon_clients_by_last_response(); + + for (_, client) in clients { + let chan = chan.clone(); + tokio::spawn(async move { + client.subscribe_to_head_events(chan).await; + }); + } + } + + /// `subscribe_to_proposer_duties` listens to new `PayloadAttributesEvent`s + /// through `rx`. Fetches the chain proposer duties every 8 slots and + /// sends them down `tx`. + pub async fn subscribe_to_proposer_duties( + self, + tx: UnboundedSender>, + mut rx: broadcast::Receiver, + ) { + let mut last_updated_slot = 0; + + while let Ok(payload) = rx.recv().await { + let new_slot = payload.data.proposal_slot; + + if last_updated_slot == 0 || + (new_slot > last_updated_slot && new_slot % PROPOSER_DUTIES_REFRESH_FREQ == 0) + { + last_updated_slot = new_slot; + tokio::spawn(fetch_and_send_duties_for_slot(new_slot, tx.clone(), self.clone())); + } + } + } + + /// Returns a list of beacon clients, prioritized by the last successful + /// response. + /// + /// The beacon client with the most recent successful response is placed at + /// the beginning of the returned vector. All other clients maintain + /// their original order. + pub fn beacon_clients_by_last_response(&self) -> Vec<(usize, Arc)> { + let mut instances = self.beacon_clients.clone(); + let index = self.best_beacon_instance.load(Ordering::Relaxed); + if index != 0 { + let pos = instances.iter().position(|(i, _)| *i == index).unwrap(); + instances.swap(0, pos); + } + instances + } +} + +/// Handles communication to a single beacon client url. +#[derive(Clone, Debug)] +pub struct BeaconClient { + pub http: reqwest::Client, + pub endpoint: Url, +} + +impl BeaconClient { + pub fn new(http: reqwest::Client, endpoint: Url) -> Self { + Self { http, endpoint } + } + + pub fn from_endpoint_str(endpoint: &str) -> Self { + let endpoint = Url::parse(endpoint).unwrap(); + let client = + reqwest::ClientBuilder::new().timeout(BEACON_CLIENT_REQUEST_TIMEOUT).build().unwrap(); + Self::new(client, endpoint) + } + + pub async fn http_get(&self, path: &str) -> Result { + let target = self.endpoint.join(path)?; + Ok(self.http.get(target).send().await?) + } + + pub async fn get( + &self, + path: &str, + ) -> Result { + let result = self.http_get(path).await?.json().await?; + match result { + ApiResult::Ok(result) => Ok(result), + ApiResult::Err(err) => Err(BeaconClientError::Api(err)), + } + } + + pub async fn sync_status(&self) -> Result { + let response: BeaconResponse = self.get("eth/v1/node/syncing").await?; + Ok(response.data) + } + + pub async fn get_proposer_duties( + &self, + epoch: u64, + ) -> Result<(B256, Vec), BeaconClientError> { + let endpoint = format!("eth/v1/validator/duties/proposer/{epoch}"); + let mut result: BeaconResponse> = self.get(&endpoint).await?; + let dependent_root_value = result.meta.remove("dependent_root").ok_or_else(|| { + BeaconClientError::MissingExpectedData( + "missing `dependent_root` in response".to_string(), + ) + })?; + let dependent_root: B256 = serde_json::from_value(dependent_root_value)?; + Ok((dependent_root, result.data)) + } + + pub async fn subscribe_to_payload_attributes_events( + &self, + chan: Sender, + ) { + self.subscribe_to_sse("payload_attributes", chan).await + } + + async fn subscribe_to_head_events(&self, chan: Sender) { + self.subscribe_to_sse("head", chan).await + } + + /// Subscribe to SSE events from the beacon client `events` endpoint. + pub async fn subscribe_to_sse( + &self, + topic: &str, + chan: Sender, + ) { + let url = format!("{}eth/v1/events?topics={}", self.endpoint, topic); + + loop { + let mut es = EventSource::get(&url); + + while let Some(event) = es.next().await { + match event { + Ok(reqwest_eventsource::Event::Message(message)) => { + match serde_json::from_str::(&message.data) { + Ok(data) => { + if chan.send(data).is_err() { + debug!("no subscribers connected to sse broadcaster"); + } + } + Err(err) => error!(err=%err, "Error parsing chunk"), + } + } + Ok(reqwest_eventsource::Event::Open) => {} + Err(err) => { + warn!(err=%err, "SSE stream ended, reconnecting..."); + es.close(); + break; + } + } + } + sleep(Duration::from_millis(500)).await; + } + } +} + +async fn fetch_and_send_duties_for_slot( + slot: u64, + tx: UnboundedSender>, + beacon_client: MultiBeaconClient, +) { + let epoch = slot / EPOCH_SLOTS; + + // Fetch for `epoch` and `epoch + 1`; + let mut all_duties = Vec::with_capacity(64); + match beacon_client.get_proposer_duties(epoch).await { + Ok((_, mut duties)) => { + all_duties.append(&mut duties); + } + Err(err) => { + warn!(?err, %epoch, "failed fetching duties") + } + } + match beacon_client.get_proposer_duties(epoch + 1).await { + Ok((_, mut duties)) => { + all_duties.append(&mut duties); + } + Err(err) => { + warn!(?err, epoch=%epoch+1, "failed fetching duties") + } + } + + if let Err(err) = tx.send(all_duties) { + error!(?err, "error sending duties"); + } +} diff --git a/examples/preconf/src/beacon_client/error.rs b/examples/preconf/src/beacon_client/error.rs new file mode 100644 index 0000000..b294466 --- /dev/null +++ b/examples/preconf/src/beacon_client/error.rs @@ -0,0 +1,20 @@ +#[derive(Debug, thiserror::Error)] +pub enum BeaconClientError { + #[error("Reqwest error: {0}")] + ReqwestError(#[from] reqwest::Error), + + #[error("URL parse error: {0}")] + UrlError(#[from] url::ParseError), + + #[error("JSON serialization/deserialization error: {0}")] + Json(#[from] serde_json::Error), + + #[error("error from API: {0}")] + Api(String), + + #[error("missing expected data in response: {0}")] + MissingExpectedData(String), + + #[error("beacon node unavailable")] + BeaconNodeUnavailable, +} diff --git a/examples/preconf/src/beacon_client/mod.rs b/examples/preconf/src/beacon_client/mod.rs new file mode 100644 index 0000000..e4f8bc5 --- /dev/null +++ b/examples/preconf/src/beacon_client/mod.rs @@ -0,0 +1,11 @@ +use serde::{Deserialize, Serialize}; + +pub mod client; +pub mod error; +pub mod types; + +#[derive(Clone, Debug, Default, Serialize, Deserialize)] +pub struct BeaconClientConfig { + pub beacon_client_addresses: Vec, + pub core: Option, +} diff --git a/examples/preconf/src/beacon_client/types.rs b/examples/preconf/src/beacon_client/types.rs new file mode 100644 index 0000000..e9ac3a8 --- /dev/null +++ b/examples/preconf/src/beacon_client/types.rs @@ -0,0 +1,89 @@ +use std::collections::HashMap; + +use alloy::rpc::types::beacon::BlsPublicKey; +use serde::{de::DeserializeOwned, Deserialize, Serialize}; +use serde_with::{serde_as, DisplayFromStr}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +#[serde(bound = "T: Serialize + serde::de::DeserializeOwned")] +#[serde(untagged)] +pub enum ApiResult { + Ok(T), + Err(String), +} + +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +#[serde(bound = "T: Serialize + serde::de::DeserializeOwned")] +pub struct BeaconResponse { + pub data: T, + #[serde(flatten)] + pub meta: HashMap, +} + +#[serde_as] +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct SyncStatus { + #[serde_as(as = "DisplayFromStr")] + pub head_slot: u64, + #[serde_as(as = "DisplayFromStr")] + pub sync_distance: usize, + pub is_syncing: bool, +} + +#[serde_as] +#[derive(Debug, Serialize, Deserialize, Clone, Default, PartialEq, Eq)] +pub struct ProposerDuty { + #[serde_as(as = "DisplayFromStr")] + #[serde(rename = "pubkey")] + pub public_key: BlsPublicKey, + #[serde_as(as = "DisplayFromStr")] + pub validator_index: u64, + #[serde_as(as = "DisplayFromStr")] + pub slot: u64, +} + +#[cfg(test)] +mod tests { + use alloy::{primitives::hex::FromHex, rpc::types::beacon::BlsPublicKey}; + + use super::{BeaconResponse, ProposerDuty}; + + #[test] + fn test_beacon_proposer() { + let data = r#"{ + "dependent_root": "0x5fd8a9bc4111be67ad969970ad3bc9ccc1a398cc8ea033650b61f58803b0a847", + "execution_optimistic": false, + "data": [ + { + "pubkey": "0xab7f3ed5f4f9d6136b90c22eeae38faa892036971e1a0245a5472da57ae7fcf6ba29d55dd4d162301fb256822e46261c", + "validator_index": "467380", + "slot": "9079424" + }, + { + "pubkey": "0xa9e4b8c958f25df42fcbccdc7547b101d9ad4c31081438479234f7f2e01a0b726dd91b9394b32efd03336794344981a9", + "validator_index": "1291439", + "slot": "9079425" + } + ] + }"#; + + let decoded = serde_json::from_str::>>(data).unwrap(); + + assert!(decoded.meta.contains_key("dependent_root")); + + let duties = decoded.data; + + assert_eq!( + duties[0].public_key, + BlsPublicKey::from_hex("0xab7f3ed5f4f9d6136b90c22eeae38faa892036971e1a0245a5472da57ae7fcf6ba29d55dd4d162301fb256822e46261c").unwrap() + ); + assert_eq!(duties[0].validator_index, 467380); + assert_eq!(duties[0].slot, 9079424); + assert_eq!( + duties[1].public_key, + BlsPublicKey::from_hex("0xa9e4b8c958f25df42fcbccdc7547b101d9ad4c31081438479234f7f2e01a0b726dd91b9394b32efd03336794344981a9").unwrap() + ); + assert_eq!(duties[1].validator_index, 1291439); + assert_eq!(duties[1].slot, 9079425); + } +} diff --git a/examples/preconf/src/config.rs b/examples/preconf/src/config.rs new file mode 100644 index 0000000..bf4d7ae --- /dev/null +++ b/examples/preconf/src/config.rs @@ -0,0 +1,7 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Default, Clone)] +pub struct ExtraConfig { + pub beacon_nodes: Vec, + pub chain_id: u64, +} diff --git a/examples/preconf/src/constants.rs b/examples/preconf/src/constants.rs new file mode 100644 index 0000000..d095b64 --- /dev/null +++ b/examples/preconf/src/constants.rs @@ -0,0 +1,7 @@ +pub const MAX_TOP_TRANSACTIONS: usize = 10_000; +pub const MAX_REST_TRANSACTIONS: usize = 10_000; +pub const MAX_TRANSACTIONS_PER_BLOCK: usize = 10_000; + +pub const ELECT_PRECONFER_PATH: &str = "/eth/v1/builder/elect_preconfer"; +pub const SET_CONSTRAINTS_PATH: &str = "/eth/v1/builder/set_constraints"; +pub const GET_NEXT_ACTIVE_SLOT: &str = "/eth/v1/builder/next_active_slot/:pubkey"; diff --git a/examples/preconf/src/elector.rs b/examples/preconf/src/elector.rs new file mode 100644 index 0000000..87dd5da --- /dev/null +++ b/examples/preconf/src/elector.rs @@ -0,0 +1,120 @@ +use std::{collections::HashMap, time::Duration}; + +use alloy::rpc::types::beacon::BlsPublicKey; +use commit_boost::prelude::*; +use futures::future::join_all; +use reqwest::Client; +use tokio::{sync::mpsc, time::sleep}; +use tracing::{error, info, warn}; + +use crate::{ + beacon_client::types::ProposerDuty, + config::ExtraConfig, + constants::ELECT_PRECONFER_PATH, + types::{PreconferElection, SignedPreconferElection}, +}; + +pub struct PreconfElector { + pub config: StartPreconfModuleConfig, + _next_slot: u64, + _elections: HashMap, + duties_rx: mpsc::UnboundedReceiver>, +} + +impl PreconfElector { + pub fn new( + config: StartPreconfModuleConfig, + duties_rx: mpsc::UnboundedReceiver>, + ) -> Self { + Self { config, _next_slot: 0, _elections: HashMap::new(), duties_rx } + } +} + +impl PreconfElector { + pub async fn run(mut self) -> eyre::Result<()> { + info!("Fetching validator pubkeys"); + + let consensus_pubkeys = match self.config.signer_client.get_pubkeys().await { + Ok(pubkeys) => pubkeys.consensus, + Err(err) => { + warn!("Failed to fetch pubkeys: {err}"); + info!("Waiting a bit before retrying"); + sleep(Duration::from_secs(10)).await; + self.config.signer_client.get_pubkeys().await?.consensus + } + }; + info!("Fetched {} pubkeys", consensus_pubkeys.len()); + + while let Some(duties) = self.duties_rx.recv().await { + let l = duties.len(); + let our_duties: Vec<_> = + duties.into_iter().filter(|d| consensus_pubkeys.contains(&d.public_key)).collect(); + + info!("Received {l} duties, we have {} to elect", our_duties.len()); + + for duty in our_duties { + // this could be done in parallel + if let Err(err) = self.elect_proposer(duty).await { + error!("Failed to elect gateway: {err}"); + }; + } + } + + Ok(()) + } + + /// Delegate preconf rights + async fn elect_proposer(&mut self, duty: ProposerDuty) -> eyre::Result<()> { + info!(slot = duty.slot, validator_pubkey = %duty.public_key, "Sending gateway delegation"); + + let election_message = PreconferElection { + preconfer_pubkey: duty.public_key, + slot_number: duty.slot, + chain_id: self.config.extra.chain_id, + gas_limit: 0, + }; + + let request = + SignRequest::builder(&self.config.id, duty.public_key).with_msg(&election_message); + + let signature = self.config.signer_client.request_signature(&request).await?; + + let signed_election = SignedPreconferElection { message: election_message, signature }; + + let mut handles = Vec::new(); + + info!("Received delegation signature: {signature}"); + info!("Sending delegation {}", serde_json::to_string(&signed_election).unwrap()); + + for relay in &self.config.relays { + let client = Client::new(); + handles.push( + client + .post(format!("{}{ELECT_PRECONFER_PATH}", relay.url)) + .json(&signed_election) + .send(), + ); + } + + let results = join_all(handles).await; + + for res in results { + match res { + Ok(response) => { + let status = response.status(); + let response_bytes = response.bytes().await.expect("failed to get bytes"); + let ans = String::from_utf8_lossy(&response_bytes).into_owned(); + if !status.is_success() { + error!(err = ans, ?status, "failed sending delegation sign request"); + continue; + } + + info!("Successful election: {ans:?}") + } + Err(err) => error!("Failed election: {err}"), + } + } + + Ok(()) + } +} diff --git a/examples/preconf/src/error.rs b/examples/preconf/src/error.rs new file mode 100644 index 0000000..323632f --- /dev/null +++ b/examples/preconf/src/error.rs @@ -0,0 +1,11 @@ +#[derive(Debug, thiserror::Error)] +pub enum PreconfError { + #[error("serde decode error: {0}")] + SerdeDecodeError(#[from] serde_json::Error), + + #[error("reqwest error: {0}")] + Reqwest(#[from] reqwest::Error), + + #[error("relay response error. Code: {code}, err: {error_msg}")] + RelayResponse { error_msg: String, code: u16 }, +} diff --git a/examples/preconf/src/main.rs b/examples/preconf/src/main.rs index 9b42cdc..e3f4a6a 100644 --- a/examples/preconf/src/main.rs +++ b/examples/preconf/src/main.rs @@ -1,14 +1,29 @@ use std::{net::SocketAddr, sync::Arc}; -use axum::{http::StatusCode, response::IntoResponse, routing::get, Json, Router}; +use api::PreconfService; +use beacon_client::client::MultiBeaconClient; use commit_boost::prelude::*; +use config::ExtraConfig; +use elector::PreconfElector; use eyre::{bail, Result}; use lazy_static::lazy_static; use prometheus::{IntCounter, Registry}; -use serde::Deserialize; -use serde_json::json; -use tokio::{net::TcpListener, sync::RwLock}; +use tokio::{ + net::TcpListener, + sync::{mpsc, RwLock}, +}; use tracing::{error, info}; +use types::AppState; + +use crate::api::create_router; + +mod api; +mod beacon_client; +mod config; +mod constants; +mod elector; +mod error; +mod types; // You can define custom metrics and a custom registry for the business logic of // your module. These will be automatically scraped by the Prometheus server @@ -19,93 +34,58 @@ lazy_static! { IntCounter::new("validators_received", "successful validators requests received").unwrap(); } -struct PreconfService { - config: StartCommitModuleConfig, -} - -#[derive(Debug, Deserialize)] -struct ExtraConfig { - port: u16, -} - -#[derive(Clone)] -struct AppState { - service: Arc>, -} - -impl PreconfService { - pub async fn run(self) -> Result<()> { - let port = self.config.extra.port; - info!("Starting server on port {}", port); - - let app_state = AppState { service: Arc::new(RwLock::new(self)) }; - - let router = Router::new() - .route( - "/v1/validators", - get({ - let app_state = app_state.clone(); - move || { - let app_state = app_state.clone(); - async move { - let service = app_state.service.read().await; - service.get_pubkeys().await - } - } - }), - ) - .with_state(app_state); - - let address = SocketAddr::from(([0, 0, 0, 0], port)); - let listener = TcpListener::bind(&address).await?; - - axum::serve(listener, router).await?; - - bail!("Server stopped unexpectedly") - } - - async fn get_pubkeys(&self) -> Result { - match self.config.signer_client.get_pubkeys().await { - Ok(pubkeys_response) => { - let response = json!(pubkeys_response); - VAL_RECEIVED_COUNTER.inc(); - Ok((StatusCode::OK, Json(response))) - } - Err(err) => { - error!(?err, "Failed to get pubkeys"); - Err(StatusCode::INTERNAL_SERVER_ERROR) - } - } - } -} - #[tokio::main] async fn main() -> Result<()> { color_eyre::install()?; initialize_tracing_log(); - // Remember to register all your metrics before starting the process + // Register metrics MY_CUSTOM_REGISTRY.register(Box::new(VAL_RECEIVED_COUNTER.clone()))?; - // Spin up a server that exposes the /metrics endpoint to Prometheus MetricsProvider::load_and_run(MY_CUSTOM_REGISTRY.clone())?; - match load_commit_module_config::() { + match load_preconf_module_config::() { Ok(config) => { + let (beacon_tx, _) = tokio::sync::broadcast::channel(10); + let multi_beacon_client = + MultiBeaconClient::from_endpoint_strs(&config.extra.beacon_nodes); + multi_beacon_client.subscribe_to_payload_attributes_events(beacon_tx.clone()).await; + let (duties_tx, duties_rx) = mpsc::unbounded_channel(); + tokio::spawn( + multi_beacon_client.subscribe_to_proposer_duties(duties_tx, beacon_tx.subscribe()), + ); + info!( module_id = config.id, - port = config.extra.port, + port = config.server_port, "Starting module with custom data" ); - let service = PreconfService { config }; + let service = PreconfService::new(config.clone()).await; + let app_state = AppState { service: Arc::new(RwLock::new(service)) }; + + let router = create_router(app_state); + let port = config.server_port; + let address = SocketAddr::from(([0, 0, 0, 0], port)); + let listener = TcpListener::bind(&address).await?; + + tokio::spawn(async move { + if let Err(err) = axum::serve(listener, router).await { + error!(?err, "Axum server encountered an error"); + } + }); - if let Err(err) = service.run().await { - error!(?err, "Service failed"); + let elector = PreconfElector::new(config.clone(), duties_rx); + + if let Err(err) = elector.run().await { + error!(?err, "Error running elector") } + + bail!("Server stopped unexpectedly"); } Err(err) => { error!(?err, "Failed to load module config"); } } + Ok(()) } diff --git a/examples/preconf/src/types.rs b/examples/preconf/src/types.rs new file mode 100644 index 0000000..0435cc3 --- /dev/null +++ b/examples/preconf/src/types.rs @@ -0,0 +1,55 @@ +use std::sync::Arc; + +use alloy::rpc::types::beacon::{BlsPublicKey, BlsSignature}; +use tokio::sync::RwLock; +use ethereum_consensus::{ + deneb::{minimal::MAX_BYTES_PER_TRANSACTION, Transaction}, ssz::prelude::* +}; +use tree_hash_derive::TreeHash; + +use crate::{api::PreconfService, constants::{MAX_REST_TRANSACTIONS, MAX_TOP_TRANSACTIONS, MAX_TRANSACTIONS_PER_BLOCK}}; + +/// Details of a signed constraints. +#[derive(Debug, Default, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)] +pub struct SignedConstraints { + pub message: ConstraintsMessage, + pub signature: BlsSignature, +} + +/// Represents the message of a constraint. +#[derive(Debug, Default, PartialEq, Eq, Clone, SimpleSerialize, serde::Serialize, serde::Deserialize)] +pub struct ConstraintsMessage { + pub slot: u64, + pub constraints: List, MAX_TRANSACTIONS_PER_BLOCK>, +} + +#[derive(Debug, Default, PartialEq, Eq, Clone, SimpleSerialize, serde::Serialize, serde::Deserialize)] + +pub struct Constraint { + pub tx: Transaction, +} + +#[derive(Clone)] +pub struct AppState { + pub service: Arc>, +} + +#[derive(Debug, Default, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)] +pub struct SignedPreconferElection { + pub message: PreconferElection, + pub signature: BlsSignature, +} + +#[derive(Debug, Default, PartialEq, Eq, Clone, TreeHash, serde::Serialize, serde::Deserialize)] +pub struct PreconferElection { + pub preconfer_pubkey: BlsPublicKey, + pub slot_number: u64, + pub chain_id: u64, + pub gas_limit: u64, +} + +#[derive(Debug, Default, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)] +pub struct ProposerConstraintsV1 { + pub top: List, MAX_TOP_TRANSACTIONS>, + pub rest: List, MAX_REST_TRANSACTIONS>, +} diff --git a/keys.example.json b/keys.example.json index de6c2b9..6f084f8 100644 --- a/keys.example.json +++ b/keys.example.json @@ -1,4 +1,3 @@ [ - "0088e364a5396a81b50febbdc8784663fb9089b5e67cbdc173991a00c587673f", - "0x16f3bec1b7f4f1b87c5e1930f944a6dda76ad211a89bc98e8c8e88b5069f8a04" + "0dce41fa73ae9f6bdfd51df4d422d75eee174553dba5fd450c4437e4ed3fc903" ] \ No newline at end of file diff --git a/scripts/build_local_modules.sh b/scripts/build_local_modules.sh index ddbad51..ede3372 100644 --- a/scripts/build_local_modules.sh +++ b/scripts/build_local_modules.sh @@ -2,5 +2,6 @@ set -euo pipefail -docker build -t test_da_commit . -f examples/da_commit/Dockerfile -docker build -t test_builder_log . -f examples/builder_log/Dockerfile \ No newline at end of file +# docker build -t test_da_commit . -f examples/da_commit/Dockerfile +# docker build -t test_builder_log . -f examples/builder_log/Dockerfile +docker build -t test_preconf . -f examples/preconf/Dockerfile \ No newline at end of file diff --git a/tests/tests/config.rs b/tests/tests/config.rs index b7775f4..69d4df0 100644 --- a/tests/tests/config.rs +++ b/tests/tests/config.rs @@ -5,8 +5,7 @@ use eyre::Result; async fn test_load_config() -> Result<()> { let config = CommitBoostConfig::from_file("../config.example.toml")?; - assert_eq!(config.chain, Chain::Holesky); - assert!(config.relays[0].headers.is_some()); + assert_eq!(config.chain, Chain::Custom); // TODO: add more Ok(()) }