Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
99 changes: 93 additions & 6 deletions crates/batcher/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ pub struct Batcher {
non_paying_config: Option<NonPayingConfig>,
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,

// Shared state access:
// Two kinds of threads interact with the shared state:
Expand Down Expand Up @@ -322,6 +325,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 @@ -333,6 +338,7 @@ impl Batcher {
batch_state: Mutex::new(batch_state),
user_states,
disabled_verifiers: Mutex::new(disabled_verifiers),
current_min_max_fee: RwLock::new(U256::zero()),
metrics,
telemetry,
}
Expand Down Expand Up @@ -853,6 +859,59 @@ 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;
if self
.is_verifier_disabled(verification_data.proving_system)
.await
{
warn!(
"Verifier for proving system {} is disabled, skipping verification",
verification_data.proving_system
);
send_message(
ws_conn_sink.clone(),
SubmitProofResponseMessage::InvalidProof(ProofInvalidReason::DisabledVerifier(
verification_data.proving_system,
)),
)
.await;
self.metrics.user_error(&[
"disabled_verifier",
&format!("{}", verification_data.proving_system),
]);
return Ok(());
}

if !zk_utils::verify(verification_data).await {
error!("Invalid proof detected. Verification failed");
send_message(
ws_conn_sink.clone(),
SubmitProofResponseMessage::InvalidProof(ProofInvalidReason::RejectedProof),
)
.await;
self.metrics.user_error(&[
"rejected_proof",
&format!("{}", verification_data.proving_system),
]);
return Ok(());
}
}

// We don't need a batch state lock here, since if the user locks its funds
// after the check, some blocks should pass until he can withdraw.
// It is safe to do just do this here.
Expand Down Expand Up @@ -958,6 +1017,13 @@ impl Batcher {
return Ok(());
};

// For now on until the message is fully processed, the batch state is locked
// This is needed because we need to query the user state to make validations and
// finally add the proof to the batch queue.

let _batch_state_lock = self.batch_state.lock().await;


let msg_max_fee = nonced_verification_data.max_fee;
let user_last_max_fee_limit = user_state_guard.last_max_fee_limit;

Expand Down Expand Up @@ -1223,17 +1289,18 @@ impl Batcher {
return;
};

// Validate that the max fee is at least higher or equal to the original fee + a configurable min_bump_percentage
let original_max_fee = entry.nonced_verification_data.max_fee;
// Require 10% fee increase to prevent DoS attacks. While this could theoretically overflow,
// it would require an attacker to have an impractical amount of Ethereum to reach U256::MAX
let min_required_fee = original_max_fee + (original_max_fee / U256::from(10)); // 10% increase (1.1x)
if replacement_max_fee < min_required_fee {
let min_bump =
original_max_fee + (original_max_fee * self.min_bump_percentage) / U256::from(100);

if replacement_max_fee < min_bump {
drop(batch_state_guard);
drop(user_state_guard);
info!("Replacement message fee increase too small for address {addr}. Original: {original_max_fee:?}, received: {replacement_max_fee:?}, minimum required: {min_required_fee:?}");
info!("Invalid replacement message for address {addr}, had max fee: {original_max_fee:?}, received fee: {replacement_max_fee:?}, minimum required: {min_bump:?}");
send_message(
ws_conn_sink.clone(),
SubmitProofResponseMessage::InvalidReplacementMessage,
SubmitProofResponseMessage::UnderpricedProof,
)
.await;
self.metrics.user_error(&["insufficient_fee_increase", ""]);
Expand Down Expand Up @@ -1887,8 +1954,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 @@ -2485,6 +2567,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)
}
Ok(SubmitProofResponseMessage::ServerBusy) => {
error!("Server is busy processing requests, please retry. Funds have not been spent.");
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
2 changes: 1 addition & 1 deletion docs/3_guides/9_aligned_cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ Submit a proof to the Aligned Layer batcher.
- `--keystore_path <path_to_local_keystore>`: Path to the local keystore.
- `--private_key <private_key>`: User's wallet private key.
- `--nonce <n>`: Proof nonce.
- By default, the nonce is set automatically. By setting the nonce manually, you can perform a proof replacement.
- By default, the nonce is set automatically. By setting the nonce manually, you can perform a proof replacement. To perform a valid replacement, the new proof must have a max_fee 10% higher than the previous one.
- One of the following, to specify which Network to interact with:
- `--network <working_network_name>`: Network name to interact with.
- Default: `devnet`
Expand Down
Loading