Skip to content
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

feat(sidecar): add max_committed_gas_per_slot to sidecar Limits #150

Merged
merged 12 commits into from
Jul 19, 2024
3 changes: 1 addition & 2 deletions bolt-sidecar/bin/sidecar.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,7 @@ async fn main() -> eyre::Result<()> {
let signer = Signer::new(config.private_key.clone().unwrap());

let state_client = StateClient::new(config.execution_api_url.clone());
let mut execution_state =
ExecutionState::new(state_client, config.limits.max_commitments_per_slot).await?;
let mut execution_state = ExecutionState::new(state_client, config.limits).await?;

let mevboost_client = MevBoostClient::new(config.mevboost_url.clone());
let beacon_client = BeaconClient::new(config.beacon_api_url.clone());
Expand Down
12 changes: 12 additions & 0 deletions bolt-sidecar/src/builder/template.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,18 @@ impl BlockTemplate {
.fold(0, |acc, sc| acc + sc.message.constraints.len())
}

/// Returns the committed gas in the block template.
#[inline]
pub fn committed_gas(&self) -> u64 {
self.signed_constraints_list.iter().fold(0, |acc, sc| {
acc + sc
.message
.constraints
.iter()
.fold(0, |acc, c| acc + &c.transaction.gas_limit())
})
}

/// Returns the blob count of the block template.
#[inline]
pub fn blob_count(&self) -> usize {
Expand Down
11 changes: 10 additions & 1 deletion bolt-sidecar/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ pub struct Opts {
/// Max number of commitments to accept per block
#[clap(short = 'm', long)]
pub(super) max_commitments: Option<NonZero<usize>>,
/// Max committed gas per slot
#[clap(short = 'g', long)]
pub(super) max_committed_gas: Option<NonZero<u64>>,
/// Validator indexes of connected validators that the sidecar
/// should accept commitments on behalf of. Accepted values:
/// - a comma-separated list of indexes (e.g. "1,2,3,4")
Expand Down Expand Up @@ -133,16 +136,18 @@ impl Default for Config {
}

/// Limits for the sidecar.
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Copy)]
pub struct Limits {
/// Maximum number of commitments to accept per block
pub max_commitments_per_slot: NonZero<usize>,
pub max_committed_gas_per_slot: NonZero<u64>,
}

impl Default for Limits {
fn default() -> Self {
Self {
max_commitments_per_slot: NonZero::new(6).expect("Valid non-zero"),
max_committed_gas_per_slot: NonZero::new(10_000_000).expect("Valid non-zero"),
}
}
}
Expand All @@ -169,6 +174,10 @@ impl TryFrom<Opts> for Config {
config.limits.max_commitments_per_slot = max_commitments;
}

if let Some(max_committed_gas) = opts.max_committed_gas {
config.limits.max_committed_gas_per_slot = max_committed_gas;
}

config.commit_boost_url = opts
.signing
.commit_boost_url
Expand Down
151 changes: 125 additions & 26 deletions bolt-sidecar/src/state/execution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ use alloy_transport::TransportError;
use reth_primitives::{
revm_primitives::EnvKzgSettings, BlobTransactionValidationError, PooledTransactionsElement,
};
use std::{collections::HashMap, num::NonZero};
use std::collections::HashMap;
use thiserror::Error;

use crate::{
builder::BlockTemplate,
common::{calculate_max_basefee, validate_transaction},
config::Limits,
primitives::{AccountState, CommitmentRequest, SignedConstraints, Slot, TransactionExt},
};

Expand Down Expand Up @@ -60,6 +61,9 @@ pub enum ValidationError {
/// The maximum commitments have been reached for the slot.
#[error("Max commitments reached for slot {0}: {1}")]
MaxCommitmentsReachedForSlot(u64, usize),
/// The maximum committed gas has been reached for the slot.
#[error("Max committed gas reached for slot {0}: {1}")]
MaxCommittedGasReachedForSlot(u64, u64),
/// The signature is invalid.
#[error("Signature error: {0:?}")]
Signature(#[from] SignatureError),
Expand Down Expand Up @@ -111,8 +115,8 @@ pub struct ExecutionState<C> {
block_templates: HashMap<Slot, BlockTemplate>,
/// The chain ID of the chain (constant).
chain_id: u64,
/// The maximum number of commitments per slot.
max_commitments_per_slot: NonZero<usize>,
/// The limits set for the sidecar.
limits: Limits,
/// The KZG settings for validating blobs.
kzg_settings: EnvKzgSettings,
/// The state fetcher client.
Expand Down Expand Up @@ -142,10 +146,7 @@ impl Default for ValidationParams {
impl<C: StateFetcher> ExecutionState<C> {
/// Creates a new state with the given client, initializing the
/// basefee and head block number.
pub async fn new(
client: C,
max_commitments_per_slot: NonZero<usize>,
) -> Result<Self, TransportError> {
pub async fn new(client: C, limits: Limits) -> Result<Self, TransportError> {
let (basefee, blob_basefee, block_number, chain_id) = tokio::try_join!(
client.get_basefee(None),
client.get_blob_basefee(None),
Expand All @@ -158,7 +159,7 @@ impl<C: StateFetcher> ExecutionState<C> {
blob_basefee,
block_number,
chain_id,
max_commitments_per_slot,
limits,
client,
slot: 0,
account_states: HashMap::new(),
Expand Down Expand Up @@ -202,14 +203,28 @@ impl<C: StateFetcher> ExecutionState<C> {

// Check if there is room for more commitments
if let Some(template) = self.get_block_template(target_slot) {
if template.transactions_len() >= self.max_commitments_per_slot.get() {
if template.transactions_len() >= self.limits.max_commitments_per_slot.get() {
return Err(ValidationError::MaxCommitmentsReachedForSlot(
self.slot,
self.max_commitments_per_slot.get(),
self.limits.max_commitments_per_slot.get(),
));
}
}

// Check if the committed gas exceeds the maximum
let template_committed_gas = self
.get_block_template(target_slot)
.map(|t| t.committed_gas())
.unwrap_or(0);
namn-grg marked this conversation as resolved.
Show resolved Hide resolved
if template_committed_gas + req.tx.gas_limit()
>= self.limits.max_committed_gas_per_slot.get()
{
return Err(ValidationError::MaxCommittedGasReachedForSlot(
self.slot,
self.limits.max_committed_gas_per_slot.get(),
));
}

// Check if the transaction size exceeds the maximum
if req.tx.size() > self.validation_params.max_tx_input_bytes {
return Err(ValidationError::TransactionSizeTooHigh);
Expand Down Expand Up @@ -460,8 +475,7 @@ mod tests {
let anvil = launch_anvil();
let client = StateClient::new(anvil.endpoint_url());

let max_comms = NonZero::new(10).unwrap();
let mut state = ExecutionState::new(client.clone(), max_comms).await?;
let mut state = ExecutionState::new(client.clone(), Limits::default()).await?;

let sender = anvil.addresses().first().unwrap();
let sender_pk = anvil.keys().first().unwrap();
Expand All @@ -486,8 +500,7 @@ mod tests {
let anvil = launch_anvil();
let client = StateClient::new(anvil.endpoint_url());

let max_comms = NonZero::new(10).unwrap();
let mut state = ExecutionState::new(client.clone(), max_comms).await?;
let mut state = ExecutionState::new(client.clone(), Limits::default()).await?;

let sender = anvil.addresses().first().unwrap();
let sender_pk = anvil.keys().first().unwrap();
Expand Down Expand Up @@ -527,8 +540,7 @@ mod tests {
let anvil = launch_anvil();
let client = StateClient::new(anvil.endpoint_url());

let max_comms = NonZero::new(10).unwrap();
let mut state = ExecutionState::new(client.clone(), max_comms).await?;
let mut state = ExecutionState::new(client.clone(), Limits::default()).await?;

let sender = anvil.addresses().first().unwrap();
let sender_pk = anvil.keys().first().unwrap();
Expand Down Expand Up @@ -580,8 +592,11 @@ mod tests {
let anvil = launch_anvil();
let client = StateClient::new(anvil.endpoint_url());

let max_comms = NonZero::new(10).unwrap();
let mut state = ExecutionState::new(client.clone(), max_comms).await?;
let limits: Limits = Limits {
max_commitments_per_slot: NonZero::new(10).unwrap(),
max_committed_gas_per_slot: NonZero::new(10_000_000).unwrap(),
};
let mut state = ExecutionState::new(client.clone(), limits).await?;

let sender = anvil.addresses().first().unwrap();
let sender_pk = anvil.keys().first().unwrap();
Expand Down Expand Up @@ -611,8 +626,7 @@ mod tests {
let anvil = launch_anvil();
let client = StateClient::new(anvil.endpoint_url());

let max_comms = NonZero::new(10).unwrap();
let mut state = ExecutionState::new(client.clone(), max_comms).await?;
let mut state = ExecutionState::new(client.clone(), Limits::default()).await?;

let sender = anvil.addresses().first().unwrap();
let sender_pk = anvil.keys().first().unwrap();
Expand Down Expand Up @@ -674,8 +688,7 @@ mod tests {
let anvil = launch_anvil();
let client = StateClient::new(anvil.endpoint_url());

let max_comms = NonZero::new(10).unwrap();
let mut state = ExecutionState::new(client.clone(), max_comms).await?;
let mut state = ExecutionState::new(client.clone(), Limits::default()).await?;

let basefee = state.basefee();

Expand All @@ -701,6 +714,38 @@ mod tests {
Ok(())
}

#[tokio::test]
async fn test_invalid_inclusion_request_with_excess_gas() -> eyre::Result<()> {
let _ = tracing_subscriber::fmt::try_init();

let anvil = launch_anvil();
let client = StateClient::new(anvil.endpoint_url());

let limits: Limits = Limits {
max_commitments_per_slot: NonZero::new(10).unwrap(),
max_committed_gas_per_slot: NonZero::new(5_000_000).unwrap(),
};
let mut state = ExecutionState::new(client.clone(), limits).await?;

let sender = anvil.addresses().first().unwrap();
let sender_pk = anvil.keys().first().unwrap();

// initialize the state by updating the head once
let slot = client.get_head().await?;
state.update_head(None, slot).await?;

let tx = default_test_transaction(*sender, None).with_gas_limit(6_000_000);

let request = create_signed_commitment_request(tx, sender_pk, 10).await?;

assert!(matches!(
state.validate_commitment_request(&request).await,
Err(ValidationError::MaxCommittedGasReachedForSlot(_, 5_000_000))
));

Ok(())
}

#[tokio::test]
async fn test_invalidate_inclusion_request() -> eyre::Result<()> {
let _ = tracing_subscriber::fmt::try_init();
Expand All @@ -709,8 +754,7 @@ mod tests {
let client = StateClient::new(anvil.endpoint_url());
let provider = ProviderBuilder::new().on_http(anvil.endpoint_url());

let max_comms = NonZero::new(10).unwrap();
let mut state = ExecutionState::new(client.clone(), max_comms).await?;
let mut state = ExecutionState::new(client.clone(), Limits::default()).await?;

let sender = anvil.addresses().first().unwrap();
let sender_pk = anvil.keys().first().unwrap();
Expand Down Expand Up @@ -776,8 +820,7 @@ mod tests {
let anvil = launch_anvil();
let client = StateClient::new(anvil.endpoint_url());

let max_comms = NonZero::new(10).unwrap();
let mut state = ExecutionState::new(client.clone(), max_comms).await?;
let mut state = ExecutionState::new(client.clone(), Limits::default()).await?;

let sender = anvil.addresses().first().unwrap();
let sender_pk = anvil.keys().first().unwrap();
Expand Down Expand Up @@ -817,4 +860,60 @@ mod tests {

Ok(())
}

#[tokio::test]
async fn test_invalidate_inclusion_request_with_excess_gas() -> eyre::Result<()> {
let _ = tracing_subscriber::fmt::try_init();

let anvil = launch_anvil();
let client = StateClient::new(anvil.endpoint_url());

let limits: Limits = Limits {
max_commitments_per_slot: NonZero::new(10).unwrap(),
max_committed_gas_per_slot: NonZero::new(5_000_000).unwrap(),
};
let mut state = ExecutionState::new(client.clone(), limits).await?;

let sender = anvil.addresses().first().unwrap();
let sender_pk = anvil.keys().first().unwrap();

// initialize the state by updating the head once
let slot = client.get_head().await?;
state.update_head(None, slot).await?;

let tx = default_test_transaction(*sender, None).with_gas_limit(4_999_999);

let target_slot = 10;
let request = create_signed_commitment_request(tx, sender_pk, target_slot).await?;
let inclusion_request = request.as_inclusion_request().unwrap().clone();

assert!(state.validate_commitment_request(&request).await.is_ok());

let bls_signer = Signer::random();
let message = ConstraintsMessage::build(0, inclusion_request);
let signature = bls_signer.sign(&message.digest()).unwrap().to_string();
let signed_constraints = SignedConstraints { message, signature };

state.add_constraint(target_slot, signed_constraints);

assert!(
state
.get_block_template(target_slot)
.unwrap()
.transactions_len()
== 1
);

// This tx will exceed the committed gas limit
let tx = default_test_transaction(*sender, Some(1));

let request = create_signed_commitment_request(tx, sender_pk, 10).await?;

assert!(matches!(
state.validate_commitment_request(&request).await,
Err(ValidationError::MaxCommittedGasReachedForSlot(_, 5_000_000))
));

Ok(())
}
}
Loading