Skip to content
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
c20832e
feat(aggregation): have constant costs proof sending
maximopalopoli Dec 1, 2025
839283a
fix clippy lint warnings
maximopalopoli Dec 1, 2025
77fbb7c
Convert the monthly eth budget in a config variable
maximopalopoli Dec 1, 2025
560c130
Set the AGGREGATOR env var inside the unit test
maximopalopoli Dec 1, 2025
7316cb7
Use a path relative to the crate to make the CI pass
maximopalopoli Dec 1, 2025
b65538a
Merge branch 'testnet' into feataggmode/constant-costs-proof-sending
maximopalopoli Dec 1, 2025
b7dba3d
fix: avoid _ when declaring monthly eth buget variable
maximopalopoli Dec 1, 2025
6c60fd1
Merge branch 'testnet' into feataggmode/constant-costs-proof-sending
maximopalopoli Dec 1, 2025
b8d32ee
Fixes on the logic to check if the proof should be sent
maximopalopoli Dec 2, 2025
d9595c0
Add more test cases
maximopalopoli Dec 2, 2025
073eee7
fix clippy lint
maximopalopoli Dec 2, 2025
eb9d481
Use a loop+break instead of a control var
maximopalopoli Dec 3, 2025
7260821
Reduce the sleep to 3 minutes before re-evaluating
maximopalopoli Dec 3, 2025
007d2c5
Rename the gas_price_in_wei var ti avoid metion it's in wei
maximopalopoli Dec 3, 2025
8b05811
Fix: sum the sleep time to the time elapsed
maximopalopoli Dec 3, 2025
4edca48
Move the proof sending to outside the loop to avoid cloning multiple …
maximopalopoli Dec 3, 2025
24244d0
Use a lib func instead of floating_eth_to_wei func
maximopalopoli Dec 3, 2025
4450ac1
Move the max_to_spend_in_wei func to a utils module
maximopalopoli Dec 3, 2025
d0bf1e1
Move the get gas price fn to the ProofAggregator
maximopalopoli Dec 3, 2025
4986c42
Revert "Move the max_to_spend_in_wei func to a utils module"
maximopalopoli Dec 3, 2025
3364963
Use RPCProvider type from types mod
maximopalopoli Dec 3, 2025
bdacfc6
Fix warning by creating a read only provider
maximopalopoli Dec 4, 2025
d51ca2f
Explicit the rpc_url type to make it clonable
maximopalopoli Dec 4, 2025
79a817d
Update aggregation_mode/src/backend/mod.rs
maximopalopoli Dec 4, 2025
7a391f8
Add an aditional check to prevent runtime panics
maximopalopoli Dec 4, 2025
7a5a5a3
Remove unneeded get_gas_price func, as its called only 1 time
maximopalopoli Dec 4, 2025
c1cd90a
Add a better description to the monthly_budget_eth config variable
maximopalopoli Dec 4, 2025
569d006
run cargo format tool
maximopalopoli Dec 4, 2025
2da166b
Use alloy instead of ethers to convert variables
maximopalopoli Dec 4, 2025
8ae4e77
Update aggregation_mode/src/backend/mod.rs
maximopalopoli Dec 4, 2025
329c65d
Show the monthly budget on eth instead of wei
maximopalopoli Dec 4, 2025
cf219bd
explicitly create an u256 from the gas price fetched value
maximopalopoli Dec 4, 2025
893209d
Improve the unit test cases and their descriptions
maximopalopoli Dec 4, 2025
616fe62
Merge branch 'staging' into feataggmode/constant-costs-proof-sending
maximopalopoli Dec 4, 2025
d94b405
fix: add the sp1_chunk_aggregator_vk_hash field to config
maximopalopoli Dec 4, 2025
24be68a
Add the program ID vars to the Proof Agg config in unit test
maximopalopoli Dec 5, 2025
acf3b44
Remove ethers from the agg mode cargo lock
maximopalopoli Dec 5, 2025
c266015
Print the current dir to check on CI
maximopalopoli Dec 5, 2025
43b94ee
Ensure the current dir is printed on CI
maximopalopoli Dec 5, 2025
ef7ab6b
Add methods for testing and use them on the unit test
maximopalopoli Dec 5, 2025
ee4cc13
Fix numbers on the examples descriptions
maximopalopoli Dec 5, 2025
e9ae7e5
Use constants for the variables that dont change in the test cases
maximopalopoli Dec 5, 2025
ab46aa1
Add a compilation flag to avoid recompiling aggregation programs on t…
maximopalopoli Dec 9, 2025
a2aa18d
Move the base case constants to before the first test case comment
maximopalopoli Dec 9, 2025
6e67363
add the cfg(test) modifier to the for_testing builders
maximopalopoli Dec 9, 2025
a211e3f
Minor fixes in test case comments
maximopalopoli Dec 9, 2025
675540b
Move the logic to decide if skip the agg programs build to a separate…
maximopalopoli Dec 9, 2025
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
1 change: 1 addition & 0 deletions aggregation_mode/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions aggregation_mode/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ sp1_aggregation_program = { path = "./aggregation_programs/sp1" }
risc0-zkvm = { version = "3.0.3" }
risc0_aggregation_program = { path = "./aggregation_programs/risc0" }
risc0-ethereum-contracts = { git = "https://github.com/risc0/risc0-ethereum/", tag = "v3.0.0" }
ethers = { version = "2.0", features = ["ws", "rustls"] }

[build-dependencies]
sp1-build = { version = "5.0.0" }
Expand Down
1 change: 1 addition & 0 deletions aggregation_mode/src/backend/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ pub struct Config {
pub ecdsa: ECDSAConfig,
pub proofs_per_chunk: u16,
pub total_proofs_limit: u16,
pub monthly_budget_eth: f64,
}

impl Config {
Expand Down
12 changes: 12 additions & 0 deletions aggregation_mode/src/backend/fetcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ use tracing::{error, info};
pub enum ProofsFetcherError {
GetLogs(String),
GetBlockNumber(String),
GasPriceError(String),
}

pub struct ProofsFetcher {
Expand Down Expand Up @@ -188,4 +189,15 @@ impl ProofsFetcher {
pub fn get_last_aggregated_block(&self) -> u64 {
self.last_aggregated_block
}

/// Try to obtain a sensible gas price from two providers.
/// Tries `primary` first, falls back to `fallback` if the first fails.
pub async fn get_gas_price(&self) -> Result<u128, ProofsFetcherError> {
match self.rpc_provider.get_gas_price().await {
Ok(price) => Ok(price),
Err(e1) => Err(ProofsFetcherError::GasPriceError(format!(
"gas price error: {e1}"
))),
}
}
Comment thread
MarcosNicolau marked this conversation as resolved.
Outdated
}
208 changes: 194 additions & 14 deletions aggregation_mode/src/backend/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ mod retry;
mod s3;
mod types;

use crate::backend::AggregatedProofSubmissionError::FetchingProofs;

use crate::aggregators::{AlignedProof, ProofAggregationError, ZKVMEngine};

use alloy::{
Expand All @@ -18,10 +20,13 @@ use alloy::{
signers::local::LocalSigner,
};
use config::Config;
use ethers::types::U256;
use fetcher::{ProofsFetcher, ProofsFetcherError};
use merkle_tree::compute_proofs_merkle_root;
use risc0_ethereum_contracts::encode_seal;
use std::str::FromStr;
use std::thread::sleep;
use std::{str::FromStr, time::Duration};
use tokio::time::Instant;
use tracing::{error, info, warn};
use types::{AlignedProofAggregationService, AlignedProofAggregationServiceContract};

Expand Down Expand Up @@ -136,23 +141,88 @@ impl ProofAggregator {
hex::encode(blob_versioned_hash)
);

info!("Sending proof to ProofAggregationService contract...");
let receipt = self
.send_proof_to_verify_on_chain(blob, blob_versioned_hash, aggregated_proof)
.await?;
info!(
"Proof sent and verified, tx hash {:?}",
receipt.transaction_hash
);
// Iterate until we can send the proof on-chain
let start_time = Instant::now();

let mut sent_proof = false;
while !sent_proof {
Comment thread
MarcosNicolau marked this conversation as resolved.
Outdated
// We add 24 hours because the proof aggregator runs once a day, so the time elapsed
// should be considered over a 24h period.
let time_elapsed: Duration =
Instant::now().duration_since(start_time) + Duration::from_secs(24 * 3600);
Comment thread
MarcosNicolau marked this conversation as resolved.
Outdated

let gas_price = self.fetcher.get_gas_price().await.map_err(FetchingProofs)?;
Comment thread
MarcosNicolau marked this conversation as resolved.
Outdated

if self.should_send_proof_to_verify_on_chain(
time_elapsed,
self.config.monthly_budget_eth,
gas_price.into(),
) {
info!("Sending proof to ProofAggregationService contract...");
let receipt = self
.send_proof_to_verify_on_chain(&blob, blob_versioned_hash, &aggregated_proof)
.await?;
info!(
"Proof sent and verified, tx hash {:?}",
receipt.transaction_hash
);

sent_proof = true;
} else {
info!("Skipping sending proof to ProofAggregationService contract due to budget/time constraints.");
}

// Sleep for 5 minutes before re-evaluating
sleep(Duration::from_secs(300));
Comment thread
MarcosNicolau marked this conversation as resolved.
Outdated
}

Ok(())
}

fn floating_eth_to_wei(eth: f64) -> U256 {
let wei_in_eth = 1_000_000_000_000_000_000f64;
let wei = eth * wei_in_eth;
U256::from(wei as u64)
}
Comment thread
MarcosNicolau marked this conversation as resolved.
Outdated

fn max_to_spend_in_wei(time_elapsed: Duration, monthly_eth_budget: f64) -> U256 {
const SECONDS_PER_MONTH: u64 = 30 * 24 * 60 * 60;

let monthly_budget_in_wei = Self::floating_eth_to_wei(monthly_eth_budget);

let elapsed_seconds = U256::from(time_elapsed.as_secs());

let budget_available_per_second_in_wei = monthly_budget_in_wei / SECONDS_PER_MONTH;

budget_available_per_second_in_wei * elapsed_seconds
}
Comment thread
MarcosNicolau marked this conversation as resolved.
Outdated

/// Decides whether to send the aggregated proof to be verified on-chain based on
/// time elapsed since last submission and monthly ETH budget.
/// We make a linear function with the eth to spend this month and the time elapsed since last submission.
/// If eth to spend / elapsed time is over the linear function, we skip the submission.
fn should_send_proof_to_verify_on_chain(
&self,
time_elapsed: Duration,
monthly_eth_budget: f64,
gas_price_in_wei: U256,
Comment thread
JuArce marked this conversation as resolved.
Outdated
) -> bool {
// We assume a fixed gas cost of 300,000 for each of the 2 transactions
const ON_CHAIN_COST_IN_GAS_UNITS: u64 = 600_000u64;

let on_chain_cost_in_gas: U256 = U256::from(ON_CHAIN_COST_IN_GAS_UNITS);
let max_to_spend_in_wei = Self::max_to_spend_in_wei(time_elapsed, monthly_eth_budget);

let expected_cost_in_wei = gas_price_in_wei * on_chain_cost_in_gas;

expected_cost_in_wei <= max_to_spend_in_wei
}

async fn send_proof_to_verify_on_chain(
&self,
blob: BlobTransactionSidecar,
blob: &BlobTransactionSidecar,
blob_versioned_hash: [u8; 32],
aggregated_proof: AlignedProof,
aggregated_proof: &AlignedProof,
Comment thread
MarcosNicolau marked this conversation as resolved.
Outdated
) -> Result<TransactionReceipt, AggregatedProofSubmissionError> {
let tx_req = match aggregated_proof {
AlignedProof::SP1(proof) => self
Expand All @@ -162,7 +232,7 @@ impl ProofAggregator {
proof.proof_with_pub_values.public_values.to_vec().into(),
proof.proof_with_pub_values.bytes().into(),
)
.sidecar(blob)
.sidecar(blob.clone())
.into_transaction_request(),
AlignedProof::Risc0(proof) => {
let encoded_seal = encode_seal(&proof.receipt).map_err(|e| {
Expand All @@ -172,9 +242,9 @@ impl ProofAggregator {
.verifyRisc0(
blob_versioned_hash.into(),
encoded_seal.into(),
proof.receipt.journal.bytes.into(),
proof.receipt.journal.bytes.clone().into(),
)
.sidecar(blob)
.sidecar(blob.clone())
.into_transaction_request()
}
};
Expand Down Expand Up @@ -284,3 +354,113 @@ impl ProofAggregator {
Ok((blob, blob_versioned_hash))
}
}

#[cfg(test)]
mod tests {
use super::*;

use super::config::Config;

fn make_aggregator() -> ProofAggregator {
// Set the AGGREGATOR env variable to "sp1" or "risc0" as it's needed by ProofAggregator::new
std::env::set_var("AGGREGATOR", "sp1");

let current_dir = env!("CARGO_MANIFEST_DIR");

// These config values are taken from config-files/config-proof-aggregator.yaml
let config = Config {
eth_rpc_url: "http://localhost:8545".to_string(),
eth_ws_url: "ws://localhost:8545".to_string(),
max_proofs_in_queue: 1000,
proof_aggregation_service_address: "0xcbEAF3BDe82155F56486Fb5a1072cb8baAf547cc"
.to_string(),
aligned_service_manager_address: "0x851356ae760d987E095750cCeb3bC6014560891C"
.to_string(),
// Use a path relative to the crate so tests work both locally and in CI
last_aggregated_block_filepath: format!(
"{current_dir}/../config-files/proof-aggregator.last_aggregated_block.json"
),
ecdsa: config::ECDSAConfig {
private_key_store_path: format!(
"{current_dir}/../config-files/anvil.proof-aggregator.ecdsa.key.json"
),
private_key_store_password: "".to_string(),
},
proofs_per_chunk: 512,
total_proofs_limit: 3968,
monthly_budget_eth: 15.0,
};

ProofAggregator::new(config)
}

#[test]
fn test_should_send_proof_to_verify_on_chain_updated_cases() {
let aggregator = make_aggregator();

let gas_price_20gwei: U256 = U256::from(20_000_000_000u64);

// With 0 seconds elapsed and 1 ETH budget, we cannot send the proof
Comment thread
JuArce marked this conversation as resolved.
Outdated
assert!(!aggregator.should_send_proof_to_verify_on_chain(
Duration::from_secs(0),
1.0,
gas_price_20gwei,
));

// After 24 hours and 1 ETH monthly budget, we can send the proof
assert!(aggregator.should_send_proof_to_verify_on_chain(
Duration::from_secs(24 * 3600),
1.0,
gas_price_20gwei,
));

// After 24 hours and a very low budget, we cannot send the proof
assert!(!aggregator.should_send_proof_to_verify_on_chain(
Duration::from_secs(24 * 3600),
0.00325,
U256::from(0_200_000_000u64),
));

// After 27 hours with the same budget as before, we can send the proof
assert!(aggregator.should_send_proof_to_verify_on_chain(
Duration::from_secs(27 * 3600),
0.00325,
U256::from(0_200_000_000u64),
));

// After 30 days but with a very low budget, we cannot send the proof
assert!(!aggregator.should_send_proof_to_verify_on_chain(
Duration::from_secs(30 * 24 * 3600),
0.001,
gas_price_20gwei,
));

// After 15 days, a moderate budget and a high gas price, we can still send the proof
assert!(aggregator.should_send_proof_to_verify_on_chain(
Duration::from_secs(15 * 24 * 3600),
10.0,
U256::from(2_000_000_000_000u64),
));

// After 30 days and a medium budget, we cannot send the proof
assert!(!aggregator.should_send_proof_to_verify_on_chain(
Duration::from_secs(30 * 24 * 3600),
0.012,
gas_price_20gwei,
));

// After 2 days and a reasonable budget, we can send the proof
assert!(aggregator.should_send_proof_to_verify_on_chain(
Duration::from_secs(2 * 24 * 3600),
5.0,
gas_price_20gwei,
));

// After 10 days and a medium budget with a very high gas price, we cannot send the proof
assert!(!aggregator.should_send_proof_to_verify_on_chain(
Duration::from_secs(10 * 24 * 3600),
2.0,
U256::from(100_000_000_000_000u64),
));
}
}
2 changes: 2 additions & 0 deletions config-files/config-proof-aggregator-ethereum-package.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ proofs_per_chunk: 512 # Amount of proofs to process per chunk
# Since each proof commitments takes 32 bytes hash
# We can aggregate as much proofs as 126.976 / 32 = 3968 per blob
total_proofs_limit: 3968
# Monthly ETH budget for on-chain proof verification
monthly_budget_eth: 15.0

ecdsa:
private_key_store_path: "config-files/anvil.proof-aggregator.ecdsa.key.json"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ proofs_per_chunk: 512 # Amount of proofs to process per chunk
# Since each proof commitments takes 32 bytes hash
# We can aggregate as much proofs as 126.976 / 32 = 3968 per blob
total_proofs_limit: 3968

# Monthly ETH budget for on-chain proof verification
monthly_budget_eth: 15.0
Comment thread
MarcosNicolau marked this conversation as resolved.
Outdated

ecdsa:
private_key_store_path: "config-files/anvil.proof-aggregator.ecdsa.key.json"
Expand Down
3 changes: 2 additions & 1 deletion config-files/config-proof-aggregator-mock.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ proofs_per_chunk: 512 # Amount of proofs to process per chunk
# Since each proof commitments takes 32 bytes hash
# We can aggregate as much proofs as 126.976 / 32 = 3968 per blob
total_proofs_limit: 3968

# Monthly ETH budget for on-chain proof verification
monthly_budget_eth: 15.0

ecdsa:
private_key_store_path: "config-files/anvil.proof-aggregator.ecdsa.key.json"
Expand Down
3 changes: 2 additions & 1 deletion config-files/config-proof-aggregator.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ proofs_per_chunk: 512 # Amount of proofs to process per chunk
# Since each proof commitments takes 32 bytes hash
# We can aggregate as much proofs as 126.976 / 32 = 3968 per blob
total_proofs_limit: 3968

# Monthly ETH budget for on-chain proof verification
monthly_budget_eth: 15.0

ecdsa:
private_key_store_path: "config-files/anvil.proof-aggregator.ecdsa.key.json"
Expand Down
Loading