Skip to content

Commit

Permalink
Merge pull request #292 from chainbound/nico/feat/sidecar-delegations
Browse files Browse the repository at this point in the history
feat(sidecar): load delegations on startup and send them upon registration
  • Loading branch information
merklefruit authored Oct 15, 2024
2 parents 4634ff9 + df26e3e commit 4a25052
Show file tree
Hide file tree
Showing 32 changed files with 739 additions and 470 deletions.
1 change: 1 addition & 0 deletions bolt-sidecar/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion bolt-sidecar/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ thiserror = "1.0"
rand = "0.8.5"
dotenvy = "0.15.7"
regex = "1.10.5"
# backtrace = "0.3.74"
toml = "0.5"

# tracing
tracing = "0.1.40"
Expand Down
20 changes: 20 additions & 0 deletions bolt-sidecar/Config.example.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# ports
port = 8000
metrics_port = 3300

# node urls
execution_api_url = "http://localhost:8545"
beacon_api_url = "http://localhost:5052"
engine_api_url = "http://localhost:8551"

# constraints options
constraints_url = "http://localhost:3030"
constraints_proxy_port = 18551

# chain options
chain = "kurtosis"
slot_time = 2

# signing options
private_key = "0x359c156600e1f5715a58c9e09f17c8d04e7ee3a9f88b39f6e489ffca0148fabe"
delegations_path = "./delegations.json"
13 changes: 8 additions & 5 deletions bolt-sidecar/bin/sidecar.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
use bolt_sidecar::{telemetry::init_telemetry_stack, Opts, SidecarDriver};
use clap::Parser;
use eyre::{bail, Result};
use tracing::info;

use bolt_sidecar::{telemetry::init_telemetry_stack, Opts, SidecarDriver};

#[tokio::main]
async fn main() -> Result<()> {
let opts = Opts::parse();
let opts = if let Ok(config_path) = std::env::var("BOLT_SIDECAR_CONFIG_PATH") {
Opts::parse_from_toml(config_path.as_str())?
} else {
Opts::parse()
};

let metrics_port =
if !opts.telemetry.disable_metrics { Some(opts.telemetry.metrics_port) } else { None };
if let Err(err) = init_telemetry_stack(metrics_port) {
if let Err(err) = init_telemetry_stack(opts.telemetry.metrics_port()) {
bail!("Failed to initialize telemetry stack: {:?}", err)
}

Expand Down
6 changes: 4 additions & 2 deletions bolt-sidecar/src/api/builder.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::{sync::Arc, time::Duration};

use axum::{
body::{self, Body},
extract::{Path, Request, State},
Expand All @@ -16,7 +18,6 @@ use ethereum_consensus::{
use parking_lot::Mutex;
use reqwest::Url;
use serde::Deserialize;
use std::{sync::Arc, time::Duration};
use thiserror::Error;
use tokio::net::TcpListener;
use tracing::{debug, error, info, warn};
Expand All @@ -26,8 +27,9 @@ use super::spec::{
STATUS_PATH,
};
use crate::{
builder::payload_fetcher::PayloadFetcher,
client::constraints_client::ConstraintsClient,
primitives::{GetPayloadResponse, PayloadFetcher, SignedBuilderBid},
primitives::{GetPayloadResponse, SignedBuilderBid},
telemetry::ApiMetrics,
};

Expand Down
7 changes: 4 additions & 3 deletions bolt-sidecar/src/api/commitments/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ use serde_json::Value;
use tracing::{debug, error, info, instrument};

use crate::{
commitments::headers::auth_from_headers, common::CARGO_PKG_VERSION,
primitives::InclusionRequest,
commitments::headers::auth_from_headers,
common::CARGO_PKG_VERSION,
primitives::{commitment::SignatureError, InclusionRequest},
};

use super::{
Expand Down Expand Up @@ -70,7 +71,7 @@ pub async fn rpc_entrypoint(
"Recovered signer does not match the provided signer"
);

return Err(Error::InvalidSignature(crate::primitives::SignatureError));
return Err(Error::InvalidSignature(SignatureError));
}

// Set the request signer
Expand Down
5 changes: 3 additions & 2 deletions bolt-sidecar/src/api/commitments/headers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ use std::str::FromStr;
use alloy::primitives::{Address, Signature};
use axum::http::HeaderMap;

use crate::primitives::commitment::SignatureError;

use super::spec::{Error, SIGNATURE_HEADER};

/// Extracts the signature ([SIGNATURE_HEADER]) from the HTTP headers.
Expand All @@ -19,8 +21,7 @@ pub fn auth_from_headers(headers: &HeaderMap) -> Result<(Address, Signature), Er
let address = Address::from_str(address).map_err(|_| Error::MalformedHeader)?;

let sig = split.next().ok_or(Error::MalformedHeader)?;
let sig = Signature::from_str(sig)
.map_err(|_| Error::InvalidSignature(crate::primitives::SignatureError))?;
let sig = Signature::from_str(sig).map_err(|_| Error::InvalidSignature(SignatureError))?;

Ok((address, sig))
}
Expand Down
2 changes: 1 addition & 1 deletion bolt-sidecar/src/api/commitments/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ pub enum Error {
NoSignature,
/// Invalid signature.
#[error(transparent)]
InvalidSignature(#[from] crate::primitives::SignatureError),
InvalidSignature(#[from] crate::primitives::commitment::SignatureError),
/// Malformed authentication header.
#[error("Malformed authentication header")]
MalformedHeader,
Expand Down
4 changes: 2 additions & 2 deletions bolt-sidecar/src/api/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,8 +172,8 @@ pub trait ConstraintsApi: BuilderApi {
) -> Result<VersionedValue<SignedBuilderBid>, BuilderApiError>;

/// Implements: <https://chainbound.github.io/bolt-docs/api/builder#delegate>
async fn delegate(&self, signed_data: SignedDelegation) -> Result<(), BuilderApiError>;
async fn delegate(&self, signed_data: &[SignedDelegation]) -> Result<(), BuilderApiError>;

/// Implements: <https://chainbound.github.io/bolt-docs/api/builder#revoke>
async fn revoke(&self, signed_data: SignedRevocation) -> Result<(), BuilderApiError>;
async fn revoke(&self, signed_data: &[SignedRevocation]) -> Result<(), BuilderApiError>;
}
15 changes: 9 additions & 6 deletions bolt-sidecar/src/builder/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ use ethereum_consensus::{
deneb::mainnet::ExecutionPayloadHeader,
ssz::prelude::{List, MerkleizationError},
};
use payload_builder::FallbackPayloadBuilder;
use signature::sign_builder_message;

use crate::{
common::BlsSecretKeyWrapper,
Expand All @@ -23,16 +21,21 @@ pub use template::BlockTemplate;

/// Builder payload signing utilities
pub mod signature;
use signature::sign_builder_message;

/// Fallback Payload builder agent that leverages the engine API's
/// `engine_newPayloadV3` response error to produce a valid payload.
pub mod payload_builder;
use payload_builder::FallbackPayloadBuilder;

/// Interface for fetching payloads from the beacon node.
pub mod payload_fetcher;

/// Compatibility types and utilities between Alloy, Reth,
/// Ethereum-consensus and other crates.
#[doc(hidden)]
mod compat;

/// Fallback Payload builder agent that leverages the engine API's
/// `engine_newPayloadV3` response error to produce a valid payload.
pub mod payload_builder;

#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
#[allow(missing_docs)]
Expand Down
57 changes: 57 additions & 0 deletions bolt-sidecar/src/builder/payload_fetcher.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
use tokio::sync::{mpsc, oneshot};
use tracing::error;

use crate::primitives::{FetchPayloadRequest, PayloadAndBid};

/// A local payload fetcher that sends requests to a channel
/// and waits for a response on a oneshot channel.
#[derive(Debug, Clone)]
pub struct LocalPayloadFetcher {
tx: mpsc::Sender<FetchPayloadRequest>,
}

impl LocalPayloadFetcher {
/// Create a new `LocalPayloadFetcher` with the given channel to send fetch requests.
pub fn new(tx: mpsc::Sender<FetchPayloadRequest>) -> Self {
Self { tx }
}
}

#[async_trait::async_trait]
impl PayloadFetcher for LocalPayloadFetcher {
async fn fetch_payload(&self, slot: u64) -> Option<PayloadAndBid> {
let (response_tx, response_rx) = oneshot::channel();

let fetch_params = FetchPayloadRequest { response_tx, slot };
self.tx.send(fetch_params).await.ok()?;

match response_rx.await {
Ok(res) => res,
Err(e) => {
error!(err = ?e, "Failed to fetch payload");
None
}
}
}
}

/// Interface for fetching payloads for the builder.
#[async_trait::async_trait]
pub trait PayloadFetcher {
/// Fetch a payload for the given slot.
async fn fetch_payload(&self, slot: u64) -> Option<PayloadAndBid>;
}

/// A payload fetcher that does nothing, used for testing.
#[derive(Debug)]
#[cfg(test)]
pub struct NoopPayloadFetcher;

#[cfg(test)]
#[async_trait::async_trait]
impl PayloadFetcher for NoopPayloadFetcher {
async fn fetch_payload(&self, slot: u64) -> Option<PayloadAndBid> {
tracing::info!(slot, "Fetch payload called");
None
}
}
36 changes: 31 additions & 5 deletions bolt-sidecar/src/client/constraints_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@
//! The Bolt sidecar's main purpose is to sit between the beacon node and Constraints client,
//! so most requests are simply proxied to its API.

use std::collections::HashSet;

use axum::http::StatusCode;
use beacon_api_client::VersionedValue;
use ethereum_consensus::{
builder::SignedValidatorRegistration, deneb::mainnet::SignedBlindedBeaconBlock, Fork,
builder::SignedValidatorRegistration, crypto::PublicKey as BlsPublicKey,
deneb::mainnet::SignedBlindedBeaconBlock, Fork,
};
use reqwest::Url;
use tracing::error;
Expand All @@ -30,6 +33,7 @@ use crate::{
pub struct ConstraintsClient {
url: Url,
client: reqwest::Client,
delegations: Vec<SignedDelegation>,
}

impl ConstraintsClient {
Expand All @@ -38,9 +42,24 @@ impl ConstraintsClient {
Self {
url: url.into(),
client: reqwest::ClientBuilder::new().user_agent("bolt-sidecar").build().unwrap(),
delegations: Vec::new(),
}
}

/// Adds a list of delegations to the client.
pub fn add_delegations(&mut self, delegations: Vec<SignedDelegation>) {
self.delegations.extend(delegations);
}

/// Finds all delegations for the given public key.
pub fn find_delegatees(&self, pubkey: &BlsPublicKey) -> HashSet<BlsPublicKey> {
self.delegations
.iter()
.filter(|d| d.message.delegatee_pubkey == *pubkey)
.map(|d| d.message.delegatee_pubkey.clone())
.collect::<HashSet<_>>()
}

fn endpoint(&self, path: &str) -> Url {
self.url.join(path).unwrap_or_else(|e| {
error!(err = ?e, "Failed to join path: {} with url: {}", path, self.url);
Expand Down Expand Up @@ -80,6 +99,13 @@ impl BuilderApi for ConstraintsClient {
return Err(BuilderApiError::FailedRegisteringValidators(error));
}

// If there are any delegations, propagate them to the relay
if self.delegations.is_empty() {
return Ok(());
} else if let Err(err) = self.delegate(&self.delegations).await {
error!(?err, "Failed to propagate delegations during validator registration");
}

Ok(())
}

Expand Down Expand Up @@ -190,12 +216,12 @@ impl ConstraintsApi for ConstraintsClient {
Ok(header)
}

async fn delegate(&self, signed_data: SignedDelegation) -> Result<(), BuilderApiError> {
async fn delegate(&self, signed_data: &[SignedDelegation]) -> Result<(), BuilderApiError> {
let response = self
.client
.post(self.endpoint(DELEGATE_PATH))
.header("content-type", "application/json")
.body(serde_json::to_string(&signed_data)?)
.body(serde_json::to_string(signed_data)?)
.send()
.await?;

Expand All @@ -207,12 +233,12 @@ impl ConstraintsApi for ConstraintsClient {
Ok(())
}

async fn revoke(&self, signed_data: SignedRevocation) -> Result<(), BuilderApiError> {
async fn revoke(&self, signed_data: &[SignedRevocation]) -> Result<(), BuilderApiError> {
let response = self
.client
.post(self.endpoint(REVOKE_PATH))
.header("content-type", "application/json")
.body(serde_json::to_string(&signed_data)?)
.body(serde_json::to_string(signed_data)?)
.send()
.await?;

Expand Down
4 changes: 2 additions & 2 deletions bolt-sidecar/src/client/test_util/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,13 @@ impl ConstraintsApi for MockConstraintsClient {
Ok(bid)
}

async fn delegate(&self, signed_data: SignedDelegation) -> Result<(), BuilderApiError> {
async fn delegate(&self, signed_data: &[SignedDelegation]) -> Result<(), BuilderApiError> {
Err(BuilderApiError::Generic(
"MockConstraintsClient does not support delegating".to_string(),
))
}

async fn revoke(&self, signed_data: SignedRevocation) -> Result<(), BuilderApiError> {
async fn revoke(&self, signed_data: &[SignedRevocation]) -> Result<(), BuilderApiError> {
Err(BuilderApiError::Generic("MockConstraintsClient does not support revoking".to_string()))
}
}
Expand Down
21 changes: 21 additions & 0 deletions bolt-sidecar/src/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use alloy::primitives::U256;
use blst::min_pk::SecretKey;
use rand::{Rng, RngCore};
use reth_primitives::PooledTransactionsElement;
use serde::{Deserialize, Deserializer};

use crate::{
primitives::{AccountState, TransactionExt},
Expand Down Expand Up @@ -107,6 +108,16 @@ impl BlsSecretKeyWrapper {
}
}

impl<'de> Deserialize<'de> for BlsSecretKeyWrapper {
fn deserialize<D>(deserializer: D) -> Result<BlsSecretKeyWrapper, D::Error>
where
D: Deserializer<'de>,
{
let sk = String::deserialize(deserializer)?;
Ok(BlsSecretKeyWrapper::from(sk.as_str()))
}
}

impl From<&str> for BlsSecretKeyWrapper {
fn from(sk: &str) -> Self {
let hex_sk = sk.strip_prefix("0x").unwrap_or(sk);
Expand Down Expand Up @@ -158,6 +169,16 @@ impl From<&str> for JwtSecretConfig {
}
}

impl<'de> Deserialize<'de> for JwtSecretConfig {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let jwt = String::deserialize(deserializer)?;
Ok(Self::from(jwt.as_str()))
}
}

impl Deref for JwtSecretConfig {
type Target = str;
fn deref(&self) -> &Self::Target {
Expand Down
Loading

0 comments on commit 4a25052

Please sign in to comment.