Skip to content
Closed
Show file tree
Hide file tree
Changes from 14 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions config-files/config-batcher-docker.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,10 @@ batcher:
non_paying:
address: '0xa0Ee7A142d267C1f36714E4a8F75612F20a79720' # Anvil address 9
replacement_private_key: ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 # Anvil address 1
# When validating if the msg covers the minimum max fee
# A batch of how many proofs should it cover
amount_of_proofs_for_min_max_fee: 32
# When replacing the message, how much higher should the max fee in comparison to the original one
# The calculation is replacement_max_fee >= original_max_fee + original_max_fee * min_bump_percentage / 100
min_bump_percentage: 10

7 changes: 7 additions & 0 deletions config-files/config-batcher-ethereum-package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,10 @@ batcher:
non_paying:
address: '0xa0Ee7A142d267C1f36714E4a8F75612F20a79720' # Anvil address 9
replacement_private_key: ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 # Anvil address 1
# When validating if the msg covers the minimum max fee
# A batch of how many proofs should it cover
amount_of_proofs_for_min_max_fee: 32
# When replacing the message, how much higher should the max fee in comparison to the original one
# The calculation is replacement_max_fee >= original_max_fee + original_max_fee * min_bump_percentage / 100
min_bump_percentage: 10

6 changes: 6 additions & 0 deletions config-files/config-batcher.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,9 @@ batcher:
non_paying:
address: '0xa0Ee7A142d267C1f36714E4a8F75612F20a79720' # Anvil address 9
replacement_private_key: ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 # Anvil address 1
# When validating if the msg covers the minimum max fee
# A batch of how many proofs should it cover
amount_of_proofs_for_min_max_fee: 32
# When replacing the message, how much higher should the max fee in comparison to the original one
# The calculation is replacement_max_fee >= original_max_fee + original_max_fee * min_bump_percentage / 100
min_bump_percentage: 10
2 changes: 2 additions & 0 deletions crates/batcher/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ pub struct BatcherConfigFromYaml {
pub metrics_port: u16,
pub telemetry_ip_port_address: String,
pub non_paying: Option<NonPayingConfigFromYaml>,
pub amount_of_proofs_for_min_max_fee: usize,
pub min_bump_percentage: u64,
}

#[derive(Debug, Deserialize)]
Expand Down
56 changes: 49 additions & 7 deletions crates/batcher/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,9 @@ pub struct Batcher {
disabled_verifiers: Mutex<U256>,
aggregator_fee_percentage_multiplier: u128,
aggregator_gas_cost: u128,
current_min_max_fee: RwLock<U256>,
amount_of_proofs_for_min_max_fee: usize,
min_bump_percentage: U256,
pub metrics: metrics::BatcherMetrics,
pub telemetry: TelemetrySender,
}
Expand Down Expand Up @@ -266,6 +269,8 @@ impl Batcher {
max_proof_size: config.batcher.max_proof_size,
max_batch_byte_size: config.batcher.max_batch_byte_size,
max_batch_proof_qty: config.batcher.max_batch_proof_qty,
amount_of_proofs_for_min_max_fee: config.batcher.amount_of_proofs_for_min_max_fee,
min_bump_percentage: U256::from(config.batcher.min_bump_percentage),
last_uploaded_batch_block: Mutex::new(last_uploaded_batch_block),
pre_verification_is_enabled: config.batcher.pre_verification_is_enabled,
non_paying_config,
Expand All @@ -276,6 +281,7 @@ impl Batcher {
posting_batch: Mutex::new(false),
batch_state: Mutex::new(batch_state),
disabled_verifiers: Mutex::new(disabled_verifiers),
current_min_max_fee: RwLock::new(U256::zero()),
metrics,
telemetry,
}
Expand Down Expand Up @@ -662,6 +668,19 @@ impl Batcher {
nonced_verification_data = aux_verification_data
}

// Before moving on to process the message, verify that the max fee covers the
// minimum max fee allowed. This prevents users from spamming with very low max fees
// the min max fee is enforced by checking if it can cover a batch of [`amount_of_proofs_for_min_max_fee`]
let msg_max_fee = nonced_verification_data.max_fee;
if !self.msg_covers_minimum_max_fee(msg_max_fee).await {
send_message(
ws_conn_sink.clone(),
SubmitProofResponseMessage::UnderpricedProof,
)
.await;
return Ok(());
};

// When pre-verification is enabled, batcher will verify proofs for faster feedback with clients
if self.pre_verification_is_enabled {
let verification_data = &nonced_verification_data.verification_data;
Expand Down Expand Up @@ -766,7 +785,6 @@ impl Batcher {

let mut batch_state_lock = self.batch_state.lock().await;

let msg_max_fee = nonced_verification_data.max_fee;
let Some(user_last_max_fee_limit) =
batch_state_lock.get_user_last_max_fee_limit(&addr).await
else {
Expand Down Expand Up @@ -977,7 +995,7 @@ impl Batcher {
) {
let replacement_max_fee = nonced_verification_data.max_fee;
let nonce = nonced_verification_data.nonce;
let Some(entry) = batch_state_lock.get_entry(addr, nonce) else {
let Some(entry_to_replace) = batch_state_lock.get_entry(addr, nonce) else {
std::mem::drop(batch_state_lock);
warn!("Invalid nonce for address {addr}. Queue entry with nonce {nonce} not found");
send_message(
Expand All @@ -989,13 +1007,17 @@ impl Batcher {
return;
};

let original_max_fee = entry.nonced_verification_data.max_fee;
if original_max_fee > replacement_max_fee {
// Validate that the max fee is at least higher or equal to the original fee + a [`min_bump_percentage`]
let original_max_fee = entry_to_replace.nonced_verification_data.max_fee;
let min_bump =
original_max_fee + (original_max_fee * self.min_bump_percentage) / U256::from(100);

if replacement_max_fee < min_bump {
std::mem::drop(batch_state_lock);
warn!("Invalid replacement message for address {addr}, had max fee: {original_max_fee:?}, received fee: {replacement_max_fee:?}");
info!("Invalid replacement message for address {addr}, had max fee: {original_max_fee:?}, received fee: {replacement_max_fee:?}");
send_message(
ws_conn_sink.clone(),
SubmitProofResponseMessage::InvalidReplacementMessage,
SubmitProofResponseMessage::UnderpricedProof,
)
.await;
self.metrics
Expand All @@ -1006,7 +1028,7 @@ impl Batcher {
info!("Replacing message for address {addr} with nonce {nonce} and max fee {replacement_max_fee}");

// The replacement entry is built from the old entry and validated for then to be replaced
let mut replacement_entry = entry.clone();
let mut replacement_entry = entry_to_replace.clone();
replacement_entry.signature = signature;
replacement_entry.verification_data_commitment =
nonced_verification_data.verification_data.clone().into();
Expand Down Expand Up @@ -1499,8 +1521,23 @@ impl Batcher {

let (gas_price, disable_verifiers) =
tokio::join!(gas_price_future, disabled_verifiers_future);

let gas_price = gas_price.map_err(|_| BatcherError::GasPriceError)?;

// compute the new min max fee
let min_max_fee = aligned_sdk::verification_layer::compute_fee_per_proof_formula(
self.amount_of_proofs_for_min_max_fee,
gas_price,
);
// Acquire a write lock to update the latest gas price.
// The lock is dropped immediately after this assignment completes.
*self.current_min_max_fee.write().await = min_max_fee;
info!(
"Updated min max-fee: {} ETH per proof (batch size: {})",
ethers::utils::format_ether(min_max_fee),
self.amount_of_proofs_for_min_max_fee
);

{
let new_disable_verifiers = disable_verifiers
.map_err(|e| BatcherError::DisabledVerifiersError(e.to_string()))?;
Expand Down Expand Up @@ -2020,6 +2057,11 @@ impl Batcher {
true
}

async fn msg_covers_minimum_max_fee(&self, msg_max_fee: U256) -> bool {
let min_max_fee_per_proof = self.current_min_max_fee.read().await;
msg_max_fee >= *min_max_fee_per_proof
}

/// Checks if the user's balance is unlocked
/// Returns false if balance is unlocked, logs the error,
/// and sends it to the metrics server
Expand Down
4 changes: 2 additions & 2 deletions crates/sdk/src/communication/messaging.rs
Original file line number Diff line number Diff line change
Expand Up @@ -266,8 +266,8 @@ async fn handle_batcher_response(msg: Message) -> Result<BatchInclusionData, Sub
Err(SubmitError::GenericError(e))
}
Ok(SubmitProofResponseMessage::UnderpricedProof) => {
error!("Batcher responded with error: queue limit has been exceeded. Funds have not been spent.");
Err(SubmitError::BatchQueueLimitExceededError)
error!("Batcher responded with error: proof underpriced. Funds have not been spent.");
Err(SubmitError::InvalidMaxFee)
}
Err(e) => {
error!(
Expand Down
39 changes: 29 additions & 10 deletions crates/sdk/src/verification_layer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,20 +171,39 @@ pub async fn calculate_fee_per_proof_for_batch_of_size(
})?;
let gas_price = fetch_gas_price(&eth_rpc_provider).await?;

// Cost for estimate `num_proofs_per_batch` proofs
let fee_per_proof = compute_fee_per_proof_formula(num_proofs_in_batch, gas_price);
Ok(fee_per_proof)
}

/// Estimates the fee per proof based on the given batch size and gas price.
///
/// This function models the cost of submitting a batch of proofs to the network
/// by computing an estimated gas cost per proof. The total gas cost is composed of:
/// - a constant base gas cost for any batch submission (`DEFAULT_CONSTANT_GAS_COST`)
/// - an additional gas cost that scales linearly with the number of proofs in the batch
/// (`ADDITIONAL_SUBMISSION_GAS_COST_PER_PROOF * num_proofs_in_batch`)
///
/// The final fee per proof is calculated by:
/// (estimated_gas_per_proof * gas_price * GAS_PRICE_PERCENTAGE_MULTIPLIER) / PERCENTAGE_DIVIDER
///
///
/// # Arguments
/// * `num_proofs_in_batch` - Number of proofs in the batch (must be > 0).
/// * `gas_price` - Current gas price (in wei).
///
/// # Returns
/// * Estimated fee per individual proof (in wei).
///
/// # Panics
/// This function panics if `num_proofs_in_batch` is 0 due to division by zero.
pub fn compute_fee_per_proof_formula(num_proofs_in_batch: usize, gas_price: U256) -> U256 {
// Gas cost for `num_proofs_per_batch` proofs
let estimated_gas_per_proof = (DEFAULT_CONSTANT_GAS_COST
+ ADDITIONAL_SUBMISSION_GAS_COST_PER_PROOF * num_proofs_in_batch as u128)
/ num_proofs_in_batch as u128;

// Price of 1 proof in a batch of size `num_proofs_in_batch` i.e. (1 / `num_proofs_in_batch`).
// The computed price is adjusted with respect to the percentage multiplier from:
// https://github.com/yetanotherco/aligned_layer/blob/staging/crates/batcher/src/lib.rs#L1401
let fee_per_proof = (U256::from(estimated_gas_per_proof)
* gas_price
* U256::from(GAS_PRICE_PERCENTAGE_MULTIPLIER))
/ U256::from(PERCENTAGE_DIVIDER);

Ok(fee_per_proof)
(U256::from(estimated_gas_per_proof) * gas_price * U256::from(GAS_PRICE_PERCENTAGE_MULTIPLIER))
/ U256::from(PERCENTAGE_DIVIDER)
}

async fn fetch_gas_price(
Expand Down
Loading