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), &current_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, &current_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;