circuits/test/utils/transaction.rs
1use zkhash::fields::bn256::FpBN256 as Scalar;
2
3use super::general::poseidon2_hash3;
4
5/// Compute a commitment using Poseidon2 hash
6///
7/// Computes `commitment = Poseidon2(amount, pubkey, blinding)` with
8/// domain separation value 1.
9///
10/// # Arguments
11///
12/// * `amount` - Transaction amount
13/// * `pubkey` - Public key
14/// * `blinding` - Blinding factor
15///
16/// # Returns
17///
18/// Returns the commitment scalar value.
19#[inline]
20pub fn commitment(amount: Scalar, pubkey: Scalar, blinding: Scalar) -> Scalar {
21 poseidon2_hash3(amount, pubkey, blinding, Some(Scalar::from(1))) // We use 1 as domain separation for Commitment
22}
23
24/// Compute a nullifier using Poseidon2 hash
25///
26/// Computes `nullifier = Poseidon2(commitment, pathIndices, signature)` with
27/// domain separation value 2.
28///
29/// # Arguments
30///
31/// * `commitment` - Commitment scalar value
32/// * `path_indices` - Merkle path indices
33/// * `signature` - Signature scalar value
34///
35/// # Returns
36///
37/// Returns the nullifier scalar value.
38#[inline]
39pub(crate) fn nullifier(commitment: Scalar, path_indices: Scalar, signature: Scalar) -> Scalar {
40 poseidon2_hash3(commitment, path_indices, signature, Some(Scalar::from(2))) // We use 2 as domain separation for Nullifier
41}
42
43// --- tiny deterministic RNG (xorshift64) ---
44#[derive(Clone)]
45struct Rng64(u64);
46impl Rng64 {
47 fn new(seed: u64) -> Self {
48 Self(seed)
49 }
50
51 fn next(&mut self) -> u64 {
52 let mut x = self.0;
53 x ^= x << 13;
54 x ^= x >> 7;
55 x ^= x << 17;
56 self.0 = x;
57 x
58 }
59}
60
61/// Generate a random-looking commitment (not tied to a real private key)
62///
63/// Creates a commitment using random values
64/// Suitable for filler leaves in test scenarios.
65///
66/// # Arguments
67///
68/// * `rng` - Mutable reference to the random number generator
69///
70/// # Returns
71///
72/// Returns a randomly generated commitment scalar value.
73fn rand_commitment(rng: &mut Rng64) -> Scalar {
74 let amount = Scalar::from(rng.next() % 1_000_000); // keep small-ish
75 let pubkey = Scalar::from(rng.next());
76 let blinding = Scalar::from(rng.next());
77 // Reuse your commitment function
78 commitment(amount, pubkey, blinding)
79}
80
81/// Build a pre-populated leaves vector of length 2^levels
82///
83/// Creates a vector of leaves for a Merkle tree, pre-populating some positions
84/// with random commitments while excluding specified indices. The excluded
85/// indices are reserved for overwriting with test case inputs.
86///
87/// # Arguments
88///
89/// * `levels` - Number of tree levels
90/// * `seed` - Seed value for the random number generator
91/// * `exclude_indices` - Indices to leave empty (will be overwritten with test
92/// inputs)
93/// * `fill_count` - Number of random commitments to place in the tree for
94/// testing cases
95///
96/// # Returns
97///
98/// Returns a vector of scalar values representing the leaves, with zeros for
99/// empty positions and random commitments for filled positions.
100pub fn prepopulated_leaves(
101 levels: usize,
102 seed: u64,
103 exclude_indices: &[usize],
104 fill_count: usize,
105) -> Vec<Scalar> {
106 let n = 1usize << levels;
107 let mut leaves = vec![Scalar::from(0u64); n];
108
109 let capacity = n.saturating_sub(exclude_indices.len());
110 assert!(
111 fill_count <= capacity,
112 "prepopulated_leaves: fill_count ({fill_count}) exceeds available capacity ({capacity}), causing an infinite loop",
113 );
114
115 let mut rng = Rng64::new(seed);
116 let mut placed = 0usize;
117
118 while placed < fill_count {
119 let idx = usize::try_from(rng.next())
120 .expect("cast to usize failed in prepopulated_leaves")
121 .checked_rem(n)
122 .expect("n must not be zero");
123 if exclude_indices.contains(&idx) || leaves[idx] != Scalar::from(0u64) {
124 continue;
125 }
126
127 leaves[idx] = rand_commitment(&mut rng);
128 placed = placed.checked_add(1).expect("placed counter overflowed");
129 }
130
131 leaves
132}