Skip to content

Add Rustic builder #15

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ members = [
"relay-api-types",
"relay-client",
"relay-server",
"rustic-builder",
"searcher-api-types",
"searcher-client",
"common"
Expand All @@ -18,7 +19,7 @@ members = [
async-trait = "0.1"
axum = { version = "0.8", features = ["ws"] }
bytes = "1.6"
eth2 = { git = "https://github.com/sigp/lighthouse.git", rev = "574b204bdb39fbfd7939c901a595647794b89274" }
eth2 = { git = "https://github.com/sigp/lighthouse.git", rev = "a0a6b9300f11b4875e18d42bc4ce1f2787f4448b" }
ethereum_serde_utils = "0.7"
ethereum_ssz = "0.8"
ethereum_ssz_derive = "0.8"
Expand All @@ -33,5 +34,5 @@ superstruct = "0.8"
tokio = { version = "1", default-features = false, features = ["signal", "rt-multi-thread", "macros"] }
tokio-tungstenite = "0.24.0"
tracing = { version = "0.1", features = ["attributes"] }
types = { git = "https://github.com/sigp/lighthouse.git", rev = "574b204bdb39fbfd7939c901a595647794b89274" }
types = { git = "https://github.com/sigp/lighthouse.git", rev = "a0a6b9300f11b4875e18d42bc4ce1f2787f4448b" }
rand = "0.8"
33 changes: 33 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
FROM rust:1.85-slim as builder

WORKDIR /usr/src/workspace

# Install build dependencies
RUN apt-get update && apt-get install -y \
build-essential \
pkg-config \
libssl-dev \
perl \
cmake \
libclang-dev \
protobuf-compiler \
&& rm -rf /var/lib/apt/lists/*

COPY . .

# Build only the rustic-builder binary from the workspace
RUN cargo build --release --bin rustic-builder

FROM debian:bookworm-slim

RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
libssl3 \
&& rm -rf /var/lib/apt/lists/*

# Copy the specific binary from the workspace target directory
COPY --from=builder /usr/src/workspace/target/release/rustic-builder /usr/local/bin/

EXPOSE 8560

ENTRYPOINT ["rustic-builder"]
1 change: 1 addition & 0 deletions common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ serde.workspace = true
serde_json.workspace = true
tracing.workspace = true
beacon-api-types = { path = "../beacon-api-types" }
types.workspace = true
54 changes: 12 additions & 42 deletions common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ use axum::{
extract::{FromRequest, Request},
response::{IntoResponse, Response},
};
use beacon_api_types::{
fork_versioned_response::EmptyMetadata, ForkName, ForkVersionDecode, ForkVersionDeserialize,
ForkVersionedResponse,
};
use beacon_api_types::{ForkName, ForkVersionDecode, ForkVersionedResponse};
use bytes::Bytes;
use flate2::read::GzDecoder;
use http::header::CONTENT_ENCODING;
Expand All @@ -16,6 +13,7 @@ use serde::{Deserialize, Serialize};
use ssz::Encode;
use std::{fmt, io::Read, str::FromStr};
use tracing::error;
use types::{beacon_response::EmptyMetadata, ContextDeserialize};

pub const CONSENSUS_VERSION_HEADER: &str = "Eth-Consensus-Version";

Expand Down Expand Up @@ -53,7 +51,7 @@ where
let body_content = match content_type {
ContentType::Json => {
let body = ForkVersionedResponse {
version: Some(fork_name),
version: fork_name,
metadata: EmptyMetadata {},
data: body,
};
Expand Down Expand Up @@ -192,7 +190,10 @@ pub struct JsonOrSszWithFork<T>(pub T);

impl<T, S> FromRequest<S> for JsonOrSszWithFork<T>
where
T: serde::de::DeserializeOwned + ForkVersionDecode + 'static,
T: serde::de::DeserializeOwned
+ ForkVersionDecode
+ for<'de> ContextDeserialize<'de, ForkName>
+ 'static,
S: Send + Sync,
{
type Rejection = Response;
Expand All @@ -204,21 +205,23 @@ where
.and_then(|value| value.to_str().ok());
let fork_name = headers
.get(CONSENSUS_VERSION_HEADER)
.and_then(|value| ForkName::from_str(value.to_str().unwrap()).ok());
.and_then(|value| ForkName::from_str(value.to_str().unwrap()).ok())
.ok_or(StatusCode::BAD_REQUEST.into_response())?;

let bytes = Bytes::from_request(req, _state)
.await
.map_err(IntoResponse::into_response)?;

if let Some(content_type) = content_type {
if content_type.starts_with(&ContentType::Json.to_string()) {
let payload: T = serde_json::from_slice(&bytes)
let mut de = serde_json::Deserializer::from_slice(&bytes);
let payload = T::context_deserialize(&mut de, fork_name)
.map_err(|_| StatusCode::BAD_REQUEST.into_response())?;
return Ok(Self(payload));
}

if content_type.starts_with(&ContentType::Ssz.to_string()) {
let payload = T::from_ssz_bytes_by_fork(&bytes, fork_name.unwrap())
let payload = T::from_ssz_bytes_by_fork(&bytes, fork_name)
.map_err(|_| StatusCode::BAD_REQUEST.into_response())?;
return Ok(Self(payload));
}
Expand Down Expand Up @@ -390,39 +393,6 @@ pub fn custom_internal_err(message: String) -> ErrorResponse {
}
}

#[must_use]
#[derive(Debug, Clone, Copy, Default)]
pub struct JsonConsensusVersionHeader<T>(pub T);

impl<T, S> FromRequest<S> for JsonConsensusVersionHeader<T>
where
T: ForkVersionDeserialize + 'static,
S: Send + Sync,
{
type Rejection = Response;

async fn from_request(req: Request, _state: &S) -> Result<Self, Self::Rejection> {
let headers = req.headers().clone();
let fork_name = headers
.get(CONSENSUS_VERSION_HEADER)
.and_then(|value| value.to_str().ok())
.and_then(|s| s.parse().ok())
.ok_or(StatusCode::BAD_REQUEST.into_response())?;

let bytes = Bytes::from_request(req, _state)
.await
.map_err(IntoResponse::into_response)?;

let result = ForkVersionDeserialize::deserialize_by_fork::<serde_json::Value>(
serde_json::de::from_slice(&bytes)
.map_err(|_| StatusCode::BAD_REQUEST.into_response())?,
fork_name,
)
.map_err(|_| StatusCode::BAD_REQUEST.into_response())?;
Ok(Self(result))
}
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Accept {
Json,
Expand Down
1 change: 1 addition & 0 deletions rustic-builder/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/target
25 changes: 25 additions & 0 deletions rustic-builder/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
[package]
name = "rustic-builder"
version = "0.1.0"
edition = "2021"

[dependencies]
clap = {version = "4.5", features = ["derive"]}
builder-server = { path = "../builder-server" }
ethereum-apis-common = { path = "../common" }
async-trait.workspace = true
tokio.workspace = true
tracing.workspace = true
tracing-subscriber = {version = "0.3.8", features = ["env-filter"]}
tracing-error = "0.2.0"
tracing-core = "0.1.21"
async-channel = "1.9.0"
futures-channel = "0.3.21"
axum.workspace = true
hex = "0.4.3"
types.workspace = true
eth2.workspace = true
execution_layer = { git = "https://github.com/sigp/lighthouse", rev = "a0a6b9300f11b4875e18d42bc4ce1f2787f4448b" }
task_executor = { git = "https://github.com/sigp/lighthouse", rev = "a0a6b9300f11b4875e18d42bc4ce1f2787f4448b" }
sensitive_url = { git = "https://github.com/sigp/lighthouse", rev = "a0a6b9300f11b4875e18d42bc4ce1f2787f4448b" }
eth2_network_config = { git = "https://github.com/sigp/lighthouse", rev = "a0a6b9300f11b4875e18d42bc4ce1f2787f4448b" }
45 changes: 45 additions & 0 deletions rustic-builder/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Rustic Builder

## Overview

A simple mock builder implementation that serves local mempool transactions from an Ethereum node through the Builder API flow.
It works as a wrapper over Lighthouse's [mock-builder](https://github.com/sigp/lighthouse/blob/unstable/beacon_node/execution_layer/src/test_utils/mock_builder.rs) which is used for lighthouse tests. This means that as Lighthouse implements support for new forks, the builder automatically gets support for the fork by just pointing it to the right lighthouse commit.

The name references both its implementation language (Rust) and its rustic nature - serving farm-to-table payloads from your local execution client.

Note: This currently does not support updating the gas limit at runtime based on the validator registrations. It is meant to use in a controlled [kurtosis](https://github.com/ethpandaops/ethereum-package) like setup where the gas limit does not change over the duration of the testnet.

## Installation

### From Source
```
cargo build --release
```

### Using Docker
```bash
docker build -t rustic-builder .
```

## Usage

Needs a fully synced ethereum node (Beacon node + Execution client)

### Running from Binary
```
./target/release/rustic-builder --execution-endpoint http://localhost:8551 --beacon-node http://localhost:5052 --jwt-secret jwt.hex --port 8560
```

### Running with Docker
```bash
docker run -p 8560:8560 \
-v /path/to/jwt.hex:/jwt.hex \
rustic-builder \
--execution-endpoint http://execution-client:8551 \
--beacon-node http://beacon-node:5052
```

Note: When running with Docker, make sure to:
- Mount your JWT secret file
- Use appropriate network settings (--network host if running nodes locally)
- Adjust the execution/beacon endpoints to match your setup
87 changes: 87 additions & 0 deletions rustic-builder/src/builder_impl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
use std::{ops::Deref, sync::Arc};

use async_trait::async_trait;
use builder_server::{builder::Builder, FullPayloadContents};
use ethereum_apis_common::{custom_internal_err, ErrorResponse};
use execution_layer::test_utils::MockBuilder;
use types::{
builder_bid::SignedBuilderBid, ChainSpec, EthSpec, ExecutionBlockHash, ForkName,
PublicKeyBytes, SignedBlindedBeaconBlock, SignedValidatorRegistrationData, Slot,
};

#[derive(Clone)]
pub struct RusticBuilder<E: EthSpec> {
builder: MockBuilder<E>,
spec: Arc<ChainSpec>,
}

impl<E: EthSpec> RusticBuilder<E> {
pub fn new(builder: MockBuilder<E>, spec: Arc<ChainSpec>) -> Self {
Self { builder, spec }
}
}

impl<E: EthSpec> Deref for RusticBuilder<E> {
type Target = MockBuilder<E>;
fn deref(&self) -> &Self::Target {
&self.builder
}
}

impl<E: EthSpec> AsRef<RusticBuilder<E>> for RusticBuilder<E> {
fn as_ref(&self) -> &RusticBuilder<E> {
self
}
}

#[async_trait]
impl<E: EthSpec> Builder<E> for RusticBuilder<E> {
fn fork_name_at_slot(&self, slot: Slot) -> ForkName {
self.spec.fork_name_at_slot::<E>(slot)
}

async fn register_validators(
&self,
registrations: Vec<SignedValidatorRegistrationData>,
) -> Result<(), ErrorResponse> {
tracing::info!("Registering validators, count: {}", registrations.len());
self.builder
.register_validators(registrations)
.await
.map_err(custom_internal_err)
}

async fn get_header(
&self,
slot: Slot,
parent_hash: ExecutionBlockHash,
pubkey: PublicKeyBytes,
) -> Result<SignedBuilderBid<E>, ErrorResponse> {
tracing::info!(
"Getting header for slot {}, parent_hash: {}, pubkey: {:?}",
slot,
parent_hash,
pubkey
);
self.builder
.get_header(slot, parent_hash, pubkey)
.await
.map_err(custom_internal_err)
}

async fn submit_blinded_block(
&self,
signed_block: SignedBlindedBeaconBlock<E>,
) -> Result<FullPayloadContents<E>, ErrorResponse> {
tracing::info!(
"Submitting signed blinded block to builder, slot: {}, root: {}, fork: {}",
signed_block.message().slot(),
signed_block.canonical_root(),
signed_block.fork_name_unchecked(),
);
self.builder
.submit_blinded_block(signed_block)
.await
.map_err(custom_internal_err)
}
}
1 change: 1 addition & 0 deletions rustic-builder/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub mod builder_impl;
Loading