use std::collections::HashMap;
use super::super::IndexerError;
#[derive(Debug, Clone, PartialEq, Default)]
pub struct OutputQueueData {
pub leaf_indices: Vec<u64>,
pub account_hashes: Vec<[u8; 32]>,
pub old_leaves: Vec<[u8; 32]>,
pub first_queue_index: u64,
pub next_index: u64,
pub leaves_hash_chains: Vec<[u8; 32]>,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct InputQueueData {
pub leaf_indices: Vec<u64>,
pub account_hashes: Vec<[u8; 32]>,
pub current_leaves: Vec<[u8; 32]>,
pub tx_hashes: Vec<[u8; 32]>,
pub nullifiers: Vec<[u8; 32]>,
pub first_queue_index: u64,
pub leaves_hash_chains: Vec<[u8; 32]>,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct StateQueueData {
pub nodes: Vec<u64>,
pub node_hashes: Vec<[u8; 32]>,
pub initial_root: [u8; 32],
pub root_seq: u64,
pub output_queue: Option<OutputQueueData>,
pub input_queue: Option<InputQueueData>,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct AddressQueueData {
pub addresses: Vec<[u8; 32]>,
pub low_element_values: Vec<[u8; 32]>,
pub low_element_next_values: Vec<[u8; 32]>,
pub low_element_indices: Vec<u64>,
pub low_element_next_indices: Vec<u64>,
pub nodes: Vec<u64>,
pub node_hashes: Vec<[u8; 32]>,
pub initial_root: [u8; 32],
pub leaves_hash_chains: Vec<[u8; 32]>,
pub subtrees: Vec<[u8; 32]>,
pub start_index: u64,
pub tree_next_insertion_index: u64,
pub root_seq: u64,
}
impl AddressQueueData {
pub const ADDRESS_TREE_HEIGHT: usize =
light_prover_client::constants::DEFAULT_BATCH_ADDRESS_TREE_HEIGHT as usize;
#[cfg(test)]
fn reconstruct_proof<const HEIGHT: usize>(
&self,
address_idx: usize,
) -> Result<[[u8; 32]; HEIGHT], IndexerError> {
let node_lookup = self.build_node_lookup();
self.reconstruct_proof_with_lookup::<HEIGHT>(address_idx, &node_lookup)
}
pub fn reconstruct_proofs<const HEIGHT: usize>(
&self,
address_range: std::ops::Range<usize>,
) -> Result<Vec<[[u8; 32]; HEIGHT]>, IndexerError> {
self.validate_proof_height::<HEIGHT>()?;
let available = self.proof_count();
if address_range.start > address_range.end {
return Err(IndexerError::InvalidParameters(format!(
"invalid address proof range {}..{}",
address_range.start, address_range.end
)));
}
if address_range.end > available {
return Err(IndexerError::InvalidParameters(format!(
"address proof range {}..{} exceeds available proofs {}",
address_range.start, address_range.end, available
)));
}
let node_lookup = self.build_node_lookup();
let mut proofs = Vec::with_capacity(address_range.len());
for address_idx in address_range {
proofs.push(self.reconstruct_proof_with_lookup::<HEIGHT>(address_idx, &node_lookup)?);
}
Ok(proofs)
}
pub fn reconstruct_all_proofs<const HEIGHT: usize>(
&self,
) -> Result<Vec<[[u8; 32]; HEIGHT]>, IndexerError> {
self.reconstruct_proofs::<HEIGHT>(0..self.addresses.len())
}
fn build_node_lookup(&self) -> HashMap<u64, usize> {
let mut lookup = HashMap::with_capacity(self.nodes.len());
for (idx, node) in self.nodes.iter().copied().enumerate() {
lookup.entry(node).or_insert(idx);
}
lookup
}
fn proof_count(&self) -> usize {
self.addresses.len().min(self.low_element_indices.len())
}
fn reconstruct_proof_with_lookup<const HEIGHT: usize>(
&self,
address_idx: usize,
node_lookup: &HashMap<u64, usize>,
) -> Result<[[u8; 32]; HEIGHT], IndexerError> {
let leaf_index = *self.low_element_indices.get(address_idx).ok_or_else(|| {
IndexerError::MissingResult {
context: "reconstruct_proof".to_string(),
message: format!(
"address_idx {} out of bounds for low_element_indices (len {})",
address_idx,
self.low_element_indices.len(),
),
}
})?;
let mut proof = [[0u8; 32]; HEIGHT];
let mut pos = leaf_index;
for (level, proof_element) in proof.iter_mut().enumerate() {
let sibling_pos = if pos.is_multiple_of(2) {
pos + 1
} else {
pos - 1
};
let sibling_idx = Self::encode_node_index(level, sibling_pos);
let hash_idx = node_lookup.get(&sibling_idx).copied().ok_or_else(|| {
IndexerError::MissingResult {
context: "reconstruct_proof".to_string(),
message: format!(
"Missing proof node at level {} position {} (encoded: {})",
level, sibling_pos, sibling_idx
),
}
})?;
let hash =
self.node_hashes
.get(hash_idx)
.ok_or_else(|| IndexerError::MissingResult {
context: "reconstruct_proof".to_string(),
message: format!(
"node_hashes index {} out of bounds (len {})",
hash_idx,
self.node_hashes.len(),
),
})?;
*proof_element = *hash;
pos /= 2;
}
Ok(proof)
}
#[inline]
fn encode_node_index(level: usize, position: u64) -> u64 {
((level as u64) << 56) | position
}
fn validate_proof_height<const HEIGHT: usize>(&self) -> Result<(), IndexerError> {
if HEIGHT == Self::ADDRESS_TREE_HEIGHT {
return Ok(());
}
Err(IndexerError::InvalidParameters(format!(
"address queue proofs require HEIGHT={} but got HEIGHT={}",
Self::ADDRESS_TREE_HEIGHT,
HEIGHT
)))
}
}
#[cfg(test)]
mod tests {
use std::{collections::BTreeMap, hint::black_box, time::Instant};
use super::AddressQueueData;
fn hash_from_node(node_index: u64) -> [u8; 32] {
let mut hash = [0u8; 32];
hash[..8].copy_from_slice(&node_index.to_le_bytes());
hash[8..16].copy_from_slice(&node_index.rotate_left(17).to_le_bytes());
hash[16..24].copy_from_slice(&node_index.rotate_right(9).to_le_bytes());
hash[24..32].copy_from_slice(&(node_index ^ 0xA5A5_A5A5_A5A5_A5A5).to_le_bytes());
hash
}
fn build_queue_data<const HEIGHT: usize>(num_addresses: usize) -> AddressQueueData {
let low_element_indices = (0..num_addresses)
.map(|i| (i as u64).saturating_mul(2))
.collect::<Vec<_>>();
let mut nodes = BTreeMap::new();
for &leaf_index in &low_element_indices {
let mut pos = leaf_index;
for level in 0..HEIGHT {
let sibling_pos = if pos.is_multiple_of(2) {
pos + 1
} else {
pos - 1
};
let node_index = ((level as u64) << 56) | sibling_pos;
nodes
.entry(node_index)
.or_insert_with(|| hash_from_node(node_index));
pos /= 2;
}
}
let (nodes, node_hashes): (Vec<_>, Vec<_>) = nodes.into_iter().unzip();
AddressQueueData {
addresses: vec![[0u8; 32]; num_addresses],
low_element_values: vec![[1u8; 32]; num_addresses],
low_element_next_values: vec![[2u8; 32]; num_addresses],
low_element_indices,
low_element_next_indices: (0..num_addresses).map(|i| (i as u64) + 1).collect(),
nodes,
node_hashes,
initial_root: [9u8; 32],
leaves_hash_chains: vec![[3u8; 32]; num_addresses.max(1)],
subtrees: vec![[4u8; 32]; HEIGHT],
start_index: 0,
tree_next_insertion_index: 0,
root_seq: 0,
}
}
#[test]
fn batched_reconstruction_matches_individual_reconstruction() {
let queue = build_queue_data::<40>(128);
let expected = (0..queue.addresses.len())
.map(|i| queue.reconstruct_proof::<40>(i).unwrap())
.collect::<Vec<_>>();
let actual = queue
.reconstruct_proofs::<40>(0..queue.addresses.len())
.unwrap();
assert_eq!(actual, expected);
}
#[test]
#[ignore = "profiling helper"]
fn profile_reconstruct_proofs_batch() {
const HEIGHT: usize = 40;
const NUM_ADDRESSES: usize = 2_048;
const ITERS: usize = 25;
let queue = build_queue_data::<HEIGHT>(NUM_ADDRESSES);
let baseline_start = Instant::now();
for _ in 0..ITERS {
let proofs = (0..queue.addresses.len())
.map(|i| queue.reconstruct_proof::<HEIGHT>(i).unwrap())
.collect::<Vec<_>>();
black_box(proofs);
}
let baseline = baseline_start.elapsed();
let batched_start = Instant::now();
for _ in 0..ITERS {
black_box(
queue
.reconstruct_proofs::<HEIGHT>(0..queue.addresses.len())
.unwrap(),
);
}
let batched = batched_start.elapsed();
println!(
"queue reconstruction profile: addresses={}, height={}, iters={}, individual={:?}, batched={:?}, speedup={:.2}x",
NUM_ADDRESSES,
HEIGHT,
ITERS,
baseline,
batched,
baseline.as_secs_f64() / batched.as_secs_f64(),
);
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct QueueElementsResult {
pub state_queue: Option<StateQueueData>,
pub address_queue: Option<AddressQueueData>,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct QueueLeafIndex {
pub hash: [u8; 32],
pub leaf_index: u64,
pub queue_index: u64,
}