Skip to content

Commit c20832e

Browse files
feat(aggregation): have constant costs proof sending
1 parent d603f66 commit c20832e

File tree

4 files changed

+159
-14
lines changed

4 files changed

+159
-14
lines changed

aggregation_mode/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

aggregation_mode/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ sp1_aggregation_program = { path = "./aggregation_programs/sp1" }
2727
risc0-zkvm = { version = "3.0.3" }
2828
risc0_aggregation_program = { path = "./aggregation_programs/risc0" }
2929
risc0-ethereum-contracts = { git = "https://github.com/risc0/risc0-ethereum/", tag = "v3.0.0" }
30+
ethers = { version = "2.0", features = ["ws", "rustls"] }
3031

3132
[build-dependencies]
3233
sp1-build = { version = "5.0.0" }

aggregation_mode/src/backend/fetcher.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ use tracing::{error, info};
2424
pub enum ProofsFetcherError {
2525
GetLogs(String),
2626
GetBlockNumber(String),
27+
GasPriceError(String),
2728
}
2829

2930
pub struct ProofsFetcher {
@@ -188,4 +189,16 @@ impl ProofsFetcher {
188189
pub fn get_last_aggregated_block(&self) -> u64 {
189190
self.last_aggregated_block
190191
}
192+
193+
/// Try to obtain a sensible gas price from two providers.
194+
/// Tries `primary` first, falls back to `fallback` if the first fails.
195+
pub async fn get_gas_price(&self) -> Result<u128, ProofsFetcherError> {
196+
match self.rpc_provider.get_gas_price().await {
197+
Ok(price) => Ok(price),
198+
Err(e1) => Err(ProofsFetcherError::GasPriceError(format!(
199+
"gas price error: {:?}",
200+
e1
201+
))),
202+
}
203+
}
191204
}

aggregation_mode/src/backend/mod.rs

Lines changed: 144 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ mod retry;
55
mod s3;
66
mod types;
77

8+
use crate::backend::AggregatedProofSubmissionError::FetchingProofs;
9+
810
use crate::aggregators::{AlignedProof, ProofAggregationError, ZKVMEngine};
911

1012
use alloy::{
@@ -18,10 +20,13 @@ use alloy::{
1820
signers::local::LocalSigner,
1921
};
2022
use config::Config;
23+
use ethers::types::U256;
2124
use fetcher::{ProofsFetcher, ProofsFetcherError};
2225
use merkle_tree::compute_proofs_merkle_root;
2326
use risc0_ethereum_contracts::encode_seal;
24-
use std::str::FromStr;
27+
use std::thread::sleep;
28+
use std::{str::FromStr, time::Duration};
29+
use tokio::time::Instant;
2530
use tracing::{error, info, warn};
2631
use types::{AlignedProofAggregationService, AlignedProofAggregationServiceContract};
2732

@@ -136,23 +141,81 @@ impl ProofAggregator {
136141
hex::encode(blob_versioned_hash)
137142
);
138143

139-
info!("Sending proof to ProofAggregationService contract...");
140-
let receipt = self
141-
.send_proof_to_verify_on_chain(blob, blob_versioned_hash, aggregated_proof)
142-
.await?;
143-
info!(
144-
"Proof sent and verified, tx hash {:?}",
145-
receipt.transaction_hash
146-
);
144+
// Iterate until we can send the proof on-chain
145+
let start_time = Instant::now();
146+
const MONTHLY_ETH_BUDGET_GWEI: u64 = 15_000_000_000;
147+
148+
let mut sent_proof = false;
149+
while !sent_proof {
150+
// We add 24 hours because the proof aggregator runs once a day, so the time elapsed
151+
// should be considered over a 24h period.
152+
let time_elapsed: Duration =
153+
Instant::now().duration_since(start_time) + Duration::from_secs(24 * 3600);
154+
155+
let gas_price = self
156+
.fetcher
157+
.get_gas_price()
158+
.await
159+
.map_err(|err| FetchingProofs(err))?;
160+
161+
if self.should_send_proof_to_verify_on_chain(
162+
time_elapsed,
163+
MONTHLY_ETH_BUDGET_GWEI,
164+
gas_price.into(),
165+
) {
166+
info!("Sending proof to ProofAggregationService contract...");
167+
let receipt = self
168+
.send_proof_to_verify_on_chain(&blob, blob_versioned_hash, &aggregated_proof)
169+
.await?;
170+
info!(
171+
"Proof sent and verified, tx hash {:?}",
172+
receipt.transaction_hash
173+
);
174+
175+
sent_proof = true;
176+
} else {
177+
info!("Skipping sending proof to ProofAggregationService contract due to budget/time constraints.");
178+
}
179+
180+
// Sleep for 5 minutes before re-evaluating
181+
sleep(Duration::from_secs(300));
182+
}
147183

148184
Ok(())
149185
}
150186

187+
/// Decides whether to send the aggregated proof to be verified on-chain based on
188+
/// time elapsed since last submission and monthly ETH budget.
189+
/// We make a linear function with the eth to spend this month and the time elapsed since last submission.
190+
/// If eth to spend / elapsed time is over the linear function, we skip the submission.
191+
fn should_send_proof_to_verify_on_chain(
192+
&self,
193+
time_elapsed: Duration,
194+
monthly_eth_to_spend: u64,
195+
gas_price: U256,
196+
) -> bool {
197+
const HOURS_PER_MONTH: f64 = 24.0 * 30.0;
198+
199+
let elapsed_hours = time_elapsed.as_secs_f64() / 3600.0;
200+
if elapsed_hours <= 0.0 {
201+
return false;
202+
}
203+
204+
let elapsed_hours = elapsed_hours.min(HOURS_PER_MONTH);
205+
206+
let hourly_budget_gwei = monthly_eth_to_spend as f64 / HOURS_PER_MONTH;
207+
let budget_so_far_gwei = hourly_budget_gwei * elapsed_hours;
208+
209+
let gas_price_gwei = gas_price.as_u64() as f64 / 1_000_000_000.0;
210+
211+
gas_price_gwei <= budget_so_far_gwei
212+
}
213+
151214
async fn send_proof_to_verify_on_chain(
152215
&self,
153-
blob: BlobTransactionSidecar,
216+
blob: &BlobTransactionSidecar,
154217
blob_versioned_hash: [u8; 32],
155-
aggregated_proof: AlignedProof,
218+
aggregated_proof: &AlignedProof,
156219
) -> Result<TransactionReceipt, AggregatedProofSubmissionError> {
157220
let tx_req = match aggregated_proof {
158221
AlignedProof::SP1(proof) => self
@@ -162,7 +225,7 @@ impl ProofAggregator {
162225
proof.proof_with_pub_values.public_values.to_vec().into(),
163226
proof.proof_with_pub_values.bytes().into(),
164227
)
165-
.sidecar(blob)
228+
.sidecar(blob.clone())
166229
.into_transaction_request(),
167230
AlignedProof::Risc0(proof) => {
168231
let encoded_seal = encode_seal(&proof.receipt).map_err(|e| {
@@ -172,9 +235,9 @@ impl ProofAggregator {
172235
.verifyRisc0(
173236
blob_versioned_hash.into(),
174237
encoded_seal.into(),
175-
proof.receipt.journal.bytes.into(),
238+
proof.receipt.journal.bytes.clone().into(),
176239
)
177-
.sidecar(blob)
240+
.sidecar(blob.clone())
178241
.into_transaction_request()
179242
}
180243
};
@@ -284,3 +347,70 @@ impl ProofAggregator {
284347
Ok((blob, blob_versioned_hash))
285348
}
286349
}
350+
351+
#[cfg(test)]
352+
mod tests {
353+
use super::*;
354+
355+
use super::config::Config;
356+
357+
#[test]
358+
fn test_should_send_proof_to_verify_on_chain() {
359+
// These config values are taken from config-files/config-proof-aggregator.yaml
360+
let config = Config {
361+
eth_rpc_url: "http://localhost:8545".to_string(),
362+
eth_ws_url: "ws://localhost:8545".to_string(),
363+
max_proofs_in_queue: 1000,
364+
proof_aggregation_service_address: "0xcbEAF3BDe82155F56486Fb5a1072cb8baAf547cc"
365+
.to_string(),
366+
aligned_service_manager_address: "0x851356ae760d987E095750cCeb3bC6014560891C"
367+
.to_string(),
368+
last_aggregated_block_filepath:
369+
"/Users/maximopalopoli/Desktop/aligned/repo/aligned_layer/config-files/proof-aggregator.last_aggregated_block.json".to_string(),
370+
ecdsa: config::ECDSAConfig {
371+
private_key_store_path: "/Users/maximopalopoli/Desktop/aligned/repo/aligned_layer/config-files/anvil.proof-aggregator.ecdsa.key.json"
372+
.to_string(),
373+
private_key_store_password: "".to_string(),
374+
},
375+
proofs_per_chunk: 512,
376+
total_proofs_limit: 3968,
377+
};
378+
379+
let aggregator = ProofAggregator::new(config);
380+
381+
// Test case 1: Just started, should not send
382+
assert!(!aggregator.should_send_proof_to_verify_on_chain(
383+
Duration::from_secs(0),
384+
15_000_000_000,
385+
20_000_000_000u64.into(),
386+
));
387+
388+
// Test case 2: Halfway through the month, low spend, should send
389+
assert!(aggregator.should_send_proof_to_verify_on_chain(
390+
Duration::from_secs(15 * 24 * 3600),
391+
5_000_000_000,
392+
20_000_000_000u64.into(),
393+
));
394+
395+
// Test case 3: Near end of month, high spend -> should send (budget_so_far >> gas_price)
396+
assert!(aggregator.should_send_proof_to_verify_on_chain(
397+
Duration::from_secs(28 * 24 * 3600),
398+
18_000_000_000,
399+
20_000_000_000u64.into(),
400+
));
401+
402+
// Test case 5: End of month, over budget -> with these units still sends
403+
assert!(aggregator.should_send_proof_to_verify_on_chain(
404+
Duration::from_secs(30 * 24 * 3600),
405+
25_000_000_000,
406+
20_000_000_000u64.into(),
407+
));
408+
409+
// Test case 6: Early month, budget_so_far still > gas_price -> should send
410+
assert!(aggregator.should_send_proof_to_verify_on_chain(
411+
Duration::from_secs(5 * 24 * 3600),
412+
10_000_000_000,
413+
20_000_000_000u64.into(),
414+
));
415+
}
416+
}

0 commit comments

Comments
 (0)