1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
use snarkvm::prelude::{Address, Field, FromStr, Network, One, Result, ToField, Zero};
/// Implements the SealanceMerkleTree protocol for generating Merkle exclusion proofs
/// for compliant stablecoin programs (USAD/USDCx) on Aleo.
///
/// The exclusion proof proves that a given address is NOT in the freeze list,
/// using a Merkle tree built from freeze list addresses with Poseidon4 hashing.
pub struct SealanceMerkleTree;
impl SealanceMerkleTree {
/// Converts an Aleo blockchain address string to a field element.
///
/// This is the Rust equivalent of the JS `SealanceMerkleTree.convertAddressToField()`.
/// It parses the bech32m-encoded address and extracts its field representation.
///
/// # Arguments
/// * `address` - The Aleo blockchain address (e.g. "aleo1...")
///
/// # Returns
/// A `Field<N>` representing the address.
pub fn convert_address_to_field<N: Network>(address: &str) -> Result<Field<N>> {
let addr = Address::<N>::from_str(address)?;
addr.to_field()
}
/// Converts an array of decimal string representations of field elements to `Field<N>`.
///
/// JS equivalent: `SealanceMerkleTree.convertTreeToBigInt()`.
/// The input is an array of decimal strings from the Provable API's merkle-tree endpoint.
pub fn convert_tree_to_fields<N: Network>(tree: &[String]) -> Result<Vec<Field<N>>> {
tree.iter().map(|element| Field::<N>::from_str(element)).collect()
}
/// Hashes two field elements using Poseidon4 with the given prefix for domain separation.
///
/// JS equivalent: `SealanceMerkleTree.hashTwoElements()`.
/// Prefix is "1field" for leaf-level hashing, "0field" for internal node hashing.
fn hash_two_elements<N: Network>(
prefix: Field<N>,
el1: Field<N>,
el2: Field<N>,
) -> Result<Field<N>> {
N::hash_psd4(&[prefix, el1, el2])
}
/// Generates the leaf field elements from an array of Aleo addresses.
///
/// JS equivalent: `SealanceMerkleTree.generateLeaves()`.
///
/// * Filters out zero addresses.
/// * Converts each address to a field element.
/// * Sorts by field value.
/// * Pads with zero fields to reach the next power of 2.
///
/// # Arguments
/// * `addresses` - Array of Aleo address strings.
/// * `max_tree_depth` - Maximum depth of the Merkle tree (default: 15).
///
/// # Returns
/// A vector of `Field<N>` ready for Merkle tree construction.
pub fn generate_leaves<N: Network>(
addresses: &[String],
max_tree_depth: u32,
) -> Result<Vec<Field<N>>> {
let max_num_leaves = 1 << (max_tree_depth - 1);
// Filter out zero addresses
let addresses: Vec<&String> = addresses
.iter()
.filter(|addr| {
*addr != "aleo1qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq3ljyzc"
})
.collect();
let num_leaves = if addresses.len() <= 1 {
2
} else {
let next_pow2 = (addresses.len() as f64).log2().ceil() as u32;
1 << next_pow2
};
if addresses.len() > max_num_leaves {
return Err(anyhow::anyhow!(
"Leaves limit exceeded. Max: {}, provided: {}",
max_num_leaves,
addresses.len()
));
}
// Convert addresses to fields and sort
let mut address_fields: Vec<Field<N>> = Vec::with_capacity(addresses.len());
for addr in &addresses {
address_fields.push(Self::convert_address_to_field(addr)?);
}
address_fields.sort();
// Pad with zero fields to reach power of 2
let mut full_tree: Vec<Field<N>> = Vec::with_capacity(num_leaves);
for _ in 0..(num_leaves - address_fields.len()) {
full_tree.push(Field::<N>::zero());
}
full_tree.extend(address_fields);
Ok(full_tree)
}
/// Builds a Merkle tree from given leaf field elements using Poseidon4.
///
/// JS equivalent: `SealanceMerkleTree.buildTree()`.
/// The tree is built bottom-up, hashing pairs at each level.
/// Leaf level uses prefix "1field", internal levels use "0field".
///
/// Returns the complete Merkle tree as a flat array (leaves first, then internal nodes).
pub fn build_tree<N: Network>(leaves: &[Field<N>]) -> Result<Vec<Field<N>>> {
if leaves.is_empty() {
return Err(anyhow::anyhow!("Leaves array cannot be empty"));
}
if leaves.len() % 2 != 0 {
return Err(anyhow::anyhow!(
"Leaves array must have even number of elements"
));
}
let leaf_count = leaves.len();
let one = Field::<N>::one(); // "1field"
let zero = Field::<N>::zero(); // "0field"
let mut current_level: Vec<Field<N>> = leaves.to_vec();
let mut tree: Vec<Field<N>> = current_level.clone();
let mut level_size = current_level.len();
while level_size > 1 {
let mut next_level = Vec::with_capacity(level_size / 2);
for i in (0..level_size).step_by(2) {
let left = current_level[i];
let right = current_level[i + 1];
// leaf level uses "1field" prefix, internal levels use "0field"
let prefix = if leaf_count == level_size { one } else { zero };
let hash = Self::hash_two_elements::<N>(prefix, left, right)?;
next_level.push(hash);
}
tree.extend(next_level.clone());
current_level = next_level;
level_size = current_level.len();
}
Ok(tree)
}
/// Finds the leaf indices for a non-inclusion proof of an address.
///
/// JS equivalent: `SealanceMerkleTree.getLeafIndices()`.
/// Returns the indices of the two adjacent leaves that surround the target address.
/// For non-inclusion proof, we need the siblings of these two leaves.
pub fn get_leaf_indices<N: Network>(
tree: &[Field<N>],
address: &str,
) -> Result<(usize, usize)> {
let num_leaves = tree.len().div_ceil(2);
let address_field = Self::convert_address_to_field::<N>(address)?;
let leaves = &tree[..num_leaves];
// Find first leaf >= address_field
let right_leaf_index = leaves.iter().position(|leaf| *leaf >= address_field);
let (left_leaf_index, right_leaf_index) = match right_leaf_index {
None => (num_leaves - 1, num_leaves - 1),
Some(0) => (0, 0),
Some(idx) => (idx - 1, idx),
};
Ok((left_leaf_index, right_leaf_index))
}
/// Generates the sibling path (Merkle proof) for a given leaf index.
///
/// JS equivalent: `SealanceMerkleTree.getSiblingPath()`.
/// Returns the sibling values needed to reconstruct the Merkle root.
pub fn get_sibling_path<N: Network>(
tree: &[Field<N>],
leaf_index: usize,
depth: u32,
) -> Result<Vec<Field<N>>> {
let num_leaves = tree.len().div_ceil(2);
let mut sibling_path: Vec<Field<N>> = Vec::with_capacity(depth as usize);
let mut index = leaf_index;
let mut parent_offset = num_leaves;
let mut level = 1u32;
// Push the leaf itself as the first element
sibling_path.push(tree[index]);
// Walk up through internal nodes
while parent_offset < tree.len() {
let sibling_index = if index % 2 == 0 { index + 1 } else { index - 1 };
sibling_path.push(tree[sibling_index]);
// Calculate the next parent level
index = parent_offset + leaf_index / (1 << level);
parent_offset += num_leaves / (1 << level);
level += 1;
}
// Pad remaining levels with zero fields
while level < depth {
sibling_path.push(Field::<N>::zero());
level += 1;
}
Ok(sibling_path)
}
/// Formats a pair of sibling paths into an Aleo-compatible exclusion proof string.
///
/// JS equivalent: `SealanceMerkleTree.formatMerkleProof()`.
/// Produces output like:
/// `[{ siblings: [0field, 1field, ...], leaf_index: 0u32 }, { siblings: [...], leaf_index: 1u32 }]`
pub fn format_merkle_proof(proofs: &[(Vec<Field<impl Network>>, usize)]) -> String {
let formatted: Vec<String> = proofs
.iter()
.map(|(siblings, leaf_index)| {
let sib_str: Vec<String> = siblings.iter().map(|s| format!("{}", s)).collect();
format!(
"{{siblings: [{}], leaf_index: {}u32}}",
sib_str.join(", "),
leaf_index
)
})
.collect();
format!("[{}]", formatted.join(", "))
}
/// Generates a complete Merkle exclusion proof for a target address against a freeze list.
///
/// This is the main entry point combining all steps into one call.
///
/// # Arguments
/// * `addresses` - Freeze list addresses (from Provable API or local)
/// * `target_address` - The address to prove is NOT on the freeze list
/// * `depth` - Merkle tree depth (default: 15, matching Sealance)
///
/// # Returns
/// A tuple of (left_sibling_path, right_sibling_path, left_leaf_index, right_leaf_index)
#[allow(clippy::type_complexity)]
pub fn generate_exclusion_proof<N: Network>(
addresses: &[String],
target_address: &str,
depth: u32,
) -> Result<(Vec<Field<N>>, Vec<Field<N>>, usize, usize)> {
// 1. Generate leaves
let leaves = Self::generate_leaves::<N>(addresses, depth)?;
// 2. Build tree
let tree = Self::build_tree::<N>(&leaves)?;
// 3. Find leaf indices
let (left_idx, right_idx) = Self::get_leaf_indices::<N>(&tree, target_address)?;
// 4. Get sibling paths
let left_path = Self::get_sibling_path::<N>(&tree, left_idx, depth)?;
let right_path = Self::get_sibling_path::<N>(&tree, right_idx, depth)?;
Ok((left_path, right_path, left_idx, right_idx))
}
}
#[cfg(test)]
mod tests {
use super::*;
use snarkvm::prelude::{Field, TestnetV0};
type N = TestnetV0;
#[test]
fn test_convert_address_to_field() {
let address = "aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px";
let field = SealanceMerkleTree::convert_address_to_field::<N>(address).unwrap();
// The field should be non-zero
assert_ne!(field, Field::<N>::zero());
println!("Address field: {}", field);
}
#[test]
fn test_generate_leaves() {
let addresses = vec![
"aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px".to_string(),
"aleo1s3ws5tra87fjycnjrwsjcrnw2qxr8jfqqdugnf0xzqqw29q9m5pqem2u4t".to_string(),
];
let leaves = SealanceMerkleTree::generate_leaves::<N>(&addresses, 15).unwrap();
// Should have 2 leaves (already power of 2)
assert_eq!(leaves.len(), 2);
println!(
"Leaves: {:?}",
leaves.iter().map(|f| format!("{}", f)).collect::<Vec<_>>()
);
}
#[test]
fn test_build_tree() {
let addresses = vec![
"aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px".to_string(),
"aleo1s3ws5tra87fjycnjrwsjcrnw2qxr8jfqqdugnf0xzqqw29q9m5pqem2u4t".to_string(),
];
let leaves = SealanceMerkleTree::generate_leaves::<N>(&addresses, 15).unwrap();
let tree = SealanceMerkleTree::build_tree::<N>(&leaves).unwrap();
// Tree should have 3 elements: 2 leaves + 1 root
assert_eq!(tree.len(), 3);
println!(
"Tree: {:?}",
tree.iter().map(|f| format!("{}", f)).collect::<Vec<_>>()
);
println!("Root: {}", tree.last().unwrap());
}
#[test]
fn test_get_leaf_indices() {
let addresses = vec![
"aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px".to_string(),
"aleo1s3ws5tra87fjycnjrwsjcrnw2qxr8jfqqdugnf0xzqqw29q9m5pqem2u4t".to_string(),
];
let leaves = SealanceMerkleTree::generate_leaves::<N>(&addresses, 15).unwrap();
let tree = SealanceMerkleTree::build_tree::<N>(&leaves).unwrap();
// Test with a non-freeze-list address
let target = "aleo1kypwp5m7qtk9mwazgcpg0tq8aal23mnrvwfvug65qgcg9xvsrqgspyjm6n";
let (left, right) = SealanceMerkleTree::get_leaf_indices::<N>(&tree, target).unwrap();
println!("Left index: {}, Right index: {}", left, right);
assert!(left <= right);
}
#[test]
fn test_get_sibling_path() {
let addresses = vec![
"aleo1rhgdu77hgyqd3xjj8ucu3jj9r2krwz6mnzyd80gncr5fxcwlh5rsvzp9px".to_string(),
"aleo1s3ws5tra87fjycnjrwsjcrnw2qxr8jfqqdugnf0xzqqw29q9m5pqem2u4t".to_string(),
];
let leaves = SealanceMerkleTree::generate_leaves::<N>(&addresses, 15).unwrap();
let tree = SealanceMerkleTree::build_tree::<N>(&leaves).unwrap();
let path = SealanceMerkleTree::get_sibling_path::<N>(&tree, 0, 15).unwrap();
assert_eq!(path.len(), 15);
println!(
"Sibling path (15 levels): {:?}",
path.iter().map(|f| format!("{}", f)).collect::<Vec<_>>()
);
}
}