openvm_recursion_circuit/transcript/merkle_verify/
trace.rs1use core::borrow::BorrowMut;
2
3use itertools::Itertools;
4pub use openvm_poseidon2_air::POSEIDON2_WIDTH;
5use openvm_stark_backend::{keygen::types::MultiStarkVerifyingKey, proof::Proof, SystemParams};
6use openvm_stark_sdk::config::baby_bear_poseidon2::{
7 poseidon2_compress_with_capacity, BabyBearPoseidon2Config, CHUNK, DIGEST_SIZE, F,
8};
9use p3_field::{PrimeCharacteristicRing, PrimeField32};
10
11use crate::{
12 system::Preflight,
13 transcript::merkle_verify::air::{compute_cum_sum, MerkleVerifyCols, MerkleVerifyLog},
14};
15
16#[tracing::instrument(level = "trace", skip_all)]
17pub fn generate_trace(
18 mvk: &MultiStarkVerifyingKey<BabyBearPoseidon2Config>,
19 proofs: &[Proof<BabyBearPoseidon2Config>],
20 preflights: &[Preflight],
21 params: &SystemParams,
22 required_height: Option<usize>,
23) -> Option<(Vec<F>, Vec<[F; POSEIDON2_WIDTH]>)> {
24 let k = params.k_whir();
25 let width = MerkleVerifyCols::<F>::width();
26 let num_leaves: usize = 1 << k;
27 let mut poseidon2_compress_inputs = vec![];
28 let logs_per_proof = preflights
29 .iter()
30 .map(|preflight| build_merkle_logs(preflight, params))
31 .collect_vec();
32 let num_rows_per_proof_per_merkle = preflights
34 .iter()
35 .enumerate()
36 .map(|(proof_idx, _)| {
37 logs_per_proof[proof_idx]
38 .iter()
39 .map(|log| log.depth + num_leaves) .collect_vec()
41 })
42 .collect_vec();
43 let num_rows_cum_sums: Vec<usize> =
45 compute_cum_sum(&num_rows_per_proof_per_merkle, |x| x.iter().sum::<usize>());
46 let num_rows_cum_sums_within_proof: Vec<Vec<usize>> = num_rows_per_proof_per_merkle
48 .iter()
49 .map(|x| compute_cum_sum(x, |y| *y))
50 .collect();
51 let num_valid_rows = *num_rows_cum_sums.last().unwrap();
52 let height = if let Some(height) = required_height {
53 if num_valid_rows > height {
54 return None;
55 }
56 height
57 } else {
58 num_valid_rows.next_power_of_two()
59 };
60 let mut trace = vec![F::ZERO; height * width];
61 let mut cur_hash = [F::ZERO; DIGEST_SIZE];
62 let mut cur_idx = 0;
63
64 let mut leaf_tree = vec![vec![[F::ZERO; DIGEST_SIZE]; num_leaves]; k + 1];
69
70 for (row_idx, row) in trace.chunks_mut(width).take(num_valid_rows).enumerate() {
71 let proof_idx = num_rows_cum_sums.partition_point(|&x| x <= row_idx);
72 let preflight = &preflights[proof_idx];
73 let proof = &proofs[proof_idx];
74 let idx_in_proof = if proof_idx == 0 {
75 row_idx
76 } else {
77 row_idx - num_rows_cum_sums[proof_idx - 1]
78 };
79 let merkle_proof_idx =
80 num_rows_cum_sums_within_proof[proof_idx].partition_point(|&x| x <= idx_in_proof);
81 let i = if merkle_proof_idx == 0 {
83 idx_in_proof
84 } else {
85 idx_in_proof - num_rows_cum_sums_within_proof[proof_idx][merkle_proof_idx - 1]
86 };
87
88 let &MerkleVerifyLog {
89 merkle_idx,
90 depth,
91 query_idx,
92 commit_major,
93 commit_minor,
94 } = &logs_per_proof[proof_idx][merkle_proof_idx];
95
96 if i == 0 {
97 if commit_major == 0 {
98 for (coset_idx, states) in preflight.initial_row_states[commit_minor][query_idx]
99 .iter()
100 .enumerate()
101 {
102 let post_state = states.last().unwrap();
103 leaf_tree[0][coset_idx].copy_from_slice(&post_state[..CHUNK]);
104 }
105 } else {
106 for (coset_idx, state) in preflight.codeword_states[commit_major - 1][query_idx]
107 .iter()
108 .enumerate()
109 {
110 leaf_tree[0][coset_idx].copy_from_slice(&state[..CHUNK]);
111 }
112 }
113 }
114
115 let mut stacking_commits = vec![proof.common_main_commit];
116 for (air_id, data) in &preflight.proof_shape.sorted_trace_vdata {
117 stacking_commits.extend(
118 mvk.inner.per_air[*air_id]
119 .preprocessed_data
120 .as_ref()
121 .into_iter()
122 .map(|pdata| pdata.commit)
123 .chain(data.cached_commitments.iter().cloned()),
124 );
125 }
126
127 let cols: &mut MerkleVerifyCols<F> = row.borrow_mut();
128 let combination_indices = compute_combination_indices(k, i);
133
134 cols.is_valid = F::ONE;
135 cols.proof_idx = F::from_usize(proof_idx);
136 cols.commit_major = F::from_usize(commit_major);
137 cols.commit_minor = F::from_usize(commit_minor);
138 cols.total_depth = F::from_usize(depth + k + 1);
139
140 if i == depth + num_leaves - 1 {
141 cols.is_last_merkle = F::ONE;
142 }
143
144 if let Some(combination_indices) = combination_indices {
145 cols.left =
147 leaf_tree[combination_indices.source_layer][combination_indices.left_source_index];
148 cols.right =
149 leaf_tree[combination_indices.source_layer][combination_indices.right_source_index];
150 let output = poseidon2_compress_with_capacity(cols.left, cols.right).0;
151 leaf_tree[combination_indices.result_layer][combination_indices.result_index] = output;
152 cols.compression_output = output;
153
154 cols.merkle_idx_bit_src = F::from_usize(merkle_idx); cols.current_idx_bit_src = F::from_usize(merkle_idx);
156 cols.height = F::from_usize(combination_indices.source_layer);
157 cols.is_last_leaf = F::from_bool(combination_indices.source_layer + 1 == k);
158 cols.recv_flag = F::TWO;
159
160 let mut input_state = [F::ZERO; POSEIDON2_WIDTH];
161 input_state[..DIGEST_SIZE].copy_from_slice(&cols.left);
162 input_state[DIGEST_SIZE..].copy_from_slice(&cols.right);
163 poseidon2_compress_inputs.push(input_state);
164
165 cols.is_combining_leaves = F::ONE;
166 cols.leaf_sub_idx = F::from_usize(combination_indices.result_index);
167 } else {
168 debug_assert!(i >= num_leaves - 1);
170 if i == num_leaves - 1 {
171 cur_hash = leaf_tree[k][0];
173 cur_idx = merkle_idx;
174 }
175 let pos = i + 1 - num_leaves;
176 let is_last = pos == depth;
177 let whir_proof = &proof.whir_proof;
178 let sibling = match (commit_major, is_last) {
179 (0, true) => stacking_commits[commit_minor],
180 (0, false) => whir_proof.initial_round_merkle_proofs[commit_minor][query_idx][pos],
181 (idx, true) => whir_proof.codeword_commits[idx - 1],
182 (idx, false) => whir_proof.codeword_merkle_proofs[idx - 1][query_idx][pos],
183 };
184
185 if cur_idx % 2 == 0 {
186 cols.left = cur_hash;
187 cols.right = sibling;
188 cols.recv_flag = F::ZERO;
189 } else {
190 cols.left = sibling;
191 cols.right = cur_hash;
192 cols.recv_flag = F::ONE;
193 }
194
195 let output = poseidon2_compress_with_capacity(cols.left, cols.right).0;
196 cols.compression_output = output;
197 let mut input_state = [F::ZERO; POSEIDON2_WIDTH];
198 input_state[..DIGEST_SIZE].copy_from_slice(&cols.left);
199 input_state[DIGEST_SIZE..].copy_from_slice(&cols.right);
200 poseidon2_compress_inputs.push(input_state);
201
202 cur_hash = output;
203 cur_idx /= 2;
204
205 cols.merkle_idx_bit_src = F::from_usize(merkle_idx);
206 cols.current_idx_bit_src = F::from_usize(cur_idx);
207 cols.height = F::from_usize(i + 1 - num_leaves + k);
208 cols.is_combining_leaves = F::ZERO;
209 }
210 }
211
212 Some((trace, poseidon2_compress_inputs))
213}
214
215fn build_merkle_logs(preflight: &Preflight, params: &SystemParams) -> Vec<MerkleVerifyLog> {
216 let num_whir_rounds = params.num_whir_rounds();
217 let mut logs = Vec::new();
218 let mut log_rs_domain_size = params.l_skip + params.n_stack + params.log_blowup;
219 let mut query_offset = 0;
220 for round_idx in 0..num_whir_rounds {
221 let num_queries = params.whir.rounds[round_idx].num_queries;
222 for query_idx in 0..num_queries {
223 let sample = preflight.whir.queries[query_offset];
224 let merkle_idx = sample.as_canonical_u32() as usize;
225 let depth = log_rs_domain_size - params.k_whir();
226
227 if round_idx == 0 {
228 for commit_minor in 0..preflight.initial_row_states.len() {
229 logs.push(MerkleVerifyLog {
230 merkle_idx,
231 depth,
232 query_idx,
233 commit_major: 0,
234 commit_minor,
235 });
236 }
237 } else {
238 logs.push(MerkleVerifyLog {
239 merkle_idx,
240 depth,
241 query_idx,
242 commit_major: round_idx,
243 commit_minor: 0,
244 });
245 }
246
247 query_offset += 1;
248 }
249 log_rs_domain_size -= 1;
250 }
251
252 logs
253}
254
255#[derive(Debug, PartialEq)]
259pub struct CombinationIndices {
260 pub source_layer: usize,
262 pub left_source_index: usize,
264 pub right_source_index: usize,
266 pub result_layer: usize,
268 pub result_index: usize,
270}
271
272pub fn compute_combination_indices(k: usize, i: usize) -> Option<CombinationIndices> {
283 if k == 0 {
284 return None;
286 }
287
288 let total_operations = (1 << k) - 1;
291
292 if i >= total_operations {
293 return None; }
295
296 let mut current_index = i;
297 let mut source_layer = 0;
298
299 while source_layer < k {
305 let exponent = k - (source_layer + 1);
307 let combinations_in_layer = 1 << exponent; if current_index < combinations_in_layer {
310 let index_within_layer = current_index;
312
313 return Some(CombinationIndices {
314 source_layer,
315 left_source_index: 2 * index_within_layer,
317 right_source_index: 2 * index_within_layer + 1,
318 result_layer: source_layer + 1,
320 result_index: index_within_layer,
321 });
322 }
323
324 current_index -= combinations_in_layer;
326 source_layer += 1;
327 }
328
329 None
331}
332
333#[cfg(feature = "cuda")]
334pub mod cuda {
335 use itertools::Itertools;
336 use openvm_cuda_backend::{base::DeviceMatrix, prelude::F};
337 use openvm_cuda_common::{copy::MemCopyH2D, d_buffer::DeviceBuffer, stream::GpuDeviceCtx};
338
339 use super::*;
340 use crate::{
341 cuda::{
342 preflight::PreflightGpu, proof::ProofGpu, types::MerkleVerifyRecord,
343 vk::VerifyingKeyGpu,
344 },
345 transcript::{cuda_abi, cuda_tracegen::TranscriptBlob},
346 };
347
348 const MAX_SUPPORTED_K: usize = 4;
349
350 pub(crate) struct MerkleVerifyBlob {
351 pub records: Vec<MerkleVerifyRecord>,
352 pub leaf_hashes: Vec<F>,
353 pub sibling_hashes: Vec<F>,
354 pub proof_row_starts: Vec<usize>,
355 pub total_rows: usize,
356 pub num_leaves: usize,
357 pub k: usize,
358 pub num_proofs: usize,
359 pub poseidon2_buffer_offset: usize,
360 }
361
362 impl MerkleVerifyBlob {
363 pub fn new(
364 child_vk: &VerifyingKeyGpu,
365 proofs: &[ProofGpu],
366 preflights: &[PreflightGpu],
367 poseidon2_buffer_offset: usize,
368 ) -> Self {
369 assert_eq!(proofs.len(), preflights.len());
370 let k = child_vk.system_params.k_whir();
371 assert!(
372 k <= MAX_SUPPORTED_K,
373 "unsupported k={} for CUDA merkle verify (max {})",
374 k,
375 MAX_SUPPORTED_K
376 );
377 let num_leaves = 1 << k;
378
379 let mut records = Vec::new();
380 let mut leaf_hashes = Vec::new();
381 let mut sibling_hashes = Vec::new();
382 let mut proof_row_starts = Vec::with_capacity(proofs.len());
383
384 let stacking_commits_per_proof = proofs
385 .iter()
386 .zip(preflights)
387 .map(|(proof, preflight)| {
388 build_stacking_commits(&child_vk.cpu, &proof.cpu, &preflight.cpu)
389 })
390 .collect_vec();
391
392 let logs_per_proof: Vec<_> = preflights
393 .iter()
394 .map(|preflight| build_merkle_logs(&preflight.cpu, &child_vk.system_params))
395 .collect();
396
397 let mut total_rows = 0usize;
398 for (proof_idx, proof) in proofs.iter().enumerate() {
399 let preflight = &preflights[proof_idx].cpu;
400 proof_row_starts.push(total_rows);
401 for (merkle_proof_idx, log) in logs_per_proof[proof_idx].iter().enumerate() {
402 let num_rows = log.depth + num_leaves;
403 let leaf_offset = leaf_hashes.len();
404 for coset_idx in 0..num_leaves {
405 if log.commit_major == 0 {
406 let states = &preflight.initial_row_states[log.commit_minor]
407 [log.query_idx][coset_idx];
408 let post_state = states.last().unwrap();
409 leaf_hashes.extend_from_slice(&post_state[..CHUNK]);
410 } else {
411 let state = &preflight.codeword_states[log.commit_major - 1]
412 [log.query_idx][coset_idx];
413 leaf_hashes.extend_from_slice(&state[..CHUNK]);
414 }
415 }
416 let path =
417 build_merkle_path(log, &proof.cpu, &stacking_commits_per_proof[proof_idx]);
418 let sibling_offset = sibling_hashes.len();
419 for sibling in path {
420 sibling_hashes.extend_from_slice(&sibling);
421 }
422 records.push(MerkleVerifyRecord {
423 proof_idx: proof_idx as u16,
424 merkle_proof_idx: merkle_proof_idx as u16,
425 start_row: total_rows as u32,
426 num_rows: num_rows as u32,
427 depth: log.depth as u16,
428 merkle_idx: log.merkle_idx as u32,
429 commit_major: log.commit_major as u16,
430 commit_minor: log.commit_minor as u16,
431 leaf_hash_offset: leaf_offset as u32,
432 siblings_offset: sibling_offset as u32,
433 });
434 total_rows += num_rows;
435 }
436 }
437
438 Self {
439 records,
440 leaf_hashes,
441 sibling_hashes,
442 proof_row_starts,
443 total_rows,
444 num_leaves,
445 k,
446 num_proofs: proofs.len(),
447 poseidon2_buffer_offset,
448 }
449 }
450 }
451
452 #[tracing::instrument(level = "trace", skip_all)]
453 pub(crate) fn generate_trace(
454 blob: &TranscriptBlob,
455 device_ctx: &GpuDeviceCtx,
456 required_height: Option<usize>,
457 ) -> Option<DeviceMatrix<F>> {
458 let merkle_blob = &blob.merkle_verify_blob;
459 let trace_width = MerkleVerifyCols::<F>::width();
460 let trace_height = if let Some(height) = required_height {
461 if height == 0 || merkle_blob.total_rows > height {
462 return None;
463 }
464 height
465 } else {
466 merkle_blob.total_rows.next_power_of_two().max(1)
467 };
468 let mut trace = DeviceMatrix::with_capacity_on(trace_height, trace_width, device_ctx);
469 trace.buffer().fill_zero_on(device_ctx).unwrap();
470
471 if merkle_blob.total_rows == 0 || merkle_blob.records.is_empty() {
472 return Some(trace);
473 }
474
475 let d_records = merkle_blob.records.to_device_on(device_ctx).unwrap();
476 let d_leaf_hashes = merkle_blob.leaf_hashes.to_device_on(device_ctx).unwrap();
477 let d_siblings = merkle_blob.sibling_hashes.to_device_on(device_ctx).unwrap();
478 let d_proof_row_starts: DeviceBuffer<usize> = merkle_blob
479 .proof_row_starts
480 .to_device_on(device_ctx)
481 .unwrap();
482 let scratch_len = merkle_blob.records.len() * merkle_blob.num_leaves * DIGEST_SIZE;
483 let d_leaf_scratch = DeviceBuffer::<F>::with_capacity_on(scratch_len, device_ctx);
484 d_leaf_scratch.fill_zero_on(device_ctx).unwrap();
485
486 unsafe {
487 cuda_abi::merkle_verify_tracegen(
488 &mut trace,
489 &d_records,
490 &d_leaf_hashes,
491 &d_siblings,
492 merkle_blob.num_leaves,
493 merkle_blob.k,
494 &blob.poseidon2_buffer,
495 merkle_blob.poseidon2_buffer_offset,
496 merkle_blob.total_rows,
497 &d_proof_row_starts,
498 merkle_blob.num_proofs,
499 &d_leaf_scratch,
500 device_ctx.stream.as_raw(),
501 )
502 .expect("failed to launch merkle verify tracegen");
503 }
504
505 Some(trace)
506 }
507
508 fn build_stacking_commits(
509 mvk: &MultiStarkVerifyingKey<BabyBearPoseidon2Config>,
510 proof: &Proof<BabyBearPoseidon2Config>,
511 preflight: &Preflight,
512 ) -> Vec<[F; DIGEST_SIZE]> {
513 let mut commits = vec![proof.common_main_commit];
514 for (air_id, data) in &preflight.proof_shape.sorted_trace_vdata {
515 if let Some(pdata) = mvk.inner.per_air[*air_id].preprocessed_data.as_ref() {
516 commits.push(pdata.commit);
517 }
518 commits.extend(data.cached_commitments.iter());
519 }
520 commits
521 }
522
523 fn build_merkle_path(
524 log: &MerkleVerifyLog,
525 proof: &Proof<BabyBearPoseidon2Config>,
526 stacking_commits: &[[F; DIGEST_SIZE]],
527 ) -> Vec<[F; DIGEST_SIZE]> {
528 let whir_proof = &proof.whir_proof;
529 (0..=log.depth)
530 .map(|pos| {
531 let is_last = pos == log.depth;
532 match (log.commit_major, is_last) {
533 (0, true) => stacking_commits[log.commit_minor],
534 (0, false) => {
535 whir_proof.initial_round_merkle_proofs[log.commit_minor][log.query_idx][pos]
536 }
537 (idx, true) => whir_proof.codeword_commits[idx - 1],
538 (idx, false) => whir_proof.codeword_merkle_proofs[idx - 1][log.query_idx][pos],
539 }
540 })
541 .collect()
542 }
543}