/src/zebra-batch-equivalence/src/sprout.rs
Line | Count | Source |
1 | | //! Sprout JoinSplit Groth16 `batch ⟺ single` verification equivalence. |
2 | | //! |
3 | | //! ## Read this before citing anything from this module |
4 | | //! |
5 | | //! **Zebra does not batch-verify Sprout JoinSplits.** `JOINSPLIT_VERIFIER` is a |
6 | | //! bare `tower::service_fn` that calls `Item::verify_single` on each JoinSplit |
7 | | //! (`groth16.rs:83-102`), with the comment *"We just need a Service to use: there |
8 | | //! is no batch verification for JoinSplits"* and a pointer to the upstream issue |
9 | | //! that proposed adding it, [ZcashFoundation/zebra#3127]. |
10 | | //! |
11 | | //! **That issue is closed — `not planned`, 2022-03-15** — so this module does not |
12 | | //! describe it as pending. The reasons given were that most JoinSplits sit below |
13 | | //! the checkpoint verifier and never reach proof verification, so the gain would |
14 | | //! be small; the closing comment redirects to the general performance tracker |
15 | | //! [ZcashFoundation/zebra#3153] with *"can be done if we detect it's a |
16 | | //! bottleneck"*. Upstream's source comment still links #3127 as though open. |
17 | | //! |
18 | | //! So the `batch ⟺ single` disagreement this module looks for **cannot occur in |
19 | | //! Zebra today** — there is no batch to disagree with. What it covers is the |
20 | | //! `bellman::groth16::batch` verifier under Sprout's parameters and real Sprout |
21 | | //! proofs: the code JoinSplit verification would run on if batching were ever |
22 | | //! switched on, and which the grant names as one of its four verifiers. |
23 | | //! |
24 | | //! That makes this the one pool where the oracle runs ahead of the deployment |
25 | | //! rather than beside it — and, since the deployment is not scheduled, ahead of |
26 | | //! a decision rather than of a release. Worth having either way: the gate is |
27 | | //! cheaper to build now than to retrofit if batching is ever revisited. But |
28 | | //! any report sentence that lets a reader think Zebra batches JoinSplits today |
29 | | //! would be false, so this module states it here rather than leaving it to be |
30 | | //! inferred. |
31 | | //! |
32 | | //! ## Two things are reproduced rather than called, and why that is safe |
33 | | //! |
34 | | //! `zebra-consensus` cannot enter this crate's dependency graph — it pulls in |
35 | | //! `zebra-state` and therefore rocksdb — so two pieces of it are reproduced here |
36 | | //! from `groth16.rs`: |
37 | | //! |
38 | | //! * the JoinSplit public-input encoding (`Item::from_joinsplit`, `:150-190`), |
39 | | //! * the `h_sig` hash (`:112-131`), computed with the same crate and version. |
40 | | //! |
41 | | //! Both line ranges are in `zebra-consensus/src/primitives/groth16.rs` **at the |
42 | | //! pinned base revision `f5c5277` (v6.3.0)**, which is what this crate builds |
43 | | //! against. They are stated with the revision because upstream has since |
44 | | //! rewritten that file: on `main`, `Item::from_joinsplit` is gone, replaced by a |
45 | | //! free function `joinsplit_to_item` taking a `zcash_primitives` `JsDescription` |
46 | | //! rather than Zebra's own `sprout::JoinSplit`, and `h_sig` now takes four bare |
47 | | //! `[u8; 32]`. The encoding those functions compute is byte-for-byte the same, so |
48 | | //! what is reproduced below is still what production computes — but a reader who |
49 | | //! follows a bare line number to `main` lands in rewritten code, and a bare |
50 | | //! symbol name does not resolve there at all. |
51 | | //! |
52 | | //! Reproduction is exactly the hazard this project keeps warning about: if the |
53 | | //! encoding were wrong, **both** paths would receive the same wrong public inputs |
54 | | //! and agree on rejecting everything — a green suite proving nothing. The guard |
55 | | //! is that the corpus is real mainnet JoinSplits, which verify only if the |
56 | | //! encoding is right. `real_joinsplits_verify` in `tests/sprout_agreement.rs` is |
57 | | //! therefore not a smoke test; it is what makes every other assertion here mean |
58 | | //! something. |
59 | | //! |
60 | | //! The verifying key is Zebra's own file, vendored byte-for-byte and checked |
61 | | //! against its hash at load time — see [`SproutKeys::bundled`]. |
62 | | //! |
63 | | //! [ZcashFoundation/zebra#3127]: https://github.com/ZcashFoundation/zebra/issues/3127 |
64 | | //! [ZcashFoundation/zebra#3153]: https://github.com/ZcashFoundation/zebra/issues/3153 |
65 | | |
66 | | use bellman::gadgets::multipack; |
67 | | use bellman::groth16::{batch, prepare_verifying_key, PreparedVerifyingKey, Proof, VerifyingKey}; |
68 | | use bls12_381::Bls12; |
69 | | |
70 | | use zebra_chain::primitives::ed25519; |
71 | | use zebra_chain::primitives::Groth16Proof; |
72 | | use zebra_chain::sprout::{JoinSplit, Nullifier, RandomSeed}; |
73 | | use zebra_chain::transaction::Transaction; |
74 | | |
75 | | use crate::seeded_rng; |
76 | | use crate::verifier::BatchVerifier; |
77 | | |
78 | | /// One Sprout verification item: a JoinSplit's Groth16 proof and its primary |
79 | | /// inputs, in the form both `bellman` paths consume. |
80 | | pub struct SproutItem { |
81 | | item: batch::Item<Bls12>, |
82 | | } |
83 | | |
84 | | impl SproutItem { |
85 | | /// The underlying batch item, cloned. Both paths consume the item, so every |
86 | | /// use is a clone. |
87 | 0 | pub fn batch_item(&self) -> batch::Item<Bls12> { |
88 | 0 | self.item.clone() |
89 | 0 | } |
90 | | } |
91 | | |
92 | | /// The Sprout JoinSplit verifying key, in both forms the two paths need. |
93 | | /// |
94 | | /// The batch path takes the raw key; the independent path takes the prepared |
95 | | /// one. Both are derived from the same bytes, so a disagreement between the |
96 | | /// paths can never be blamed on the keys differing. |
97 | | pub struct SproutKeys { |
98 | | vk: VerifyingKey<Bls12>, |
99 | | pvk: PreparedVerifyingKey<Bls12>, |
100 | | } |
101 | | |
102 | | /// Zebra's Sprout verifying key, vendored from |
103 | | /// `zebra-consensus/src/primitives/groth16/sprout-groth16.vk` at the pinned |
104 | | /// revision (`f5c5277f`). Re-hashed at the v6.2.3 -> v6.3.0 bump and found |
105 | | /// byte-identical upstream, so this copy carries over unmodified. |
106 | | /// |
107 | | /// Vendored rather than read from the dependency: `zebra-consensus` is not in |
108 | | /// this crate's graph, and reaching into a cargo git checkout by path would not |
109 | | /// survive a clean clone. 1,828 bytes, unmodified. |
110 | | const SPROUT_VK_BYTES: &[u8] = include_bytes!("../vendor/sprout-groth16.vk"); |
111 | | |
112 | | /// Keyed BLAKE2b-256 digest of [`SPROUT_VK_BYTES`] as vendored, filled in from a |
113 | | /// measured value (see the test below). |
114 | | /// |
115 | | /// Checked at load time so the file cannot be swapped or corrupted silently. |
116 | | /// A verifying key that changed without anyone noticing would not make this |
117 | | /// suite fail — it would make every proof reject, and assertions about |
118 | | /// *equivalence* rather than acceptance stay perfectly green while verifying |
119 | | /// against the wrong key. |
120 | | /// |
121 | | /// BLAKE2b rather than SHA-256 because `blake2b_simd` is already in the graph |
122 | | /// for `h_sig`; a hand-written hash would be one more implementation that can |
123 | | /// be wrong, in a crate whose entire argument is that it does not reimplement |
124 | | /// the things it checks. |
125 | | pub const SPROUT_VK_DIGEST: [u8; 32] = [ |
126 | | 0x36, 0x82, 0x89, 0xf0, 0xee, 0x6f, 0xa0, 0x18, 0xa5, 0xc2, 0x8b, 0xa8, 0x4b, 0x30, 0xb3, 0x58, |
127 | | 0xc8, 0x5a, 0x2e, 0xe9, 0x89, 0x0d, 0xa9, 0x4b, 0xcd, 0xf6, 0xe6, 0xdb, 0xfa, 0xf5, 0x37, 0x9a, |
128 | | ]; |
129 | | |
130 | | impl SproutKeys { |
131 | | /// Zebra's Sprout verifying key, parsed exactly as `SproutParams::default` |
132 | | /// parses it (`groth16/params.rs:28`), after checking the vendored bytes |
133 | | /// against [`SPROUT_VK_DIGEST`]. |
134 | 114 | pub fn bundled() -> &'static Self { |
135 | | use std::sync::OnceLock; |
136 | | static KEYS: OnceLock<SproutKeys> = OnceLock::new(); |
137 | 114 | KEYS.get_or_init(|| { |
138 | 2 | assert_eq!( |
139 | 2 | vk_digest(SPROUT_VK_BYTES), |
140 | | SPROUT_VK_DIGEST, |
141 | 0 | "vendored Sprout verifying key does not match its recorded hash: the file has \ |
142 | 0 | been modified or replaced" |
143 | | ); |
144 | 2 | let vk = VerifyingKey::<Bls12>::read(SPROUT_VK_BYTES) |
145 | 2 | .expect("vendored Sprout verifying key must parse"); |
146 | 2 | let pvk = prepare_verifying_key(&vk); |
147 | 2 | SproutKeys { vk, pvk } |
148 | 2 | }) |
149 | 114 | } |
150 | | } |
151 | | |
152 | | /// Digest of the vendored key bytes, for the integrity check in |
153 | | /// [`SproutKeys::bundled`]. Personalised so the value cannot be confused with |
154 | | /// any other digest of the same bytes. |
155 | 2 | fn vk_digest(data: &[u8]) -> [u8; 32] { |
156 | 2 | blake2b_simd::Params::new() |
157 | 2 | .hash_length(32) |
158 | 2 | .personal(b"zbe-sprout-vk-v1") |
159 | 2 | .hash(data) |
160 | 2 | .as_bytes() |
161 | 2 | .try_into() |
162 | 2 | .expect("32 byte digest") |
163 | 2 | } |
164 | | |
165 | | /// The `h_sig` hash function a JoinSplit's proof commits to |
166 | | /// ([protocol spec §5.4.1.5][hsig]). |
167 | | /// |
168 | | /// Reproduced from `groth16.rs:112-131`, using the same crate at the same |
169 | | /// version, because `zebra-consensus` cannot be a dependency here. |
170 | | /// |
171 | | /// [hsig]: https://zips.z.cash/protocol/protocol.pdf#hsigcrh |
172 | 1.72k | pub fn h_sig( |
173 | 1.72k | random_seed: &RandomSeed, |
174 | 1.72k | nf1: &Nullifier, |
175 | 1.72k | nf2: &Nullifier, |
176 | 1.72k | joinsplit_pub_key: &ed25519::VerificationKeyBytes, |
177 | 1.72k | ) -> [u8; 32] { |
178 | 1.72k | blake2b_simd::Params::new() |
179 | 1.72k | .hash_length(32) |
180 | 1.72k | .personal(b"ZcashComputehSig") |
181 | 1.72k | .to_state() |
182 | 1.72k | .update(&(<[u8; 32]>::from(random_seed))[..]) |
183 | 1.72k | .update(&(<[u8; 32]>::from(nf1))[..]) |
184 | 1.72k | .update(&(<[u8; 32]>::from(nf2))[..]) |
185 | 1.72k | .update(joinsplit_pub_key.as_ref()) |
186 | 1.72k | .finalize() |
187 | 1.72k | .as_bytes() |
188 | 1.72k | .try_into() |
189 | 1.72k | .expect("32 byte array") |
190 | 1.72k | } |
191 | | |
192 | | /// Build the verification item for one JoinSplit, encoding its primary inputs |
193 | | /// exactly as `Item::from_joinsplit` does (`groth16.rs:150-190` at the pinned |
194 | | /// base `f5c5277`; upstream `main` has since renamed it `joinsplit_to_item` and |
195 | | /// changed its input type — see this module's header). |
196 | | /// |
197 | | /// `None` if the proof bytes do not decode — the same fail-closed outcome |
198 | | /// production reaches there, by way of `TransactionError::MalformedGroth16`. |
199 | | /// |
200 | | /// All JoinSplits in a transaction share one validating key, which is why it is |
201 | | /// a separate argument rather than something the JoinSplit carries. |
202 | 1.72k | pub fn item_from_joinsplit( |
203 | 1.72k | joinsplit: &JoinSplit<Groth16Proof>, |
204 | 1.72k | joinsplit_pub_key: &ed25519::VerificationKeyBytes, |
205 | 1.72k | ) -> Option<SproutItem> { |
206 | 1.72k | let rt: [u8; 32] = joinsplit.anchor.into(); |
207 | 1.72k | let mac1: [u8; 32] = (&joinsplit.vmacs[0]).into(); |
208 | 1.72k | let mac2: [u8; 32] = (&joinsplit.vmacs[1]).into(); |
209 | 1.72k | let nf1: [u8; 32] = (&joinsplit.nullifiers[0]).into(); |
210 | 1.72k | let nf2: [u8; 32] = (&joinsplit.nullifiers[1]).into(); |
211 | 1.72k | let cm1: [u8; 32] = (&joinsplit.commitments[0]).into(); |
212 | 1.72k | let cm2: [u8; 32] = (&joinsplit.commitments[1]).into(); |
213 | 1.72k | let vpub_old = joinsplit.vpub_old.to_bytes(); |
214 | 1.72k | let vpub_new = joinsplit.vpub_new.to_bytes(); |
215 | | |
216 | 1.72k | let h_sig = h_sig( |
217 | 1.72k | &joinsplit.random_seed, |
218 | 1.72k | &joinsplit.nullifiers[0], |
219 | 1.72k | &joinsplit.nullifiers[1], |
220 | 1.72k | joinsplit_pub_key, |
221 | | ); |
222 | | |
223 | | // Field order is consensus-critical and matches the reference implementation |
224 | | // (librustzcash `zcash_proofs/src/sprout.rs`), which is what `groth16.rs` |
225 | | // follows. |
226 | 1.72k | let mut public_input = Vec::with_capacity((32 * 8) + (8 * 2)); |
227 | 1.72k | public_input.extend(rt); |
228 | 1.72k | public_input.extend(h_sig); |
229 | 1.72k | public_input.extend(nf1); |
230 | 1.72k | public_input.extend(mac1); |
231 | 1.72k | public_input.extend(nf2); |
232 | 1.72k | public_input.extend(mac2); |
233 | 1.72k | public_input.extend(cm1); |
234 | 1.72k | public_input.extend(cm2); |
235 | 1.72k | public_input.extend(vpub_old); |
236 | 1.72k | public_input.extend(vpub_new); |
237 | | |
238 | 1.72k | let public_input = multipack::bytes_to_bits(&public_input); |
239 | 1.72k | let primary_inputs = multipack::compute_multipacking(&public_input); |
240 | | |
241 | 1.72k | let proof = Proof::read(&joinsplit.zkproof.0[..]).ok()?; |
242 | | |
243 | 1.17k | Some(SproutItem { |
244 | 1.17k | item: batch::Item::from((proof, primary_inputs)), |
245 | 1.17k | }) |
246 | 1.72k | } |
247 | | |
248 | | /// Every Groth16 JoinSplit item in a transaction. |
249 | | /// |
250 | | /// Empty for transactions with no JoinSplits, and for those whose JoinSplits |
251 | | /// carry BCTV14 proofs — pre-Sapling history that no shipping verifier accepts. |
252 | | /// `sprout_groth16_joinsplits` makes that distinction; `joinsplit_count` does |
253 | | /// not, which is how a corpus survey can overstate Sprout material several-fold. |
254 | | /// |
255 | | /// **No shielded-only filter here**, unlike every other pool in this crate: a |
256 | | /// Sprout Groth16 proof is not bound to a sighash, so a transparent input cannot |
257 | | /// invalidate it. (The Ed25519 signature over the same JoinSplit *is* bound to |
258 | | /// one — a different item stream, with a different usability rule.) |
259 | 166 | pub fn items_from_tx(tx: &Transaction) -> Vec<SproutItem> { |
260 | 166 | let Some(pub_key) = tx.sprout_joinsplit_pub_key() else { |
261 | 42 | return Vec::new(); |
262 | | }; |
263 | 124 | tx.sprout_groth16_joinsplits() |
264 | 1.72k | .filter_map(|joinsplit| item_from_joinsplit(joinsplit, &pub_key)) |
265 | 124 | .collect() |
266 | 166 | } |
267 | | |
268 | | /// Sprout JoinSplit Groth16 proofs, driven through `bellman::groth16::batch`. |
269 | | /// |
270 | | /// See the module docs: Zebra verifies these one at a time today, and the issue |
271 | | /// proposing batch support (#3127) was closed as not planned, so the batch side |
272 | | /// of this verifier is a path Zebra's dependency can reach but Zebra does not |
273 | | /// run. |
274 | | pub struct Sprout; |
275 | | |
276 | | impl BatchVerifier for Sprout { |
277 | | type Item = SproutItem; |
278 | | type Context = SproutKeys; |
279 | | const NAME: &'static str = "sprout (JoinSplit Groth16)"; |
280 | | |
281 | | /// All proofs in one batch, one verdict per item. |
282 | | /// |
283 | | /// Every item receives the same verdict: `queue` cannot fail, and batch |
284 | | /// verification is one equation over the whole set. Structurally identical to |
285 | | /// [`crate::redjubjub`], and for the same reason — comparing this against the |
286 | | /// singles position by position would report a false reject for every valid |
287 | | /// proof sharing a batch with a bad one, which is what batch verification |
288 | | /// means rather than a finding. |
289 | 224 | fn validate_batch(items: &[&Self::Item], ctx: &Self::Context, seed: u64) -> Vec<bool> { |
290 | 224 | let mut verifier = batch::Verifier::new(); |
291 | 2.57k | for item in items { |
292 | 2.35k | verifier.queue(item.item.clone()); |
293 | 2.35k | } |
294 | 224 | let shared = verifier.verify(seeded_rng(seed), &ctx.vk).is_ok(); |
295 | 224 | vec![shared; items.len()] |
296 | 224 | } |
297 | | |
298 | | /// One proof through the batch machinery alone — the aggregation path at |
299 | | /// N=1, which is what makes layer 1 a comparison of batch sizes rather than |
300 | | /// of algorithms. |
301 | 1.79k | fn validate_one(item: &Self::Item, ctx: &Self::Context, seed: u64) -> bool { |
302 | 1.79k | let mut verifier = batch::Verifier::new(); |
303 | 1.79k | verifier.queue(item.item.clone()); |
304 | 1.79k | verifier.verify(seeded_rng(seed), &ctx.vk).is_ok() |
305 | 1.79k | } |
306 | | |
307 | | /// The independent path: `batch::Item::verify_single`, which runs |
308 | | /// `bellman::verify_proof` against the prepared key — no randomised linear |
309 | | /// combination, no RNG. |
310 | | /// |
311 | | /// bellman documents it as *"non-batched verification ... useful for |
312 | | /// implementing fallback logic"*, and it is what Zebra calls for every |
313 | | /// JoinSplit today. So, as with RedJubjub, layer 2 here is production code |
314 | | /// rather than a second opinion nobody runs. |
315 | | /// |
316 | | /// The two paths share the primary-input encoding built in |
317 | | /// [`item_from_joinsplit`] and diverge at the verification equation: a |
318 | | /// randomised linear combination checked with one multi-Miller loop, against |
319 | | /// a per-proof pairing check. An error in the encoding is invisible to this |
320 | | /// check, which is why the corpus has to be real proofs that must verify. |
321 | 0 | fn validate_one_independent(item: &Self::Item, ctx: &Self::Context) -> Option<bool> { |
322 | 0 | Some(item.item.clone().verify_single(&ctx.pvk).is_ok()) |
323 | 0 | } |
324 | | } |
325 | | |
326 | | #[cfg(test)] |
327 | | mod tests { |
328 | | use super::*; |
329 | | |
330 | | /// The vendored key parses and still matches its recorded digest. |
331 | | /// |
332 | | /// This is the assertion that makes every other Sprout result meaningful: a |
333 | | /// swapped key would reject every proof, and equivalence assertions would |
334 | | /// stay green throughout. |
335 | | #[test] |
336 | | fn vendored_verifying_key_is_intact() { |
337 | | assert_eq!( |
338 | | vk_digest(SPROUT_VK_BYTES), |
339 | | SPROUT_VK_DIGEST, |
340 | | "vendored Sprout verifying key digest changed" |
341 | | ); |
342 | | assert_eq!(SPROUT_VK_BYTES.len(), 1828); |
343 | | // Forces the parse and the assertion inside `bundled`. |
344 | | let _ = SproutKeys::bundled(); |
345 | | } |
346 | | } |