risc0_ethereum_contracts/
set_verifier.rs

1// Copyright 2025 RISC Zero, Inc.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use core::time::Duration;
16
17use crate::{
18    event_query::EventQueryConfig,
19    receipt::{decode_seal, decode_seal_with_claim},
20    IRiscZeroSetVerifier::{self, IRiscZeroSetVerifierErrors, IRiscZeroSetVerifierInstance},
21    IRiscZeroVerifier,
22};
23use alloy::{
24    network::Ethereum,
25    primitives::{Address, Bytes, B256},
26    providers::Provider,
27};
28use anyhow::{bail, Context, Result};
29use risc0_aggregation::{merkle_path_root, GuestState, MerkleMountainRange, SetInclusionReceipt};
30use risc0_zkvm::{
31    sha::{Digest, Digestible},
32    ReceiptClaim,
33};
34
35const TXN_CONFIRM_TIMEOUT: Duration = Duration::from_secs(45);
36
37#[derive(Clone)]
38pub struct SetVerifierService<P> {
39    instance: IRiscZeroSetVerifierInstance<P, Ethereum>,
40    caller: Address,
41    tx_timeout: Duration,
42    event_query_config: EventQueryConfig,
43}
44
45impl<P> SetVerifierService<P>
46where
47    P: Provider<Ethereum> + 'static + Clone,
48{
49    pub fn new(address: Address, provider: P, caller: Address) -> Self {
50        let instance = IRiscZeroSetVerifier::new(address, provider);
51
52        Self {
53            instance,
54            caller,
55            tx_timeout: TXN_CONFIRM_TIMEOUT,
56            event_query_config: EventQueryConfig::default(),
57        }
58    }
59
60    pub fn instance(&self) -> &IRiscZeroSetVerifierInstance<P, Ethereum> {
61        &self.instance
62    }
63
64    pub fn with_timeout(self, tx_timeout: Duration) -> Self {
65        Self { tx_timeout, ..self }
66    }
67
68    /// Sets the event query configuration.
69    pub fn with_event_query_config(self, config: EventQueryConfig) -> Self {
70        Self {
71            event_query_config: config,
72            ..self
73        }
74    }
75
76    pub async fn contains_root(&self, root: B256) -> Result<bool> {
77        tracing::debug!("Calling containsRoot({:?})", root);
78        let call = self.instance.containsRoot(root);
79
80        call.call().await.context("call failed")
81    }
82
83    pub async fn submit_merkle_root(&self, root: B256, seal: Bytes) -> Result<()> {
84        tracing::debug!("Calling submitMerkleRoot({:?},{:?})", root, seal);
85        let call = self.instance.submitMerkleRoot(root, seal).from(self.caller);
86        let pending_tx = call
87            .send()
88            .await
89            .map_err(IRiscZeroSetVerifierErrors::decode_error)?;
90        tracing::debug!("Broadcasting tx {}", pending_tx.tx_hash());
91        let tx_hash = pending_tx
92            .with_timeout(Some(self.tx_timeout))
93            .watch()
94            .await
95            .context("failed to confirm tx")?;
96
97        tracing::info!("Submitted Merkle root {}: {}", root, tx_hash);
98
99        Ok(())
100    }
101
102    pub async fn verify(&self, seal: Bytes, image_id: B256, journal_digest: B256) -> Result<()> {
103        tracing::debug!(
104            "Calling verify({:?},{:?},{:?})",
105            seal,
106            image_id,
107            journal_digest
108        );
109        let verifier = IRiscZeroVerifier::new(
110            *self.instance().address(),
111            self.instance().provider().clone(),
112        );
113        verifier
114            .verify(seal, image_id, journal_digest)
115            .call()
116            .await
117            .map_err(|_| anyhow::anyhow!("Verification failed"))?;
118
119        Ok(())
120    }
121
122    pub async fn image_info(&self) -> Result<(B256, String)> {
123        tracing::debug!("Calling imageInfo()");
124        let (image_id, image_url) = self
125            .instance
126            .imageInfo()
127            .call()
128            .await
129            .context("call failed")?
130            .into();
131
132        Ok((image_id, image_url))
133    }
134
135    /// Returns the seal if of the given verified root.
136    pub async fn fetch_verified_root_seal(&self, root: B256) -> Result<Bytes> {
137        self.query_verified_root_event(root, None, None).await
138    }
139
140    async fn get_latest_block(&self) -> Result<u64> {
141        self.instance
142            .provider()
143            .get_block_number()
144            .await
145            .context("Failed to get latest block number")
146    }
147
148    /// Query the VerifiedRoot event based on the root and block options.
149    ///
150    /// For each iteration, we query a range of blocks. If the event is not found, we move the
151    /// range down and repeat until we find the event. If the event is not found after the
152    /// configured max iterations, we return an error. The default range is set to 100 blocks for
153    /// each iteration, and the default maximum number of iterations is 1000. This means that the
154    /// search will cover a maximum of 100,000 blocks (~14 days at 12s block times). Optionally,
155    /// you can specify a lower and upper bound to limit the search range.
156    async fn query_verified_root_event(
157        &self,
158        root: B256,
159        lower_bound: Option<u64>,
160        upper_bound: Option<u64>,
161    ) -> Result<Bytes> {
162        let mut upper_block = if let Some(upper_bound) = upper_bound {
163            upper_bound
164        } else {
165            self.get_latest_block().await?
166        };
167        let start_block = lower_bound.unwrap_or_else(|| {
168            upper_block.saturating_sub(
169                self.event_query_config.block_range * self.event_query_config.max_iterations,
170            )
171        });
172
173        // Loop to progressively search through blocks
174        for _ in 0..self.event_query_config.max_iterations {
175            // If the current end block is less than the starting block, stop searching
176            if upper_block < start_block {
177                break;
178            }
179
180            // Calculate the block range to query: from [lower_block] to [upper_block]
181            let lower_block = upper_block.saturating_sub(self.event_query_config.block_range);
182
183            // Set up the event filter for the specified block range
184            let mut event_filter = self.instance.VerifiedRoot_filter();
185            event_filter.filter = event_filter
186                .filter
187                .topic1(root)
188                .from_block(lower_block)
189                .to_block(upper_block);
190
191            // Query the logs for the event
192            let logs = event_filter.query().await?;
193
194            // If we find a log, return the seal
195            if let Some((verified_root, _)) = logs.first() {
196                let seal = verified_root.seal.clone();
197                return Ok(seal);
198            }
199            // Move the upper_block down for the next iteration
200            upper_block = lower_block.saturating_sub(1);
201        }
202
203        // Return error if no logs are found after all iterations
204        bail!("VerifiedRoot event not found for root {:?}", root);
205    }
206
207    /// Decodes a seal into a [SetInclusionReceipt] including a [risc0_zkvm::Groth16Receipt] as its
208    /// root.
209    pub async fn fetch_receipt(
210        &self,
211        seal: Bytes,
212        image_id: impl Into<Digest>,
213        journal: impl Into<Vec<u8>>,
214    ) -> Result<SetInclusionReceipt<ReceiptClaim>> {
215        let journal = journal.into();
216        let claim = ReceiptClaim::ok(image_id, journal.clone());
217        self.fetch_receipt_with_claim(seal, claim, journal).await
218    }
219
220    /// Decodes a seal into a [SetInclusionReceipt] including a [risc0_zkvm::Groth16Receipt] as its
221    /// root.
222    pub async fn fetch_receipt_with_claim(
223        &self,
224        seal: Bytes,
225        claim: ReceiptClaim,
226        journal: impl Into<Vec<u8>>,
227    ) -> Result<SetInclusionReceipt<ReceiptClaim>> {
228        let receipt = decode_seal_with_claim(seal, claim.clone(), journal)
229            .map_err(|e| anyhow::anyhow!("Failed to decode seal: {:?}", e))?;
230
231        let set_inclusion_receipt = receipt
232            .set_inclusion_receipt()
233            .ok_or_else(|| anyhow::anyhow!("Seal is not a SetInclusionReceipt"))?;
234
235        let root = merkle_path_root(claim.digest(), &set_inclusion_receipt.merkle_path);
236        let root_seal = self
237            .fetch_verified_root_seal(<[u8; 32]>::from(root).into())
238            .await?;
239
240        let set_builder_id = Digest::from_bytes(self.image_info().await?.0 .0);
241        let state = GuestState {
242            self_image_id: set_builder_id,
243            mmr: MerkleMountainRange::new_finalized(root),
244        };
245        let aggregation_set_journal = state.encode();
246
247        let root_receipt = decode_seal(root_seal, set_builder_id, aggregation_set_journal)?
248            .receipt()
249            .cloned()
250            .ok_or_else(|| anyhow::anyhow!("Failed to decode root seal"))?;
251
252        let receipt = set_inclusion_receipt.clone().with_root(root_receipt);
253
254        Ok(receipt)
255    }
256}