Skip to content

Commit 2b87455

Browse files
authored
Merge pull request #4 from sigp/add-builder-server-client
Add builder server client
2 parents a696b28 + fbc5732 commit 2b87455

File tree

15 files changed

+627
-244
lines changed

15 files changed

+627
-244
lines changed

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,13 @@
22
resolver = "2"
33
members = [
44
"builder-api-types",
5+
"builder-server",
56
"relay-client",
67
"beacon-client",
78
"relay-api-types",
89
"beacon-api-types",
910
"relay-server",
11+
"common"
1012
]
1113

1214
[workspace.dependencies]

builder-client/src/lib.rs

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
use async_trait::async_trait;
2+
use ethereum_apis_common::{build_response, ErrorResponse};
3+
use reqwest::Client;
4+
use reqwest::Url;
5+
use serde::de::DeserializeOwned;
6+
use types::{
7+
builder_bid::SignedBuilderBid, eth_spec::EthSpec, ExecutionBlockHash, ExecutionPayload,
8+
ForkName, PublicKeyBytes, SignedBlindedBeaconBlock, SignedValidatorRegistrationData, Slot,
9+
};
10+
11+
#[derive(Debug)]
12+
pub enum Error {
13+
Reqwest(reqwest::Error),
14+
InvalidJson(serde_json::Error, String),
15+
ServerMessage(ErrorResponse),
16+
StatusCode(reqwest::StatusCode),
17+
InvalidUrl(Url),
18+
}
19+
20+
impl From<reqwest::Error> for Error {
21+
fn from(e: reqwest::Error) -> Self {
22+
Error::Reqwest(e)
23+
}
24+
}
25+
26+
#[derive(Clone)]
27+
pub struct BuilderClient {
28+
client: Client,
29+
base_url: Url,
30+
}
31+
32+
impl BuilderClient {
33+
pub fn new(base_url: Url) -> Self {
34+
Self {
35+
client: Client::new(),
36+
base_url,
37+
}
38+
}
39+
40+
async fn build_response<T>(&self, response: reqwest::Response) -> Result<T, Error>
41+
where
42+
T: DeserializeOwned,
43+
{
44+
let status = response.status();
45+
let text = response.text().await?;
46+
47+
if status.is_success() {
48+
serde_json::from_str(&text).map_err(|e| Error::InvalidJson(e, text))
49+
} else {
50+
Err(Error::ServerMessage(
51+
serde_json::from_str(&text).map_err(|e| Error::InvalidJson(e, text))?,
52+
))
53+
}
54+
}
55+
56+
pub async fn register_validators(
57+
&self,
58+
registrations: Vec<SignedValidatorRegistrationData>,
59+
) -> Result<(), Error> {
60+
let mut url = self.base_url.clone();
61+
url.path_segments_mut()
62+
.map_err(|_| Error::InvalidUrl(self.base_url.clone()))?
63+
.extend(&["eth", "v1", "builder", "validators"]);
64+
65+
let response = self.client.post(url).json(&registrations).send().await?;
66+
67+
self.build_response(response).await
68+
}
69+
70+
pub async fn submit_blinded_block<E: EthSpec>(
71+
&self,
72+
block: SignedBlindedBeaconBlock<E>,
73+
) -> Result<ExecutionPayload<E>, Error> {
74+
let mut url = self.base_url.clone();
75+
url.path_segments_mut()
76+
.map_err(|_| Error::InvalidUrl(self.base_url.clone()))?
77+
.extend(&["eth", "v1", "builder", "blinded_blocks"]);
78+
79+
let response = self.client.post(url).json(&block).send().await?;
80+
81+
self.build_response(response).await
82+
}
83+
84+
pub async fn get_header<E: EthSpec>(
85+
&self,
86+
slot: Slot,
87+
parent_hash: ExecutionBlockHash,
88+
pubkey: PublicKeyBytes,
89+
) -> Result<SignedBuilderBid<E>, Error> {
90+
let mut url = self.base_url.clone();
91+
url.path_segments_mut()
92+
.map_err(|_| Error::InvalidUrl(self.base_url.clone()))?
93+
.extend(&[
94+
"eth",
95+
"v1",
96+
"builder",
97+
"header",
98+
&slot.to_string(),
99+
&parent_hash.to_string(),
100+
&pubkey.to_string(),
101+
]);
102+
103+
let response = self.client.get(url).send().await?;
104+
105+
self.build_response(response).await
106+
}
107+
108+
pub async fn get_status(&self) -> Result<(), Error> {
109+
let mut url = self.base_url.clone();
110+
url.path_segments_mut()
111+
.map_err(|_| Error::InvalidUrl(self.base_url.clone()))?
112+
.extend(&["eth", "v1", "builder", "status"]);
113+
114+
let response = self.client.get(url).send().await?;
115+
116+
if response.status().is_success() {
117+
Ok(())
118+
} else {
119+
Err(Error::StatusCode(response.status()))
120+
}
121+
}
122+
}
123+

builder-server/Cargo.toml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
[package]
2+
name = "builder-server"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
async-trait.workspace = true
8+
axum.workspace = true
9+
bytes.workspace = true
10+
ethereum_ssz.workspace = true
11+
flate2.workspace = true
12+
futures.workspace = true
13+
http.workspace = true
14+
builder-api-types = { path = "../builder-api-types" }
15+
ethereum-apis-common = { path = "../common" }
16+
serde.workspace = true
17+
serde_json.workspace = true
18+
tokio.workspace = true
19+
tracing.workspace = true
20+
types.workspace = true

builder-server/src/builder.rs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
use async_trait::async_trait;
2+
use ethereum_apis_common::ErrorResponse;
3+
use types::{
4+
builder_bid::SignedBuilderBid, eth_spec::EthSpec, ExecutionBlockHash, ExecutionPayload,
5+
ForkName, PublicKeyBytes, SignedBlindedBeaconBlock, SignedValidatorRegistrationData, Slot,
6+
};
7+
8+
#[async_trait]
9+
pub trait Builder<E: EthSpec> {
10+
async fn register_validators(
11+
&self,
12+
registrations: Vec<SignedValidatorRegistrationData>,
13+
) -> Result<(), ErrorResponse>;
14+
15+
async fn submit_blinded_block(
16+
&self,
17+
block: SignedBlindedBeaconBlock<E>,
18+
) -> Result<ExecutionPayload<E>, ErrorResponse>;
19+
20+
async fn get_header(
21+
&self,
22+
slot: Slot,
23+
parent_hash: ExecutionBlockHash,
24+
pubkey: PublicKeyBytes,
25+
) -> Result<SignedBuilderBid<E>, ErrorResponse>;
26+
27+
fn fork_name_at_slot(&self, slot: Slot) -> ForkName;
28+
}

builder-server/src/lib.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
pub use builder_api_types::*;
2+
3+
pub mod builder;
4+
pub mod server;

builder-server/src/server.rs

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
use axum::{
2+
body::Body,
3+
extract::{Path, State},
4+
http::StatusCode,
5+
response::Response,
6+
routing::{get, post},
7+
Json, Router,
8+
};
9+
use ethereum_apis_common::build_response;
10+
use types::{
11+
eth_spec::EthSpec, fork_versioned_response::EmptyMetadata, ExecutionBlockHash,
12+
ForkVersionedResponse, PublicKeyBytes, SignedBlindedBeaconBlock,
13+
SignedValidatorRegistrationData, Slot,
14+
};
15+
16+
use crate::builder::Builder;
17+
18+
pub fn new<I, A, E>(api_impl: I) -> Router
19+
where
20+
E: EthSpec,
21+
I: AsRef<A> + Clone + Send + Sync + 'static,
22+
A: Builder<E> + 'static,
23+
{
24+
Router::new()
25+
.route(
26+
"/eth/v1/builder/validators",
27+
post(register_validators::<I, A, E>),
28+
)
29+
.route(
30+
"/eth/v1/builder/blinded_blocks",
31+
post(submit_blinded_block::<I, A, E>),
32+
)
33+
.route("/eth/v1/builder/status", get(get_status))
34+
.route(
35+
"/eth/v1/builder/header/:slot/:parent_hash/:pubkey",
36+
get(get_header::<I, A, E>),
37+
)
38+
.with_state(api_impl)
39+
}
40+
41+
async fn register_validators<I, A, E>(
42+
State(api_impl): State<I>,
43+
Json(registrations): Json<Vec<SignedValidatorRegistrationData>>,
44+
) -> Result<Response<Body>, StatusCode>
45+
where
46+
E: EthSpec,
47+
I: AsRef<A> + Send + Sync,
48+
A: Builder<E>,
49+
{
50+
let res = api_impl.as_ref().register_validators(registrations).await;
51+
build_response(res).await
52+
}
53+
54+
async fn submit_blinded_block<I, A, E>(
55+
State(api_impl): State<I>,
56+
Json(block): Json<SignedBlindedBeaconBlock<E>>,
57+
) -> Result<Response<Body>, StatusCode>
58+
where
59+
E: EthSpec,
60+
I: AsRef<A> + Send + Sync,
61+
A: Builder<E>,
62+
{
63+
let res = api_impl
64+
.as_ref()
65+
.submit_blinded_block(block)
66+
.await
67+
.map(|payload| ForkVersionedResponse {
68+
version: Some(payload.fork_name()),
69+
metadata: EmptyMetadata {},
70+
data: payload,
71+
});
72+
build_response(res).await
73+
}
74+
75+
async fn get_status() -> StatusCode {
76+
StatusCode::OK
77+
}
78+
79+
async fn get_header<I, A, E>(
80+
State(api_impl): State<I>,
81+
Path((slot, parent_hash, pubkey)): Path<(Slot, ExecutionBlockHash, PublicKeyBytes)>,
82+
) -> Result<Response<Body>, StatusCode>
83+
where
84+
E: EthSpec,
85+
I: AsRef<A> + Send + Sync,
86+
A: Builder<E>,
87+
{
88+
let res = api_impl
89+
.as_ref()
90+
.get_header(slot, parent_hash, pubkey)
91+
.await
92+
.map(|signed_bid| ForkVersionedResponse {
93+
version: Some(api_impl.as_ref().fork_name_at_slot(slot)),
94+
metadata: EmptyMetadata {},
95+
data: signed_bid,
96+
});
97+
build_response(res).await
98+
}

common/Cargo.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[package]
2+
name = "ethereum-apis-common"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[dependencies]
7+
async-trait.workspace = true
8+
axum.workspace = true
9+
bytes.workspace = true
10+
ethereum_ssz.workspace = true
11+
flate2.workspace = true
12+
futures.workspace = true
13+
http.workspace = true
14+
serde.workspace = true
15+
serde_json.workspace = true
16+
tokio.workspace = true
17+
tracing.workspace = true
18+
types.workspace = true

0 commit comments

Comments
 (0)