Skip to main content

zebra_consensus/primitives/
sapling.rs

1//! Async Sapling batch verifier service
2
3use core::fmt;
4use std::{
5    future::Future,
6    mem,
7    pin::Pin,
8    task::{Context, Poll},
9};
10
11use futures::{future::BoxFuture, FutureExt};
12use once_cell::sync::Lazy;
13use rand::thread_rng;
14use tokio::sync::watch;
15use tower::{util::ServiceFn, Service};
16use tower_batch_control::{Batch, BatchControl, RequestWeight};
17use tower_fallback::Fallback;
18
19use sapling_crypto::{bundle::Authorized, BatchValidator, Bundle};
20use zcash_proofs::prover::LocalTxProver;
21use zcash_protocol::value::ZatBalance;
22use zebra_chain::transaction::SigHash;
23
24use crate::{error::TransactionError, BoxError};
25
26/// Sapling prover containing spend and output params for the Sapling circuit.
27///
28/// Used to:
29///
30/// - construct Sapling outputs in coinbase txs, and
31/// - verify Sapling shielded data in the tx verifier.
32static SAPLING: Lazy<LocalTxProver> = Lazy::new(LocalTxProver::bundled);
33
34#[derive(Clone)]
35pub struct Item {
36    /// The bundle containing the Sapling shielded data to verify.
37    bundle: Bundle<Authorized, ZatBalance>,
38    /// The sighash of the transaction that contains the Sapling shielded data.
39    sighash: SigHash,
40}
41
42impl Item {
43    /// Creates a new [`Item`] from a Sapling bundle and sighash.
44    pub fn new(bundle: Bundle<Authorized, ZatBalance>, sighash: SigHash) -> Self {
45        Self { bundle, sighash }
46    }
47}
48
49impl RequestWeight for Item {}
50
51/// A service that verifies Sapling shielded data in batches.
52///
53/// Handles batching incoming requests, driving batches to completion, and reporting results.
54#[derive(Default)]
55pub struct Verifier {
56    /// A batch verifier for Sapling shielded data.
57    batch: BatchValidator,
58
59    /// A channel for broadcasting the verification result of the batch.
60    ///
61    /// Each batch gets a newly created channel, so there is only ever one result sent per channel.
62    /// Tokio doesn't have a oneshot multi-consumer channel, so we use a watch channel.
63    tx: watch::Sender<Option<bool>>,
64}
65
66impl fmt::Debug for Verifier {
67    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
68        f.debug_struct("Verifier")
69            .field("batch", &"..")
70            .field("tx", &self.tx)
71            .finish()
72    }
73}
74
75impl Drop for Verifier {
76    // Flush the current batch in case there are still any pending futures.
77    //
78    // Flushing the batch means we need to validate it. This function fires off the validation and
79    // returns immediately, usually before the validation finishes.
80    fn drop(&mut self) {
81        let batch = mem::take(&mut self.batch);
82        let tx = mem::take(&mut self.tx);
83
84        // The validation is CPU-intensive; do it on a dedicated thread so it does not block.
85        rayon::spawn_fifo(move || {
86            let (spend_vk, output_vk) = SAPLING.verifying_keys();
87
88            // Validate the batch and send the result through the channel.
89            let res = batch.validate(&spend_vk, &output_vk, thread_rng());
90            let _ = tx.send(Some(res));
91        });
92    }
93}
94
95impl Service<BatchControl<Item>> for Verifier {
96    type Response = ();
97    type Error = Box<dyn std::error::Error + Send + Sync>;
98    type Future = Pin<Box<dyn Future<Output = Result<(), Self::Error>> + Send>>;
99
100    fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
101        Poll::Ready(Ok(()))
102    }
103
104    fn call(&mut self, req: BatchControl<Item>) -> Self::Future {
105        match req {
106            BatchControl::Item(item) => {
107                let mut rx = self.tx.subscribe();
108
109                let bundle_check = self
110                    .batch
111                    .check_bundle(item.bundle, item.sighash.into())
112                    .then_some(())
113                    .ok_or(TransactionError::SaplingVerificationFailed);
114
115                async move {
116                    bundle_check.map_err(BoxError::from)?;
117
118                    rx.changed()
119                        .await
120                        .map_err(|_| BoxError::from("verifier was dropped without flushing"))?;
121
122                    // We use a new channel for each batch, so we always get the correct
123                    // batch result here.
124                    let is_valid = rx.borrow().ok_or_else(|| {
125                        BoxError::from("threadpool unexpectedly dropped channel sender")
126                    })?;
127
128                    if is_valid {
129                        metrics::counter!("proofs.sapling.verified").increment(1);
130                        Ok(())
131                    } else {
132                        metrics::counter!("proofs.sapling.invalid").increment(1);
133                        Err(BoxError::from(TransactionError::SaplingVerificationFailed))
134                    }
135                }
136                .boxed()
137            }
138
139            BatchControl::Flush => {
140                let batch = mem::take(&mut self.batch);
141                let tx = mem::take(&mut self.tx);
142
143                async move {
144                    let start = std::time::Instant::now();
145                    let spawn_result = tokio::task::spawn_blocking(move || {
146                        let (spend_vk, output_vk) = SAPLING.verifying_keys();
147                        batch.validate(&spend_vk, &output_vk, thread_rng())
148                    })
149                    .await;
150                    let duration = start.elapsed().as_secs_f64();
151
152                    let result_label = match &spawn_result {
153                        Ok(true) => "success",
154                        _ => "failure",
155                    };
156                    metrics::histogram!(
157                        "zebra.consensus.batch.duration_seconds",
158                        "verifier" => "groth16_sapling",
159                        "result" => result_label
160                    )
161                    .record(duration);
162
163                    // Extract the value before consuming spawn_result
164                    let is_valid = spawn_result.as_ref().ok().copied();
165                    let _ = tx.send(is_valid);
166                    spawn_result.map(|_| ()).map_err(Self::Error::from)
167                }
168                .boxed()
169            }
170        }
171    }
172}
173
174/// Verifies a single [`Item`].
175pub fn verify_single(
176    item: Item,
177) -> Pin<Box<dyn Future<Output = Result<(), Box<dyn std::error::Error + Send + Sync>>> + Send>> {
178    async move {
179        let mut verifier = Verifier::default();
180
181        let check = verifier
182            .batch
183            .check_bundle(item.bundle, item.sighash.into())
184            .then_some(())
185            .ok_or(TransactionError::SaplingVerificationFailed);
186        check.map_err(BoxError::from)?;
187
188        let is_valid = tokio::task::spawn_blocking(move || {
189            let (spend_vk, output_vk) = SAPLING.verifying_keys();
190
191            mem::take(&mut verifier.batch).validate(&spend_vk, &output_vk, thread_rng())
192        })
193        .await
194        .map_err(|_| BoxError::from("Sapling bundle validation thread panicked"))?;
195
196        if is_valid {
197            Ok(())
198        } else {
199            Err(BoxError::from(TransactionError::SaplingVerificationFailed))
200        }
201    }
202    .boxed()
203}
204
205/// Global batch verification context for Sapling shielded data.
206pub static VERIFIER: Lazy<
207    Fallback<
208        Batch<Verifier, Item>,
209        ServiceFn<
210            fn(Item) -> BoxFuture<'static, Result<(), Box<dyn std::error::Error + Send + Sync>>>,
211        >,
212    >,
213> = Lazy::new(|| {
214    Fallback::new(
215        Batch::new(
216            Verifier::default(),
217            super::MAX_BATCH_SIZE,
218            None,
219            super::MAX_BATCH_LATENCY,
220        ),
221        tower::service_fn(verify_single),
222    )
223});