From eb4d032abf7c55d3d1d498b8f08a00250fe0a14a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petar=20Vujovi=C4=87?= Date: Thu, 10 Oct 2024 16:17:47 +0200 Subject: [PATCH] feat(core,host): initial aggregation API (#375) * initial proof aggregation implementation * aggregation improvements + risc0 aggregation * sp1 aggregation fixes * sp1 aggregation elf * uuid support for risc0 aggregation * risc0 aggregation circuit compile fixes * fix sgx proof aggregation * fmt * feat(core,host): initial aggregation API * fix(core,host,sgx): fix compiler and clippy errors * fix(core,lib,provers): revert merge bugs and add sp1 stubs * fix(core): remove double member * fix(sp1): fix dependency naming * refactor(risc0): clean up aggregation file * fix(sp1): enable verification for proof aggregation * feat(host): migrate to v3 API * feat(sp1): run cargo fmt * feat(core): make `l1_inclusion_block_number` optional * fixproof req input into prove state manager Signed-off-by: smtmfft * feat(core,host,lib,tasks): add aggregation tasks and API * fix(core): fix typo * fix v3 error return Signed-off-by: smtmfft * feat(sp1): implement aggregate function * fix sgx aggregation for back compatibility Signed-off-by: smtmfft * fix(lib): fix typo * fix risc0 aggregation Signed-off-by: smtmfft * fix(host,sp1): handle statuses * enable sp1 aggregation Signed-off-by: smtmfft * feat(host): error out on empty proof array request * fix(host): return proper status report * feat(host,tasks): adding details to error statuses * fix sp1 aggregation Signed-off-by: smtmfft * update prove-block script Signed-off-by: smtmfft * fix(fmt): run cargo fmt * fix(clippy): fix clippy issues * chore(repo): cleanup captured vars in format calls * fix(sp1): convert to proper types * chore(sp1): remove the unneccessary --------- Signed-off-by: smtmfft Co-authored-by: Brecht Devos Co-authored-by: smtmfft --- Cargo.lock | 1 + core/src/interfaces.rs | 169 +++++++++++- core/src/lib.rs | 71 ++++- core/src/preflight/util.rs | 5 +- core/src/prover.rs | 14 + host/src/cache.rs | 5 +- host/src/interfaces.rs | 24 +- host/src/lib.rs | 10 +- host/src/proof.rs | 144 +++++++++- host/src/server/api/mod.rs | 9 +- host/src/server/api/v2/mod.rs | 4 +- host/src/server/api/v2/proof/mod.rs | 10 +- host/src/server/api/v3/mod.rs | 172 ++++++++++++ host/src/server/api/v3/proof/aggregate.rs | 114 ++++++++ host/src/server/api/v3/proof/cancel.rs | 76 ++++++ host/src/server/api/v3/proof/mod.rs | 219 +++++++++++++++ lib/src/builder.rs | 6 +- lib/src/input.rs | 40 ++- lib/src/protocol_instance.rs | 44 ++- lib/src/prover.rs | 17 +- pipeline/src/builder.rs | 2 +- pipeline/src/executor.rs | 6 +- provers/risc0/builder/src/main.rs | 5 +- provers/risc0/driver/Cargo.toml | 4 +- provers/risc0/driver/src/bonsai.rs | 29 +- provers/risc0/driver/src/lib.rs | 115 +++++++- provers/risc0/driver/src/methods/mod.rs | 1 + .../driver/src/methods/risc0_aggregation.rs | 5 + .../risc0/driver/src/methods/risc0_guest.rs | 2 +- provers/risc0/driver/src/snarks.rs | 48 +++- provers/risc0/guest/Cargo.toml | 4 + provers/risc0/guest/src/aggregation.rs | 27 ++ provers/sgx/guest/src/app_args.rs | 2 + provers/sgx/guest/src/main.rs | 5 + provers/sgx/guest/src/one_shot.rs | 88 +++++- provers/sgx/prover/Cargo.toml | 1 + provers/sgx/prover/src/lib.rs | 142 +++++++++- provers/sp1/builder/src/main.rs | 2 +- provers/sp1/driver/src/lib.rs | 254 ++++++++++++++++-- .../proof_verify/remote_contract_verify.rs | 8 +- provers/sp1/driver/src/verifier.rs | 2 +- provers/sp1/guest/Cargo.lock | 35 ++- provers/sp1/guest/Cargo.toml | 15 +- provers/sp1/guest/elf/sp1-aggregation | Bin 0 -> 200852 bytes provers/sp1/guest/src/aggregation.rs | 31 +++ provers/sp1/guest/src/benchmark/bn254_add.rs | 4 +- provers/sp1/guest/src/benchmark/bn254_mul.rs | 2 +- provers/sp1/guest/src/benchmark/sha256.rs | 2 +- provers/sp1/guest/src/sys.rs | 3 +- provers/sp1/guest/src/zk_op.rs | 9 +- script/prove-block.sh | 14 +- tasks/src/adv_sqlite.rs | 33 ++- tasks/src/lib.rs | 182 +++++++++++-- tasks/src/mem_db.rs | 150 ++++++++++- 54 files changed, 2195 insertions(+), 191 deletions(-) create mode 100644 host/src/server/api/v3/mod.rs create mode 100644 host/src/server/api/v3/proof/aggregate.rs create mode 100644 host/src/server/api/v3/proof/cancel.rs create mode 100644 host/src/server/api/v3/proof/mod.rs create mode 100644 provers/risc0/driver/src/methods/risc0_aggregation.rs create mode 100644 provers/risc0/guest/src/aggregation.rs create mode 100755 provers/sp1/guest/elf/sp1-aggregation create mode 100644 provers/sp1/guest/src/aggregation.rs diff --git a/Cargo.lock b/Cargo.lock index d47ec8685..590a56b4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8285,6 +8285,7 @@ dependencies = [ "alloy-transport-http", "anyhow", "bincode", + "hex", "once_cell", "pem 3.0.4", "raiko-lib", diff --git a/core/src/interfaces.rs b/core/src/interfaces.rs index 3099565ed..63ad41140 100644 --- a/core/src/interfaces.rs +++ b/core/src/interfaces.rs @@ -3,13 +3,15 @@ use alloy_primitives::{Address, B256}; use clap::{Args, ValueEnum}; use raiko_lib::{ consts::VerifierType, - input::{BlobProofType, GuestInput, GuestOutput}, + input::{ + AggregationGuestInput, AggregationGuestOutput, BlobProofType, GuestInput, GuestOutput, + }, prover::{IdStore, IdWrite, Proof, ProofKey, Prover, ProverError}, }; use serde::{Deserialize, Serialize}; use serde_json::Value; use serde_with::{serde_as, DisplayFromStr}; -use std::{collections::HashMap, path::Path, str::FromStr}; +use std::{collections::HashMap, fmt::Display, path::Path, str::FromStr}; use utoipa::ToSchema; #[derive(Debug, thiserror::Error, ToSchema)] @@ -203,6 +205,47 @@ impl ProofType { } } + /// Run the prover driver depending on the proof type. + pub async fn aggregate_proofs( + &self, + input: AggregationGuestInput, + output: &AggregationGuestOutput, + config: &Value, + store: Option<&mut dyn IdWrite>, + ) -> RaikoResult { + let proof = match self { + ProofType::Native => NativeProver::aggregate(input.clone(), output, config, store) + .await + .map_err(>::into), + ProofType::Sp1 => { + #[cfg(feature = "sp1")] + return sp1_driver::Sp1Prover::aggregate(input.clone(), output, config, store) + .await + .map_err(|e| e.into()); + #[cfg(not(feature = "sp1"))] + Err(RaikoError::FeatureNotSupportedError(*self)) + } + ProofType::Risc0 => { + #[cfg(feature = "risc0")] + return risc0_driver::Risc0Prover::aggregate(input.clone(), output, config, store) + .await + .map_err(|e| e.into()); + #[cfg(not(feature = "risc0"))] + Err(RaikoError::FeatureNotSupportedError(*self)) + } + ProofType::Sgx => { + #[cfg(feature = "sgx")] + return sgx_prover::SgxProver::aggregate(input.clone(), output, config, store) + .await + .map_err(|e| e.into()); + #[cfg(not(feature = "sgx"))] + Err(RaikoError::FeatureNotSupportedError(*self)) + } + }?; + + Ok(proof) + } + pub async fn cancel_proof( &self, proof_key: ProofKey, @@ -302,7 +345,7 @@ pub struct ProofRequestOpt { pub prover_args: ProverSpecificOpts, } -#[derive(Default, Clone, Serialize, Deserialize, Debug, ToSchema, Args)] +#[derive(Default, Clone, Serialize, Deserialize, Debug, ToSchema, Args, PartialEq, Eq, Hash)] pub struct ProverSpecificOpts { /// Native prover specific options. pub native: Option, @@ -398,3 +441,123 @@ impl TryFrom for ProofRequest { }) } } + +#[derive(Default, Clone, Serialize, Deserialize, Debug, ToSchema)] +#[serde(default)] +/// A request for proof aggregation of multiple proofs. +pub struct AggregationRequest { + /// The block numbers and l1 inclusion block numbers for the blocks to aggregate proofs for. + pub block_numbers: Vec<(u64, Option)>, + /// The network to generate the proof for. + pub network: Option, + /// The L1 network to generate the proof for. + pub l1_network: Option, + // Graffiti. + pub graffiti: Option, + /// The protocol instance data. + pub prover: Option, + /// The proof type. + pub proof_type: Option, + /// Blob proof type. + pub blob_proof_type: Option, + #[serde(flatten)] + /// Any additional prover params in JSON format. + pub prover_args: ProverSpecificOpts, +} + +impl AggregationRequest { + /// Merge proof request options into aggregation request options. + pub fn merge(&mut self, opts: &ProofRequestOpt) -> RaikoResult<()> { + let this = serde_json::to_value(&self)?; + let mut opts = serde_json::to_value(opts)?; + merge(&mut opts, &this); + *self = serde_json::from_value(opts)?; + Ok(()) + } +} + +impl From for Vec { + fn from(value: AggregationRequest) -> Self { + value + .block_numbers + .iter() + .map( + |&(block_number, l1_inclusion_block_number)| ProofRequestOpt { + block_number: Some(block_number), + l1_inclusion_block_number, + network: value.network.clone(), + l1_network: value.l1_network.clone(), + graffiti: value.graffiti.clone(), + prover: value.prover.clone(), + proof_type: value.proof_type.clone(), + blob_proof_type: value.blob_proof_type.clone(), + prover_args: value.prover_args.clone(), + }, + ) + .collect() + } +} + +impl From for AggregationRequest { + fn from(value: ProofRequestOpt) -> Self { + let block_numbers = if let Some(block_number) = value.block_number { + vec![(block_number, value.l1_inclusion_block_number)] + } else { + vec![] + }; + + Self { + block_numbers, + network: value.network, + l1_network: value.l1_network, + graffiti: value.graffiti, + prover: value.prover, + proof_type: value.proof_type, + blob_proof_type: value.blob_proof_type, + prover_args: value.prover_args, + } + } +} + +#[derive(Default, Clone, Serialize, Deserialize, Debug, ToSchema, PartialEq, Eq, Hash)] +#[serde(default)] +/// A request for proof aggregation of multiple proofs. +pub struct AggregationOnlyRequest { + /// The block numbers and l1 inclusion block numbers for the blocks to aggregate proofs for. + pub proofs: Vec, + /// The proof type. + pub proof_type: Option, + #[serde(flatten)] + /// Any additional prover params in JSON format. + pub prover_args: ProverSpecificOpts, +} + +impl Display for AggregationOnlyRequest { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&format!( + "AggregationOnlyRequest {{ {:?}, {:?} }}", + self.proof_type, self.prover_args + )) + } +} + +impl From<(AggregationRequest, Vec)> for AggregationOnlyRequest { + fn from((request, proofs): (AggregationRequest, Vec)) -> Self { + Self { + proofs, + proof_type: request.proof_type, + prover_args: request.prover_args, + } + } +} + +impl AggregationOnlyRequest { + /// Merge proof request options into aggregation request options. + pub fn merge(&mut self, opts: &ProofRequestOpt) -> RaikoResult<()> { + let this = serde_json::to_value(&self)?; + let mut opts = serde_json::to_value(opts)?; + merge(&mut opts, &this); + *self = serde_json::from_value(opts)?; + Ok(()) + } +} diff --git a/core/src/lib.rs b/core/src/lib.rs index cd026952b..48064e326 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -226,8 +226,9 @@ mod tests { use clap::ValueEnum; use raiko_lib::{ consts::{Network, SupportedChainSpecs}, - input::BlobProofType, + input::{AggregationGuestInput, AggregationGuestOutput, BlobProofType}, primitives::B256, + prover::Proof, }; use serde_json::{json, Value}; use std::{collections::HashMap, env}; @@ -242,7 +243,7 @@ mod tests { ci == "1" } - fn test_proof_params() -> HashMap { + fn test_proof_params(enable_aggregation: bool) -> HashMap { let mut prover_args = HashMap::new(); prover_args.insert( "native".to_string(), @@ -256,7 +257,7 @@ mod tests { "sp1".to_string(), json! { { - "recursion": "core", + "recursion": if enable_aggregation { "compressed" } else { "plonk" }, "prover": "mock", "verify": true } @@ -278,8 +279,8 @@ mod tests { json! { { "instance_id": 121, - "setup": true, - "bootstrap": true, + "setup": enable_aggregation, + "bootstrap": enable_aggregation, "prove": true, } }, @@ -291,7 +292,7 @@ mod tests { l1_chain_spec: ChainSpec, taiko_chain_spec: ChainSpec, proof_request: ProofRequest, - ) { + ) -> Proof { let provider = RpcBlockDataProvider::new(&taiko_chain_spec.rpc, proof_request.block_number - 1) .expect("Could not create RpcBlockDataProvider"); @@ -301,10 +302,10 @@ mod tests { .await .expect("input generation failed"); let output = raiko.get_output(&input).expect("output generation failed"); - let _proof = raiko + raiko .prove(input, &output, None) .await - .expect("proof generation failed"); + .expect("proof generation failed") } #[ignore] @@ -332,7 +333,7 @@ mod tests { l1_network, proof_type, blob_proof_type: BlobProofType::ProofOfEquivalence, - prover_args: test_proof_params(), + prover_args: test_proof_params(false), }; prove_block(l1_chain_spec, taiko_chain_spec, proof_request).await; } @@ -361,7 +362,7 @@ mod tests { l1_network, proof_type, blob_proof_type: BlobProofType::ProofOfEquivalence, - prover_args: test_proof_params(), + prover_args: test_proof_params(false), }; prove_block(l1_chain_spec, taiko_chain_spec, proof_request).await; } @@ -399,7 +400,7 @@ mod tests { l1_network, proof_type, blob_proof_type: BlobProofType::ProofOfEquivalence, - prover_args: test_proof_params(), + prover_args: test_proof_params(false), }; prove_block(l1_chain_spec, taiko_chain_spec, proof_request).await; } @@ -432,9 +433,55 @@ mod tests { l1_network, proof_type, blob_proof_type: BlobProofType::ProofOfEquivalence, - prover_args: test_proof_params(), + prover_args: test_proof_params(false), }; prove_block(l1_chain_spec, taiko_chain_spec, proof_request).await; } } + + #[tokio::test(flavor = "multi_thread")] + async fn test_prove_block_taiko_a7_aggregated() { + let proof_type = get_proof_type_from_env(); + let l1_network = Network::Holesky.to_string(); + let network = Network::TaikoA7.to_string(); + // Give the CI an simpler block to test because it doesn't have enough memory. + // Unfortunately that also means that kzg is not getting fully verified by CI. + let block_number = if is_ci() { 105987 } else { 101368 }; + let taiko_chain_spec = SupportedChainSpecs::default() + .get_chain_spec(&network) + .unwrap(); + let l1_chain_spec = SupportedChainSpecs::default() + .get_chain_spec(&l1_network) + .unwrap(); + + let proof_request = ProofRequest { + block_number, + l1_inclusion_block_number: 0, + network, + graffiti: B256::ZERO, + prover: Address::ZERO, + l1_network, + proof_type, + blob_proof_type: BlobProofType::ProofOfEquivalence, + prover_args: test_proof_params(true), + }; + let proof = prove_block(l1_chain_spec, taiko_chain_spec, proof_request).await; + + let input = AggregationGuestInput { + proofs: vec![proof.clone(), proof], + }; + + let output = AggregationGuestOutput { hash: B256::ZERO }; + + let aggregated_proof = proof_type + .aggregate_proofs( + input, + &output, + &serde_json::to_value(&test_proof_params(false)).unwrap(), + None, + ) + .await + .expect("proof aggregation failed"); + println!("aggregated proof: {aggregated_proof:?}"); + } } diff --git a/core/src/preflight/util.rs b/core/src/preflight/util.rs index 889134d94..10fb6394c 100644 --- a/core/src/preflight/util.rs +++ b/core/src/preflight/util.rs @@ -136,11 +136,8 @@ pub async fn prepare_taiko_chain_input( RaikoError::Preflight("No L1 inclusion block hash for the requested block".to_owned()) })?; info!( - "L1 inclusion block number: {:?}, hash: {:?}. L1 state block number: {:?}, hash: {:?}", - l1_inclusion_block_number, - l1_inclusion_block_hash, + "L1 inclusion block number: {l1_inclusion_block_number:?}, hash: {l1_inclusion_block_hash:?}. L1 state block number: {:?}, hash: {l1_state_block_hash:?}", l1_state_header.number, - l1_state_block_hash ); // Fetch the tx data from either calldata or blobdata diff --git a/core/src/prover.rs b/core/src/prover.rs index 577c5318a..de89d859e 100644 --- a/core/src/prover.rs +++ b/core/src/prover.rs @@ -58,14 +58,28 @@ impl Prover for NativeProver { } Ok(Proof { + input: None, proof: None, quote: None, + uuid: None, + kzg_proof: None, }) } async fn cancel(_proof_key: ProofKey, _read: Box<&mut dyn IdStore>) -> ProverResult<()> { Ok(()) } + + async fn aggregate( + _input: raiko_lib::input::AggregationGuestInput, + _output: &raiko_lib::input::AggregationGuestOutput, + _config: &ProverConfig, + _store: Option<&mut dyn IdWrite>, + ) -> ProverResult { + Ok(Proof { + ..Default::default() + }) + } } #[ignore = "Only used to test serialized data"] diff --git a/host/src/cache.rs b/host/src/cache.rs index c4cd99815..52fe34a53 100644 --- a/host/src/cache.rs +++ b/host/src/cache.rs @@ -55,10 +55,7 @@ pub async fn validate_input( let cached_block_hash = cache_input.block.header.hash_slow(); let real_block_hash = block.header.hash.unwrap(); - debug!( - "cache_block_hash={:?}, real_block_hash={:?}", - cached_block_hash, real_block_hash - ); + debug!("cache_block_hash={cached_block_hash:?}, real_block_hash={real_block_hash:?}"); // double check if cache is valid if cached_block_hash == real_block_hash { diff --git a/host/src/interfaces.rs b/host/src/interfaces.rs index 728d7710a..330446ef4 100644 --- a/host/src/interfaces.rs +++ b/host/src/interfaces.rs @@ -121,12 +121,12 @@ impl From for TaskStatus { | HostError::JoinHandle(_) | HostError::InvalidAddress(_) | HostError::InvalidRequestConfig(_) => unreachable!(), - HostError::Conversion(_) - | HostError::Serde(_) - | HostError::Core(_) - | HostError::Anyhow(_) - | HostError::FeatureNotSupportedError(_) - | HostError::Io(_) => TaskStatus::UnspecifiedFailureReason, + HostError::Conversion(e) => TaskStatus::NonDbFailure(e), + HostError::Serde(e) => TaskStatus::NonDbFailure(e.to_string()), + HostError::Core(e) => TaskStatus::NonDbFailure(e.to_string()), + HostError::Anyhow(e) => TaskStatus::NonDbFailure(e.to_string()), + HostError::FeatureNotSupportedError(e) => TaskStatus::NonDbFailure(e.to_string()), + HostError::Io(e) => TaskStatus::NonDbFailure(e.to_string()), HostError::RPC(_) => TaskStatus::NetworkFailure, HostError::Guest(_) => TaskStatus::ProofFailure_Generic, HostError::TaskManager(_) => TaskStatus::SqlDbCorruption, @@ -142,12 +142,12 @@ impl From<&HostError> for TaskStatus { | HostError::JoinHandle(_) | HostError::InvalidAddress(_) | HostError::InvalidRequestConfig(_) => unreachable!(), - HostError::Conversion(_) - | HostError::Serde(_) - | HostError::Core(_) - | HostError::Anyhow(_) - | HostError::FeatureNotSupportedError(_) - | HostError::Io(_) => TaskStatus::UnspecifiedFailureReason, + HostError::Conversion(e) => TaskStatus::NonDbFailure(e.to_owned()), + HostError::Serde(e) => TaskStatus::NonDbFailure(e.to_string()), + HostError::Core(e) => TaskStatus::NonDbFailure(e.to_string()), + HostError::Anyhow(e) => TaskStatus::NonDbFailure(e.to_string()), + HostError::FeatureNotSupportedError(e) => TaskStatus::NonDbFailure(e.to_string()), + HostError::Io(e) => TaskStatus::NonDbFailure(e.to_string()), HostError::RPC(_) => TaskStatus::NetworkFailure, HostError::Guest(_) => TaskStatus::ProofFailure_Generic, HostError::TaskManager(_) => TaskStatus::SqlDbCorruption, diff --git a/host/src/lib.rs b/host/src/lib.rs index a4df64dc9..6927314b2 100644 --- a/host/src/lib.rs +++ b/host/src/lib.rs @@ -4,7 +4,7 @@ use anyhow::Context; use cap::Cap; use clap::Parser; use raiko_core::{ - interfaces::{ProofRequest, ProofRequestOpt}, + interfaces::{AggregationOnlyRequest, ProofRequest, ProofRequestOpt}, merge, }; use raiko_lib::consts::SupportedChainSpecs; @@ -152,6 +152,8 @@ pub struct ProverState { pub enum Message { Cancel(TaskDescriptor), Task(ProofRequest), + CancelAggregate(AggregationOnlyRequest), + Aggregate(AggregationOnlyRequest), } impl From<&ProofRequest> for Message { @@ -166,6 +168,12 @@ impl From<&TaskDescriptor> for Message { } } +impl From for Message { + fn from(value: AggregationOnlyRequest) -> Self { + Self::Aggregate(value) + } +} + impl ProverState { pub fn init() -> HostResult { // Read the command line arguments; diff --git a/host/src/proof.rs b/host/src/proof.rs index 31a56e72a..215a5b4f7 100644 --- a/host/src/proof.rs +++ b/host/src/proof.rs @@ -1,16 +1,19 @@ -use std::{collections::HashMap, sync::Arc}; +use std::{collections::HashMap, str::FromStr, sync::Arc}; +use anyhow::anyhow; use raiko_core::{ - interfaces::{ProofRequest, RaikoError}, + interfaces::{AggregationOnlyRequest, ProofRequest, ProofType, RaikoError}, provider::{get_task_data, rpc::RpcBlockDataProvider}, Raiko, }; use raiko_lib::{ consts::SupportedChainSpecs, + input::{AggregationGuestInput, AggregationGuestOutput}, prover::{IdWrite, Proof}, Measurement, }; use raiko_tasks::{get_task_manager, TaskDescriptor, TaskManager, TaskManagerWrapper, TaskStatus}; +use reth_primitives::B256; use tokio::{ select, sync::{mpsc::Receiver, Mutex, OwnedSemaphorePermit, Semaphore}, @@ -33,6 +36,7 @@ pub struct ProofActor { opts: Opts, chain_specs: SupportedChainSpecs, tasks: Arc>>, + aggregate_tasks: Arc>>, receiver: Receiver, } @@ -41,9 +45,14 @@ impl ProofActor { let tasks = Arc::new(Mutex::new( HashMap::::new(), )); + let aggregate_tasks = Arc::new(Mutex::new(HashMap::< + AggregationOnlyRequest, + CancellationToken, + >::new())); Self { tasks, + aggregate_tasks, opts, chain_specs, receiver, @@ -125,6 +134,74 @@ impl ProofActor { }); } + pub async fn cancel_aggregation_task( + &mut self, + request: AggregationOnlyRequest, + ) -> HostResult<()> { + let tasks_map = self.aggregate_tasks.lock().await; + let Some(task) = tasks_map.get(&request) else { + warn!("No task with those keys to cancel"); + return Ok(()); + }; + + // TODO:(petar) implement cancel_proof_aggregation + // let mut manager = get_task_manager(&self.opts.clone().into()); + // let proof_type = ProofType::from_str( + // request + // .proof_type + // .as_ref() + // .ok_or_else(|| anyhow!("No proof type"))?, + // )?; + // proof_type + // .cancel_proof_aggregation(request, Box::new(&mut manager)) + // .await + // .or_else(|e| { + // if e.to_string().contains("No data for query") { + // warn!("Task already cancelled or not yet started!"); + // Ok(()) + // } else { + // Err::<(), HostError>(e.into()) + // } + // })?; + task.cancel(); + Ok(()) + } + + pub async fn run_aggregate( + &mut self, + request: AggregationOnlyRequest, + _permit: OwnedSemaphorePermit, + ) { + let cancel_token = CancellationToken::new(); + + let mut tasks = self.aggregate_tasks.lock().await; + tasks.insert(request.clone(), cancel_token.clone()); + + let request_clone = request.clone(); + let tasks = self.aggregate_tasks.clone(); + let opts = self.opts.clone(); + + tokio::spawn(async move { + select! { + _ = cancel_token.cancelled() => { + info!("Task cancelled"); + } + result = Self::handle_aggregate(request_clone, &opts) => { + match result { + Ok(()) => { + info!("Host handling message"); + } + Err(error) => { + error!("Worker failed due to: {error:?}"); + } + }; + } + } + let mut tasks = tasks.lock().await; + tasks.remove(&request); + }); + } + pub async fn run(&mut self) { let semaphore = Arc::new(Semaphore::new(self.opts.concurrency_limit)); @@ -142,6 +219,18 @@ impl ProofActor { .expect("Couldn't acquire permit"); self.run_task(proof_request, permit).await; } + Message::CancelAggregate(request) => { + if let Err(error) = self.cancel_aggregation_task(request).await { + error!("Failed to cancel task: {error}") + } + } + Message::Aggregate(request) => { + let permit = Arc::clone(&semaphore) + .acquire_owned() + .await + .expect("Couldn't acquire permit"); + self.run_aggregate(request, permit).await; + } } } } @@ -158,7 +247,7 @@ impl ProofActor { if let Some(latest_status) = status.iter().last() { if !matches!(latest_status.0, TaskStatus::Registered) { - return Ok(latest_status.0); + return Ok(latest_status.0.clone()); } } @@ -176,11 +265,58 @@ impl ProofActor { }; manager - .update_task_progress(key, status, proof.as_deref()) + .update_task_progress(key, status.clone(), proof.as_deref()) .await .map_err(HostError::from)?; Ok(status) } + + pub async fn handle_aggregate(request: AggregationOnlyRequest, opts: &Opts) -> HostResult<()> { + let mut manager = get_task_manager(&opts.clone().into()); + + let status = manager + .get_aggregation_task_proving_status(&request) + .await?; + + if let Some(latest_status) = status.iter().last() { + if !matches!(latest_status.0, TaskStatus::Registered) { + return Ok(()); + } + } + + manager + .update_aggregation_task_progress(&request, TaskStatus::WorkInProgress, None) + .await?; + let proof_type = ProofType::from_str( + request + .proof_type + .as_ref() + .ok_or_else(|| anyhow!("No proof type"))?, + )?; + let input = AggregationGuestInput { + proofs: request.clone().proofs, + }; + let output = AggregationGuestOutput { hash: B256::ZERO }; + let config = serde_json::to_value(request.clone().prover_args)?; + let mut manager = get_task_manager(&opts.clone().into()); + + let (status, proof) = match proof_type + .aggregate_proofs(input, &output, &config, Some(&mut manager)) + .await + { + Err(error) => { + error!("{error}"); + (HostError::from(error).into(), None) + } + Ok(proof) => (TaskStatus::Success, Some(serde_json::to_vec(&proof)?)), + }; + + manager + .update_aggregation_task_progress(&request, status, proof.as_deref()) + .await?; + + Ok(()) + } } pub async fn handle_proof( diff --git a/host/src/server/api/mod.rs b/host/src/server/api/mod.rs index 4aa8e0981..45be92f15 100644 --- a/host/src/server/api/mod.rs +++ b/host/src/server/api/mod.rs @@ -18,6 +18,7 @@ use crate::ProverState; pub mod v1; pub mod v2; +pub mod v3; pub fn create_router(concurrency_limit: usize, jwt_secret: Option<&str>) -> Router { let cors = CorsLayer::new() @@ -37,11 +38,13 @@ pub fn create_router(concurrency_limit: usize, jwt_secret: Option<&str>) -> Rout let v1_api = v1::create_router(concurrency_limit); let v2_api = v2::create_router(); + let v3_api = v3::create_router(); let router = Router::new() .nest("/v1", v1_api) - .nest("/v2", v2_api.clone()) - .merge(v2_api) + .nest("/v2", v2_api) + .nest("/v3", v3_api.clone()) + .merge(v3_api) .layer(middleware) .layer(middleware::from_fn(check_max_body_size)) .layer(trace) @@ -58,7 +61,7 @@ pub fn create_router(concurrency_limit: usize, jwt_secret: Option<&str>) -> Rout } pub fn create_docs() -> utoipa::openapi::OpenApi { - v2::create_docs() + v3::create_docs() } async fn check_max_body_size(req: Request, next: Next) -> Response { diff --git a/host/src/server/api/v2/mod.rs b/host/src/server/api/v2/mod.rs index 6985369bc..f4fc046a7 100644 --- a/host/src/server/api/v2/mod.rs +++ b/host/src/server/api/v2/mod.rs @@ -11,7 +11,7 @@ use crate::{ ProverState, }; -mod proof; +pub mod proof; #[derive(OpenApi)] #[openapi( @@ -157,6 +157,8 @@ pub fn create_router() -> Router { // Only add the concurrency limit to the proof route. We want to still be able to call // healthchecks and metrics to have insight into the system. .nest("/proof", proof::create_router()) + // TODO: Separate task or try to get it into /proof somehow? Probably separate + .nest("/aggregate", proof::create_router()) .nest("/health", v1::health::create_router()) .nest("/metrics", v1::metrics::create_router()) .merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", docs.clone())) diff --git a/host/src/server/api/v2/proof/mod.rs b/host/src/server/api/v2/proof/mod.rs index ce089375c..d57335cdf 100644 --- a/host/src/server/api/v2/proof/mod.rs +++ b/host/src/server/api/v2/proof/mod.rs @@ -11,10 +11,10 @@ use crate::{ Message, ProverState, }; -mod cancel; -mod list; -mod prune; -mod report; +pub mod cancel; +pub mod list; +pub mod prune; +pub mod report; #[utoipa::path(post, path = "/proof", tag = "Proving", @@ -98,7 +98,7 @@ async fn proof_handler( Ok(proof.into()) } // For all other statuses just return the status. - status => Ok((*status).into()), + status => Ok(status.clone().into()), } } diff --git a/host/src/server/api/v3/mod.rs b/host/src/server/api/v3/mod.rs new file mode 100644 index 000000000..faf46b61e --- /dev/null +++ b/host/src/server/api/v3/mod.rs @@ -0,0 +1,172 @@ +use axum::{response::IntoResponse, Json, Router}; +use raiko_lib::prover::Proof; +use raiko_tasks::TaskStatus; +use serde::{Deserialize, Serialize}; +use utoipa::{OpenApi, ToSchema}; +use utoipa_scalar::{Scalar, Servable}; +use utoipa_swagger_ui::SwaggerUi; + +use crate::{ + server::api::v1::{self, GuestOutputDoc}, + ProverState, +}; + +mod proof; + +#[derive(OpenApi)] +#[openapi( + info( + title = "Raiko Proverd Server API", + version = "3.0", + description = "Raiko Proverd Server API", + contact( + name = "API Support", + url = "https://community.taiko.xyz", + email = "info@taiko.xyz", + ), + license( + name = "MIT", + url = "https://github.com/taikoxyz/raiko/blob/main/LICENSE" + ), + ), + components( + schemas( + raiko_core::interfaces::ProofRequestOpt, + raiko_core::interfaces::ProverSpecificOpts, + crate::interfaces::HostError, + GuestOutputDoc, + ProofResponse, + TaskStatus, + CancelStatus, + PruneStatus, + Proof, + Status, + ) + ), + tags( + (name = "Proving", description = "Routes that handle proving requests"), + (name = "Health", description = "Routes that report the server health status"), + (name = "Metrics", description = "Routes that give detailed insight into the server") + ) +)] +/// The root API struct which is generated from the `OpenApi` derive macro. +pub struct Docs; + +#[derive(Debug, Deserialize, Serialize, ToSchema)] +#[serde(untagged)] +pub enum ProofResponse { + Status { + /// The status of the submitted task. + status: TaskStatus, + }, + Proof { + /// The proof. + proof: Proof, + }, +} + +#[derive(Debug, Deserialize, Serialize, ToSchema)] +#[serde(tag = "status", rename_all = "lowercase")] +pub enum Status { + Ok { data: ProofResponse }, + Error { error: String, message: String }, +} + +impl From> for Status { + fn from(proof: Vec) -> Self { + Self::Ok { + data: ProofResponse::Proof { + proof: serde_json::from_slice(&proof).unwrap_or_default(), + }, + } + } +} + +impl From for Status { + fn from(proof: Proof) -> Self { + Self::Ok { + data: ProofResponse::Proof { proof }, + } + } +} + +impl From for Status { + fn from(status: TaskStatus) -> Self { + match status { + TaskStatus::Success | TaskStatus::WorkInProgress | TaskStatus::Registered => Self::Ok { + data: ProofResponse::Status { status }, + }, + _ => Self::Error { + error: "task_failed".to_string(), + message: format!("Task failed with status: {status:?}"), + }, + } + } +} + +impl IntoResponse for Status { + fn into_response(self) -> axum::response::Response { + Json(serde_json::to_value(self).unwrap()).into_response() + } +} + +#[derive(Debug, Deserialize, Serialize, ToSchema)] +#[serde(tag = "status", rename_all = "lowercase")] +/// Status of cancellation request. +/// Can be `ok` for a successful cancellation or `error` with message and error type for errors. +pub enum CancelStatus { + /// Cancellation was successful. + Ok, + /// Cancellation failed. + Error { error: String, message: String }, +} + +impl IntoResponse for CancelStatus { + fn into_response(self) -> axum::response::Response { + Json(serde_json::to_value(self).unwrap()).into_response() + } +} + +#[derive(Debug, Serialize, ToSchema, Deserialize)] +#[serde(tag = "status", rename_all = "lowercase")] +/// Status of prune request. +/// Can be `ok` for a successful prune or `error` with message and error type for errors. +pub enum PruneStatus { + /// Prune was successful. + Ok, + /// Prune failed. + Error { error: String, message: String }, +} + +impl IntoResponse for PruneStatus { + fn into_response(self) -> axum::response::Response { + Json(serde_json::to_value(self).unwrap()).into_response() + } +} + +#[must_use] +pub fn create_docs() -> utoipa::openapi::OpenApi { + [ + v1::health::create_docs(), + v1::metrics::create_docs(), + proof::create_docs(), + ] + .into_iter() + .fold(Docs::openapi(), |mut doc, sub_doc| { + doc.merge(sub_doc); + doc + }) +} + +pub fn create_router() -> Router { + let docs = create_docs(); + + Router::new() + // Only add the concurrency limit to the proof route. We want to still be able to call + // healthchecks and metrics to have insight into the system. + .nest("/proof", proof::create_router()) + .nest("/health", v1::health::create_router()) + .nest("/metrics", v1::metrics::create_router()) + .merge(SwaggerUi::new("/swagger-ui").url("/api-docs/openapi.json", docs.clone())) + .merge(Scalar::with_url("/scalar", docs)) +} diff --git a/host/src/server/api/v3/proof/aggregate.rs b/host/src/server/api/v3/proof/aggregate.rs new file mode 100644 index 000000000..3bbffa00f --- /dev/null +++ b/host/src/server/api/v3/proof/aggregate.rs @@ -0,0 +1,114 @@ +use std::str::FromStr; + +use axum::{debug_handler, extract::State, routing::post, Json, Router}; +use raiko_core::interfaces::{AggregationOnlyRequest, ProofType}; +use raiko_tasks::{TaskManager, TaskStatus}; +use utoipa::OpenApi; + +use crate::{ + interfaces::HostResult, + metrics::{inc_current_req, inc_guest_req_count, inc_host_req_count}, + server::api::v3::Status, + Message, ProverState, +}; + +#[utoipa::path(post, path = "/proof/aggregate", + tag = "Proving", + request_body = AggregationRequest, + responses ( + (status = 200, description = "Successfully submitted proof aggregation task, queried aggregation tasks in progress or retrieved aggregated proof.", body = Status) + ) +)] +#[debug_handler(state = ProverState)] +/// Submit a proof aggregation task with requested config, get task status or get proof value. +/// +/// Accepts a proof request and creates a proving task with the specified guest prover. +/// The guest provers currently available are: +/// - native - constructs a block and checks for equality +/// - sgx - uses the sgx environment to construct a block and produce proof of execution +/// - sp1 - uses the sp1 prover +/// - risc0 - uses the risc0 prover +async fn aggregation_handler( + State(prover_state): State, + Json(mut aggregation_request): Json, +) -> HostResult { + inc_current_req(); + // Override the existing proof request config from the config file and command line + // options with the request from the client. + aggregation_request.merge(&prover_state.request_config())?; + + let proof_type = ProofType::from_str( + aggregation_request + .proof_type + .as_deref() + .unwrap_or_default(), + )?; + inc_host_req_count(0); + inc_guest_req_count(&proof_type, 0); + + if aggregation_request.proofs.is_empty() { + return Err(anyhow::anyhow!("No proofs provided").into()); + } + + let mut manager = prover_state.task_manager(); + + let status = manager + .get_aggregation_task_proving_status(&aggregation_request) + .await?; + + let Some((latest_status, ..)) = status.last() else { + // If there are no tasks with provided config, create a new one. + manager + .enqueue_aggregation_task(&aggregation_request) + .await?; + + prover_state + .task_channel + .try_send(Message::from(aggregation_request.clone()))?; + return Ok(Status::from(TaskStatus::Registered)); + }; + + match latest_status { + // If task has been cancelled add it to the queue again + TaskStatus::Cancelled + | TaskStatus::Cancelled_Aborted + | TaskStatus::Cancelled_NeverStarted + | TaskStatus::CancellationInProgress => { + manager + .update_aggregation_task_progress( + &aggregation_request, + TaskStatus::Registered, + None, + ) + .await?; + + prover_state + .task_channel + .try_send(Message::from(aggregation_request))?; + + Ok(Status::from(TaskStatus::Registered)) + } + // If the task has succeeded, return the proof. + TaskStatus::Success => { + let proof = manager + .get_aggregation_task_proof(&aggregation_request) + .await?; + + Ok(proof.into()) + } + // For all other statuses just return the status. + status => Ok(status.clone().into()), + } +} + +#[derive(OpenApi)] +#[openapi(paths(aggregation_handler))] +struct Docs; + +pub fn create_docs() -> utoipa::openapi::OpenApi { + Docs::openapi() +} + +pub fn create_router() -> Router { + Router::new().route("/", post(aggregation_handler)) +} diff --git a/host/src/server/api/v3/proof/cancel.rs b/host/src/server/api/v3/proof/cancel.rs new file mode 100644 index 000000000..6e721c716 --- /dev/null +++ b/host/src/server/api/v3/proof/cancel.rs @@ -0,0 +1,76 @@ +use axum::{debug_handler, extract::State, routing::post, Json, Router}; +use raiko_core::{ + interfaces::{AggregationRequest, ProofRequest, ProofRequestOpt}, + provider::get_task_data, +}; +use raiko_tasks::{TaskDescriptor, TaskManager, TaskStatus}; +use utoipa::OpenApi; + +use crate::{interfaces::HostResult, server::api::v2::CancelStatus, Message, ProverState}; + +#[utoipa::path(post, path = "/proof/cancel", + tag = "Proving", + request_body = ProofRequestOpt, + responses ( + (status = 200, description = "Successfully cancelled proof task", body = CancelStatus) + ) +)] +#[debug_handler(state = ProverState)] +/// Cancel a proof task with requested config. +/// +/// Accepts a proof request and cancels a proving task with the specified guest prover. +/// The guest provers currently available are: +/// - native - constructs a block and checks for equality +/// - sgx - uses the sgx environment to construct a block and produce proof of execution +/// - sp1 - uses the sp1 prover +/// - risc0 - uses the risc0 prover +async fn cancel_handler( + State(prover_state): State, + Json(mut aggregation_request): Json, +) -> HostResult { + // Override the existing proof request config from the config file and command line + // options with the request from the client. + aggregation_request.merge(&prover_state.request_config())?; + + let proof_request_opts: Vec = aggregation_request.into(); + + for opt in proof_request_opts { + let proof_request = ProofRequest::try_from(opt)?; + + let (chain_id, block_hash) = get_task_data( + &proof_request.network, + proof_request.block_number, + &prover_state.chain_specs, + ) + .await?; + + let key = TaskDescriptor::from(( + chain_id, + block_hash, + proof_request.proof_type, + proof_request.prover.clone().to_string(), + )); + + prover_state.task_channel.try_send(Message::from(&key))?; + + let mut manager = prover_state.task_manager(); + + manager + .update_task_progress(key, TaskStatus::Cancelled, None) + .await?; + } + + Ok(CancelStatus::Ok) +} + +#[derive(OpenApi)] +#[openapi(paths(cancel_handler))] +struct Docs; + +pub fn create_docs() -> utoipa::openapi::OpenApi { + Docs::openapi() +} + +pub fn create_router() -> Router { + Router::new().route("/", post(cancel_handler)) +} diff --git a/host/src/server/api/v3/proof/mod.rs b/host/src/server/api/v3/proof/mod.rs new file mode 100644 index 000000000..2e739cc58 --- /dev/null +++ b/host/src/server/api/v3/proof/mod.rs @@ -0,0 +1,219 @@ +use axum::{debug_handler, extract::State, routing::post, Json, Router}; +use raiko_core::{ + interfaces::{AggregationOnlyRequest, AggregationRequest, ProofRequest, ProofRequestOpt}, + provider::get_task_data, +}; +use raiko_tasks::{TaskDescriptor, TaskManager, TaskStatus}; +use utoipa::OpenApi; + +use crate::{ + interfaces::HostResult, + metrics::{inc_current_req, inc_guest_req_count, inc_host_req_count}, + server::api::{v2, v3::Status}, + Message, ProverState, +}; +use tracing::{debug, info}; + +mod aggregate; +mod cancel; + +#[utoipa::path(post, path = "/proof", + tag = "Proving", + request_body = AggregationRequest, + responses ( + (status = 200, description = "Successfully submitted proof task, queried tasks in progress or retrieved proof.", body = Status) + ) +)] +#[debug_handler(state = ProverState)] +/// Submit a proof aggregation task with requested config, get task status or get proof value. +/// +/// Accepts a proof request and creates a proving task with the specified guest prover. +/// The guest provers currently available are: +/// - native - constructs a block and checks for equality +/// - sgx - uses the sgx environment to construct a block and produce proof of execution +/// - sp1 - uses the sp1 prover +/// - risc0 - uses the risc0 prover +async fn proof_handler( + State(prover_state): State, + Json(mut aggregation_request): Json, +) -> HostResult { + inc_current_req(); + // Override the existing proof request config from the config file and command line + // options with the request from the client. + aggregation_request.merge(&prover_state.request_config())?; + + let mut tasks = Vec::with_capacity(aggregation_request.block_numbers.len()); + + let proof_request_opts: Vec = aggregation_request.clone().into(); + + if proof_request_opts.is_empty() { + return Err(anyhow::anyhow!("No blocks for proving provided").into()); + } + + // Construct the actual proof request from the available configs. + for proof_request_opt in proof_request_opts { + let proof_request = ProofRequest::try_from(proof_request_opt)?; + + inc_host_req_count(proof_request.block_number); + inc_guest_req_count(&proof_request.proof_type, proof_request.block_number); + + let (chain_id, blockhash) = get_task_data( + &proof_request.network, + proof_request.block_number, + &prover_state.chain_specs, + ) + .await?; + + let key = TaskDescriptor::from(( + chain_id, + blockhash, + proof_request.proof_type, + proof_request.prover.to_string(), + )); + + tasks.push((key, proof_request)); + } + + let mut manager = prover_state.task_manager(); + + let mut is_registered = false; + let mut is_success = true; + let mut statuses = Vec::with_capacity(tasks.len()); + + for (key, req) in tasks.iter() { + let status = manager.get_task_proving_status(key).await?; + + let Some((latest_status, ..)) = status.last() else { + // If there are no tasks with provided config, create a new one. + manager.enqueue_task(key).await?; + + prover_state.task_channel.try_send(Message::from(req))?; + is_registered = true; + continue; + }; + + match latest_status { + // If task has been cancelled add it to the queue again + TaskStatus::Cancelled + | TaskStatus::Cancelled_Aborted + | TaskStatus::Cancelled_NeverStarted + | TaskStatus::CancellationInProgress => { + manager + .update_task_progress(key.clone(), TaskStatus::Registered, None) + .await?; + + prover_state.task_channel.try_send(Message::from(req))?; + + is_registered = true; + is_success = false; + } + // If the task has succeeded, return the proof. + TaskStatus::Success => {} + // For all other statuses just return the status. + status => { + statuses.push(status.clone()); + is_registered = false; + is_success = false; + } + } + } + + if is_registered { + Ok(TaskStatus::Registered.into()) + } else if is_success { + info!("All tasks are successful, aggregating proofs"); + let mut proofs = Vec::with_capacity(tasks.len()); + for (task, req) in tasks { + let raw_proof = manager.get_task_proof(&task).await?; + let proof = serde_json::from_slice(&raw_proof)?; + debug!("req: {req:?} gets proof: {proof:?}"); + proofs.push(proof); + } + + let aggregation_request = AggregationOnlyRequest { + proofs, + proof_type: aggregation_request.proof_type, + prover_args: aggregation_request.prover_args, + }; + + let status = manager + .get_aggregation_task_proving_status(&aggregation_request) + .await?; + + let Some((latest_status, ..)) = status.last() else { + // If there are no tasks with provided config, create a new one. + manager + .enqueue_aggregation_task(&aggregation_request) + .await?; + + prover_state + .task_channel + .try_send(Message::from(aggregation_request.clone()))?; + return Ok(Status::from(TaskStatus::Registered)); + }; + + match latest_status { + // If task has been cancelled add it to the queue again + TaskStatus::Cancelled + | TaskStatus::Cancelled_Aborted + | TaskStatus::Cancelled_NeverStarted + | TaskStatus::CancellationInProgress => { + manager + .update_aggregation_task_progress( + &aggregation_request, + TaskStatus::Registered, + None, + ) + .await?; + + prover_state + .task_channel + .try_send(Message::from(aggregation_request))?; + + Ok(Status::from(TaskStatus::Registered)) + } + // If the task has succeeded, return the proof. + TaskStatus::Success => { + let proof = manager + .get_aggregation_task_proof(&aggregation_request) + .await?; + + Ok(proof.into()) + } + // For all other statuses just return the status. + status => Ok(status.clone().into()), + } + } else { + let status = statuses.into_iter().collect::(); + Ok(status.into()) + } +} + +#[derive(OpenApi)] +#[openapi(paths(proof_handler))] +struct Docs; + +pub fn create_docs() -> utoipa::openapi::OpenApi { + [ + cancel::create_docs(), + aggregate::create_docs(), + v2::proof::report::create_docs(), + v2::proof::list::create_docs(), + v2::proof::prune::create_docs(), + ] + .into_iter() + .fold(Docs::openapi(), |mut docs, curr| { + docs.merge(curr); + docs + }) +} + +pub fn create_router() -> Router { + Router::new() + .route("/", post(proof_handler)) + .nest("/cancel", cancel::create_router()) + .nest("/aggregate", aggregate::create_router()) + .nest("/report", v2::proof::report::create_router()) + .nest("/list", v2::proof::list::create_router()) + .nest("/prune", v2::proof::prune::create_router()) +} diff --git a/lib/src/builder.rs b/lib/src/builder.rs index 3269f98bb..b60be8f00 100644 --- a/lib/src/builder.rs +++ b/lib/src/builder.rs @@ -160,7 +160,7 @@ impl + DatabaseCommit + OptimisticDatabase> } = executor .execute((&block, total_difficulty).into()) .map_err(|e| { - error!("Error executing block: {:?}", e); + error!("Error executing block: {e:?}"); e })?; // Filter out the valid transactions so that the header checks only take these into account @@ -294,8 +294,8 @@ impl RethBlockBuilder { state_trie.insert_rlp(&state_trie_index, state_account)?; } - debug!("Accounts touched {:?}", account_touched); - debug!("Storages touched {:?}", storage_touched); + debug!("Accounts touched {account_touched:?}"); + debug!("Storages touched {storage_touched:?}"); Ok(state_trie.hash()) } diff --git a/lib/src/input.rs b/lib/src/input.rs index 1b0688b16..bb9c9ed9b 100644 --- a/lib/src/input.rs +++ b/lib/src/input.rs @@ -12,7 +12,9 @@ use serde_with::serde_as; #[cfg(not(feature = "std"))] use crate::no_std::*; -use crate::{consts::ChainSpec, primitives::mpt::MptNode, utils::zlib_compress_data}; +use crate::{ + consts::ChainSpec, primitives::mpt::MptNode, prover::Proof, utils::zlib_compress_data, +}; /// Represents the state of an account's storage. /// The storage trie together with the used storage slots allow us to reconstruct all the @@ -41,6 +43,42 @@ pub struct GuestInput { pub taiko: TaikoGuestInput, } +/// External aggregation input. +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +pub struct AggregationGuestInput { + /// All block proofs to prove + pub proofs: Vec, +} + +/// The raw proof data necessary to verify a proof +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +pub struct RawProof { + /// The actual proof + pub proof: Vec, + /// The resulting hash + pub input: B256, +} + +/// External aggregation input. +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +pub struct RawAggregationGuestInput { + /// All block proofs to prove + pub proofs: Vec, +} + +/// External aggregation input. +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +pub struct AggregationGuestOutput { + /// The resulting hash + pub hash: B256, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ZkAggregationGuestInput { + pub image_id: [u32; 8], + pub block_inputs: Vec, +} + impl From<(Block, Header, ChainSpec, TaikoGuestInput)> for GuestInput { fn from( (block, parent_header, chain_spec, taiko): (Block, Header, ChainSpec, TaikoGuestInput), diff --git a/lib/src/protocol_instance.rs b/lib/src/protocol_instance.rs index 5036173f7..3f6271ef9 100644 --- a/lib/src/protocol_instance.rs +++ b/lib/src/protocol_instance.rs @@ -18,7 +18,7 @@ use crate::{ }, CycleTracker, }; -use log::info; +use log::{debug, info}; use reth_evm_ethereum::taiko::ANCHOR_GAS_LIMIT; #[derive(Debug, Clone)] @@ -275,6 +275,18 @@ impl ProtocolInstance { pub fn instance_hash(&self) -> B256 { // packages/protocol/contracts/verifiers/libs/LibPublicInput.sol // "VERIFY_PROOF", _chainId, _verifierContract, _tran, _newInstance, _prover, _metaHash + debug!( + "calculate instance_hash from: + chain_id: {:?}, verifier: {:?}, transition: {:?}, sgx_instance: {:?}, + prover: {:?}, block_meta: {:?}, meta_hash: {:?}", + self.chain_id, + self.verifier_address, + self.transition.clone(), + self.sgx_instance, + self.prover, + self.block_metadata, + self.meta_hash(), + ); let data = ( "VERIFY_PROOF", self.chain_id, @@ -315,6 +327,36 @@ fn bytes_to_bytes32(input: &[u8]) -> [u8; 32] { bytes } +pub fn words_to_bytes_le(words: &[u32; 8]) -> [u8; 32] { + let mut bytes = [0u8; 32]; + for i in 0..8 { + let word_bytes = words[i].to_le_bytes(); + bytes[i * 4..(i + 1) * 4].copy_from_slice(&word_bytes); + } + bytes +} + +pub fn words_to_bytes_be(words: &[u32; 8]) -> [u8; 32] { + let mut bytes = [0u8; 32]; + for i in 0..8 { + let word_bytes = words[i].to_be_bytes(); + bytes[i * 4..(i + 1) * 4].copy_from_slice(&word_bytes); + } + bytes +} + +pub fn aggregation_output_combine(public_inputs: Vec) -> Vec { + let mut output = Vec::with_capacity(public_inputs.len() * 32); + for public_input in public_inputs.iter() { + output.extend_from_slice(&public_input.0); + } + output +} + +pub fn aggregation_output(program: B256, public_inputs: Vec) -> Vec { + aggregation_output_combine([vec![program], public_inputs].concat()) +} + #[cfg(test)] mod tests { use alloy_primitives::{address, b256}; diff --git a/lib/src/prover.rs b/lib/src/prover.rs index 948f57af4..08de0229a 100644 --- a/lib/src/prover.rs +++ b/lib/src/prover.rs @@ -2,7 +2,7 @@ use reth_primitives::{ChainId, B256}; use serde::{Deserialize, Serialize}; use utoipa::ToSchema; -use crate::input::{GuestInput, GuestOutput}; +use crate::input::{AggregationGuestInput, AggregationGuestOutput, GuestInput, GuestOutput}; #[derive(thiserror::Error, Debug)] pub enum ProverError { @@ -26,13 +26,19 @@ pub type ProverResult = core::result::Result; pub type ProverConfig = serde_json::Value; pub type ProofKey = (ChainId, B256, u8); -#[derive(Debug, Serialize, ToSchema, Deserialize, Default)] +#[derive(Clone, Debug, Serialize, ToSchema, Deserialize, Default, PartialEq, Eq, Hash)] /// The response body of a proof request. pub struct Proof { /// The proof either TEE or ZK. pub proof: Option, + /// The public input + pub input: Option, /// The TEE quote. pub quote: Option, + /// The assumption UUID. + pub uuid: Option, + /// The kzg proof. + pub kzg_proof: Option, } #[async_trait::async_trait] @@ -56,5 +62,12 @@ pub trait Prover { store: Option<&mut dyn IdWrite>, ) -> ProverResult; + async fn aggregate( + input: AggregationGuestInput, + output: &AggregationGuestOutput, + config: &ProverConfig, + store: Option<&mut dyn IdWrite>, + ) -> ProverResult; + async fn cancel(proof_key: ProofKey, read: Box<&mut dyn IdStore>) -> ProverResult<()>; } diff --git a/pipeline/src/builder.rs b/pipeline/src/builder.rs index 7282cd858..9972c80de 100644 --- a/pipeline/src/builder.rs +++ b/pipeline/src/builder.rs @@ -140,7 +140,7 @@ impl CommandBuilder { println!("Using {tool}: {out}"); Some(PathBuf::from(out)) } else { - println!("Command succeeded with unknown output: {:?}", stdout); + println!("Command succeeded with unknown output: {stdout:?}"); None } } else { diff --git a/pipeline/src/executor.rs b/pipeline/src/executor.rs index a46128a09..5055018e3 100644 --- a/pipeline/src/executor.rs +++ b/pipeline/src/executor.rs @@ -100,7 +100,11 @@ impl Executor { let elf = std::fs::read(&dest.join(&name.replace('_', "-")))?; let prover = CpuProver::new(); let key_pair = prover.setup(&elf); - println!("sp1 elf vk is: {}", key_pair.1.bytes32()); + println!("sp1 elf vk bn256 is: {}", key_pair.1.bytes32()); + println!( + "sp1 elf vk hash_bytes is: {}", + hex::encode(key_pair.1.hash_bytes()) + ); } Ok(()) diff --git a/provers/risc0/builder/src/main.rs b/provers/risc0/builder/src/main.rs index b0de9edb1..523824f40 100644 --- a/provers/risc0/builder/src/main.rs +++ b/provers/risc0/builder/src/main.rs @@ -5,7 +5,10 @@ use std::path::PathBuf; fn main() { let pipeline = Risc0Pipeline::new("provers/risc0/guest", "release"); - pipeline.bins(&["risc0-guest"], "provers/risc0/driver/src/methods"); + pipeline.bins( + &["risc0-guest", "risc0-aggregation"], + "provers/risc0/driver/src/methods", + ); #[cfg(feature = "test")] pipeline.tests(&["risc0-guest"], "provers/risc0/driver/src/methods"); #[cfg(feature = "bench")] diff --git a/provers/risc0/driver/Cargo.toml b/provers/risc0/driver/Cargo.toml index a1f5e11e7..3274acce2 100644 --- a/provers/risc0/driver/Cargo.toml +++ b/provers/risc0/driver/Cargo.toml @@ -63,9 +63,9 @@ enable = [ "serde_json", "hex", "reqwest", - "lazy_static" + "lazy_static", ] cuda = ["risc0-zkvm?/cuda"] metal = ["risc0-zkvm?/metal"] bench = [] -bonsai-auto-scaling = [] \ No newline at end of file +bonsai-auto-scaling = [] diff --git a/provers/risc0/driver/src/bonsai.rs b/provers/risc0/driver/src/bonsai.rs index 2129799d8..0c8d8565f 100644 --- a/provers/risc0/driver/src/bonsai.rs +++ b/provers/risc0/driver/src/bonsai.rs @@ -1,15 +1,16 @@ use crate::{ methods::risc0_guest::RISC0_GUEST_ID, - snarks::{stark2snark, verify_groth16_snark}, + snarks::{stark2snark, verify_groth16_from_snark_receipt}, Risc0Response, }; +use alloy_primitives::B256; use log::{debug, error, info, warn}; use raiko_lib::{ primitives::keccak::keccak, prover::{IdWrite, ProofKey, ProverError, ProverResult}, }; use risc0_zkvm::{ - compute_image_id, is_dev_mode, serde::to_vec, sha::Digest, Assumption, ExecutorEnv, + compute_image_id, is_dev_mode, serde::to_vec, sha::Digest, AssumptionReceipt, ExecutorEnv, ExecutorImpl, Receipt, }; use serde::{de::DeserializeOwned, Serialize}; @@ -106,10 +107,9 @@ pub async fn verify_bonsai_receipt( let client = bonsai_sdk::alpha_async::get_client_from_env(risc0_zkvm::VERSION).await?; let bonsai_err_log = session.logs(&client); return Err(BonsaiExecutionError::Fatal(format!( - "Workflow exited: {} - | err: {} | log: {:?}", + "Workflow exited: {} - | err: {} | log: {bonsai_err_log:?}", res.status, res.error_msg.unwrap_or_default(), - bonsai_err_log ))); } } @@ -120,7 +120,7 @@ pub async fn maybe_prove, elf: &[u8], expected_output: &O, - assumptions: (Vec, Vec), + assumptions: (Vec>, Vec), proof_key: ProofKey, id_store: &mut Option<&mut dyn IdWrite>, ) -> Option<(String, Receipt)> { @@ -283,20 +283,27 @@ pub async fn prove_bonsai( pub async fn bonsai_stark_to_snark( stark_uuid: String, stark_receipt: Receipt, + input: B256, ) -> ProverResult { let image_id = Digest::from(RISC0_GUEST_ID); - let (snark_uuid, snark_receipt) = stark2snark(image_id, stark_uuid, stark_receipt) - .await - .map_err(|err| format!("Failed to convert STARK to SNARK: {err:?}"))?; + let (snark_uuid, snark_receipt) = + stark2snark(image_id, stark_uuid.clone(), stark_receipt.clone()) + .await + .map_err(|err| format!("Failed to convert STARK to SNARK: {err:?}"))?; info!("Validating SNARK uuid: {snark_uuid}"); - let enc_proof = verify_groth16_snark(image_id, snark_receipt) + let enc_proof = verify_groth16_from_snark_receipt(image_id, snark_receipt) .await .map_err(|err| format!("Failed to verify SNARK: {err:?}"))?; let snark_proof = format!("0x{}", hex::encode(enc_proof)); - Ok(Risc0Response { proof: snark_proof }) + Ok(Risc0Response { + proof: snark_proof, + receipt: serde_json::to_string(&stark_receipt).unwrap(), + uuid: stark_uuid, + input, + }) } /// Prove the given ELF locally with the given input and assumptions. The segments are @@ -305,7 +312,7 @@ pub fn prove_locally( segment_limit_po2: u32, encoded_input: Vec, elf: &[u8], - assumptions: Vec, + assumptions: Vec>, profile: bool, ) -> ProverResult { debug!("Proving with segment_limit_po2 = {segment_limit_po2:?}"); diff --git a/provers/risc0/driver/src/lib.rs b/provers/risc0/driver/src/lib.rs index 177ba6742..6dd8a200c 100644 --- a/provers/risc0/driver/src/lib.rs +++ b/provers/risc0/driver/src/lib.rs @@ -2,15 +2,24 @@ #[cfg(feature = "bonsai-auto-scaling")] use crate::bonsai::auto_scaling::shutdown_bonsai; -use crate::methods::risc0_guest::RISC0_GUEST_ELF; +use crate::{ + methods::risc0_aggregation::RISC0_AGGREGATION_ELF, + methods::risc0_guest::{RISC0_GUEST_ELF, RISC0_GUEST_ID}, +}; use alloy_primitives::{hex::ToHexExt, B256}; -pub use bonsai::*; -use log::warn; +use bonsai::{cancel_proof, maybe_prove}; +use log::{info, warn}; use raiko_lib::{ - input::{GuestInput, GuestOutput}, + input::{ + AggregationGuestInput, AggregationGuestOutput, GuestInput, GuestOutput, + ZkAggregationGuestInput, + }, prover::{IdStore, IdWrite, Proof, ProofKey, Prover, ProverConfig, ProverError, ProverResult}, }; -use risc0_zkvm::serde::to_vec; +use risc0_zkvm::{ + compute_image_id, default_prover, serde::to_vec, sha::Digestible, ExecutorEnv, ProverOpts, + Receipt, +}; use serde::{Deserialize, Serialize}; use serde_with::serde_as; use std::fmt::Debug; @@ -32,13 +41,19 @@ pub struct Risc0Param { #[derive(Clone, Serialize, Deserialize)] pub struct Risc0Response { pub proof: String, + pub receipt: String, + pub uuid: String, + pub input: B256, } impl From for Proof { fn from(value: Risc0Response) -> Self { Self { proof: Some(value.proof), - quote: None, + quote: Some(value.receipt), + input: Some(value.input), + uuid: Some(value.uuid), + kzg_proof: None, } } } @@ -70,25 +85,30 @@ impl Prover for Risc0Prover { encoded_input, RISC0_GUEST_ELF, &output.hash, - Default::default(), + (Vec::::new(), Vec::new()), proof_key, &mut id_store, ) .await; + let receipt = result.clone().unwrap().1.clone(); + let uuid = result.clone().unwrap().0; + let proof_gen_result = if result.is_some() { if config.snark && config.bonsai { let (stark_uuid, stark_receipt) = result.clone().unwrap(); - bonsai::bonsai_stark_to_snark(stark_uuid, stark_receipt) + bonsai::bonsai_stark_to_snark(stark_uuid, stark_receipt, output.hash) .await .map(|r0_response| r0_response.into()) .map_err(|e| ProverError::GuestError(e.to_string())) } else { warn!("proof is not in snark mode, please check."); let (_, stark_receipt) = result.clone().unwrap(); - Ok(Risc0Response { proof: stark_receipt.journal.encode_hex_with_prefix(), + receipt: serde_json::to_string(&receipt).unwrap(), + uuid, + input: output.hash, } .into()) } @@ -109,6 +129,83 @@ impl Prover for Risc0Prover { proof_gen_result } + async fn aggregate( + input: AggregationGuestInput, + _output: &AggregationGuestOutput, + config: &ProverConfig, + _id_store: Option<&mut dyn IdWrite>, + ) -> ProverResult { + let config = Risc0Param::deserialize(config.get("risc0").unwrap()).unwrap(); + assert!( + config.snark && config.bonsai, + "Aggregation must be in bonsai snark mode" + ); + + // Extract the block proof receipts + let assumptions: Vec = input + .proofs + .iter() + .map(|proof| { + let receipt: Receipt = serde_json::from_str(&proof.quote.clone().unwrap()) + .expect("Failed to deserialize"); + receipt + }) + .collect::>(); + let block_inputs: Vec = input + .proofs + .iter() + .map(|proof| proof.input.unwrap()) + .collect::>(); + let input = ZkAggregationGuestInput { + image_id: RISC0_GUEST_ID, + block_inputs, + }; + info!("Start aggregate proofs"); + // add_assumption makes the receipt to be verified available to the prover. + let env = { + let mut env = ExecutorEnv::builder(); + for assumption in assumptions { + env.add_assumption(assumption); + } + env.write(&input).unwrap().build().unwrap() + }; + + let opts = ProverOpts::groth16(); + let receipt = default_prover() + .prove_with_opts(env, RISC0_AGGREGATION_ELF, &opts) + .unwrap() + .receipt; + + info!( + "Generate aggregatino receipt journal: {:?}", + receipt.journal + ); + let aggregation_image_id = compute_image_id(RISC0_AGGREGATION_ELF).unwrap(); + let enc_proof = + snarks::verify_groth16_snark_from_receipt(aggregation_image_id, receipt.clone()) + .await + .map_err(|err| format!("Failed to verify SNARK: {err:?}"))?; + let snark_proof = format!("0x{}", hex::encode(enc_proof)); + + let proof_gen_result = Ok(Risc0Response { + proof: snark_proof, + receipt: serde_json::to_string(&receipt).unwrap(), + uuid: "".to_owned(), + input: B256::from_slice(&receipt.journal.digest().as_bytes()), + } + .into()); + + #[cfg(feature = "bonsai-auto-scaling")] + if config.bonsai { + // shutdown bonsai + shutdown_bonsai() + .await + .map_err(|e| ProverError::GuestError(e.to_string()))?; + } + + proof_gen_result + } + async fn cancel(key: ProofKey, id_store: Box<&mut dyn IdStore>) -> ProverResult<()> { let uuid = match id_store.read_id(key).await { Ok(uuid) => uuid, diff --git a/provers/risc0/driver/src/methods/mod.rs b/provers/risc0/driver/src/methods/mod.rs index 0211d22de..19219d8af 100644 --- a/provers/risc0/driver/src/methods/mod.rs +++ b/provers/risc0/driver/src/methods/mod.rs @@ -1,3 +1,4 @@ +pub mod risc0_aggregation; pub mod risc0_guest; // To build the following `$ cargo run --features test,bench --bin risc0-builder` diff --git a/provers/risc0/driver/src/methods/risc0_aggregation.rs b/provers/risc0/driver/src/methods/risc0_aggregation.rs new file mode 100644 index 000000000..06ad39e27 --- /dev/null +++ b/provers/risc0/driver/src/methods/risc0_aggregation.rs @@ -0,0 +1,5 @@ +pub const RISC0_AGGREGATION_ELF: &[u8] = + include_bytes!("../../../guest/target/riscv32im-risc0-zkvm-elf/release/risc0-aggregation"); +pub const RISC0_AGGREGATION_ID: [u32; 8] = [ + 440526723, 3767976668, 67051936, 881100330, 2605787818, 1152192925, 943988177, 1141581874, +]; diff --git a/provers/risc0/driver/src/methods/risc0_guest.rs b/provers/risc0/driver/src/methods/risc0_guest.rs index 19d5fdfdc..159152655 100644 --- a/provers/risc0/driver/src/methods/risc0_guest.rs +++ b/provers/risc0/driver/src/methods/risc0_guest.rs @@ -1,5 +1,5 @@ pub const RISC0_GUEST_ELF: &[u8] = include_bytes!("../../../guest/target/riscv32im-risc0-zkvm-elf/release/risc0-guest"); pub const RISC0_GUEST_ID: [u32; 8] = [ - 2724640415, 1388818056, 2370444677, 1329173777, 2657825669, 1524407056, 1629931902, 314750851, + 2426111784, 2252773481, 4093155148, 2853313326, 836865213, 1159934005, 790932950, 229907112, ]; diff --git a/provers/risc0/driver/src/snarks.rs b/provers/risc0/driver/src/snarks.rs index 5cc00d232..a766ccf7a 100644 --- a/provers/risc0/driver/src/snarks.rs +++ b/provers/risc0/driver/src/snarks.rs @@ -30,7 +30,7 @@ use risc0_zkvm::{ use tracing::{error as tracing_err, info as tracing_info}; -use crate::save_receipt; +use crate::bonsai::save_receipt; sol!( /// A Groth16 seal over the claimed receipt claim. @@ -150,9 +150,31 @@ pub async fn stark2snark( Ok(snark_data) } -pub async fn verify_groth16_snark( +pub async fn verify_groth16_from_snark_receipt( image_id: Digest, snark_receipt: SnarkReceipt, +) -> Result> { + let seal = encode(snark_receipt.snark.to_vec())?; + let journal_digest = snark_receipt.journal.digest(); + let post_state_digest = snark_receipt.post_state_digest.digest(); + verify_groth16_snark_impl(image_id, seal, journal_digest, post_state_digest).await +} + +pub async fn verify_groth16_snark_from_receipt( + image_id: Digest, + receipt: Receipt, +) -> Result> { + let seal = receipt.inner.groth16().unwrap().seal.clone(); + let journal_digest = receipt.journal.digest(); + let post_state_digest = receipt.claim()?.as_value().unwrap().post.digest(); + verify_groth16_snark_impl(image_id, seal, journal_digest, post_state_digest).await +} + +pub async fn verify_groth16_snark_impl( + image_id: Digest, + seal: Vec, + journal_digest: Digest, + post_state_digest: Digest, ) -> Result> { let verifier_rpc_url = std::env::var("GROTH16_VERIFIER_RPC_URL").expect("env GROTH16_VERIFIER_RPC_URL"); @@ -167,19 +189,15 @@ pub async fn verify_groth16_snark( 500, )?); - let seal = encode(snark_receipt.snark.to_vec())?; - let journal_digest = snark_receipt.journal.digest(); + let enc_seal = encode(seal)?; tracing_info!("Verifying SNARK:"); - tracing_info!("Seal: {}", hex::encode(&seal)); + tracing_info!("Seal: {}", hex::encode(&enc_seal)); tracing_info!("Image ID: {}", hex::encode(image_id.as_bytes())); - tracing_info!( - "Post State Digest: {}", - hex::encode(&snark_receipt.post_state_digest) - ); + tracing_info!("Post State Digest: {}", hex::encode(&post_state_digest)); tracing_info!("Journal Digest: {}", hex::encode(journal_digest)); let verify_call_res = IRiscZeroVerifier::new(groth16_verifier_addr, http_client) .verify( - seal.clone().into(), + enc_seal.clone().into(), image_id.as_bytes().try_into().unwrap(), journal_digest.into(), ) @@ -188,13 +206,17 @@ pub async fn verify_groth16_snark( if verify_call_res.is_ok() { tracing_info!("SNARK verified successfully using {groth16_verifier_addr:?}!"); } else { - tracing_err!("SNARK verification failed: {:?}!", verify_call_res); + tracing_err!("SNARK verification failed: {verify_call_res:?}!"); } - Ok((seal, B256::from_slice(image_id.as_bytes())) + Ok(make_risc0_groth16_proof(enc_seal, image_id)) +} + +pub fn make_risc0_groth16_proof(seal: Vec, image_id: Digest) -> Vec { + (seal, B256::from_slice(image_id.as_bytes())) .abi_encode() .iter() .skip(32) .copied() - .collect()) + .collect() } diff --git a/provers/risc0/guest/Cargo.toml b/provers/risc0/guest/Cargo.toml index 28091f3c9..190ac9a60 100644 --- a/provers/risc0/guest/Cargo.toml +++ b/provers/risc0/guest/Cargo.toml @@ -9,6 +9,10 @@ edition = "2021" name = "zk_op" path = "src/zk_op.rs" +[[bin]] +name = "risc0-aggregation" +path = "src/aggregation.rs" + [[bin]] name = "sha256" path = "src/benchmark/sha256.rs" diff --git a/provers/risc0/guest/src/aggregation.rs b/provers/risc0/guest/src/aggregation.rs new file mode 100644 index 000000000..240711d7d --- /dev/null +++ b/provers/risc0/guest/src/aggregation.rs @@ -0,0 +1,27 @@ +//! Aggregates multiple block proofs +#![no_main] +harness::entrypoint!(main); + +use risc0_zkvm::{guest::env, serde}; + +use raiko_lib::{ + input::ZkAggregationGuestInput, + primitives::B256, + protocol_instance::{aggregation_output, words_to_bytes_le}, +}; + +pub fn main() { + // Read the aggregation input + let input = env::read::(); + + // Verify the proofs. + for block_input in input.block_inputs.iter() { + env::verify(input.image_id, &serde::to_vec(block_input).unwrap()).unwrap(); + } + + // The aggregation output + env::commit_slice(&aggregation_output( + B256::from(words_to_bytes_le(&input.image_id)), + input.block_inputs, + )); +} diff --git a/provers/sgx/guest/src/app_args.rs b/provers/sgx/guest/src/app_args.rs index 35020f272..10f8ca18e 100644 --- a/provers/sgx/guest/src/app_args.rs +++ b/provers/sgx/guest/src/app_args.rs @@ -17,6 +17,8 @@ pub struct App { pub enum Command { /// Prove (i.e. sign) a single block and exit. OneShot(OneShotArgs), + /// Aggregate proofs + Aggregate(OneShotArgs), /// Bootstrap the application and then exit. The bootstrapping process generates the /// initial public-private key pair and stores it on the disk in an encrypted /// format using SGX encryption primitives. diff --git a/provers/sgx/guest/src/main.rs b/provers/sgx/guest/src/main.rs index accd54913..c7af5db30 100644 --- a/provers/sgx/guest/src/main.rs +++ b/provers/sgx/guest/src/main.rs @@ -3,6 +3,7 @@ extern crate secp256k1; use anyhow::{anyhow, Result}; use clap::Parser; +use one_shot::aggregate; use crate::{ app_args::{App, Command}, @@ -22,6 +23,10 @@ pub async fn main() -> Result<()> { println!("Starting one shot mode"); one_shot(args.global_opts, one_shot_args).await? } + Command::Aggregate(one_shot_args) => { + println!("Starting one shot mode"); + aggregate(args.global_opts, one_shot_args).await? + } Command::Bootstrap => { println!("Bootstrapping the app"); bootstrap(args.global_opts)? diff --git a/provers/sgx/guest/src/one_shot.rs b/provers/sgx/guest/src/one_shot.rs index 4c4cfee71..156f92f9a 100644 --- a/provers/sgx/guest/src/one_shot.rs +++ b/provers/sgx/guest/src/one_shot.rs @@ -8,8 +8,11 @@ use std::{ use anyhow::{anyhow, bail, Context, Error, Result}; use base64_serde::base64_serde_type; use raiko_lib::{ - builder::calculate_block_header, consts::VerifierType, input::GuestInput, primitives::Address, - protocol_instance::ProtocolInstance, + builder::calculate_block_header, + consts::VerifierType, + input::{GuestInput, RawAggregationGuestInput}, + primitives::{keccak, Address, B256}, + protocol_instance::{aggregation_output_combine, ProtocolInstance}, }; use secp256k1::{Keypair, SecretKey}; use serde::Serialize; @@ -143,6 +146,7 @@ pub async fn one_shot(global_opts: GlobalOpts, args: OneShotArgs) -> Result<()> let sig = sign_message(&prev_privkey, pi_hash)?; // Create the proof for the onchain SGX verifier + // 4(id) + 20(new) + 65(sig) = 89 const SGX_PROOF_LEN: usize = 89; let mut proof = Vec::with_capacity(SGX_PROOF_LEN); proof.extend(args.sgx_instance_id.to_be_bytes()); @@ -160,6 +164,86 @@ pub async fn one_shot(global_opts: GlobalOpts, args: OneShotArgs) -> Result<()> "quote": hex::encode(quote), "public_key": format!("0x{new_pubkey}"), "instance_address": new_instance.to_string(), + "input": pi_hash.to_string(), + }); + println!("{data}"); + + // Print out general SGX information + print_sgx_info() +} + +pub async fn aggregate(global_opts: GlobalOpts, args: OneShotArgs) -> Result<()> { + // Make sure this SGX instance was bootstrapped + let prev_privkey = load_bootstrap(&global_opts.secrets_dir) + .or_else(|_| bail!("Application was not bootstrapped or has a deprecated bootstrap.")) + .unwrap(); + + println!("Global options: {global_opts:?}, OneShot options: {args:?}"); + + let new_pubkey = public_key(&prev_privkey); + let new_instance = public_key_to_address(&new_pubkey); + + let input: RawAggregationGuestInput = + bincode::deserialize_from(std::io::stdin()).expect("unable to deserialize input"); + + // Make sure the chain of old/new public keys is preserved + let old_instance = Address::from_slice(&input.proofs[0].proof.clone()[4..24]); + let mut cur_instance = old_instance; + + // Verify the proofs + for proof in input.proofs.iter() { + // TODO: verify protocol instance data so we can trust the old/new instance data + assert_eq!( + recover_signer_unchecked(&proof.proof.clone()[24..].try_into().unwrap(), &proof.input,) + .unwrap(), + cur_instance, + ); + cur_instance = Address::from_slice(&proof.proof.clone()[4..24]); + } + + // Current public key needs to match latest proof new public key + assert_eq!(cur_instance, new_instance); + + // Calculate the aggregation hash + let aggregation_hash = keccak::keccak(aggregation_output_combine( + [ + vec![ + B256::left_padding_from(old_instance.as_ref()), + B256::left_padding_from(new_instance.as_ref()), + ], + input + .proofs + .iter() + .map(|proof| proof.input) + .collect::>(), + ] + .concat(), + )); + + // Sign the public aggregation hash + let sig = sign_message(&prev_privkey, aggregation_hash.into())?; + + // Create the proof for the onchain SGX verifier + const SGX_PROOF_LEN: usize = 109; + // 4(id) + 20(old) + 20(new) + 65(sig) = 109 + let mut proof = Vec::with_capacity(SGX_PROOF_LEN); + proof.extend(args.sgx_instance_id.to_be_bytes()); + proof.extend(old_instance); + proof.extend(new_instance); + proof.extend(sig); + let proof = hex::encode(proof); + + // Store the public key address in the attestation data + save_attestation_user_report_data(new_instance)?; + + // Print out the proof and updated public info + let quote = get_sgx_quote()?; + let data = serde_json::json!({ + "proof": format!("0x{proof}"), + "quote": hex::encode(quote), + "public_key": format!("0x{new_pubkey}"), + "instance_address": new_instance.to_string(), + "input": B256::from(aggregation_hash).to_string(), }); println!("{data}"); diff --git a/provers/sgx/prover/Cargo.toml b/provers/sgx/prover/Cargo.toml index 69c0c3570..0c5f5a6c9 100644 --- a/provers/sgx/prover/Cargo.toml +++ b/provers/sgx/prover/Cargo.toml @@ -24,6 +24,7 @@ alloy-transport-http = { workspace = true } pem = { version = "3.0.4", optional = true } url = { workspace = true } anyhow = { workspace = true } +hex = { workspace = true } [features] default = ["dep:pem"] diff --git a/provers/sgx/prover/src/lib.rs b/provers/sgx/prover/src/lib.rs index 7f7688ac7..a74ee0e06 100644 --- a/provers/sgx/prover/src/lib.rs +++ b/provers/sgx/prover/src/lib.rs @@ -5,12 +5,16 @@ use std::{ fs::{copy, create_dir_all, remove_file}, path::{Path, PathBuf}, process::{Command as StdCommand, Output, Stdio}, - str, + str::{self, FromStr}, }; use once_cell::sync::Lazy; use raiko_lib::{ - input::{GuestInput, GuestOutput}, + input::{ + AggregationGuestInput, AggregationGuestOutput, GuestInput, GuestOutput, + RawAggregationGuestInput, RawProof, + }, + primitives::B256, prover::{IdStore, IdWrite, Proof, ProofKey, Prover, ProverConfig, ProverError, ProverResult}, }; use serde::{Deserialize, Serialize}; @@ -42,13 +46,17 @@ pub struct SgxResponse { /// proof format: 4b(id)+20b(pubkey)+65b(signature) pub proof: String, pub quote: String, + pub input: B256, } impl From for Proof { fn from(value: SgxResponse) -> Self { Self { proof: Some(value.proof), + input: Some(value.input), quote: Some(value.quote), + uuid: None, + kzg_proof: None, } } } @@ -147,6 +155,87 @@ impl Prover for SgxProver { sgx_proof.map(|r| r.into()) } + async fn aggregate( + input: AggregationGuestInput, + _output: &AggregationGuestOutput, + config: &ProverConfig, + _id_store: Option<&mut dyn IdWrite>, + ) -> ProverResult { + let sgx_param = SgxParam::deserialize(config.get("sgx").unwrap()).unwrap(); + + // Support both SGX and the direct backend for testing + let direct_mode = match env::var("SGX_DIRECT") { + Ok(value) => value == "1", + Err(_) => false, + }; + + println!( + "WARNING: running SGX in {} mode!", + if direct_mode { + "direct (a.k.a. simulation)" + } else { + "hardware" + } + ); + + // The working directory + let mut cur_dir = env::current_exe() + .expect("Fail to get current directory") + .parent() + .unwrap() + .to_path_buf(); + + // When running in tests we might be in a child folder + if cur_dir.ends_with("deps") { + cur_dir = cur_dir.parent().unwrap().to_path_buf(); + } + + println!("Current directory: {cur_dir:?}\n"); + // Working paths + PRIVATE_KEY + .get_or_init(|| async { cur_dir.join("secrets").join(PRIV_KEY_FILENAME) }) + .await; + GRAMINE_MANIFEST_TEMPLATE + .get_or_init(|| async { + cur_dir + .join(CONFIG) + .join("sgx-guest.local.manifest.template") + }) + .await; + + // The gramine command (gramine or gramine-direct for testing in non-SGX environment) + let gramine_cmd = || -> StdCommand { + let mut cmd = if direct_mode { + StdCommand::new("gramine-direct") + } else { + let mut cmd = StdCommand::new("sudo"); + cmd.arg("gramine-sgx"); + cmd + }; + cmd.current_dir(&cur_dir).arg(ELF_NAME); + cmd + }; + + // Setup: run this once while setting up your SGX instance + if sgx_param.setup { + setup(&cur_dir, direct_mode).await?; + } + + let mut sgx_proof = if sgx_param.bootstrap { + bootstrap(cur_dir.clone().join("secrets"), gramine_cmd()).await + } else { + // Dummy proof: it's ok when only setup/bootstrap was requested + Ok(SgxResponse::default()) + }; + + if sgx_param.prove { + // overwrite sgx_proof as the bootstrap quote stays the same in bootstrap & prove. + sgx_proof = aggregate(gramine_cmd(), input.clone(), sgx_param.instance_id).await + } + + sgx_proof.map(|r| r.into()) + } + async fn cancel(_proof_key: ProofKey, _read: Box<&mut dyn IdStore>) -> ProverResult<()> { Ok(()) } @@ -303,6 +392,54 @@ async fn prove( .map_err(|e| ProverError::GuestError(e.to_string()))? } +async fn aggregate( + mut gramine_cmd: StdCommand, + input: AggregationGuestInput, + instance_id: u64, +) -> ProverResult { + // Extract the useful parts of the proof here so the guest doesn't have to do it + let raw_input = RawAggregationGuestInput { + proofs: input + .proofs + .iter() + .map(|proof| RawProof { + input: proof.clone().input.unwrap(), + proof: hex::decode(&proof.clone().proof.unwrap()[2..]).unwrap(), + }) + .collect(), + }; + + tokio::task::spawn_blocking(move || { + let mut child = gramine_cmd + .arg("aggregate") + .arg("--sgx-instance-id") + .arg(instance_id.to_string()) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(|e| format!("Could not spawn gramine cmd: {e}"))?; + let stdin = child.stdin.as_mut().expect("Failed to open stdin"); + let input_success = bincode::serialize_into(stdin, &raw_input); + let output_success = child.wait_with_output(); + + match (input_success, output_success) { + (Ok(_), Ok(output)) => { + handle_output(&output, "SGX prove")?; + Ok(parse_sgx_result(output.stdout)?) + } + (Err(i), output_success) => Err(ProverError::GuestError(format!( + "Can not serialize input for SGX {i}, output is {output_success:?}" + ))), + (Ok(_), Err(output_err)) => Err(ProverError::GuestError( + handle_gramine_error("Could not run SGX guest prover", output_err).to_string(), + )), + } + }) + .await + .map_err(|e| ProverError::GuestError(e.to_string()))? +} + fn parse_sgx_result(output: Vec) -> ProverResult { let mut json_value: Option = None; let output = String::from_utf8(output).map_err(|e| e.to_string())?; @@ -324,6 +461,7 @@ fn parse_sgx_result(output: Vec) -> ProverResult { Ok(SgxResponse { proof: extract_field("proof"), quote: extract_field("quote"), + input: B256::from_str(&extract_field("input")).unwrap(), }) } diff --git a/provers/sp1/builder/src/main.rs b/provers/sp1/builder/src/main.rs index 7db899a13..fe696594e 100644 --- a/provers/sp1/builder/src/main.rs +++ b/provers/sp1/builder/src/main.rs @@ -5,7 +5,7 @@ use std::path::PathBuf; fn main() { let pipeline = Sp1Pipeline::new("provers/sp1/guest", "release"); - pipeline.bins(&["sp1-guest"], "provers/sp1/guest/elf"); + pipeline.bins(&["sp1-guest", "sp1-aggregation"], "provers/sp1/guest/elf"); #[cfg(feature = "test")] pipeline.tests(&["sp1-guest"], "provers/sp1/guest/elf"); #[cfg(feature = "bench")] diff --git a/provers/sp1/driver/src/lib.rs b/provers/sp1/driver/src/lib.rs index c0c3f60d1..8de517f52 100644 --- a/provers/sp1/driver/src/lib.rs +++ b/provers/sp1/driver/src/lib.rs @@ -1,8 +1,12 @@ #![cfg(feature = "enable")] #![feature(iter_advance_by)] +use once_cell::sync::Lazy; use raiko_lib::{ - input::{GuestInput, GuestOutput}, + input::{ + AggregationGuestInput, AggregationGuestOutput, GuestInput, GuestOutput, + ZkAggregationGuestInput, + }, prover::{IdStore, IdWrite, Proof, ProofKey, Prover, ProverConfig, ProverError, ProverResult}, Measurement, }; @@ -13,16 +17,27 @@ use sp1_sdk::{ action, network::client::NetworkClient, proto::network::{ProofMode, UnclaimReason}, + SP1Proof, SP1ProofWithPublicValues, SP1VerifyingKey, }; use sp1_sdk::{HashableKey, ProverClient, SP1Stdin}; -use std::{borrow::BorrowMut, env}; -use tracing::info; +use std::{ + borrow::BorrowMut, + env, fs, + path::{Path, PathBuf}, +}; +use tracing::{debug, error, info}; mod proof_verify; use proof_verify::remote_contract_verify::verify_sol_by_contract_call; pub const ELF: &[u8] = include_bytes!("../../guest/elf/sp1-guest"); +pub const AGGREGATION_ELF: &[u8] = include_bytes!("../../guest/elf/sp1-aggregation"); const SP1_PROVER_CODE: u8 = 1; +static FIXTURE_PATH: Lazy = + Lazy::new(|| Path::new(env!("CARGO_MANIFEST_DIR")).join("../contracts/src/fixtures/")); +static CONTRACT_PATH: Lazy = + Lazy::new(|| Path::new(env!("CARGO_MANIFEST_DIR")).join("../contracts/src/exports/")); +pub static VERIFIER: Lazy> = Lazy::new(init_verifier); #[serde_as] #[derive(Clone, Debug, Serialize, Deserialize)] @@ -67,15 +82,27 @@ pub enum ProverMode { impl From for Proof { fn from(value: Sp1Response) -> Self { Self { - proof: Some(value.proof), - quote: None, + proof: value.proof, + quote: value + .sp1_proof + .as_ref() + .map(|p| serde_json::to_string(&p.proof).unwrap()), + input: value + .sp1_proof + .as_ref() + .map(|p| B256::from_slice(p.public_values.as_slice())), + uuid: value.vkey.map(|v| serde_json::to_string(&v).unwrap()), + kzg_proof: None, } } } #[derive(Clone, Serialize, Deserialize)] pub struct Sp1Response { - pub proof: String, + pub proof: Option, + /// for aggregation + pub sp1_proof: Option, + pub vkey: Option, } pub struct Sp1Prover; @@ -90,6 +117,8 @@ impl Prover for Sp1Prover { let param = Sp1Param::deserialize(config.get("sp1").unwrap()).unwrap(); let mode = param.prover.clone().unwrap_or_else(get_env_mock); + println!("param: {param:?}"); + let mut stdin = SP1Stdin::new(); stdin.write(&input); @@ -118,8 +147,7 @@ impl Prover for Sp1Prover { RecursionMode::Compressed => prove_action.compressed().run(), RecursionMode::Plonk => prove_action.plonk().run(), } - .map_err(|e| ProverError::GuestError(format!("Sp1: local proving failed: {}", e))) - .unwrap() + .map_err(|e| ProverError::GuestError(format!("Sp1: local proving failed: {e}")))? } else { let network_prover = sp1_sdk::NetworkProver::new(); @@ -138,17 +166,22 @@ impl Prover for Sp1Prover { .await?; } info!( - "Sp1 Prover: block {:?} - proof id {:?}", - output.header.number, proof_id + "Sp1 Prover: block {:?} - proof id {proof_id:?}", + output.header.number ); network_prover .wait_proof::(&proof_id, None) .await - .map_err(|e| ProverError::GuestError(format!("Sp1: network proof failed {:?}", e))) - .unwrap() + .map_err(|e| ProverError::GuestError(format!("Sp1: network proof failed {e:?}")))? }; - let proof_bytes = prove_result.bytes(); + let proof_bytes = match param.recursion { + RecursionMode::Compressed => { + info!("Compressed proof is used in aggregation mode only"); + vec![] + } + _ => prove_result.bytes(), + }; if param.verify { let time = Measurement::start("verify", false); let pi_hash = prove_result @@ -158,34 +191,36 @@ impl Prover for Sp1Prover { .read::<[u8; 32]>(); let fixture = RaikoProofFixture { vkey: vk.bytes32(), - public_values: pi_hash.into(), - proof: proof_bytes.clone(), + public_values: B256::from_slice(&pi_hash).to_string(), + proof: reth_primitives::hex::encode_prefixed(&proof_bytes), }; verify_sol_by_contract_call(&fixture).await?; time.stop_with("==> Verification complete"); } - let proof_string = if proof_bytes.is_empty() { - None - } else { + let proof_string = (!proof_bytes.is_empty()).then_some( // 0x + 64 bytes of the vkey + the proof // vkey itself contains 0x prefix - Some(format!( + format!( "{}{}", vk.bytes32(), reth_primitives::hex::encode(proof_bytes) - )) - }; + ), + ); info!( - "Sp1 Prover: block {:?} completed! proof: {:?}", - output.header.number, proof_string + "Sp1 Prover: block {:?} completed! proof: {proof_string:?}", + output.header.number, ); - Ok::<_, ProverError>(Proof { - proof: proof_string, - quote: None, - }) + Ok::<_, ProverError>( + Sp1Response { + proof: proof_string, + sp1_proof: Some(prove_result), + vkey: Some(vk), + } + .into(), + ) } async fn cancel(key: ProofKey, id_store: Box<&mut dyn IdStore>) -> ProverResult<()> { @@ -210,6 +245,110 @@ impl Prover for Sp1Prover { id_store.remove_id(key).await?; Ok(()) } + + async fn aggregate( + input: AggregationGuestInput, + _output: &AggregationGuestOutput, + config: &ProverConfig, + _store: Option<&mut dyn IdWrite>, + ) -> ProverResult { + let param = Sp1Param::deserialize(config.get("sp1").unwrap()).unwrap(); + + info!("aggregate proof with param: {param:?}"); + + let block_inputs: Vec = input + .proofs + .iter() + .map(|proof| proof.input.unwrap()) + .collect::>(); + let block_proof_vk = serde_json::from_str::( + &input.proofs.first().unwrap().uuid.clone().unwrap(), + ) + .map_err(|e| ProverError::GuestError(format!("Failed to parse SP1 vk: {e}")))?; + let stark_vk = block_proof_vk.vk.clone(); + let image_id = block_proof_vk.hash_u32(); + let aggregation_input = ZkAggregationGuestInput { + image_id, + block_inputs, + }; + info!( + "Aggregating {:?} proofs with input: {aggregation_input:?}", + input.proofs.len(), + ); + + let mut stdin = SP1Stdin::new(); + stdin.write(&aggregation_input); + for proof in input.proofs.iter() { + let sp1_proof = serde_json::from_str::(&proof.quote.clone().unwrap()) + .map_err(|e| ProverError::GuestError(format!("Failed to parse SP1 proof: {e}")))?; + match sp1_proof { + SP1Proof::Compressed(block_proof) => { + stdin.write_proof(block_proof, stark_vk.clone()); + } + _ => { + error!("unsupported proof type for aggregation: {sp1_proof:?}"); + } + } + } + + // Generate the proof for the given program. + let client = param + .prover + .map(|mode| match mode { + ProverMode::Mock => ProverClient::mock(), + ProverMode::Local => ProverClient::local(), + ProverMode::Network => ProverClient::network(), + }) + .unwrap_or_else(ProverClient::new); + + let (pk, vk) = client.setup(AGGREGATION_ELF); + info!( + "sp1 aggregate: {:?} based {:?} blocks with vk {:?}", + reth_primitives::hex::encode_prefixed(stark_vk.hash_bytes()), + input.proofs.len(), + vk.bytes32() + ); + + let prove_result = client + .prove(&pk, stdin) + .plonk() + .run() + .expect("proving failed"); + + let proof_bytes = prove_result.bytes(); + if param.verify { + let time = Measurement::start("verify", false); + let aggregation_pi = prove_result.clone().borrow_mut().public_values.raw(); + let fixture = RaikoProofFixture { + vkey: vk.bytes32().to_string(), + public_values: reth_primitives::hex::encode_prefixed(aggregation_pi), + proof: reth_primitives::hex::encode_prefixed(&proof_bytes), + }; + + verify_sol(&fixture)?; + time.stop_with("==> Verification complete"); + } + + let proof = (!proof_bytes.is_empty()).then_some( + // 0x + 64 bytes of the vkey + the proof + // vkey itself contains 0x prefix + format!( + "{}{}{}", + vk.bytes32(), + reth_primitives::hex::encode(stark_vk.hash_bytes()), + reth_primitives::hex::encode(proof_bytes) + ), + ); + + Ok::<_, ProverError>( + Sp1Response { + proof, + sp1_proof: None, + vkey: None, + } + .into(), + ) + } } fn get_env_mock() -> ProverMode { @@ -225,13 +364,65 @@ fn get_env_mock() -> ProverMode { } } +fn init_verifier() -> Result { + // In cargo run, Cargo sets the working directory to the root of the workspace + let contract_path = &*CONTRACT_PATH; + info!("Contract dir: {contract_path:?}"); + let artifacts_dir = sp1_sdk::install::try_install_circuit_artifacts(); + // Create the destination directory if it doesn't exist + fs::create_dir_all(contract_path)?; + + // Read the entries in the source directory + for entry in fs::read_dir(artifacts_dir)? { + let entry = entry?; + let src = entry.path(); + + // Check if the entry is a file and ends with .sol + if src.is_file() && src.extension().map(|s| s == "sol").unwrap_or(false) { + let out = contract_path.join(src.file_name().unwrap()); + fs::copy(&src, &out)?; + println!("Copied: {:?}", src.file_name().unwrap()); + } + } + Ok(contract_path.clone()) +} + /// A fixture that can be used to test the verification of SP1 zkVM proofs inside Solidity. #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub(crate) struct RaikoProofFixture { vkey: String, - public_values: B256, - proof: Vec, + public_values: String, + proof: String, +} + +fn verify_sol(fixture: &RaikoProofFixture) -> ProverResult<()> { + assert!(VERIFIER.is_ok()); + debug!("===> Fixture: {fixture:#?}"); + + // Save the fixture to a file. + let fixture_path = &*FIXTURE_PATH; + info!("Writing fixture to: {fixture_path:?}"); + + if !fixture_path.exists() { + std::fs::create_dir_all(fixture_path.clone()) + .map_err(|e| ProverError::GuestError(format!("Failed to create fixture path: {e}")))?; + } + std::fs::write( + fixture_path.join("fixture.json"), + serde_json::to_string_pretty(&fixture).unwrap(), + ) + .map_err(|e| ProverError::GuestError(format!("Failed to write fixture: {e}")))?; + + let child = std::process::Command::new("forge") + .arg("test") + .current_dir(&*CONTRACT_PATH) + .stdout(std::process::Stdio::inherit()) // Inherit the parent process' stdout + .spawn(); + info!("Verification started {child:?}"); + child.map_err(|e| ProverError::GuestError(format!("Failed to run forge: {e}")))?; + + Ok(()) } #[cfg(test)] @@ -261,6 +452,11 @@ mod test { println!("{json:?} {deserialized:?}"); } + #[test] + fn test_init_verifier() { + VERIFIER.as_ref().expect("Failed to init verifier"); + } + #[test] fn run_unittest_elf() { // TODO(Cecilia): imple GuestInput::mock() for unit test diff --git a/provers/sp1/driver/src/proof_verify/remote_contract_verify.rs b/provers/sp1/driver/src/proof_verify/remote_contract_verify.rs index 7474fad4e..6606a041d 100644 --- a/provers/sp1/driver/src/proof_verify/remote_contract_verify.rs +++ b/provers/sp1/driver/src/proof_verify/remote_contract_verify.rs @@ -32,13 +32,11 @@ pub(crate) async fn verify_sol_by_contract_call(fixture: &RaikoProofFixture) -> let provider = ProviderBuilder::new().on_http(Url::parse(&sp1_verifier_rpc_url).unwrap()); let program_key: B256 = B256::from_str(&fixture.vkey).unwrap(); - let public_value = fixture.public_values; + let public_value = fixture.public_values.clone(); let proof_bytes = fixture.proof.clone(); info!( - "verify sp1 proof with program key: {:?} public value: {:?} proof: {:?}", - program_key, - public_value, + "verify sp1 proof with program key: {program_key:?} public value: {public_value:?} proof: {:?}", reth_primitives::hex::encode(&proof_bytes) ); @@ -50,7 +48,7 @@ pub(crate) async fn verify_sol_by_contract_call(fixture: &RaikoProofFixture) -> if verify_call_res.is_ok() { info!("SP1 proof verified successfully using {sp1_verifier_addr:?}!"); } else { - error!("SP1 proof verification failed: {:?}!", verify_call_res); + error!("SP1 proof verification failed: {verify_call_res:?}!"); } Ok(()) diff --git a/provers/sp1/driver/src/verifier.rs b/provers/sp1/driver/src/verifier.rs index f1f2454c9..20c760e92 100644 --- a/provers/sp1/driver/src/verifier.rs +++ b/provers/sp1/driver/src/verifier.rs @@ -31,7 +31,7 @@ async fn main() { } }) .unwrap_or_else(|| PathBuf::from(DATA).join("taiko_mainnet-328833.json")); - println!("Reading GuestInput from {:?}", path); + println!("Reading GuestInput from {path:?}"); let json = std::fs::read_to_string(path).unwrap(); // Deserialize the input. diff --git a/provers/sp1/guest/Cargo.lock b/provers/sp1/guest/Cargo.lock index 3f00879a3..cfa9f3a4d 100644 --- a/provers/sp1/guest/Cargo.lock +++ b/provers/sp1/guest/Cargo.lock @@ -3504,7 +3504,7 @@ dependencies = [ "size", "snowbridge-amcl", "sp1-derive", - "sp1-primitives", + "sp1-primitives 1.1.1", "static_assertions", "strum", "strum_macros", @@ -3549,8 +3549,9 @@ dependencies = [ [[package]] name = "sp1-lib" -version = "1.2.0-rc1" -source = "git+https://github.com/succinctlabs/sp1?branch=dev#e8efd0019c8be52c6c4cecfea6259ab90db4148a" +version = "1.2.0-rc2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b85660c40c7b40a65c706816d9157ef1b084099a80275c9b4d650f53067e667f" dependencies = [ "anyhow", "bincode", @@ -3562,9 +3563,9 @@ dependencies = [ [[package]] name = "sp1-lib" -version = "1.2.0-rc2" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b85660c40c7b40a65c706816d9157ef1b084099a80275c9b4d650f53067e667f" +checksum = "413956de14568d7fb462213b9505ad4607d75c875301b9eca567cfb2e58eaac1" dependencies = [ "anyhow", "bincode", @@ -3588,10 +3589,25 @@ dependencies = [ "p3-symmetric", ] +[[package]] +name = "sp1-primitives" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efbeba375fe59917f162f1808c280d2e39e4698dc7eeac83936b6e70c2f8dbbc" +dependencies = [ + "itertools 0.13.0", + "lazy_static", + "p3-baby-bear", + "p3-field", + "p3-poseidon2", + "p3-symmetric", +] + [[package]] name = "sp1-zkvm" -version = "1.2.0-rc1" -source = "git+https://github.com/succinctlabs/sp1?branch=dev#e8efd0019c8be52c6c4cecfea6259ab90db4148a" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66c525f67cfd3f65950f01c713a72c41a5d44d289155644c8ace4ec264098039" dependencies = [ "bincode", "cfg-if", @@ -3599,10 +3615,13 @@ dependencies = [ "lazy_static", "libm", "once_cell", + "p3-baby-bear", + "p3-field", "rand", "serde", "sha2", - "sp1-lib 1.2.0-rc1", + "sp1-lib 2.0.0", + "sp1-primitives 2.0.0", ] [[package]] diff --git a/provers/sp1/guest/Cargo.toml b/provers/sp1/guest/Cargo.toml index efa74446b..3063cc5be 100644 --- a/provers/sp1/guest/Cargo.toml +++ b/provers/sp1/guest/Cargo.toml @@ -9,6 +9,10 @@ edition = "2021" name = "zk_op" path = "src/zk_op.rs" +[[bin]] +name = "sp1-aggregation" +path = "src/aggregation.rs" + [[bin]] name = "sha256" path = "src/benchmark/sha256.rs" @@ -33,9 +37,9 @@ path = "src/benchmark/bn254_mul.rs" [dependencies] raiko-lib = { path = "../../../lib", features = ["std", "sp1"] } -sp1-zkvm = { git = "https://github.com/succinctlabs/sp1", branch = "dev" } -sp1-core = { version = "1.1.1"} -sha2-v0-10-8 = { git = "https://github.com/sp1-patches/RustCrypto-hashes", package = "sha2", branch = "patch-v0.10.8" } +sp1-zkvm = { version = "2.0.0", features = ["verify"] } +sp1-core = { version = "1.1.1" } +sha2 = { git = "https://github.com/sp1-patches/RustCrypto-hashes", package = "sha2", branch = "patch-v0.10.8" } secp256k1 = { git = "https://github.com/sp1-patches/rust-secp256k1", branch = "patch-secp256k1-v0.29.0" } harness-core = { path = "../../../harness/core" } harness = { path = "../../../harness/macro", features = ["sp1"] } @@ -46,7 +50,10 @@ revm-precompile = { git = "https://github.com/taikoxyz/revm.git", branch = "v36- "c-kzg", ] } bincode = "1.3.3" -reth-primitives = { git = "https://github.com/taikoxyz/taiko-reth.git", branch = "v1.0.0-rc.2-taiko", default-features = false, features = ["alloy-compat", "taiko"] } +reth-primitives = { git = "https://github.com/taikoxyz/taiko-reth.git", branch = "v1.0.0-rc.2-taiko", default-features = false, features = [ + "alloy-compat", + "taiko", +] } lazy_static = "1.4.0" num-bigint = { version = "0.4.6", default-features = false } diff --git a/provers/sp1/guest/elf/sp1-aggregation b/provers/sp1/guest/elf/sp1-aggregation new file mode 100755 index 0000000000000000000000000000000000000000..ed3c2c31d1a876e990aba6bbebb6171307dd2861 GIT binary patch literal 200852 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åHKaql}}A^8a#OlzL0Bvbtz3dg6aX7maG~ur6w6 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 literal 0 HcmV?d00001 diff --git a/provers/sp1/guest/src/aggregation.rs b/provers/sp1/guest/src/aggregation.rs new file mode 100644 index 000000000..84d4bde31 --- /dev/null +++ b/provers/sp1/guest/src/aggregation.rs @@ -0,0 +1,31 @@ +//! Aggregates multiple block proofs +#![no_main] +sp1_zkvm::entrypoint!(main); + +use sha2::{Digest, Sha256}; + +use raiko_lib::{ + input::ZkAggregationGuestInput, + primitives::B256, + protocol_instance::{aggregation_output, words_to_bytes_be}, +}; + +pub fn main() { + // Read the aggregation input + let input = sp1_zkvm::io::read::(); + + // Verify the block proofs. + for block_input in input.block_inputs.iter() { + sp1_zkvm::lib::verify::verify_sp1_proof( + &input.image_id, + &Sha256::digest(block_input).into(), + ); + } + + // The aggregation output + sp1_zkvm::io::commit_slice(&aggregation_output( + B256::from(words_to_bytes_be(&input.image_id)), + input.block_inputs, + )); +} + diff --git a/provers/sp1/guest/src/benchmark/bn254_add.rs b/provers/sp1/guest/src/benchmark/bn254_add.rs index 096b65468..1f5729639 100644 --- a/provers/sp1/guest/src/benchmark/bn254_add.rs +++ b/provers/sp1/guest/src/benchmark/bn254_add.rs @@ -17,11 +17,11 @@ fn main() { ]); let op = Sp1Operator {}; - + let ct = CycleTracker::start("bn128_run_add"); let res = op.bn128_run_add(&input).unwrap(); ct.end(); - + let hi = res[..32].to_vec(); let lo = res[32..].to_vec(); diff --git a/provers/sp1/guest/src/benchmark/bn254_mul.rs b/provers/sp1/guest/src/benchmark/bn254_mul.rs index 664947de0..ae1ede10e 100644 --- a/provers/sp1/guest/src/benchmark/bn254_mul.rs +++ b/provers/sp1/guest/src/benchmark/bn254_mul.rs @@ -19,7 +19,7 @@ fn main() { let ct = CycleTracker::start("bn128_run_mul"); let res = op.bn128_run_mul(&input).unwrap(); ct.end(); - + let hi = res[..32].to_vec(); let lo = res[32..].to_vec(); sp1_zkvm::io::commit(&hi); diff --git a/provers/sp1/guest/src/benchmark/sha256.rs b/provers/sp1/guest/src/benchmark/sha256.rs index 9c5908b13..e6c574333 100644 --- a/provers/sp1/guest/src/benchmark/sha256.rs +++ b/provers/sp1/guest/src/benchmark/sha256.rs @@ -13,7 +13,7 @@ fn main() { ]); let op = Sp1Operator {}; - + let ct = CycleTracker::start("sha256_run"); let res = op.sha256_run(&input).unwrap(); ct.end(); diff --git a/provers/sp1/guest/src/sys.rs b/provers/sp1/guest/src/sys.rs index 04a3c18d7..f9eed1c93 100644 --- a/provers/sp1/guest/src/sys.rs +++ b/provers/sp1/guest/src/sys.rs @@ -39,4 +39,5 @@ pub unsafe extern "C" fn free(_size: *const c_void) { #[no_mangle] pub extern "C" fn __ctzsi2(x: u32) -> u32 { x.trailing_zeros() -} \ No newline at end of file +} + diff --git a/provers/sp1/guest/src/zk_op.rs b/provers/sp1/guest/src/zk_op.rs index e6ed28be4..b28be5e20 100644 --- a/provers/sp1/guest/src/zk_op.rs +++ b/provers/sp1/guest/src/zk_op.rs @@ -1,15 +1,14 @@ -use num_bigint::BigUint; use ::secp256k1::SECP256K1; +use num_bigint::BigUint; use reth_primitives::public_key_to_address; use revm_precompile::{bn128::ADD_INPUT_LEN, utilities::right_pad, zk_op::ZkvmOperator, Error}; use secp256k1::{ ecdsa::{RecoverableSignature, RecoveryId}, Message, }; -use sha2_v0_10_8 as sp1_sha2; +use sha2 as sp1_sha2; use sp1_core::utils::ec::{weierstrass::bn254::Bn254, AffinePoint}; - #[derive(Debug)] pub struct Sp1Operator; @@ -117,7 +116,7 @@ harness::zk_suits!( p.x().to_big_endian(&mut p_x).unwrap(); p.y().to_big_endian(&mut p_y).unwrap(); - println!("{:?}, {:?}:?", p_x, p_y); + println!("{p_x:?}, {p_y:?}:?"); // Deserialize AffinePoint in Sp1 let p = be_bytes_to_point(&input); @@ -154,4 +153,4 @@ harness::zk_suits!( assert!(G1_LE == [p.x.to_bytes_le(), p.y.to_bytes_le()].concat()); } } -); \ No newline at end of file +); diff --git a/script/prove-block.sh b/script/prove-block.sh index 7b0d387e7..8e3113e89 100755 --- a/script/prove-block.sh +++ b/script/prove-block.sh @@ -58,6 +58,16 @@ elif [ "$proof" == "sp1" ]; then "verify": false } ' +elif [ "$proof" == "sp1-aggregation" ]; then + proofParam=' + "proof_type": "sp1", + "blob_proof_type": "proof_of_equivalence", + "sp1": { + "recursion": "compressed", + "prover": "network", + "verify": false + } + ' elif [ "$proof" == "sgx" ]; then proofParam=' "proof_type": "sgx", @@ -134,13 +144,13 @@ for block in $(eval echo {$rangeStart..$rangeEnd}); do fi echo "- proving block $block" - curl --location --request POST 'http://localhost:8080/proof' \ + curl --location --request POST 'http://localhost:8080/v3/proof' \ --header 'Content-Type: application/json' \ --header 'Authorization: Bearer 4cbd753fbcbc2639de804f8ce425016a50e0ecd53db00cb5397912e83f5e570e' \ --data-raw "{ \"network\": \"$chain\", \"l1_network\": \"$l1_network\", - \"block_number\": $block, + \"block_numbers\": [[$block, null], [$(($block+1)), null]], \"prover\": \"$prover\", \"graffiti\": \"$graffiti\", $proofParam diff --git a/tasks/src/adv_sqlite.rs b/tasks/src/adv_sqlite.rs index 120c0d43d..96f9e4bb3 100644 --- a/tasks/src/adv_sqlite.rs +++ b/tasks/src/adv_sqlite.rs @@ -159,6 +159,7 @@ use std::{ }; use chrono::{DateTime, Utc}; +use raiko_core::interfaces::AggregationOnlyRequest; use raiko_lib::{ primitives::B256, prover::{IdStore, IdWrite, ProofKey, ProverError, ProverResult}, @@ -575,7 +576,7 @@ impl TaskDb { ":blockhash": blockhash.to_vec(), ":proofsys_id": proof_system as u8, ":prover": prover, - ":status_id": status as i32, + ":status_id": i32::from(status), ":proof": proof.map(hex::encode) })?; @@ -943,6 +944,36 @@ impl TaskManager for SqliteTaskManager { let task_db = self.arc_task_db.lock().await; task_db.list_stored_ids() } + + async fn enqueue_aggregation_task( + &mut self, + _request: &AggregationOnlyRequest, + ) -> TaskManagerResult<()> { + todo!() + } + + async fn get_aggregation_task_proving_status( + &mut self, + _request: &AggregationOnlyRequest, + ) -> TaskManagerResult { + todo!() + } + + async fn update_aggregation_task_progress( + &mut self, + _request: &AggregationOnlyRequest, + _status: TaskStatus, + _proof: Option<&[u8]>, + ) -> TaskManagerResult<()> { + todo!() + } + + async fn get_aggregation_task_proof( + &mut self, + _request: &AggregationOnlyRequest, + ) -> TaskManagerResult> { + todo!() + } } #[cfg(test)] diff --git a/tasks/src/lib.rs b/tasks/src/lib.rs index 2abd2e741..cc7523e35 100644 --- a/tasks/src/lib.rs +++ b/tasks/src/lib.rs @@ -4,8 +4,7 @@ use std::{ }; use chrono::{DateTime, Utc}; -use num_enum::{FromPrimitive, IntoPrimitive}; -use raiko_core::interfaces::ProofType; +use raiko_core::interfaces::{AggregationOnlyRequest, ProofType}; use raiko_lib::{ primitives::{ChainId, B256}, prover::{IdStore, IdWrite, ProofKey, ProverResult}, @@ -61,24 +60,83 @@ impl From for TaskManagerError { #[allow(non_camel_case_types)] #[rustfmt::skip] -#[derive(PartialEq, Debug, Copy, Clone, IntoPrimitive, FromPrimitive, Deserialize, Serialize, ToSchema)] -#[repr(i32)] +#[derive(PartialEq, Debug, Clone, Deserialize, Serialize, ToSchema, Eq, PartialOrd, Ord)] #[serde(rename_all = "snake_case")] pub enum TaskStatus { - Success = 0, - Registered = 1000, - WorkInProgress = 2000, - ProofFailure_Generic = -1000, - ProofFailure_OutOfMemory = -1100, - NetworkFailure = -2000, - Cancelled = -3000, - Cancelled_NeverStarted = -3100, - Cancelled_Aborted = -3200, - CancellationInProgress = -3210, - InvalidOrUnsupportedBlock = -4000, - UnspecifiedFailureReason = -9999, - #[num_enum(default)] - SqlDbCorruption = -99999, + Success, + Registered, + WorkInProgress, + ProofFailure_Generic, + ProofFailure_OutOfMemory, + NetworkFailure, + Cancelled, + Cancelled_NeverStarted, + Cancelled_Aborted, + CancellationInProgress, + InvalidOrUnsupportedBlock, + NonDbFailure(String), + UnspecifiedFailureReason, + SqlDbCorruption, +} + +impl From for i32 { + fn from(status: TaskStatus) -> i32 { + match status { + TaskStatus::Success => 0, + TaskStatus::Registered => 1000, + TaskStatus::WorkInProgress => 2000, + TaskStatus::ProofFailure_Generic => -1000, + TaskStatus::ProofFailure_OutOfMemory => -1100, + TaskStatus::NetworkFailure => -2000, + TaskStatus::Cancelled => -3000, + TaskStatus::Cancelled_NeverStarted => -3100, + TaskStatus::Cancelled_Aborted => -3200, + TaskStatus::CancellationInProgress => -3210, + TaskStatus::InvalidOrUnsupportedBlock => -4000, + TaskStatus::NonDbFailure(_) => -5000, + TaskStatus::UnspecifiedFailureReason => -9999, + TaskStatus::SqlDbCorruption => -99999, + } + } +} + +impl From for TaskStatus { + fn from(value: i32) -> TaskStatus { + match value { + 0 => TaskStatus::Success, + 1000 => TaskStatus::Registered, + 2000 => TaskStatus::WorkInProgress, + -1000 => TaskStatus::ProofFailure_Generic, + -1100 => TaskStatus::ProofFailure_OutOfMemory, + -2000 => TaskStatus::NetworkFailure, + -3000 => TaskStatus::Cancelled, + -3100 => TaskStatus::Cancelled_NeverStarted, + -3200 => TaskStatus::Cancelled_Aborted, + -3210 => TaskStatus::CancellationInProgress, + -4000 => TaskStatus::InvalidOrUnsupportedBlock, + -5000 => TaskStatus::NonDbFailure("".to_string()), + -9999 => TaskStatus::UnspecifiedFailureReason, + -99999 => TaskStatus::SqlDbCorruption, + _ => TaskStatus::UnspecifiedFailureReason, + } + } +} + +impl FromIterator for TaskStatus { + fn from_iter>(iter: T) -> Self { + iter.into_iter() + .min() + .unwrap_or(TaskStatus::UnspecifiedFailureReason) + } +} + +impl<'a> FromIterator<&'a TaskStatus> for TaskStatus { + fn from_iter>(iter: T) -> Self { + iter.into_iter() + .min() + .cloned() + .unwrap_or(TaskStatus::UnspecifiedFailureReason) + } } #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] @@ -166,6 +224,32 @@ pub trait TaskManager: IdStore + IdWrite { /// List all stored ids. async fn list_stored_ids(&mut self) -> TaskManagerResult>; + + /// Enqueue a new aggregation task to the tasks database. + async fn enqueue_aggregation_task( + &mut self, + request: &AggregationOnlyRequest, + ) -> TaskManagerResult<()>; + + /// Update a specific aggregation tasks progress. + async fn update_aggregation_task_progress( + &mut self, + request: &AggregationOnlyRequest, + status: TaskStatus, + proof: Option<&[u8]>, + ) -> TaskManagerResult<()>; + + /// Returns the latest triplet (status, proof - if any, last update time). + async fn get_aggregation_task_proving_status( + &mut self, + request: &AggregationOnlyRequest, + ) -> TaskManagerResult; + + /// Returns the proof for the given aggregation task. + async fn get_aggregation_task_proof( + &mut self, + request: &AggregationOnlyRequest, + ) -> TaskManagerResult>; } pub fn ensure(expression: bool, message: &str) -> TaskManagerResult<()> { @@ -297,6 +381,68 @@ impl TaskManager for TaskManagerWrapper { TaskManagerInstance::Sqlite(manager) => manager.list_stored_ids().await, } } + + async fn enqueue_aggregation_task( + &mut self, + request: &AggregationOnlyRequest, + ) -> TaskManagerResult<()> { + match &mut self.manager { + TaskManagerInstance::InMemory(ref mut manager) => { + manager.enqueue_aggregation_task(request).await + } + TaskManagerInstance::Sqlite(ref mut manager) => { + manager.enqueue_aggregation_task(request).await + } + } + } + + async fn update_aggregation_task_progress( + &mut self, + request: &AggregationOnlyRequest, + status: TaskStatus, + proof: Option<&[u8]>, + ) -> TaskManagerResult<()> { + match &mut self.manager { + TaskManagerInstance::InMemory(ref mut manager) => { + manager + .update_aggregation_task_progress(request, status, proof) + .await + } + TaskManagerInstance::Sqlite(ref mut manager) => { + manager + .update_aggregation_task_progress(request, status, proof) + .await + } + } + } + + async fn get_aggregation_task_proving_status( + &mut self, + request: &AggregationOnlyRequest, + ) -> TaskManagerResult { + match &mut self.manager { + TaskManagerInstance::InMemory(ref mut manager) => { + manager.get_aggregation_task_proving_status(request).await + } + TaskManagerInstance::Sqlite(ref mut manager) => { + manager.get_aggregation_task_proving_status(request).await + } + } + } + + async fn get_aggregation_task_proof( + &mut self, + request: &AggregationOnlyRequest, + ) -> TaskManagerResult> { + match &mut self.manager { + TaskManagerInstance::InMemory(ref mut manager) => { + manager.get_aggregation_task_proof(request).await + } + TaskManagerInstance::Sqlite(ref mut manager) => { + manager.get_aggregation_task_proof(request).await + } + } + } } pub fn get_task_manager(opts: &TaskManagerOpts) -> TaskManagerWrapper { diff --git a/tasks/src/mem_db.rs b/tasks/src/mem_db.rs index ad6550004..f3bee7883 100644 --- a/tasks/src/mem_db.rs +++ b/tasks/src/mem_db.rs @@ -13,6 +13,7 @@ use std::{ }; use chrono::Utc; +use raiko_core::interfaces::AggregationOnlyRequest; use raiko_lib::prover::{IdStore, IdWrite, ProofKey, ProverError, ProverResult}; use tokio::sync::Mutex; use tracing::{debug, info}; @@ -29,14 +30,16 @@ pub struct InMemoryTaskManager { #[derive(Debug)] pub struct InMemoryTaskDb { - enqueue_task: HashMap, + tasks_queue: HashMap, + aggregation_tasks_queue: HashMap, store: HashMap, } impl InMemoryTaskDb { fn new() -> InMemoryTaskDb { InMemoryTaskDb { - enqueue_task: HashMap::new(), + tasks_queue: HashMap::new(), + aggregation_tasks_queue: HashMap::new(), store: HashMap::new(), } } @@ -44,7 +47,7 @@ impl InMemoryTaskDb { fn enqueue_task(&mut self, key: &TaskDescriptor) { let task_status = (TaskStatus::Registered, None, Utc::now()); - match self.enqueue_task.get(key) { + match self.tasks_queue.get(key) { Some(task_proving_records) => { debug!( "Task already exists: {:?}", @@ -53,7 +56,7 @@ impl InMemoryTaskDb { } // do nothing None => { info!("Enqueue new task: {key:?}"); - self.enqueue_task.insert(key.clone(), vec![task_status]); + self.tasks_queue.insert(key.clone(), vec![task_status]); } } } @@ -64,9 +67,9 @@ impl InMemoryTaskDb { status: TaskStatus, proof: Option<&[u8]>, ) -> TaskManagerResult<()> { - ensure(self.enqueue_task.contains_key(&key), "no task found")?; + ensure(self.tasks_queue.contains_key(&key), "no task found")?; - self.enqueue_task.entry(key).and_modify(|entry| { + self.tasks_queue.entry(key).and_modify(|entry| { if let Some(latest) = entry.last() { if latest.0 != status { entry.push((status, proof.map(hex::encode), Utc::now())); @@ -81,14 +84,14 @@ impl InMemoryTaskDb { &mut self, key: &TaskDescriptor, ) -> TaskManagerResult { - Ok(self.enqueue_task.get(key).cloned().unwrap_or_default()) + Ok(self.tasks_queue.get(key).cloned().unwrap_or_default()) } fn get_task_proof(&mut self, key: &TaskDescriptor) -> TaskManagerResult> { - ensure(self.enqueue_task.contains_key(key), "no task found")?; + ensure(self.tasks_queue.contains_key(key), "no task found")?; let proving_status_records = self - .enqueue_task + .tasks_queue .get(key) .ok_or_else(|| TaskManagerError::SqlError("no task in db".to_owned()))?; @@ -107,20 +110,22 @@ impl InMemoryTaskDb { } fn size(&mut self) -> TaskManagerResult<(usize, Vec<(String, usize)>)> { - Ok((self.enqueue_task.len(), vec![])) + Ok((self.tasks_queue.len(), vec![])) } fn prune(&mut self) -> TaskManagerResult<()> { - self.enqueue_task.clear(); + self.tasks_queue.clear(); Ok(()) } fn list_all_tasks(&mut self) -> TaskManagerResult> { Ok(self - .enqueue_task + .tasks_queue .iter() .flat_map(|(descriptor, statuses)| { - statuses.last().map(|status| (descriptor.clone(), status.0)) + statuses + .last() + .map(|status| (descriptor.clone(), status.0.clone())) }) .collect()) } @@ -145,6 +150,91 @@ impl InMemoryTaskDb { .cloned() .ok_or(TaskManagerError::NoData) } + + fn enqueue_aggregation_task( + &mut self, + request: &AggregationOnlyRequest, + ) -> TaskManagerResult<()> { + let task_status = (TaskStatus::Registered, None, Utc::now()); + + match self.aggregation_tasks_queue.get(request) { + Some(task_proving_records) => { + debug!( + "Task already exists: {:?}", + task_proving_records.last().unwrap().0 + ); + } // do nothing + None => { + info!("Enqueue new task: {request}"); + self.aggregation_tasks_queue + .insert(request.clone(), vec![task_status]); + } + } + Ok(()) + } + + fn get_aggregation_task_proving_status( + &mut self, + request: &AggregationOnlyRequest, + ) -> TaskManagerResult { + Ok(self + .aggregation_tasks_queue + .get(request) + .cloned() + .unwrap_or_default()) + } + + fn update_aggregation_task_progress( + &mut self, + request: &AggregationOnlyRequest, + status: TaskStatus, + proof: Option<&[u8]>, + ) -> TaskManagerResult<()> { + ensure( + self.aggregation_tasks_queue.contains_key(request), + "no task found", + )?; + + self.aggregation_tasks_queue + .entry(request.clone()) + .and_modify(|entry| { + if let Some(latest) = entry.last() { + if latest.0 != status { + entry.push((status, proof.map(hex::encode), Utc::now())); + } + } + }); + + Ok(()) + } + + fn get_aggregation_task_proof( + &mut self, + request: &AggregationOnlyRequest, + ) -> TaskManagerResult> { + ensure( + self.aggregation_tasks_queue.contains_key(request), + "no task found", + )?; + + let proving_status_records = self + .aggregation_tasks_queue + .get(request) + .ok_or_else(|| TaskManagerError::SqlError("no task in db".to_owned()))?; + + let (_, proof, ..) = proving_status_records + .iter() + .filter(|(status, ..)| (status == &TaskStatus::Success)) + .last() + .ok_or_else(|| TaskManagerError::SqlError("no successful task in db".to_owned()))?; + + let Some(proof) = proof else { + return Ok(vec![]); + }; + + hex::decode(proof) + .map_err(|_| TaskManagerError::SqlError("couldn't decode from hex".to_owned())) + } } #[async_trait::async_trait] @@ -248,6 +338,40 @@ impl TaskManager for InMemoryTaskManager { let mut db = self.db.lock().await; db.list_stored_ids() } + + async fn enqueue_aggregation_task( + &mut self, + request: &AggregationOnlyRequest, + ) -> TaskManagerResult<()> { + let mut db = self.db.lock().await; + db.enqueue_aggregation_task(request) + } + + async fn get_aggregation_task_proving_status( + &mut self, + request: &AggregationOnlyRequest, + ) -> TaskManagerResult { + let mut db = self.db.lock().await; + db.get_aggregation_task_proving_status(request) + } + + async fn update_aggregation_task_progress( + &mut self, + request: &AggregationOnlyRequest, + status: TaskStatus, + proof: Option<&[u8]>, + ) -> TaskManagerResult<()> { + let mut db = self.db.lock().await; + db.update_aggregation_task_progress(request, status, proof) + } + + async fn get_aggregation_task_proof( + &mut self, + request: &AggregationOnlyRequest, + ) -> TaskManagerResult> { + let mut db = self.db.lock().await; + db.get_aggregation_task_proof(request) + } } #[cfg(test)]