├── .gitignore ├── keys.example.json ├── src ├── config.rs ├── types.rs ├── lookahead │ ├── error.rs │ └── mod.rs ├── pbs │ └── mod.rs ├── main.rs ├── inclusion_boost │ ├── error.rs │ ├── types.rs │ ├── mod.rs │ └── sidecar.rs └── test │ └── mod.rs ├── Dockerfile ├── config.toml ├── cb.docker-compose.yml ├── cb-config.toml ├── Cargo.toml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /keys.example.json: -------------------------------------------------------------------------------- 1 | [ 2 | "0088e364a5396a81b50febbdc8784663fb9089b5e67cbdc173991a00c587673f", 3 | "0x16f3bec1b7f4f1b87c5e1930f944a6dda76ad211a89bc98e8c8e88b5069f8a04" 4 | ] -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Serialize, Deserialize, Default, Clone)] 4 | pub struct InclusionListConfig { 5 | pub beacon_api: String, 6 | pub execution_api: String, 7 | pub relay: String, 8 | } 9 | -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Debug, Default, Clone, Serialize, Deserialize)] 4 | pub struct Module { 5 | pub id: String, 6 | } 7 | 8 | #[derive(Debug, Default, Clone, Serialize, Deserialize)] 9 | pub struct MainConfig { 10 | pub modules: Vec, 11 | } 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM lukemathwalker/cargo-chef:latest-rust-1 AS chef 2 | WORKDIR /app 3 | 4 | FROM chef AS planner 5 | COPY . . 6 | RUN cargo chef prepare --recipe-path recipe.json 7 | 8 | FROM chef AS builder 9 | COPY --from=planner /app/recipe.json recipe.json 10 | 11 | RUN cargo chef cook --release --recipe-path recipe.json 12 | 13 | COPY . . 14 | RUN cargo build --release --bin il-boost 15 | 16 | 17 | FROM ubuntu AS runtime 18 | WORKDIR /app 19 | 20 | RUN apt-get update 21 | RUN apt-get install -y openssl ca-certificates libssl3 libssl-dev 22 | 23 | COPY --from=builder /app/target/release/il-boost /usr/local/bin 24 | ENTRYPOINT ["/usr/local/bin/il-boost"] 25 | -------------------------------------------------------------------------------- /config.toml: -------------------------------------------------------------------------------- 1 | chain = "Holesky" 2 | 3 | [pbs] 4 | port = 18550 5 | relays = [] 6 | relay_check = true 7 | timeout_get_header_ms = 950 8 | timeout_get_payload_ms = 4000 9 | timeout_register_validator_ms = 3000 10 | skip_sigverify = true 11 | min_bid_eth = 0.0 12 | beacon_api = "http://beacon.api.url" 13 | execution_api = "http://execution.api.url" 14 | relay = "http://relay.url" 15 | 16 | [headers] 17 | X-MyCustomHeader = "MyCustomValue" 18 | 19 | [signer] 20 | [signer.loader] 21 | key_path = "./keys.example.json" 22 | # keys_path = "" 23 | # secrets_path = "" 24 | 25 | [metrics] 26 | prometheus_config = "./docker/prometheus.yml" 27 | use_grafana = true 28 | 29 | [[modules]] 30 | id = "IL_COMMIT" 31 | docker_image = "test_il_commit" 32 | sleep_secs = 5 33 | beacon_api = "http://beacon.api.url" 34 | execution_api = "http://execution.api.url" 35 | relay = "http://relay.url" 36 | -------------------------------------------------------------------------------- /src/lookahead/error.rs: -------------------------------------------------------------------------------- 1 | use std::num::ParseIntError; 2 | 3 | #[derive(Debug)] 4 | pub enum LookaheadError { 5 | BeaconApiClientError(beacon_api_client::Error), 6 | Reqwest(reqwest::Error), 7 | Serde(serde_json::Error), 8 | ParseIntError(ParseIntError), 9 | FailedLookahead, 10 | } 11 | 12 | impl From for LookaheadError { 13 | fn from(value: beacon_api_client::Error) -> Self { 14 | LookaheadError::BeaconApiClientError(value) 15 | } 16 | } 17 | 18 | impl From for LookaheadError { 19 | fn from(value: reqwest::Error) -> Self { 20 | LookaheadError::Reqwest(value) 21 | } 22 | } 23 | 24 | impl From for LookaheadError { 25 | fn from(value: serde_json::Error) -> Self { 26 | LookaheadError::Serde(value) 27 | } 28 | } 29 | 30 | impl From for LookaheadError { 31 | fn from(value: ParseIntError) -> Self { 32 | LookaheadError::ParseIntError(value) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /cb.docker-compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | il-boost: 3 | image: il-boost 4 | container_name: il-boost 5 | environment: 6 | CB_MODULE_ID: IL_COMMIT 7 | CB_CONFIG: /cb-config.toml 8 | CB_SIGNER_JWT: ${JWT} 9 | METRICS_SERVER: 10000 10 | SIGNER_SERVER: cb_signer:20000 11 | ROLLING_DURATION: daily 12 | RUST_LOG: debug 13 | MAX_LOG_FILES: 30 14 | HOST_DOCKER_INTERNAL: host.docker.internal 15 | volumes: 16 | - ./cb-config.toml:/cb-config.toml:ro 17 | - ./logs:/var/logs/commit-boost 18 | networks: 19 | - monitoring_network 20 | - signer_network 21 | depends_on: 22 | - cb_signer 23 | cb_signer: 24 | image: commitboost_signer 25 | container_name: cb_signer 26 | environment: 27 | CB_CONFIG: /cb-config.toml 28 | CB_JWTS: "{\"IL_COMMIT\":\"${JWT}\"}" 29 | METRICS_SERVER: 10000 30 | SIGNER_SERVER: 20000 31 | ROLLING_DURATION: daily 32 | RUST_LOG: debug 33 | MAX_LOG_FILES: 30 34 | CB_SIGNER_FILE: /keys.json 35 | SIGNER_LOADER_DIR_KEYS: /keys 36 | SIGNER_LOADER_DIR_SECRETS: /secrets 37 | volumes: 38 | - ./cb-config.toml:/cb-config.toml:ro 39 | - ./logs:/var/logs/commit-boost 40 | - ${YOUR_PATH_TO_KEYS_DIR}:/keys:ro 41 | - ${YOUR_PATH_TO_SERETS_DIR}:/secrets:ro 42 | networks: 43 | - monitoring_network 44 | - signer_network 45 | volumes: 46 | prometheus-data: 47 | driver: local 48 | grafana-data: 49 | driver: local 50 | networks: 51 | monitoring_network: 52 | driver: bridge 53 | signer_network: 54 | driver: bridge 55 | -------------------------------------------------------------------------------- /src/pbs/mod.rs: -------------------------------------------------------------------------------- 1 | use std::marker::PhantomData; 2 | 3 | use axum::{ 4 | async_trait, 5 | body::Body, 6 | extract::State, 7 | http::{HeaderMap, Response}, 8 | response::IntoResponse, 9 | routing::post, 10 | Json, Router, 11 | }; 12 | use cb_pbs::{BuilderApi, BuilderApiState, PbsState}; 13 | use reqwest::StatusCode; 14 | use serde::Deserialize; 15 | 16 | use crate::{config::InclusionListConfig, inclusion_boost::types::InclusionList}; 17 | 18 | // Any method that is not overriden will default to the normal MEV boost flow 19 | pub struct InclusionBoostApi; 20 | 21 | impl BuilderApiState for InclusionListConfig {} 22 | 23 | #[async_trait] 24 | impl BuilderApi for InclusionBoostApi { 25 | fn extra_routes() -> Option>> { 26 | let router = Router::new().route("/constraints", post(handle_post_constraints)); 27 | 28 | Some(router) 29 | } 30 | 31 | // fn get_header( 32 | // params: GetHeaderParams, 33 | // req_headers: HeaderMap, 34 | // state: PbsState, 35 | // ) -> Result, ()> { 36 | // todo!() 37 | // } 38 | } 39 | 40 | async fn handle_post_constraints( 41 | State(state): State>, 42 | _: HeaderMap, 43 | Json(inclusion_list): Json, 44 | ) -> Response { 45 | // TODO unwrap 46 | let response = state 47 | .relays() 48 | .first() 49 | .unwrap() 50 | .client 51 | .post("test") 52 | .json(&inclusion_list) 53 | .send() 54 | .await 55 | .unwrap(); 56 | 57 | let response_body = Body::from(response.bytes().await.unwrap()); 58 | (StatusCode::OK, response_body).into_response() 59 | } 60 | -------------------------------------------------------------------------------- /cb-config.toml: -------------------------------------------------------------------------------- 1 | chain = "Holesky" 2 | 3 | [pbs] 4 | port = 18550 5 | relays = [] 6 | relay_check = true 7 | timeout_get_header_ms = 950 8 | timeout_get_payload_ms = 4000 9 | timeout_register_validator_ms = 3000 10 | skip_sigverify = true 11 | min_bid_eth = 0.0 12 | beacon_api = "http://host.docker.internal:4000" 13 | execution_api = "http://host.docker.internal:8545" 14 | relay = "http://relay.url" 15 | 16 | [[relays]] 17 | # Relay ID to use in telemetry 18 | # OPTIONAL, DEFAULT: URL hostname 19 | id = "example-relay" 20 | # Relay URL in the format scheme://pubkey@host 21 | url = "http://0xa1cec75a3f0661e99299274182938151e8433c61a19222347ea1313d839229cb4ce4e3e5aa2bdeb71c8fcf1b084963c2@abc.xyz" 22 | # Headers to send with each request for this relay 23 | # OPTIONAL 24 | headers = { X-MyCustomHeader = "MyCustomValue" } 25 | # Whether to enable timing games, as tuned by `target_first_request_ms` and `frequency_get_header_ms`. 26 | # These values should be carefully chosen for each relay, as each relay has different latency and timing games setups. 27 | # They should only be used by advanced users, and if mis-configured can result in unforeseen effects, e.g. fetching a lower header value, 28 | # or getting a temporary IP ban. 29 | 30 | [headers] 31 | X-MyCustomHeader = "MyCustomValue" 32 | 33 | [signer] 34 | docker_image = "commitboost_signer" 35 | [signer.loader] 36 | keys_path = "./keys" 37 | secrets_path = "./secrets" 38 | 39 | [metrics] 40 | prometheus_config = "./docker/prometheus.yml" 41 | use_grafana = true 42 | 43 | [[modules]] 44 | id = "IL_COMMIT" 45 | type = "commit" 46 | docker_image = "il_boost" 47 | sleep_secs = 5 48 | beacon_api = "http://host.docker.internal:4000" 49 | execution_api = "http://host.docker.internal:8545" 50 | relay = "http://0xaa58208899c6105603b74396734a6263cc7d947f444f396a90f7b7d3e65d102aec7e5e5291b27e08d02c50a050825c2f@18.192.244.122:4040" -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "il-boost" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | # Commit Boost 8 | cb-metrics = { git = "https://github.com/Commit-Boost/commit-boost-client", branch = "main" } 9 | cb-common = { git = "https://github.com/Commit-Boost/commit-boost-client", branch = "main" } 10 | cb-pbs = { git = "https://github.com/Commit-Boost/commit-boost-client", branch = "main" } 11 | eth2_keystore = { git = "https://github.com/sigp/lighthouse", rev = "9e12c21f268c80a3f002ae0ca27477f9f512eb6f" } 12 | 13 | toml = "0.5" 14 | 15 | # Async / Threads 16 | tokio = { version = "1.37.0", features = ["full"] } 17 | futures = "0.3.30" 18 | 19 | # Serialization 20 | serde = { version = "1.0.203", features = ["derive"] } 21 | serde_json = "1.0.115" 22 | 23 | # preconf-boost 24 | # preconf = { git = "https://github.com/gattaca-com/preconf-boost", rev = "720f870d40718c9689df73763bc6d8a748bf33d0" } 25 | 26 | 27 | # Ethereum 28 | alloy = { version = "0.1.3", features = ["rpc", "rpc-types", "full", "rpc-types-beacon", "provider-txpool-api", "signer-local", "node-bindings", "ssz"] } 29 | ethereum-consensus = { git = "https://github.com/ralexstokes/ethereum-consensus", rev = "cf3c404" } 30 | beacon-api-client = { git = "https://github.com/ralexstokes/ethereum-consensus" } 31 | ethereum_serde_utils = "0.5.2" 32 | 33 | # Crypto 34 | tree_hash = { git = "https://github.com/eserilev/tree_hash", branch = "alloy-deps"} 35 | tree_hash_derive = { git = "https://github.com/eserilev/tree_hash", branch = "alloy-deps"} 36 | 37 | reqwest = "0.12" 38 | 39 | # types 40 | parking_lot = "0.12.1" 41 | ssz_types = { git = "https://github.com/eserilev/ssz_types", branch = "alloy-tweaks"} 42 | 43 | # tracing 44 | tracing = "0.1.40" 45 | tracing-subscriber = "0.3.18" 46 | 47 | mev-share-sse = { git = "https://github.com/paradigmxyz/mev-share-rs" } 48 | 49 | # reth 50 | reth-transaction-pool = { git = "https://github.com/paradigmxyz/reth", features = ["test-utils"]} 51 | 52 | 53 | axum = { version = "0.7.5", features = ["macros"] } 54 | axum-extra = { version = "0.9.3", features = ["typed-header"] } -------------------------------------------------------------------------------- /src/lookahead/mod.rs: -------------------------------------------------------------------------------- 1 | use beacon_api_client::{mainnet::Client, Error, ProposerDuty}; 2 | use error::LookaheadError; 3 | use reqwest::Url; 4 | 5 | pub mod error; 6 | 7 | pub struct LookaheadProvider { 8 | client: Client, 9 | url: String, 10 | } 11 | 12 | impl LookaheadProvider { 13 | pub fn new(url: &str) -> Self { 14 | Self { 15 | url: url.to_string(), 16 | client: Client::new(Url::parse(url).unwrap()), 17 | } 18 | } 19 | 20 | /// Get proposer duties for the current epoch 21 | pub async fn get_current_lookahead(&self) -> Result, LookaheadError> { 22 | tracing::info!("Getting current lookahead duties"); 23 | 24 | let current_slot = get_slot(&self.url, "head").await? 25 | .ok_or(LookaheadError::FailedLookahead)?; 26 | 27 | let epoch = current_slot / 32; 28 | tracing::info!("Getting proposer duties for epoch: {}", epoch); 29 | 30 | let (_, duties) = self.client.get_proposer_duties(epoch).await?; 31 | 32 | Ok(duties 33 | .into_iter() 34 | .filter(|d| d.slot > current_slot) 35 | .collect::>()) 36 | } 37 | 38 | // TODO refactor 39 | /// Get proposer duties for the next epoch 40 | pub async fn get_next_epoch_lookahead(&self) -> Result, LookaheadError> { 41 | tracing::info!("Getting next lookahead duties"); 42 | 43 | let current_slot = get_slot(&self.url, "head").await? 44 | .ok_or(LookaheadError::FailedLookahead)?; 45 | 46 | let epoch = current_slot / 32; 47 | let next_epoch = epoch + 1; 48 | tracing::info!("Getting next duties for epoch: {}", next_epoch); 49 | 50 | let (_, duties) = self.client.get_proposer_duties(next_epoch).await?; 51 | 52 | Ok(duties 53 | .into_iter() 54 | .filter(|d| d.slot > current_slot) 55 | .collect::>()) 56 | } 57 | } 58 | 59 | 60 | 61 | async fn get_slot(beacon_url: &str, slot: &str) -> Result, LookaheadError> { 62 | let url = format!("{}/eth/v1/beacon/headers/{}", beacon_url, slot); 63 | let res = reqwest::get(url).await?; 64 | let json: serde_json::Value = serde_json::from_str(&res.text().await?)?; 65 | 66 | let Some(slot) = json.pointer("/data/header/message/slot") else { 67 | return Ok(None); 68 | }; 69 | let Some(slot_str) = slot.as_str() else { 70 | return Ok(None); 71 | }; 72 | Ok(Some(slot_str.parse::()?)) 73 | } 74 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, fs, sync::Arc}; 2 | 3 | use cb_common::{ 4 | config::{load_pbs_custom_config, load_commit_module_config, StaticModuleConfig}, 5 | utils::initialize_tracing_log, 6 | }; 7 | use cb_pbs::{PbsService, PbsState}; 8 | use config::InclusionListConfig; 9 | use serde::Deserialize; 10 | 11 | use inclusion_boost::{ 12 | error::InclusionListBoostError, sidecar::InclusionSideCar, types::InclusionBoostCache, 13 | }; 14 | use types::MainConfig; 15 | 16 | use crate::pbs::InclusionBoostApi; 17 | use alloy::{ 18 | providers::{ProviderBuilder, RootProvider}, 19 | transports::http::Http, 20 | }; 21 | use parking_lot::RwLock; 22 | 23 | mod config; 24 | mod inclusion_boost; 25 | mod lookahead; 26 | mod pbs; 27 | mod test; 28 | mod types; 29 | 30 | #[tokio::main] 31 | async fn main() -> Result<(), InclusionListBoostError> { 32 | // parse_toml(); 33 | let config = load_commit_module_config::().expect("failed to load config"); 34 | let _ = initialize_tracing_log(&config.id); 35 | 36 | let eth_provider: RootProvider> = 37 | ProviderBuilder::new().on_http(config.extra.execution_api.parse().unwrap()); 38 | let cache = Arc::new(InclusionBoostCache { 39 | block_cache: Arc::new(RwLock::new(HashMap::new())), 40 | inclusion_list_cache: Arc::new(RwLock::new(HashMap::new())), 41 | }); 42 | 43 | let (pbs_module, pbs_module_custom_data) = load_pbs_custom_config::().expect("failed to load pbs config"); 44 | 45 | let state = PbsState::new(pbs_module).with_data(pbs_module_custom_data); 46 | 47 | let mut inclusion_sidecar = 48 | InclusionSideCar::new(config, eth_provider, cache); 49 | 50 | let pbs_server = tokio::spawn(async move { 51 | let _ = PbsService::run::(state).await; 52 | }); 53 | 54 | let il_sidecar = tokio::spawn(async move { 55 | let _ = inclusion_sidecar.run().await; 56 | }); 57 | 58 | 59 | let _ = tokio::join!(pbs_server, il_sidecar); 60 | 61 | Ok(()) 62 | } 63 | 64 | fn parse_toml() { 65 | let config_str = fs::read_to_string("./cb-config.toml").expect("Failed to read config file"); 66 | 67 | let config: MainConfig = toml::from_str(&config_str).expect("Failed to parse config file"); 68 | 69 | std::env::set_var("CB_MODULE_ID", config.modules.first().unwrap().id.clone()); 70 | std::env::set_var("CB_SIGNER_JWT", config.modules.first().unwrap().id.clone()); 71 | std::env::set_var("SIGNER_SERVER", "2000"); 72 | std::env::set_var("CB_CONFIG", "./config.toml"); 73 | } 74 | -------------------------------------------------------------------------------- /src/inclusion_boost/error.rs: -------------------------------------------------------------------------------- 1 | use std::{num::ParseIntError, str::Utf8Error}; 2 | 3 | use alloy::transports::TransportErrorKind; 4 | use cb_common::commit::error::SignerClientError; 5 | 6 | use crate::lookahead::error::LookaheadError; 7 | 8 | #[derive(Debug)] 9 | pub enum InclusionListBoostError { 10 | GenericError(String), 11 | BeaconApiError(beacon_api_client::Error), 12 | Reqwest(reqwest::Error), 13 | SseError(mev_share_sse::client::SseError), 14 | AlloyRpcError(alloy::transports::RpcError), 15 | SignerClientError(SignerClientError), 16 | // Utf8Error(Utf8Error), 17 | LookaheadError(LookaheadError), 18 | ParseIntError(ParseIntError), 19 | Serde(serde_json::Error), 20 | } 21 | 22 | impl From for InclusionListBoostError { 23 | fn from(value: String) -> Self { 24 | InclusionListBoostError::GenericError(value) 25 | } 26 | } 27 | 28 | impl From for InclusionListBoostError { 29 | fn from(value: beacon_api_client::Error) -> Self { 30 | InclusionListBoostError::BeaconApiError(value) 31 | } 32 | } 33 | 34 | impl From for InclusionListBoostError { 35 | fn from(value: reqwest::Error) -> Self { 36 | InclusionListBoostError::Reqwest(value) 37 | } 38 | } 39 | 40 | impl From for InclusionListBoostError { 41 | fn from(value: mev_share_sse::client::SseError) -> Self { 42 | InclusionListBoostError::SseError(value) 43 | } 44 | } 45 | 46 | impl From> for InclusionListBoostError { 47 | fn from(value: alloy::transports::RpcError) -> Self { 48 | InclusionListBoostError::AlloyRpcError(value) 49 | } 50 | } 51 | 52 | impl From for InclusionListBoostError { 53 | fn from(value: SignerClientError) -> Self { 54 | InclusionListBoostError::SignerClientError(value) 55 | } 56 | } 57 | 58 | impl From for InclusionListBoostError { 59 | fn from(value: LookaheadError) -> Self { 60 | InclusionListBoostError::LookaheadError(value) 61 | } 62 | } 63 | 64 | 65 | impl From for InclusionListBoostError { 66 | fn from(value: ParseIntError) -> Self { 67 | InclusionListBoostError::ParseIntError(value) 68 | } 69 | } 70 | 71 | impl From for InclusionListBoostError { 72 | fn from(value: serde_json::Error) -> Self { 73 | InclusionListBoostError::Serde(value) 74 | } 75 | } 76 | 77 | 78 | // impl From for InclusionListBoostError { 79 | // fn from(value: Utf8Error) -> Self { 80 | // InclusionListBoostError::Utf8Error(value) 81 | // } 82 | // } 83 | -------------------------------------------------------------------------------- /src/test/mod.rs: -------------------------------------------------------------------------------- 1 | mod test { 2 | 3 | use alloy::{primitives::Bytes, rpc::types::Block}; 4 | use axum::{ 5 | response::IntoResponse, 6 | routing::{post, IntoMakeService}, 7 | Json, Router, 8 | }; 9 | use cb_common::commit::client::SignerClient; 10 | use reqwest::StatusCode; 11 | use reth_transaction_pool::{test_utils::{MockTransactionFactory, TestPoolBuilder}, TransactionOrigin, TransactionPool}; 12 | use std::{collections::HashMap, net::SocketAddr}; 13 | use tokio::net::TcpListener; 14 | 15 | 16 | use crate::inclusion_boost::{ 17 | types::{InclusionList, Transaction}, 18 | InclusionBoost, 19 | }; 20 | const ID: &str = "IL_COMMIT"; 21 | struct MockRelay { 22 | tpc_listener: TcpListener, 23 | service: IntoMakeService, 24 | } 25 | 26 | impl MockRelay { 27 | pub async fn new(port: u16) -> Self { 28 | let app = Router::new().route("/", post(MockRelay::handle_request)); 29 | 30 | // Define an address to bind the server to 31 | let addr = SocketAddr::from(([127, 0, 0, 1], port)); 32 | 33 | let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); 34 | let service = app.into_make_service(); 35 | 36 | MockRelay { 37 | tpc_listener: listener, 38 | service, 39 | } 40 | } 41 | 42 | async fn handle_request(Json(payload): Json) -> impl IntoResponse { 43 | (StatusCode::OK, Json(payload)) 44 | } 45 | } 46 | 47 | #[tokio::test(flavor = "multi_thread")] 48 | pub async fn build_mock_inclusion_list_request() { 49 | // TODO load via config 50 | let mock_signer_client = SignerClient::new(format!("127.0.0.1:20000"), "IL_COMMIT").unwrap(); 51 | 52 | let mock_relay = MockRelay::new(33950).await; 53 | 54 | // Run the server 55 | axum::serve(mock_relay.tpc_listener, mock_relay.service); 56 | 57 | let mut mock_validator_pubkeys = HashMap::new(); 58 | let pubkey_result = mock_signer_client.get_pubkeys().await.unwrap(); 59 | let pubkey = pubkey_result.consensus.first().unwrap(); 60 | mock_validator_pubkeys.insert(1, pubkey.clone()); 61 | 62 | let inclusion_module = InclusionBoost::new( 63 | ID.to_string(), 64 | mock_signer_client, 65 | mock_validator_pubkeys, 66 | // "http://localhost:33950/".to_string(), 67 | "http://0xaa58208899c6105603b74396734a6263cc7d947f444f396a90f7b7d3e65d102aec7e5e5291b27e08d02c50a050825c2f@18.192.244.122:4040/".to_string(), 68 | ); 69 | 70 | let txpool = TestPoolBuilder::default(); 71 | let mut mock_tx_factory = MockTransactionFactory::default(); 72 | let mut transaction = mock_tx_factory.create_eip1559(); 73 | transaction.transaction.set_input(Bytes::from("0x123")); 74 | let added_result = txpool 75 | .add_transaction(TransactionOrigin::Local, transaction.transaction.clone()) 76 | .await; 77 | let hash = transaction.transaction.get_hash(); 78 | assert_eq!(added_result.unwrap(), hash); 79 | 80 | let transactions = txpool 81 | .all_transactions() 82 | .pending 83 | .iter() 84 | .map(|t| t.clone().into()) 85 | .collect::>(); 86 | 87 | let mut mock_previous_block: Block = Block::default(); 88 | mock_previous_block.header.gas_limit = u128::MAX; 89 | 90 | assert_eq!(transactions.len(), 1); 91 | 92 | let filtered_transactions = 93 | InclusionBoost::get_filtered_transactions(&transactions, &mock_previous_block); 94 | 95 | assert_eq!(filtered_transactions.len(), 1); 96 | 97 | let mock_inclusion_list = InclusionList::new(1, 1, filtered_transactions); 98 | 99 | let response = inclusion_module 100 | .submit_inclusion_list_to_relay(1, mock_inclusion_list) 101 | .await 102 | .unwrap(); 103 | 104 | assert_eq!(response, Some(())) 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/inclusion_boost/types.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::sync::Arc; 3 | 4 | use alloy::hex::ToHexExt; 5 | use alloy::primitives::{keccak256, Bytes}; 6 | use alloy::rpc::types::beacon::{BlsPublicKey, BlsSignature}; 7 | use alloy::{network::TransactionResponse, primitives::B256}; 8 | use ethereum_consensus::ssz::prelude::List; 9 | use parking_lot::RwLock; 10 | use reth_transaction_pool::PoolTransaction; 11 | use reth_transaction_pool::{test_utils::MockTransaction, ValidPoolTransaction}; 12 | use serde::ser::SerializeStruct; 13 | use serde::{Deserialize, Serialize, Serializer}; 14 | use ssz_types::typenum::{U1, U32}; 15 | use ssz_types::{FixedVector, VariableList}; 16 | use tree_hash_derive::TreeHash; 17 | 18 | /// The BLS Domain Separator used in Ethereum 2.0. 19 | 20 | type MaxInclusionListLength = U1; 21 | 22 | #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] 23 | pub struct InclusionListDelegateSignedMessage { 24 | pub message: InclusionListDelegateMessage, 25 | pub signature: BlsSignature 26 | } 27 | 28 | #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, TreeHash)] 29 | pub struct InclusionListDelegateMessage { 30 | pub preconfer_pubkey: BlsPublicKey, 31 | pub slot_number: u64, 32 | pub chain_id: u64, 33 | pub gas_limit: u64 34 | } 35 | 36 | 37 | #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, TreeHash)] 38 | pub struct InclusionList { 39 | pub slot: u64, 40 | pub validator_index: usize, 41 | pub constraints: FixedVector, MaxInclusionListLength>, 42 | } 43 | 44 | #[derive(Debug, Default, Clone, PartialEq, Deserialize, TreeHash)] 45 | pub struct Constraint { 46 | pub tx: [u8; 32], 47 | } 48 | 49 | impl Serialize for Constraint { 50 | fn serialize(&self, serializer: S) -> Result 51 | where 52 | S: Serializer, 53 | { 54 | let mut item = serializer.serialize_struct("Constraint", 1)?; 55 | item.serialize_field("tx", &self.tx.encode_hex())?; 56 | item.end() 57 | } 58 | } 59 | 60 | impl InclusionList { 61 | pub fn new(slot: u64, validator_index: usize, transactions: Vec) -> Self { 62 | 63 | let list_of_lists = vec![transactions.into()]; 64 | 65 | Self { 66 | slot, 67 | validator_index, 68 | constraints: list_of_lists.into(), 69 | } 70 | } 71 | } 72 | 73 | #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] 74 | pub struct InclusionRequest { 75 | pub message: InclusionList, 76 | pub signature: BlsSignature, 77 | } 78 | 79 | #[derive(Debug, Serialize, Deserialize)] 80 | pub struct InclusionProof { 81 | transaction_hashes: VariableList, 82 | generalized_indices: VariableList, 83 | merkle_hashes: Vec, 84 | } 85 | 86 | impl InclusionProof { 87 | pub fn verify(&self) -> bool { 88 | // TODO 89 | true 90 | } 91 | } 92 | 93 | #[derive(Debug)] 94 | pub struct Transaction { 95 | pub is_eip4844: bool, 96 | pub gas_limit: u128, 97 | pub gas: u128, 98 | pub max_priority_fee_per_gas: Option, 99 | pub tx_hash: B256, 100 | pub bytes: Bytes, 101 | pub index: Option, 102 | } 103 | 104 | impl From>> for Transaction { 105 | fn from(value: Arc>) -> Self { 106 | Self { 107 | tx_hash: value.hash().clone(), 108 | is_eip4844: value.is_eip4844(), 109 | gas: value.gas_limit().into(), 110 | gas_limit: value.gas_limit().into(), 111 | max_priority_fee_per_gas: Some(value.priority_fee_or_price()), 112 | bytes: value.transaction.get_input().into(), 113 | index: None, 114 | } 115 | } 116 | } 117 | 118 | impl From for Transaction { 119 | fn from(value: alloy::rpc::types::Transaction) -> Self { 120 | Self { 121 | tx_hash: value.tx_hash(), 122 | is_eip4844: false, 123 | gas: value.gas, 124 | gas_limit: value.gas, 125 | max_priority_fee_per_gas: value.max_priority_fee_per_gas, 126 | bytes: value.input, 127 | index: value.transaction_index, 128 | } 129 | } 130 | } 131 | 132 | pub struct InclusionBoostCache { 133 | pub block_cache: Arc>>>, 134 | pub inclusion_list_cache: Arc>>, 135 | } 136 | -------------------------------------------------------------------------------- /src/inclusion_boost/mod.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, time::Duration}; 2 | 3 | use alloy::{ 4 | hex::ToHexExt, primitives::Bytes, rpc::types::{ 5 | beacon::{BlsPublicKey, BlsSignature}, 6 | Block, 7 | } 8 | }; 9 | use cb_common::commit::{client::SignerClient, error::SignerClientError, request::SignRequest}; 10 | use error::InclusionListBoostError; 11 | use tree_hash::TreeHash; 12 | use types::{Constraint, InclusionList, InclusionListDelegateMessage, InclusionListDelegateSignedMessage, InclusionRequest, Transaction}; 13 | 14 | pub mod error; 15 | pub mod sidecar; 16 | pub mod types; 17 | 18 | const CONSTRAINTS_PATH: &str = "/eth/v1/builder/set_constraints"; 19 | const DELEGATE_PATH: &str = "/eth/v1/builder/elect_preconfer"; 20 | 21 | /// Implements an inclusion list flavor 22 | /// of commit-boost 23 | pub struct InclusionBoost { 24 | pub module_id: String, 25 | pub signer_client: SignerClient, 26 | pub validator_keys: HashMap, 27 | pub relay_client: reqwest::Client, 28 | pub relay_url: String, 29 | } 30 | 31 | fn bytes_to_array(bytes: Bytes) -> [u8; 32] { 32 | let mut buffer = [0x0; 32]; 33 | let bytes_to_copy = bytes.len().min(buffer.len()); 34 | // Panic-free because bytes_to_copy <= buffer.len() 35 | let start_index = buffer.len().saturating_sub(bytes_to_copy); 36 | // Panic-free because start_index <= buffer.len() 37 | // and bytes_to_copy <= value_bytes.len() 38 | buffer 39 | .get_mut(start_index..) 40 | .expect("start_index <= buffer.len()") 41 | .copy_from_slice( 42 | bytes.encode_hex().as_bytes() 43 | .get(..bytes_to_copy) 44 | .expect("bytes_to_copy <= value_byte.len()"), 45 | ); 46 | buffer 47 | } 48 | 49 | impl InclusionBoost { 50 | pub fn new( 51 | module_id: String, 52 | signer_client: SignerClient, 53 | validator_keys: HashMap, 54 | relay_url: String, 55 | ) -> Self { 56 | Self { 57 | module_id, 58 | signer_client, 59 | validator_keys, 60 | relay_client: reqwest::Client::new(), 61 | relay_url, 62 | } 63 | } 64 | 65 | /// Calculate which transactions may be filtered from a list of transactions by 66 | /// comparing if any of these transactions could have made it into `block` 67 | pub fn get_filtered_transactions( 68 | transactions: &Vec, 69 | block: &Block, 70 | ) -> Vec { 71 | let mut filtered_transactions = vec![]; 72 | let mut gas_left = block.header.gas_limit - block.header.gas_used; 73 | 74 | for tx in transactions { 75 | if let Some(max_priority_fee_per_gas) = tx.max_priority_fee_per_gas { 76 | if max_priority_fee_per_gas > 0 && gas_left > 0 { 77 | gas_left = gas_left.saturating_sub(tx.gas); 78 | 79 | filtered_transactions.push(Constraint { 80 | tx: bytes_to_array(tx.bytes.clone()), 81 | }); 82 | tracing::info!( 83 | tx_hash = ?tx.tx_hash, 84 | "Added transaction to inclusion list" 85 | ); 86 | } 87 | } 88 | } 89 | 90 | filtered_transactions 91 | } 92 | 93 | pub async fn delegate_inclusion_list_authority( 94 | &self, 95 | validator_index: usize, 96 | slot: u64, 97 | ) -> Result, InclusionListBoostError>{ 98 | let Some(validator_key) = self.validator_keys.get(&validator_index) else { 99 | return Ok(None); 100 | }; 101 | 102 | tracing::info!( 103 | validator_index, 104 | slot, 105 | "Delegating inclusion list building responsibilities to IL Boost" 106 | ); 107 | 108 | let message = InclusionListDelegateMessage { 109 | preconfer_pubkey: validator_key.clone(), 110 | slot_number: slot, 111 | chain_id: 7014190335, 112 | gas_limit: u64::MAX, 113 | }; 114 | 115 | let message_root = message.tree_hash_root(); 116 | let sign_request = SignRequest::builder(validator_key.clone()) 117 | .with_root(message_root.into()); 118 | 119 | let signature = self.signer_client.request_signature(&sign_request).await?; 120 | 121 | let signed_message = InclusionListDelegateSignedMessage { 122 | message, 123 | signature 124 | }; 125 | 126 | self.post_inclusion_delegate_request(signed_message).await 127 | } 128 | 129 | /// Submit the inclusion list to the relay 130 | /// This using the commit-boost signing module to sign the list 131 | /// And then forwards the signed list to the constraints API 132 | pub async fn submit_inclusion_list_to_relay( 133 | &self, 134 | validator_index: usize, 135 | inclusion_list: InclusionList, 136 | ) -> Result, InclusionListBoostError> { 137 | 138 | let Some(validator_key) = self.validator_keys.get(&validator_index) else { 139 | return Ok(None); 140 | }; 141 | 142 | tracing::info!( 143 | validator_index, 144 | "Submitting inclusion list to relay" 145 | ); 146 | 147 | 148 | let signature = self 149 | .sign_inclusion_list(&inclusion_list, *validator_key) 150 | .await?; 151 | 152 | tracing::info!( 153 | "Inclusion list signed" 154 | ); 155 | 156 | match self.post_inclusion_request(signature, inclusion_list) 157 | .await { 158 | Ok(res) => println!("{:?}", res), 159 | Err(e) => return Err(e) 160 | }; 161 | 162 | tracing::info!( 163 | "Inclusion list sent" 164 | ); 165 | 166 | Ok(Some(())) 167 | } 168 | 169 | /// Sign an inclusion list via the commit-boost signing module 170 | async fn sign_inclusion_list( 171 | &self, 172 | inclusion_list: &InclusionList, 173 | validator_key: BlsPublicKey, 174 | ) -> Result { 175 | let inclusion_list_root = inclusion_list.tree_hash_root(); 176 | let sign_request = SignRequest::builder(validator_key) 177 | .with_root(inclusion_list_root.into()); 178 | 179 | self.signer_client.request_signature(&sign_request).await 180 | } 181 | 182 | async fn post_inclusion_delegate_request( 183 | &self, 184 | signed_message: InclusionListDelegateSignedMessage 185 | ) -> Result, InclusionListBoostError> { 186 | let url = format!("{}{DELEGATE_PATH}", self.relay_url); 187 | 188 | tracing::info!(url, payload=?signed_message, "POST request sent"); 189 | 190 | let response = match self.relay_client.post(url).timeout(Duration::from_secs(10)).json(&signed_message).send().await { 191 | Ok(res) => res, 192 | Err(e) => { 193 | println!("{:?}", e); 194 | return Err(e.into()) 195 | } 196 | }; 197 | 198 | 199 | let status = response.status(); 200 | let response_bytes = response.bytes().await?; 201 | 202 | if !status.is_success() { 203 | let err = String::from_utf8_lossy(&response_bytes).into_owned(); 204 | tracing::error!(err, "failed to get signature"); 205 | return Ok(None); 206 | } 207 | 208 | Ok(Some(())) 209 | } 210 | 211 | /// Post a signed inclusion list to a relay 212 | async fn post_inclusion_request( 213 | &self, 214 | signature: BlsSignature, 215 | inclusion_list: InclusionList, 216 | ) -> Result, InclusionListBoostError> { 217 | let url = format!("{}{CONSTRAINTS_PATH}", self.relay_url); 218 | 219 | let request = InclusionRequest { 220 | message: inclusion_list, 221 | signature, 222 | }; 223 | 224 | 225 | tracing::info!(url, payload=?request, "POST request sent"); 226 | 227 | let response = match self.relay_client.post(url).timeout(Duration::from_secs(10)).json(&request).send().await { 228 | Ok(res) => res, 229 | Err(e) => { 230 | println!("{:?}", e); 231 | return Err(e.into()) 232 | } 233 | }; 234 | 235 | 236 | let status = response.status(); 237 | let response_bytes = response.bytes().await?; 238 | 239 | if !status.is_success() { 240 | let err = String::from_utf8_lossy(&response_bytes).into_owned(); 241 | tracing::error!(err, "failed to get signature"); 242 | return Ok(None); 243 | } 244 | 245 | Ok(Some(())) 246 | } 247 | } 248 | -------------------------------------------------------------------------------- /src/inclusion_boost/sidecar.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, sync::Arc, thread::sleep, time::Duration}; 2 | 3 | use alloy::{ 4 | eips::BlockId, 5 | providers::{ext::TxPoolApi, Provider, RootProvider}, 6 | rpc::types::{beacon::events::HeadEvent, Block, BlockTransactionsKind}, 7 | transports::http::Http, 8 | }; 9 | 10 | use cb_common::config::StartCommitModuleConfig; 11 | use futures::StreamExt; 12 | use mev_share_sse::EventClient; 13 | 14 | use crate::{ 15 | config::InclusionListConfig, inclusion_boost::types::InclusionList, 16 | lookahead::{error::LookaheadError, LookaheadProvider}, 17 | }; 18 | 19 | use super::{ 20 | error::InclusionListBoostError, 21 | types::{InclusionBoostCache, Transaction}, 22 | InclusionBoost, 23 | }; 24 | 25 | pub struct InclusionSideCar { 26 | inclusion_boost: InclusionBoost, 27 | eth_provider: RootProvider>, 28 | cache: Arc, 29 | il_config: InclusionListConfig, 30 | } 31 | 32 | impl InclusionSideCar { 33 | pub fn new( 34 | config: StartCommitModuleConfig, 35 | eth_provider: RootProvider>, 36 | cache: Arc, 37 | ) -> Self { 38 | let inclusion_boost = InclusionBoost::new( 39 | config.id.to_string(), 40 | config.signer_client, 41 | HashMap::new(), 42 | config.extra.clone().relay, // TODO get from config 43 | ); 44 | 45 | Self { 46 | inclusion_boost, 47 | eth_provider, 48 | cache, 49 | il_config: config.extra, 50 | } 51 | } 52 | 53 | pub async fn run(&mut self) -> Result<(), InclusionListBoostError> { 54 | let lookahead_provider = LookaheadProvider::new(&self.il_config.beacon_api); 55 | sleep(Duration::from_secs(60)); 56 | let pubkeys = self.inclusion_boost.signer_client.get_pubkeys().await?; 57 | 58 | for p in pubkeys.consensus { 59 | let index = get_validator_index(&self.il_config.beacon_api, &p.to_string()).await?; 60 | if let Some(validator_index) = index { 61 | println!("validator_index {}", validator_index); 62 | self.inclusion_boost.validator_keys.insert(validator_index as usize, p); 63 | } 64 | } 65 | 66 | let mut lookahead = lookahead_provider.get_current_lookahead().await?; 67 | let mut next_lookahead = lookahead_provider.get_next_epoch_lookahead().await?; 68 | 69 | for future_proposer in next_lookahead { 70 | let res = self.inclusion_boost.delegate_inclusion_list_authority(future_proposer.validator_index, future_proposer.slot).await; 71 | println!("{:?}", res); 72 | } 73 | 74 | let lookahead_size = lookahead.len(); 75 | tracing::info!(lookahead_size, "Initial proposer lookahead fetched"); 76 | 77 | let event_client = EventClient::default(); 78 | let target = format!("{}/eth/v1/events?topics=head", self.il_config.beacon_api); 79 | let mut sub = event_client.subscribe::(&target).await?; 80 | 81 | while let Some(head_event) = sub.next().await { 82 | let head_event = head_event?; 83 | 84 | if head_event.epoch_transition { 85 | lookahead = lookahead_provider.get_current_lookahead().await?; 86 | next_lookahead = lookahead_provider.get_next_epoch_lookahead().await?; 87 | for future_proposer in next_lookahead { 88 | let res = self.inclusion_boost.delegate_inclusion_list_authority(future_proposer.validator_index, future_proposer.slot).await; 89 | println!("{:?}", res); 90 | } 91 | tracing::info!("Epoch transition, fetched new proposer lookahead..."); 92 | } 93 | 94 | // Get the next slots proposer 95 | let Some(next_proposer) = lookahead 96 | .iter() 97 | .find(|duty| duty.slot == &head_event.slot + 1) 98 | else { 99 | tracing::info!("At end of epoch, waiting"); 100 | continue; 101 | }; 102 | 103 | // TODO check if next slots proposer is ours 104 | let block_number = self.get_block_number_by_slot(head_event.slot - 1).await?; 105 | 106 | let Some(block_number) = block_number else { 107 | continue; 108 | }; 109 | 110 | 111 | let Some(latest_block) = self.get_block_by_number(block_number).await? else { 112 | continue; 113 | }; 114 | 115 | tracing::info!( 116 | block_number = latest_block.header.number, 117 | transaction_count = latest_block.transactions.len(), 118 | current_slot = head_event.slot, 119 | "Fetched latest block" 120 | ); 121 | 122 | // TODO we'll probably want to cache the inclusion list so we can validate merkle proofs later 123 | 124 | let Some(inclusion_list) = self 125 | .build_inclusion_list( 126 | &latest_block, 127 | next_proposer.slot, 128 | next_proposer.validator_index, 129 | ) 130 | .await? 131 | else { 132 | continue; 133 | }; 134 | 135 | self.inclusion_boost 136 | .submit_inclusion_list_to_relay(next_proposer.validator_index, inclusion_list) 137 | .await?; 138 | } 139 | 140 | Ok(()) 141 | } 142 | 143 | async fn get_block_by_number(&self, block_number: u64) -> Result, InclusionListBoostError> { 144 | self.eth_provider 145 | .get_block_by_number(alloy::eips::BlockNumberOrTag::Number(block_number), true) 146 | .await 147 | .map_err(|e| { 148 | e.into() 149 | }) 150 | } 151 | 152 | async fn get_block_number_by_slot(&self, slot: u64) -> Result, InclusionListBoostError> { 153 | 154 | tracing::info!(slot, "Get block number by slot"); 155 | 156 | let url = format!("{}/eth/v1/beacon/blocks/{}", self.il_config.beacon_api, slot); 157 | let res = reqwest::get(url).await?; 158 | let json: serde_json::Value = serde_json::from_str(&res.text().await?)?; 159 | 160 | let Some(block_number) = json.pointer("/data/message/body/execution_payload/block_number") else { 161 | return Ok(None); 162 | }; 163 | 164 | let Some(block_number_str) = block_number.as_str() else { 165 | return Ok(None); 166 | }; 167 | Ok(Some(block_number_str.parse::()?)) 168 | } 169 | 170 | /// Builds an inclusion list for slot N by comparing pending transactions in the mem pool 171 | /// with the block from slot N - 1 172 | async fn build_inclusion_list( 173 | &self, 174 | latest_block: &Block, 175 | slot: u64, 176 | validator_index: usize, 177 | ) -> Result, InclusionListBoostError> { 178 | let mut pending_txs = vec![]; 179 | let tx_pool = self.eth_provider.txpool_content().await?; 180 | 181 | tracing::info!( 182 | transaction_count = tx_pool.pending.len(), 183 | "Fetched pending transactions from the local memory pool" 184 | ); 185 | 186 | for (_, transactions) in tx_pool.pending { 187 | let transactions = transactions 188 | .iter() 189 | .map(|(_, tx)| tx.clone().into()) 190 | .collect::>(); 191 | 192 | pending_txs.extend(transactions); 193 | } 194 | 195 | let filtered_transactions = 196 | InclusionBoost::get_filtered_transactions(&pending_txs, latest_block); 197 | 198 | tracing::info!( 199 | transaction_count = filtered_transactions.len(), 200 | "Identified a list of potentially filtered transactions" 201 | ); 202 | 203 | // if filtered_transactions.len() == 0 { 204 | // return Ok(None); 205 | // }; 206 | 207 | Ok(Some(InclusionList::new( 208 | slot, 209 | validator_index, 210 | filtered_transactions, 211 | ))) 212 | } 213 | } 214 | 215 | 216 | async fn get_validator_index(beacon_url: &str, validator_pubkey: &str) -> Result, LookaheadError>{ 217 | let url = format!("{beacon_url}/eth/v1/beacon/states/head/validators?id={validator_pubkey}"); 218 | let res = reqwest::get(url).await?; 219 | let json: serde_json::Value = serde_json::from_str(&res.text().await?)?; 220 | 221 | let Some(validator_index) = json.pointer("/data/0/index") else { 222 | return Ok(None); 223 | }; 224 | 225 | let Some(validator_index_str) = validator_index.as_str() else { 226 | return Ok(None); 227 | }; 228 | 229 | Ok(Some(validator_index_str.parse::()?)) 230 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Out-of-Protocol Inclusion Lists via Commit-Boost 2 | 3 | *Many thanks to Eitan Seri-Levi, Drew van der Werff, Barnabé Monnot, and the entire Inclusion List Module via Commit-Boost team for feedback on this design document (feedback does not imply endorsement).* 4 | 5 | Below, we discuss a [Commit-Boost](https://github.com/Commit-Boost/commit-boost-client) [module](https://commit-boost.github.io/commit-boost-client/category/developing) that allows a proposer of a slot to create a list of transactions that it wants to be included in the block produced for its slot while still outsourcing block construction via [MEV-Boost](https://boost.flashbots.net/). In this module, the proposer constructs an inclusion list and propagates it to one or more relays that enforce the inclusion list. The proposer expresses a certain value that it has for getting the transactions from the inclusion list included in the block. This design trades off the adoption of the out-of-protocol inclusion list with the effectiveness of the inclusion list. 6 | 7 | The inclusion list module uses [Commit-Boost](https://github.com/Commit-Boost/commit-boost-client) and the [Bolt builder constraints API](https://chainbound.github.io/bolt-docs/api/builder-api). A beacon node can treat this module as an MEV-Boost relay. 8 | 9 | ## Construction 10 | 11 | The proposer of slot $N$ constructs a list of transactions that it wants to include in the block proposed during slot $N$. 12 | 13 | The list is constructed using the `get_filtered_transactions` function that takes as input a vector of `Transaction` objects; these are transactions that were pending in the mempool but not included in an earlier block and a `Block` that is the block proposed during slot $N-1$. For each of these transactions in `Transaction`, it checks whether it would have been possible to include the transaction based on two conditions: 1) does the transaction pay a strictly positive priority fee, and 2) is the gas limit of the transaction smaller or equal to the amount of gas leftover in the `Block`. The transaction is added to the inclusion list if these two conditions are satisfied. 14 | 15 | The transactions included in the `Transaction` object are transactions that were seen by the proposer of slot $N$ at the beginning of the slot and were not included in any earlier block. Setting the freeze deadline of transactions to be included in the `Transaction` object earlier means the proposer only considers transactions that are more likely to be excluded. In contrast, it increases the average wait time for transactions to be included. 16 | 17 | ## Propagation 18 | 19 | The proposer sends its inclusion list to the MEV-Boost relay(s) of its choosing as soon as it is constructed. Next to the inclusion list, the proposer forwards the value that it attaches to the inclusion list being satisfied. 20 | 21 | ## Auction impact 22 | 23 | If the relay receives an inclusion list from a proposer, it first checks whether this proposer is indeed the proposer of the slot to which the inclusion list corresponds. Then, it broadcasts the inclusion list so that the builders who interact with the relay are aware of it and the value that the proposer attaches to it. 24 | 25 | Each builder then builds blocks and submits bids to the relay in the auction. When the proposer calls `get_header`, the relay forwards the header corresponding to the highest adjusted bid, where the highest adjusted bid is computed as follows. Note that the highest adjusted bid is purely to incorporate the value that the proposer assigns to the inclusion list, and it is not the value that the proposer receives. The relay should also forward the bid to the proposer. 26 | 27 | $$ 28 | \text{Highest Adjusted Bid} = \max\{\text{IL-Satisfying Bid} + \text{Proposer IL Value}, \enskip \text{Non-IL-Satisfying Bid} \} 29 | $$ 30 | 31 | Along with the header, the relay also forwards whether the inclusion list has been satisfied or not. This is useful if the proposer wants to compare bids across relays. 32 | 33 | The relay could either compute the highest adjusted bid [optimistically](https://github.com/michaelneuder/optimistic-relay-documentation/blob/main/proposal.md) by letting builders state whether they satisfy the IL or not or non-optimistically by 1) ensuring that the block is valid and 2) ensuring that all transactions from the inclusion list are included in the block. 34 | 35 | ## Properties 36 | 37 | The inclusion list can have various properties, as described in the research on in-protocol inclusion lists. This section details the design trade-offs for this inclusion list module. 38 | 39 | ### Optimistic vs. Non-Optimistic 40 | 41 | The proposer could either enforce the inclusion list optimistically or non-optimistically. In this design, we have chosen optimistic enforcement. This means that the proposer does not require proof from the builder or relay that the block that corresponds to the header that the proposer commits to includes the transactions from the inclusion list. Instead, the proposer trusts that the relay correctly reports whether or not the header that the proposer receives corresponds to a payload that satisfies the inclusion list. 42 | 43 | If the relay makes an error and says a payload satisfies the inclusion list, while this is not the case, the relay must compensate the proposer for the value that the proposer had attached to its inclusion list being validated. 44 | 45 | We chose optimistic enforcement because the proposer must trust the relay, regardless of whether it uses the inclusion list module, to ensure that the proposer receives the value of the bid it commits to. This means that either the relay must check that the block is valid and pay the bid, or it must guarantee payment. 46 | 47 | The inclusion list module does not introduce qualitatively new trust assumptions. The proposer must now trust the relay that the block is valid and satisfies the inclusion. If one of these two conditions is not met, the relay must guarantee payment to the proposer. 48 | 49 | Using a proof-based system in which the proposer only signs the header if it knows that the inclusion list has been satisfied would ensure that the proposer knows whether the inclusion list is satisfied if the block is valid. However, the block could still be invalid. Moreover, the proof increases latency, which would result in a reduction of expected profit and would, therefore, likely lead to reduced adoption. 50 | 51 | ### [Forward](https://notes.ethereum.org/@fradamt/forward-inclusion-lists) vs. Spot 52 | 53 | The inclusion list applies to the slot of the proposer who constructs it and could thus be deemed a spot inclusion list. Spot inclusion lists are known to be not incentive compatible as the proposer restricts the possible blocks that can be built for its slot and, therefore, also restricts the maximum value it can extract. 54 | 55 | This design, however, assumes that a proposer has some private value for a block to satisfy its inclusion list. The proposer communicates this private value to the relay alongside its inclusion list, and this value is then used to compute the highest adjusted bid, as detailed earlier. Given this private valuation, the inclusion list is incentive-compatible. 56 | 57 | The private valuation could originate from various sources; for example, the validator may value contributing to the network's credible neutrality by including all transactions it sees. 58 | 59 | ### Conditional vs. [Unconditional](https://ethresear.ch/t/unconditional-inclusion-lists/18500) 60 | 61 | The inclusion list is applied unconditionally, regardless of congestion. Hence, if the block is full, the inclusion list is still only satisfied if all transactions from the inclusion list are included in the block. This is different from the conditional inclusion lists that are often discussed for in-protocol inclusion lists. 62 | 63 | This design chooses unconditional inclusion lists because it wants to convey the proposer's preference for certain transactions as clearly as possible. A proposer may want certain transactions included regardless of whether other transactions can be included. 64 | 65 | Finally, the inclusion list is conditional on the relative value between a payload that satisfies it and one that does not. Times of congestion may also be accompanied by higher priority fees; hence, in practice, during congestion, the bid of a payload that does not satisfy the inclusion list may be higher than the sum of the bid of a payload that satisfies the inclusion list and the private value. 66 | 67 | ### [Uncrowdability](https://ethresear.ch/t/uncrowdable-inclusion-lists-the-tension-between-chain-neutrality-preconfirmations-and-proposer-commitments/19372) 68 | 69 | Uncrowdability means that an inclusion list will be used for credible neutrality and not for other profit-driven motives. This is not a concern with out-of-protocol inclusion lists via Commit-Boost because these inclusion lists do not have any special properties that make it more attractive to use for profit-driven motives as there are other modules built on Commit-Boost that are specifically designed for things like preconfirmations. Unlike in-protocol inclusion lists, the block validity is not tied to whether the inclusion list is satisfied. If a proposer wants to pursue profits, they could use another module on Commit-Boost rather than the inclusion list module. 70 | 71 | ## Evaluation 72 | 73 | Since the inclusion list is validated optimistically, there must be a check to determine whether it was indeed satisfied. This can be the same check as determining whether the block indeed paid the validator the bid. 74 | 75 | ## Timing and Double Submitting 76 | 77 | Unlike in-protocol inclusion lists, there is no consensus on the inclusion list, so there are no strict timing deadlines. When the proposer calls `get_header`, the relay communicates whether the payload satisfies the inclusion list or not. If the inclusion list was not submitted on time, the relay communicates that the payload does not satisfy the inclusion list. 78 | 79 | If the are two or more inclusion lists that the proposer has specified, the relay must not be able to be grieved and (socially) forced to pay out to a proposer. Therefore, in this design, the relay will return that the payload satisfies the inclusion list as long as the payload satisfies an inclusion list that the proposer has broadcasted. 80 | 81 | ## Default Preference Value 82 | 83 | The proposer specifies a value that it attaches to the inclusion list being satisfied by a payload. This value functions as the maximum value a proposer may lose if it made the inclusion list for purely altruistic purposes. So, this value is similar to the [min-bid](https://writings.flashbots.net/the-cost-of-resilience) parameter in MEV-Boost. As Data Always has shown, it is very important to set this value properly since it is a careful balance between the cost of altruism and the effectiveness of the policy. 84 | 85 | ### [Encrypted Mempool](https://joncharbonneau.substack.com/p/encrypted-mempools) / min-bid Model 86 | 87 | Encrypted mempools increase the [cost of censorship](https://cdn.prod.website-files.com/642f3d0236c604d1022330f2/6499f35e0bd0f43471a95adc_MEV_Auctions_ArXiV_6.pdf) from the priority fee a transaction pays to the priority fees that all transactions pay since an adversary cannot selectively exclude one specific transaction but must exclude all transactions. This can be seen as a large constant addition to the cost of censorship. The out-of-protocol inclusion lists could emulate this model by setting the default preference value to a fixed constant, much like the min-bid parameter. This would also form a clear maximum loss that an altruistic proposer may face. 88 | 89 | ### [Multiple Proposers](https://cdn.prod.website-files.com/642f3d0236c604d1022330f2/6499f35e0bd0f43471a95adc_MEV_Auctions_ArXiV_6.pdf) / local block value boost Model 90 | 91 | Multiple proposers increase the cost of censorship from the priority fee to the priority fee multiplied by the number of proposers since an adversary must bribe all proposers to exclude the transaction. The out-of-protocol inclusion list could achieve a similar cost of censorship by setting the default preference value to be a multiple of the priority fees of the transactions included in the inclusion list. This is more similar to the [`local-block-value-boost` parameter](https://docs.prylabs.network/docs/advanced/builder#prioritizing-local-blocks) implemented by clients. 92 | 93 | I would argue that this design should use the encrypted mempool / min-bid model since it is easier for the proposer to reason about a correct value and because clients are implementing a maximum value for the local block value boost based on [Data Always’ recent post](https://hackmd.io/@dataalways/censorship-resistance-today). 94 | 95 | 96 | # How to run 97 | 98 | The following commands will run the IL-Boost module. 99 | 100 | `cargo build --release` 101 | 102 | `docker compose -f cb.docker-compose.yml up -d` 103 | 104 | `docker build -t il-boost . -f ./Dockerfile` 105 | 106 | But first you will need to update the `cb.docker-compose.yml` and `cb-config.toml` files 107 | 108 | For `cb.docker-compose.yml` you'll need to change the following values 109 | 110 | - `${YOUR_PATH_TO_KEYS_DIR}` should be replaced with the relative/absolute path to your keys directory 111 | - `${YOUR_PATH_TO_SERETS_DIR}` should be replaced with the relative/absolute path to your secrets directory 112 | - `${JWT}` should be replaced with your nodes JWT key. Note that this value needs to be replaced in two places 113 | 114 | For `cb-config.toml` please see the list of example configurations and update them accordingly 115 | 116 | ## EL configs 117 | 118 | Make sure to enable the following web api features 119 | 120 | `admin,engine,net,eth,web3,debug,txpool` 121 | 122 | i.e `--http.api=admin,engine,net,eth,web3,debug,txpool` in Geth 123 | 124 | ## CL/VC configs 125 | 126 | Make sure to enable external block building capabilities and point the block builder URL to your local Commit-Boost PBS module 127 | --------------------------------------------------------------------------------