@@ -5,6 +5,8 @@ mod retry;
55mod s3;
66mod types;
77
8+ use crate :: backend:: AggregatedProofSubmissionError :: FetchingProofs ;
9+
810use crate :: aggregators:: { AlignedProof , ProofAggregationError , ZKVMEngine } ;
911
1012use alloy:: {
@@ -18,10 +20,13 @@ use alloy::{
1820 signers:: local:: LocalSigner ,
1921} ;
2022use config:: Config ;
23+ use ethers:: types:: U256 ;
2124use fetcher:: { ProofsFetcher , ProofsFetcherError } ;
2225use merkle_tree:: compute_proofs_merkle_root;
2326use 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 ;
2530use tracing:: { error, info, warn} ;
2631use 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