Skip to content

Commit

Permalink
Merge pull request #732 from chainbound/lore/feat/metadata-pubkey
Browse files Browse the repository at this point in the history
feat(sidecar): verify validator public key in bolt_metadata endpoint
  • Loading branch information
thedevbirb authored Jan 23, 2025
2 parents 2f264dc + 6b1c036 commit 4c1e5e1
Show file tree
Hide file tree
Showing 8 changed files with 101 additions and 36 deletions.
50 changes: 39 additions & 11 deletions bolt-sidecar/src/api/commitments/firewall/processor.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
use ethereum_consensus::crypto::PublicKey;
use futures::{
pin_mut,
stream::{FuturesUnordered, SplitSink, SplitStream},
FutureExt, SinkExt, StreamExt,
};
use serde::Serialize;
use serde_json::{json, Value};
use std::time::Duration;
use std::{collections::HashSet, sync::Arc, time::Duration};
use std::{collections::VecDeque, future::Future, pin::Pin, task::Poll};
use tokio::{
net::TcpStream,
Expand Down Expand Up @@ -82,16 +83,18 @@ impl Future for PendingCommitmentResponse {
}

/// The internal state of the [CommitmentRequestProcessor].
#[derive(Debug, Clone, Copy, Default)]
#[derive(Debug, Clone, Default)]
pub struct ProcessorState {
/// The running limits of the sidecar.
limits: LimitsOpts,
/// The available validator public keys in the sidecar.
available_validators: HashSet<PublicKey>,
}

impl ProcessorState {
/// Creates a new instance of the [ProcessorState].
pub fn new(limits: LimitsOpts) -> Self {
Self { limits }
pub fn new(limits: LimitsOpts, available_validators: HashSet<PublicKey>) -> Self {
Self { limits, available_validators }
}
}

Expand All @@ -102,7 +105,7 @@ pub struct CommitmentRequestProcessor {
/// The URL of the connected websocket server.
url: String,
/// The internal state of the processor.
state: ProcessorState,
state: Arc<ProcessorState>,
/// The channel to send commitment events to be processed.
api_events_tx: mpsc::Sender<CommitmentEvent>,
/// The websocket writer sink.
Expand All @@ -125,7 +128,7 @@ impl CommitmentRequestProcessor {
/// Creates a new instance of the [CommitmentRequestProcessor].
pub fn new(
url: String,
state: ProcessorState,
state: Arc<ProcessorState>,
tx: mpsc::Sender<CommitmentEvent>,
stream: WebSocketStream<MaybeTlsStream<TcpStream>>,
shutdown_rx: watch::Receiver<()>,
Expand Down Expand Up @@ -322,12 +325,37 @@ impl CommitmentRequestProcessor {
self.send_response(response);
}
GET_METADATA_METHOD => {
let metadata = MetadataResponse {
limits: self.state.limits,
version: BOLT_SIDECAR_VERSION.to_string(),
let public_key = request
.params
.first()
.and_then(|p| p.as_str())
.and_then(|p| hex::decode(p.trim_start_matches("0x")).ok())
.and_then(|p| PublicKey::try_from(p.as_slice()).ok());

let response: JsonRpcResponse = if let Some(public_key) = public_key {
if self.state.available_validators.contains(&public_key) {
let metadata = MetadataResponse {
limits: self.state.limits,
version: BOLT_SIDECAR_VERSION.to_string(),
};
JsonRpcSuccessResponse::new(json!(metadata)).into()
} else {
JsonRpcErrorResponse::new(
CommitmentError::ValidatorNotAvailable(public_key).into(),
)
.into()
}
} else {
JsonRpcErrorResponse::new(
CommitmentError::InvalidParams(
"missing or invalid validator public key".into(),
)
.into(),
)
.into()
};
let response = JsonRpcSuccessResponse::new(json!(metadata)).with_uuid(id).into();
self.send_response(response);

self.send_response(response.with_uuid(id));
}
REQUEST_INCLUSION_METHOD => {
let Some(param) = request.params.first().cloned() else {
Expand Down
18 changes: 15 additions & 3 deletions bolt-sidecar/src/api/commitments/firewall/receiver.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
use alloy::signers::local::PrivateKeySigner;
use std::fmt::{self, Debug, Formatter};
use ethereum_consensus::crypto::PublicKey;
use std::{
collections::HashSet,
fmt::{self, Debug, Formatter},
sync::Arc,
};
use thiserror::Error;
use tokio::sync::{mpsc, watch};
use tokio_tungstenite::{
Expand Down Expand Up @@ -59,6 +64,8 @@ pub struct CommitmentsReceiver {
signal: ShutdownSignal,
/// The sidecar running limits.
limits: LimitsOpts,
/// The available validator public keys on the sidecar.
available_validators: HashSet<PublicKey>,
}

impl Debug for CommitmentsReceiver {
Expand All @@ -78,12 +85,14 @@ impl CommitmentsReceiver {
chain: Chain,
limits: LimitsOpts,
urls: Vec<Url>,
available_validators: HashSet<PublicKey>,
) -> Self {
Self {
operator_private_key,
chain,
urls,
limits,
available_validators,
signal: Box::pin(async {
let _ = tokio::signal::ctrl_c().await;
}),
Expand All @@ -107,7 +116,7 @@ impl CommitmentsReceiver {
ShutdownTicker::new(self.signal).spawn(shutdown_tx);

let signer = PrivateKeySigner::from_signing_key(self.operator_private_key.0);
let state = ProcessorState::new(self.limits);
let state = Arc::new(ProcessorState::new(self.limits, self.available_validators));
let retry_config = RetryConfig { initial_delay_ms: 100, max_delay_secs: 2, factor: 2 };

for url in &self.urls {
Expand All @@ -117,6 +126,7 @@ impl CommitmentsReceiver {
let api_events_tx = api_events_tx.clone();
let shutdown_rx = shutdown_rx.clone();
let signer = signer.clone();
let state = state.clone();

tokio::spawn(async move {
retry_with_backoff_if(
Expand All @@ -137,6 +147,7 @@ impl CommitmentsReceiver {

let api_events_tx = api_events_tx.clone();
let shutdown_rx = shutdown_rx.clone();
let state = state.clone();

async move {
handle_connection(url, state, jwt, api_events_tx, shutdown_rx).await
Expand All @@ -160,7 +171,7 @@ impl CommitmentsReceiver {
/// Opens the websocket connection and starts the commitment request processor.
async fn handle_connection(
url: String,
state: ProcessorState,
state: Arc<ProcessorState>,
jwt: String,
api_events_tx: mpsc::Sender<CommitmentEvent>,
shutdown_rx: watch::Receiver<()>,
Expand Down Expand Up @@ -310,6 +321,7 @@ mod tests {
format!("ws://127.0.0.1:{}{}", port_1, FIREWALL_STREAM_PATH).parse().unwrap(),
// format!("ws://127.0.0.1:{}{}", port_2, FIREWALL_STREAM_PATH).parse().unwrap(),
],
HashSet::new(),
)
.with_shutdown(async move { shutdown_connections_rx.recv().await.unwrap() }.boxed());

Expand Down
10 changes: 9 additions & 1 deletion bolt-sidecar/src/api/commitments/spec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use axum::{
Json,
};
use serde::{Deserialize, Serialize};
use serde_json::json;
use thiserror::Error;

use crate::{
Expand All @@ -15,7 +16,7 @@ use crate::{
commitment::InclusionCommitment,
jsonrpc::{JsonRpcError, JsonRpcErrorResponse},
signature::SignatureError,
InclusionRequest,
BlsPublicKey, InclusionRequest,
},
state::{consensus::ConsensusError, ValidationError},
};
Expand Down Expand Up @@ -66,6 +67,9 @@ pub enum CommitmentError {
/// Invalid JSON-RPC request params.
#[error("Invalid JSON-RPC request params: {0}")]
InvalidParams(String),
/// The requested validator is not available on this sidecar.
#[error("Validator not available on this sidecar")]
ValidatorNotAvailable(BlsPublicKey),
/// Invalid JSON.
/// FIXME: (thedevbirb, 2025-13-01) this should be removed because it is dead code,
/// but it allows Rust to pull the correct axum version and not older ones from
Expand Down Expand Up @@ -93,6 +97,9 @@ impl From<CommitmentError> for JsonRpcError {
CommitmentError::InvalidParams(err) => Self::new(-32602, err.to_string()),
CommitmentError::Internal => Self::new(-32603, err.to_string()),
CommitmentError::RejectedJson(err) => Self::new(-32604, err.to_string()),
CommitmentError::ValidatorNotAvailable(ref pk) => {
Self::new(600, err.to_string()).with_data(json!(pk))
}
}
}
}
Expand All @@ -112,6 +119,7 @@ impl From<&CommitmentError> for StatusCode {
| CommitmentError::RejectedJson(_)
| CommitmentError::InvalidJson(_) => Self::BAD_REQUEST,
CommitmentError::Internal => Self::INTERNAL_SERVER_ERROR,
CommitmentError::ValidatorNotAvailable(_) => Self::NOT_FOUND,
}
}
}
Expand Down
18 changes: 9 additions & 9 deletions bolt-sidecar/src/chain_io/manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ impl BoltManager {
/// NOTE: it also checks the operator associated to the `commitment_signer_pubkey` exists.
pub async fn verify_validator_pubkeys(
&self,
keys: Vec<BlsPublicKey>,
keys: &[BlsPublicKey],
commitment_signer_pubkey: Address,
) -> eyre::Result<Vec<ProposerStatus>> {
let hashes_with_preimages = utils::pubkey_hashes(keys);
Expand Down Expand Up @@ -250,17 +250,17 @@ mod tests {
let keys = vec![BlsPublicKey::try_from([0; 48].as_ref()).expect("valid bls public key")];
let commitment_signer_pubkey = Address::ZERO;

let res = manager.verify_validator_pubkeys(keys, commitment_signer_pubkey).await;
let res = manager.verify_validator_pubkeys(&keys, commitment_signer_pubkey).await;
assert!(res.unwrap_err().to_string().contains("ValidatorDoesNotExist"));

let keys = vec![
BlsPublicKey::try_from(
hex!("87cbbfe6f08a0fd424507726cfcf5b9df2b2fd6b78a65a3d7bb6db946dca3102eb8abae32847d5a9a27e414888414c26")
.as_ref()).expect("valid bls public key")];
let res = manager.verify_validator_pubkeys(keys.clone(), commitment_signer_pubkey).await;
let res = manager.verify_validator_pubkeys(&keys, commitment_signer_pubkey).await;
assert!(
res.unwrap_err().to_string() ==
generate_operator_keys_mismatch_error(
res.unwrap_err().to_string()
== generate_operator_keys_mismatch_error(
pubkey_hash(&keys[0]),
commitment_signer_pubkey,
operator
Expand All @@ -269,7 +269,7 @@ mod tests {

let commitment_signer_pubkey = operator;
let res = manager
.verify_validator_pubkeys(keys, commitment_signer_pubkey)
.verify_validator_pubkeys(&keys, commitment_signer_pubkey)
.await
.expect("active validator and correct operator");
assert!(res[0].active);
Expand Down Expand Up @@ -308,11 +308,11 @@ mod tests {
let operator =
Address::from_hex("725028b0b7c3db8b8242d35cd3a5779838b217b1").expect("valid address");

let result = manager.verify_validator_pubkeys(keys.clone(), commitment_signer_pubkey).await;
let result = manager.verify_validator_pubkeys(&keys, commitment_signer_pubkey).await;

assert!(
result.unwrap_err().to_string() ==
generate_operator_keys_mismatch_error(
result.unwrap_err().to_string()
== generate_operator_keys_mismatch_error(
pubkey_hash(&keys[0]),
commitment_signer_pubkey,
operator
Expand Down
4 changes: 2 additions & 2 deletions bolt-sidecar/src/chain_io/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ pub(crate) type CompressedHash = FixedBytes<20>;
/// pre-images.
///
/// This follows the same implementation done on-chain in the BoltValidators contract.
pub fn pubkey_hashes(keys: Vec<BlsPublicKey>) -> HashMap<CompressedHash, BlsPublicKey> {
HashMap::from_iter(keys.into_iter().map(|key| (pubkey_hash(&key), key)))
pub fn pubkey_hashes(keys: &[BlsPublicKey]) -> HashMap<CompressedHash, BlsPublicKey> {
HashMap::from_iter(keys.iter().map(|key| (pubkey_hash(key), key.clone())))
}

/// Hash the public key of the proposer. This follows the same
Expand Down
3 changes: 2 additions & 1 deletion bolt-sidecar/src/driver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ impl<C: StateFetcher, ECDSA: SignerECDSA> SidecarDriver<C, ECDSA> {
);

manager
.verify_validator_pubkeys(validator_pubkeys, commitment_signer.public_key())
.verify_validator_pubkeys(&validator_pubkeys, commitment_signer.public_key())
.await?;

info!("Successfully verified validators and operator keys with BoltManager");
Expand Down Expand Up @@ -236,6 +236,7 @@ impl<C: StateFetcher, ECDSA: SignerECDSA> SidecarDriver<C, ECDSA> {
opts.chain.chain,
opts.limits,
urls,
validator_pubkeys.into_iter().collect(),
)
.run()
} else {
Expand Down
18 changes: 17 additions & 1 deletion bolt-sidecar/src/primitives/jsonrpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,14 @@ impl JsonRpcResponse {
_ => None,
}
}

/// Set the ID of the response from a UUID.
pub fn with_uuid(self, id: Uuid) -> Self {
match self {
Self::Success(success) => Self::Success(success.with_uuid(id)),
Self::Error(error) => Self::Error(error.with_uuid(id)),
}
}
}

/// A response object for successful JSON-RPC requests.
Expand Down Expand Up @@ -138,11 +146,19 @@ pub struct JsonRpcError {
pub code: i32,
/// The error message
pub message: String,
/// The optional data of the error
pub data: Option<Value>,
}

impl JsonRpcError {
/// Create a new JSON-RPC error object
pub fn new(code: i32, message: String) -> Self {
Self { code, message }
Self { code, message, data: None }
}

/// Set the data of the error
pub fn with_data(mut self, data: Value) -> Self {
self.data = Some(data);
self
}
}
16 changes: 8 additions & 8 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -146,8 +146,8 @@ send-preconf count='1' raw="":
--devnet.beacon_url $(kurtosis port print bolt-devnet cl-1-lighthouse-geth http) \
--devnet.sidecar_url http://$(kurtosis port print bolt-devnet bolt-sidecar-1-lighthouse-geth api) \
--private-key 53321db7c1e331d93a11a41d16f004d7ff63972ec8ec7c25db329728ceeb1710 \
--max-fee 4 \
--priority-fee 3 \
--max-fee 5 \
--priority-fee 4 \
--count {{count}} \
{{ if raw == "true" { "--raw" } else { "" } }}

Expand All @@ -158,8 +158,8 @@ send-preconf-rpc count='1' raw="" rpc='http://127.0.0.1:8015/rpc':
--devnet.beacon_url $(kurtosis port print bolt-devnet cl-1-lighthouse-geth http) \
--devnet.sidecar_url {{ rpc }} \
--private-key 53321db7c1e331d93a11a41d16f004d7ff63972ec8ec7c25db329728ceeb1710 \
--max-fee 4 \
--priority-fee 3 \
--max-fee 5 \
--priority-fee 4 \
--count {{count}} \
{{ if raw == "true" { "--raw" } else { "" } }}

Expand All @@ -172,8 +172,8 @@ send-blob-preconf count='1' raw="":
--devnet.sidecar_url http://$(kurtosis port print bolt-devnet bolt-sidecar-1-lighthouse-geth api) \
--private-key 53321db7c1e331d93a11a41d16f004d7ff63972ec8ec7c25db329728ceeb1710 \
--blob \
--max-fee 4 \
--priority-fee 3 \
--max-fee 5 \
--priority-fee 4 \
--count {{count}} \
{{ if raw == "true" { "--raw" } else { "" } }}

Expand All @@ -185,8 +185,8 @@ send-blob-preconf-rpc count='1' raw="" rpc='http://127.0.0.1:8015/rpc':
--devnet.sidecar_url {{ rpc }} \
--private-key 53321db7c1e331d93a11a41d16f004d7ff63972ec8ec7c25db329728ceeb1710 \
--blob \
--max-fee 4 \
--priority-fee 3 \
--max-fee 5 \
--priority-fee 4 \
--count {{count}} \
{{ if raw == "true" { "--raw" } else { "" } }}

Expand Down

0 comments on commit 4c1e5e1

Please sign in to comment.