Skip to content
This repository was archived by the owner on Feb 23, 2026. It is now read-only.

Commit 55b66b3

Browse files
authored
Merge pull request #381 from chainbound/lore/feat/unsafe-disable-consensus-checks
feat(sidecar): add flag to disable consensus checks for testing purposes
2 parents e240d03 + 9656b68 commit 55b66b3

File tree

10 files changed

+449
-332
lines changed

10 files changed

+449
-332
lines changed

bolt-sidecar/Cargo.lock

Lines changed: 330 additions & 230 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bolt-sidecar/src/api/commitments/server.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ mod test {
175175

176176
use crate::{
177177
primitives::commitment::ECDSASignatureExt,
178-
test_util::{create_signed_commitment_request, default_test_transaction},
178+
test_util::{create_signed_inclusion_request, default_test_transaction},
179179
};
180180

181181
use super::*;
@@ -194,7 +194,7 @@ mod test {
194194
let sk = SecretKey::random(&mut rand::thread_rng());
195195
let signer = PrivateKeySigner::from(sk.clone());
196196
let tx = default_test_transaction(signer.address(), None);
197-
let req = create_signed_commitment_request(&[tx], &sk, 12).await.unwrap();
197+
let req = create_signed_inclusion_request(&[tx], &sk, 12).await.unwrap();
198198

199199
let payload = json!({
200200
"jsonrpc": "2.0",
@@ -236,9 +236,9 @@ mod test {
236236
let sk = SecretKey::random(&mut rand::thread_rng());
237237
let signer = PrivateKeySigner::from(sk.clone());
238238
let tx = default_test_transaction(signer.address(), None);
239-
let req = create_signed_commitment_request(&[tx], &sk, 12).await.unwrap();
239+
let req = create_signed_inclusion_request(&[tx], &sk, 12).await.unwrap();
240240

241-
let sig = req.signature().unwrap().to_hex();
241+
let sig = req.signature.unwrap().to_hex();
242242

243243
let payload = json!({
244244
"jsonrpc": "2.0",

bolt-sidecar/src/config/mod.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,14 @@ pub struct Opts {
8181
/// then used when registering the operator in the `BoltManager` contract.
8282
#[clap(long, env = "BOLT_SIDECAR_COMMITMENT_PRIVATE_KEY")]
8383
pub commitment_private_key: EcdsaSecretKeyWrapper,
84+
/// Unsafely disables consensus checks when validating commitments.
85+
///
86+
/// If enabled, the sidecar will sign every commitment request with the first private key
87+
/// available without checking if connected validators are scheduled to propose a block.
88+
#[clap(long, env = "BOLT_SIDECAR_UNSAFE_DISABLE_CONSENSUS_CHECKS", default_value_t = false)]
89+
pub unsafe_disable_consensus_checks: bool,
8490
/// Operating limits for the sidecar
8591
#[clap(flatten)]
86-
#[serde(default)]
8792
pub limits: LimitsOpts,
8893
/// Chain config for the chain on which the sidecar is running
8994
#[clap(flatten)]

bolt-sidecar/src/crypto/bls.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@ pub trait SignableBLS {
1818
}
1919

2020
/// Convert a BLS public key from Consensus Types to a byte array.
21-
pub fn cl_public_key_to_arr(pubkey: BlsPublicKey) -> [u8; BLS_PUBLIC_KEY_BYTES_LEN] {
22-
pubkey.as_ref().try_into().expect("BLS keys are 48 bytes")
21+
pub fn cl_public_key_to_arr(pubkey: impl AsRef<BlsPublicKey>) -> [u8; BLS_PUBLIC_KEY_BYTES_LEN] {
22+
pubkey.as_ref().as_ref().try_into().expect("BLS keys are 48 bytes")
2323
}

bolt-sidecar/src/driver.rs

Lines changed: 54 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use alloy::{rpc::types::beacon::events::HeadEvent, signers::local::PrivateKeySig
44
use beacon_api_client::mainnet::Client as BeaconClient;
55
use ethereum_consensus::{
66
clock::{self, SlotStream, SystemTimeProvider},
7-
phase0::mainnet::SLOTS_PER_EPOCH,
7+
phase0::mainnet::{BlsPublicKey, SLOTS_PER_EPOCH},
88
};
99
use eyre::Context;
1010
use futures::StreamExt;
@@ -27,8 +27,8 @@ use crate::{
2727
config::Opts,
2828
crypto::{SignableBLS, SignerECDSA},
2929
primitives::{
30-
read_signed_delegations_from_file, CommitmentRequest, ConstraintsMessage,
31-
FetchPayloadRequest, SignedConstraints, TransactionExt,
30+
commitment::SignedCommitment, read_signed_delegations_from_file, CommitmentRequest,
31+
ConstraintsMessage, FetchPayloadRequest, SignedConstraints, TransactionExt,
3232
},
3333
signer::{keystore::KeystoreSigner, local::LocalSigner, CommitBoostSigner, SignerBLS},
3434
state::{fetcher::StateFetcher, ConsensusState, ExecutionState, HeadTracker, StateClient},
@@ -66,6 +66,8 @@ pub struct SidecarDriver<C, ECDSA> {
6666
payload_requests_rx: mpsc::Receiver<FetchPayloadRequest>,
6767
/// Stream of slots made from the consensus clock
6868
slot_stream: SlotStream<SystemTimeProvider>,
69+
/// Whether to skip consensus checks (should only be used for testing)
70+
unsafe_skip_consensus_checks: bool,
6971
}
7072

7173
impl SidecarDriver<StateClient, PrivateKeySigner> {
@@ -224,7 +226,10 @@ impl<C: StateFetcher, ECDSA: SignerECDSA> SidecarDriver<C, ECDSA> {
224226
let (api_events_tx, api_events_rx) = mpsc::channel(1024);
225227
CommitmentsApiServer::new(api_addr).run(api_events_tx).await;
226228

229+
let unsafe_skip_consensus_checks = opts.unsafe_disable_consensus_checks;
230+
227231
Ok(SidecarDriver {
232+
unsafe_skip_consensus_checks,
228233
head_tracker,
229234
execution,
230235
consensus,
@@ -268,67 +273,83 @@ impl<C: StateFetcher, ECDSA: SignerECDSA> SidecarDriver<C, ECDSA> {
268273

269274
/// Handle an incoming API event, validating the request and responding with a commitment.
270275
async fn handle_incoming_api_event(&mut self, event: CommitmentEvent) {
271-
let CommitmentEvent { mut request, response } = event;
276+
let CommitmentEvent { request, response } = event;
277+
272278
info!("Received new commitment request: {:?}", request);
273279
ApiMetrics::increment_inclusion_commitments_received();
274280

275281
let start = Instant::now();
276282

277-
let validator_pubkey = match self.consensus.validate_request(&request) {
278-
Ok(pubkey) => pubkey,
279-
Err(err) => {
280-
warn!(?err, "Consensus: failed to validate request");
281-
let _ = response.send(Err(CommitmentError::Consensus(err)));
283+
// When we'll add more commitment types, we'll need to match on the request type here.
284+
// For now, we only support inclusion requests so the flow is straightforward.
285+
let CommitmentRequest::Inclusion(mut inclusion_request) = request;
286+
let target_slot = inclusion_request.slot;
287+
288+
let available_pubkeys = self.constraint_signer.available_pubkeys();
289+
290+
// Determine the constraint signing public key for this request. Rationale:
291+
// - If we're skipping consensus checks, we can use any available pubkey in the keystore.
292+
// - On regular operation, we need to validate the request against the consensus state to
293+
// determine if the sidecar is the proposer for the given slot. If so, we use the
294+
// validator pubkey or any of its active delegatees to sign constraints.
295+
let signing_pubkey = if self.unsafe_skip_consensus_checks {
296+
// PERF: this is inefficient, but it's only used for testing purposes.
297+
let mut ap = available_pubkeys.iter().collect::<Vec<_>>();
298+
ap.sort();
299+
ap.first().cloned().cloned().expect("at least one available pubkey")
300+
} else {
301+
let validator_pubkey = match self.consensus.validate_request(&inclusion_request) {
302+
Ok(pubkey) => pubkey,
303+
Err(err) => {
304+
warn!(?err, "Consensus: failed to validate request");
305+
let _ = response.send(Err(CommitmentError::Consensus(err)));
306+
return;
307+
}
308+
};
309+
310+
// Find a public key to sign new constraints with for this slot.
311+
// This can either be the validator pubkey or a delegatee (if one is available).
312+
let Some(signing_key) =
313+
self.constraints_client.find_signing_key(validator_pubkey, available_pubkeys)
314+
else {
315+
error!(%target_slot, "No available public key to sign constraints with");
316+
let _ = response.send(Err(CommitmentError::Internal));
282317
return;
283-
}
318+
};
319+
320+
signing_key
284321
};
285322

286-
if let Err(err) = self.execution.validate_request(&mut request).await {
323+
if let Err(err) = self.execution.validate_request(&mut inclusion_request).await {
287324
warn!(?err, "Execution: failed to validate request");
288325
ApiMetrics::increment_validation_errors(err.to_tag_str().to_owned());
289326
let _ = response.send(Err(CommitmentError::Validation(err)));
290327
return;
291328
}
292329

293-
// When we'll add more commitment types, we'll need to match on the request type here.
294-
// For now, we only support inclusion requests so the flow is straightforward.
295-
let CommitmentRequest::Inclusion(inclusion_request) = request.clone();
296-
let target_slot = inclusion_request.slot;
297-
298330
info!(
299331
target_slot,
300332
elapsed = ?start.elapsed(),
301333
"Validation against execution state passed"
302334
);
303335

304-
let available_pubkeys = self.constraint_signer.available_pubkeys();
305-
306-
// Find a public key to sign new constraints with for this slot.
307-
// This can either be the validator pubkey or a delegatee (if one is available).
308-
let Some(signing_pubkey) =
309-
self.constraints_client.find_signing_key(validator_pubkey, available_pubkeys)
310-
else {
311-
error!(%target_slot, "No available public key to sign constraints with");
312-
let _ = response.send(Err(CommitmentError::Internal));
313-
return;
314-
};
315-
316336
// NOTE: we iterate over the transactions in the request and generate a signed constraint
317337
// for each one. This is because the transactions in the commitment request are not supposed
318338
// to be treated as a relative-ordering bundle, but a batch with no ordering guarantees.
319339
//
320340
// For more information, check out the constraints API docs:
321341
// https://docs.boltprotocol.xyz/technical-docs/api/builder#constraints
322-
for tx in inclusion_request.txs {
342+
for tx in inclusion_request.txs.iter() {
323343
let tx_type = tx.tx_type();
324-
let message = ConstraintsMessage::from_tx(signing_pubkey.clone(), target_slot, tx);
344+
let message =
345+
ConstraintsMessage::from_tx(signing_pubkey.clone(), target_slot, tx.clone());
325346
let digest = message.digest();
326347

327348
let signature_result = match &self.constraint_signer {
328349
SignerBLS::Local(signer) => signer.sign_commit_boost_root(digest),
329350
SignerBLS::CommitBoost(signer) => signer.sign_commit_boost_root(digest).await,
330351
SignerBLS::Keystore(signer) => {
331-
signer.sign_commit_boost_root(digest, signing_pubkey.clone())
352+
signer.sign_commit_boost_root(digest, &signing_pubkey)
332353
}
333354
};
334355

@@ -346,10 +367,10 @@ impl<C: StateFetcher, ECDSA: SignerECDSA> SidecarDriver<C, ECDSA> {
346367
}
347368

348369
// Create a commitment by signing the request
349-
match request.commit_and_sign(&self.commitment_signer).await {
370+
match inclusion_request.commit_and_sign(&self.commitment_signer).await {
350371
Ok(commitment) => {
351372
debug!(target_slot, elapsed = ?start.elapsed(), "Commitment signed and sent");
352-
response.send(Ok(commitment)).ok()
373+
response.send(Ok(SignedCommitment::Inclusion(commitment))).ok()
353374
}
354375
Err(err) => {
355376
error!(?err, "Failed to sign commitment");

bolt-sidecar/src/primitives/commitment.rs

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,7 @@ impl CommitmentRequest {
6363
) -> eyre::Result<SignedCommitment> {
6464
match self {
6565
CommitmentRequest::Inclusion(req) => {
66-
let digest = req.digest();
67-
let signature = signer.sign_hash(&digest).await?;
68-
Ok(SignedCommitment::Inclusion(InclusionCommitment { request: req, signature }))
66+
req.commit_and_sign(signer).await.map(SignedCommitment::Inclusion)
6967
}
7068
}
7169
}
@@ -97,6 +95,16 @@ pub struct InclusionRequest {
9795
}
9896

9997
impl InclusionRequest {
98+
/// Commits and signs the request with the provided signer. Returns an [InclusionCommitment].
99+
pub async fn commit_and_sign<S: SignerECDSA>(
100+
self,
101+
signer: &S,
102+
) -> eyre::Result<InclusionCommitment> {
103+
let digest = self.digest();
104+
let signature = signer.sign_hash(&digest).await?;
105+
Ok(InclusionCommitment { request: self, signature })
106+
}
107+
100108
/// Validates the transaction fees against a minimum basefee.
101109
/// Returns true if the fee is greater than or equal to the min, false otherwise.
102110
pub fn validate_basefee(&self, min: u128) -> bool {

bolt-sidecar/src/signer/keystore.rs

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,7 @@ use lighthouse_bls::Keypair;
1212
use lighthouse_eth2_keystore::Keystore;
1313
use ssz::Encode;
1414

15-
use crate::{
16-
builder::signature::compute_signing_root,
17-
config::ChainConfig,
18-
crypto::bls::{cl_public_key_to_arr, BLSSig},
19-
};
15+
use crate::{builder::signature::compute_signing_root, config::ChainConfig, crypto::bls::BLSSig};
2016

2117
use super::SignerResult;
2218

@@ -116,7 +112,7 @@ impl KeystoreSigner {
116112
pub fn sign_commit_boost_root(
117113
&self,
118114
root: [u8; 32],
119-
public_key: BlsPublicKey,
115+
public_key: &BlsPublicKey,
120116
) -> SignerResult<BLSSig> {
121117
self.sign_root(root, public_key, self.chain.commit_boost_domain())
122118
}
@@ -125,17 +121,15 @@ impl KeystoreSigner {
125121
fn sign_root(
126122
&self,
127123
root: [u8; 32],
128-
public_key: BlsPublicKey,
124+
public_key: &BlsPublicKey,
129125
domain: [u8; 32],
130126
) -> SignerResult<BLSSig> {
131127
let sk = self
132128
.keypairs
133129
.iter()
134130
// `as_ssz_bytes` returns the raw bytes we need
135131
.find(|kp| kp.pk.as_ssz_bytes() == public_key.as_ref())
136-
.ok_or(KeystoreError::UnknownPublicKey(hex::encode(cl_public_key_to_arr(
137-
public_key,
138-
))))?;
132+
.ok_or(KeystoreError::UnknownPublicKey(public_key.to_string()))?;
139133

140134
let signing_root = compute_signing_root(root, domain);
141135

@@ -377,7 +371,7 @@ mod tests {
377371
let sig_keystore = keystore_signer_from_password
378372
.sign_commit_boost_root(
379373
[0; 32],
380-
BlsPublicKey::try_from(public_key_bytes.as_ref()).unwrap(),
374+
&BlsPublicKey::try_from(public_key_bytes.as_ref()).unwrap(),
381375
)
382376
.expect("to sign message");
383377
assert_eq!(sig_local, sig_keystore);

bolt-sidecar/src/state/consensus.rs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use tracing::debug;
1111
use super::CommitmentDeadline;
1212
use crate::{
1313
client::BeaconClient,
14-
primitives::{CommitmentRequest, Slot},
14+
primitives::{InclusionRequest, Slot},
1515
telemetry::ApiMetrics,
1616
};
1717

@@ -112,12 +112,7 @@ impl ConsensusState {
112112
/// 2. The request hasn't passed the slot deadline.
113113
///
114114
/// If the request is valid, return the validator public key for the target slot.
115-
pub fn validate_request(
116-
&self,
117-
request: &CommitmentRequest,
118-
) -> Result<BlsPublicKey, ConsensusError> {
119-
let CommitmentRequest::Inclusion(req) = request;
120-
115+
pub fn validate_request(&self, req: &InclusionRequest) -> Result<BlsPublicKey, ConsensusError> {
121116
// Check if the slot is in the current epoch or next epoch (if unsafe lookahead is enabled)
122117
if req.slot < self.epoch.start_slot || req.slot >= self.furthest_slot() {
123118
return Err(ConsensusError::InvalidSlot(req.slot));

0 commit comments

Comments
 (0)