asp_membership/lib.rs
1//! ASP Membership Contract
2//!
3//! This contract implements a Merkle tree-based membership system using
4//! Poseidon2 hash function for Anonymous Service Provider (ASP) membership
5//! tracking. The contract maintains a Merkle tree where each leaf represents a
6//! member, and the root serves as a commitment to the entire membership set.
7#![no_std]
8use soroban_sdk::{
9 Address, Env, U256, Vec, contract, contracterror, contractevent, contractimpl, contracttype,
10};
11use soroban_utils::{get_zeroes, poseidon2_compress};
12
13/// Storage keys for contract persistent data
14#[contracttype]
15#[derive(Clone, Debug)]
16enum DataKey {
17 /// Administrator address with permissions to modify the tree
18 Admin,
19 /// Filled subtree hashes at each level (indexed by level)
20 FilledSubtrees(u32),
21 /// Zero hash values for each level (indexed by level)
22 Zeroes(u32),
23 /// Number of levels in the Merkle tree
24 Levels,
25 /// Next available index for leaf insertion
26 NextIndex,
27 /// Current Merkle root
28 Root,
29 /// Whether admin permission is required to insert a leaf
30 AdminInsertOnly,
31}
32
33/// Contract error types
34#[contracterror]
35#[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
36#[repr(u32)]
37pub enum Error {
38 /// Caller is not authorized to perform this operation
39 NotAuthorized = 1,
40 /// Merkle tree has reached maximum capacity
41 MerkleTreeFull = 2,
42 /// Wrong Number of levels specified
43 WrongLevels = 3,
44 /// The contract has not been yet initialized
45 NotInitialized = 4,
46}
47
48/// Event emitted when a new leaf is added to the Merkle tree
49#[contractevent(topics = ["LeafAdded"])]
50struct LeafAddedEvent {
51 /// The leaf value that was inserted
52 leaf: U256,
53 /// Index position where the leaf was inserted
54 index: u64,
55 /// New Merkle root after insertion
56 root: U256,
57}
58
59/// ASP Membership contract
60#[contract]
61pub struct ASPMembership;
62
63#[contractimpl]
64impl ASPMembership {
65 /// Constructor: initialize the ASP Membership contract
66 ///
67 /// Creates a new Merkle tree with the specified number of levels and sets
68 /// the admin address. The tree is initialized with zero hashes at each
69 /// level.
70 ///
71 /// # Arguments
72 /// * `env` - The Soroban environment
73 /// * `admin` - Address of the contract administrator
74 /// * `levels` - Number of levels in the Merkle tree (must be in range
75 /// [1..32])
76 ///
77 /// # Returns
78 /// Returns `Ok(())` on success, or an error if already initialized
79 ///
80 /// # Panics
81 /// Panics if levels is 0 or greater than 32
82 pub fn __constructor(env: Env, admin: Address, levels: u32) -> Result<(), Error> {
83 let store = env.storage().persistent();
84
85 if levels == 0 || levels > 32 {
86 return Err(Error::WrongLevels);
87 }
88
89 // Initialize admin and tree parameters
90 store.set(&DataKey::Admin, &admin);
91 store.set(&DataKey::Levels, &levels);
92 store.set(&DataKey::NextIndex, &0u64);
93 store.set(&DataKey::AdminInsertOnly, &true);
94
95 // Initialize an empty tree with zero hashes at each level
96 let zeros: Vec<U256> = get_zeroes(&env);
97 for lvl in 0..levels + 1 {
98 let zero_val = zeros.get(lvl).unwrap();
99 store.set(&DataKey::FilledSubtrees(lvl), &zero_val);
100 store.set(&DataKey::Zeroes(lvl), &zero_val);
101 }
102
103 // Set initial root to the zero hash at the top level
104 let root_val = zeros.get(levels).unwrap();
105 store.set(&DataKey::Root, &root_val);
106
107 Ok(())
108 }
109
110 /// Update the contract administrator
111 ///
112 /// Changes the admin address to a new address. Only the current admin
113 /// can call this function.
114 ///
115 /// # Arguments
116 /// * `env` - The Soroban environment
117 /// * `new_admin` - Address of the new administrator
118 pub fn update_admin(env: Env, new_admin: Address) -> Result<(), Error> {
119 if !env.storage().persistent().has(&DataKey::Admin) {
120 return Err(Error::NotInitialized);
121 }
122 soroban_utils::update_admin(&env, &DataKey::Admin, &new_admin);
123 Ok(())
124 }
125
126 /// Set whether admin permission is required to insert a leaf
127 ///
128 /// When `admin_only` is true (default), only the admin can insert leaves.
129 /// When false, anyone can insert leaves. Only the admin can change this
130 /// setting.
131 ///
132 /// # Arguments
133 /// * `env` - The Soroban environment
134 /// * `admin_only` - Whether admin permission is required for leaf insertion
135 pub fn set_admin_insert_only(env: Env, admin_only: bool) -> Result<(), Error> {
136 let store = env.storage().persistent();
137 let admin: Address = store.get(&DataKey::Admin).ok_or(Error::NotInitialized)?;
138 admin.require_auth();
139 store.set(&DataKey::AdminInsertOnly, &admin_only);
140 Ok(())
141 }
142
143 /// Get the current Merkle root
144 ///
145 /// Returns the current root hash of the Merkle tree.
146 ///
147 /// # Arguments
148 /// * `env` - The Soroban environment
149 ///
150 /// # Returns
151 /// The current Merkle root as U256
152 ///
153 /// # Panics
154 /// Panics if the contract has not been initialized
155 pub fn get_root(env: Env) -> Result<U256, Error> {
156 env.storage()
157 .persistent()
158 .get(&DataKey::Root)
159 .ok_or(Error::NotInitialized)
160 }
161
162 /// Hash two U256 values using Poseidon2 compression
163 ///
164 /// Computes the Poseidon2 hash of two field elements in compression mode.
165 /// This is the core hashing function used for Merkle tree operations.
166 ///
167 /// # Arguments
168 /// * `env` - The Soroban environment
169 /// * `left` - Left input value
170 /// * `right` - Right input value
171 ///
172 /// # Returns
173 /// The Poseidon2 hash result as U256
174 pub fn hash_pair(env: &Env, left: U256, right: U256) -> U256 {
175 poseidon2_compress(env, left, right)
176 }
177
178 /// Insert a new leaf into the Merkle tree
179 ///
180 /// Adds a new member to the Merkle tree and updates the root. The leaf is
181 /// inserted at the next available index and the tree is updated efficiently
182 /// by only recomputing the hashes along the path to the root. If
183 /// `admin_insert_only` is enabled (the default), only the admin can insert
184 /// leaves; otherwise, anyone can call this function.
185 ///
186 /// # Arguments
187 /// * `env` - The Soroban environment
188 /// * `leaf` - The leaf value to insert (typically a commitment or hash)
189 ///
190 /// # Returns
191 /// Returns `Ok(())` on success, or `MerkleTreeFull` if the tree is at
192 /// capacity
193 pub fn insert_leaf(env: Env, leaf: U256) -> Result<(), Error> {
194 let store = env.storage().persistent();
195 let admin_only: bool = store.get(&DataKey::AdminInsertOnly).unwrap_or(true);
196 if admin_only {
197 let admin: Address = store.get(&DataKey::Admin).unwrap();
198 admin.require_auth();
199 }
200
201 let levels: u32 = store.get(&DataKey::Levels).unwrap();
202 let actual_index: u64 = store.get(&DataKey::NextIndex).unwrap();
203 let mut current_index = actual_index;
204
205 // Check if tree is full (capacity is 2^levels leaves)
206 if current_index >= (1 << levels) {
207 return Err(Error::MerkleTreeFull);
208 }
209 let mut current_hash = leaf.clone();
210
211 // Update tree by recomputing hashes along the path to root
212 for lvl in 0..levels {
213 let is_right = current_index & 1 == 1;
214 if is_right {
215 // Leaf is right child, get the stored left sibling
216 let left: U256 = store.get(&DataKey::FilledSubtrees(lvl)).unwrap();
217 current_hash = poseidon2_compress(&env, left, current_hash);
218 } else {
219 // Leaf is left child, store it and pair with zero hash
220 store.set(&DataKey::FilledSubtrees(lvl), ¤t_hash);
221 let zero_val: U256 = store.get(&DataKey::Zeroes(lvl)).unwrap();
222 current_hash = poseidon2_compress(&env, current_hash, zero_val);
223 }
224 current_index >>= 1;
225 }
226
227 // Update the root with the computed hash
228 store.set(&DataKey::Root, ¤t_hash);
229
230 // Emit event with leaf details
231 LeafAddedEvent {
232 leaf: leaf.clone(),
233 index: actual_index,
234 root: current_hash,
235 }
236 .publish(&env);
237
238 // Update NextIndex
239 store.set(&DataKey::NextIndex, &(actual_index + 1));
240 Ok(())
241 }
242}
243
244mod test;