Skip to content

Commit

Permalink
Merge pull request #150 from chainbound/ng/feat/sidecar/committed-gas…
Browse files Browse the repository at this point in the history
…-limit

feat(sidecar): add max_committed_gas_per_slot to sidecar Limits
  • Loading branch information
thedevbirb authored Jul 19, 2024
2 parents d53da9b + d2e6a8e commit 3e4ad89
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 29 deletions.
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 @@ -107,6 +107,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
147 changes: 121 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);
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,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 @@ -611,8 +622,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 +684,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 +710,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 +750,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 +816,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 +856,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(())
}
}

0 comments on commit 3e4ad89

Please sign in to comment.