sale.rs
1 // Copyright (c) 2025 ADnet Contributors 2 // SPDX-License-Identifier: Apache-2.0 3 4 //! DELTA Sale Circuit (F-E31) 5 //! 6 //! Proves ownership of DELTA tokens for atomic swap with synthetic ALPHA. 7 //! 8 //! The circuit enables: 9 //! - Selling DELTA for sALPHA without revealing seller identity 10 //! - Atomic settlement on the DELTA chain 11 //! - Optional change output back to seller 12 13 use crate::{DeltaTokenRecord, TokenCommitment, TokenNullifier}; 14 use anyhow::{ensure, Result}; 15 use serde::{Deserialize, Serialize}; 16 use sha2::{Digest, Sha256}; 17 18 // ============================================================================ 19 // Sale Constants 20 // ============================================================================ 21 22 /// Minimum sale amount (10 DELTA) 23 pub const MIN_SALE_AMOUNT: u64 = 10_000_000; 24 25 /// Maximum sale amount per transaction (1M DELTA) 26 pub const MAX_SALE_AMOUNT: u64 = 1_000_000_000_000; 27 28 // ============================================================================ 29 // Sale Order 30 // ============================================================================ 31 32 /// A DELTA sale order 33 #[derive(Clone, Debug, Serialize, Deserialize)] 34 pub struct SaleOrder { 35 /// DELTA amount being sold 36 pub delta_amount: u64, 37 /// sALPHA amount to receive 38 pub alpha_amount: u64, 39 /// Order expiry block 40 pub expiry_block: u64, 41 /// Order nonce (for uniqueness) 42 pub nonce: u64, 43 } 44 45 impl SaleOrder { 46 /// Create a new sale order 47 pub fn new( 48 delta_amount: u64, 49 alpha_amount: u64, 50 expiry_block: u64, 51 nonce: u64, 52 ) -> Result<Self> { 53 ensure!( 54 delta_amount >= MIN_SALE_AMOUNT, 55 "DELTA amount below minimum" 56 ); 57 ensure!( 58 delta_amount <= MAX_SALE_AMOUNT, 59 "DELTA amount above maximum" 60 ); 61 ensure!(alpha_amount > 0, "ALPHA amount must be positive"); 62 63 Ok(Self { 64 delta_amount, 65 alpha_amount, 66 expiry_block, 67 nonce, 68 }) 69 } 70 71 /// Get order hash (for matching) 72 pub fn order_hash(&self) -> [u8; 32] { 73 let mut hasher = Sha256::new(); 74 hasher.update(b"DELTA_SALE_ORDER"); 75 hasher.update(self.delta_amount.to_le_bytes()); 76 hasher.update(self.alpha_amount.to_le_bytes()); 77 hasher.update(self.expiry_block.to_le_bytes()); 78 hasher.update(self.nonce.to_le_bytes()); 79 80 let hash = hasher.finalize(); 81 let mut result = [0u8; 32]; 82 result.copy_from_slice(&hash); 83 result 84 } 85 86 /// Check if order has expired 87 pub fn is_expired(&self, current_block: u64) -> bool { 88 current_block > self.expiry_block 89 } 90 } 91 92 // ============================================================================ 93 // Sale Proof 94 // ============================================================================ 95 96 /// Zero-knowledge proof of DELTA ownership for sale 97 /// 98 /// Proves seller owns the DELTA being sold without revealing identity. 99 #[derive(Clone, Debug, Serialize, Deserialize)] 100 pub struct SaleProof { 101 /// Input record commitment (DELTA being sold) 102 input_commitment: TokenCommitment, 103 /// Nullifier (marks input as spent) 104 nullifier: TokenNullifier, 105 /// Change commitment (if any DELTA returned to seller) 106 change_commitment: Option<TokenCommitment>, 107 /// Amount being sold (public - needed for order matching) 108 sale_amount: u64, 109 /// Order hash being filled 110 order_hash: [u8; 32], 111 /// The actual ZK proof data 112 proof_data: Vec<u8>, 113 } 114 115 impl SaleProof { 116 /// Get input commitment 117 pub fn input_commitment(&self) -> &TokenCommitment { 118 &self.input_commitment 119 } 120 121 /// Get nullifier 122 pub fn nullifier(&self) -> &TokenNullifier { 123 &self.nullifier 124 } 125 126 /// Get change commitment 127 pub fn change_commitment(&self) -> Option<&TokenCommitment> { 128 self.change_commitment.as_ref() 129 } 130 131 /// Get sale amount 132 pub fn sale_amount(&self) -> u64 { 133 self.sale_amount 134 } 135 136 /// Get order hash 137 pub fn order_hash(&self) -> &[u8; 32] { 138 &self.order_hash 139 } 140 141 /// Verify the proof 142 pub fn verify(&self) -> bool { 143 // 1. Verify commitment is valid 144 if !self.input_commitment.verify() { 145 return false; 146 } 147 148 // 2. Verify nullifier is valid 149 if !self.nullifier.is_valid() { 150 return false; 151 } 152 153 // 3. Verify change commitment if present 154 if let Some(change) = &self.change_commitment { 155 if !change.verify() { 156 return false; 157 } 158 } 159 160 // 4. Verify sale amount is within range 161 if self.sale_amount < MIN_SALE_AMOUNT || self.sale_amount > MAX_SALE_AMOUNT { 162 return false; 163 } 164 165 // 5. In production: verify ZK proof 166 !self.proof_data.is_empty() 167 } 168 } 169 170 // ============================================================================ 171 // Sale Circuit 172 // ============================================================================ 173 174 /// Input to sale circuit 175 #[derive(Clone, Debug)] 176 pub struct SaleInput { 177 /// Record being sold 178 pub record: DeltaTokenRecord, 179 /// Owner secret for nullifier 180 pub owner_secret: u64, 181 /// Total amount in record 182 pub record_amount: u64, 183 } 184 185 /// Circuit for proving DELTA sales 186 pub struct SaleCircuit; 187 188 impl SaleCircuit { 189 /// Create a sale proof 190 /// 191 /// # Arguments 192 /// * `input` - DELTA record being sold (fully or partially) 193 /// * `order` - The sale order being filled 194 /// 195 /// # Returns 196 /// Sale proof and optional change record 197 pub fn prove( 198 input: SaleInput, 199 order: &SaleOrder, 200 ) -> Result<(SaleProof, Option<DeltaTokenRecord>)> { 201 ensure!( 202 input.record_amount >= order.delta_amount, 203 "Insufficient DELTA balance" 204 ); 205 ensure!( 206 order.delta_amount >= MIN_SALE_AMOUNT, 207 "Sale amount below minimum" 208 ); 209 210 // Calculate change 211 let change_amount = input.record_amount - order.delta_amount; 212 213 // Generate nullifier 214 let nullifier = input.record.nullifier(input.owner_secret); 215 216 // Create change record if needed 217 let (change_commitment, change_record) = if change_amount > 0 { 218 let change_randomness = rand::random::<u64>(); 219 // Change goes back to seller (use zero address as placeholder) 220 let change_record = 221 DeltaTokenRecord::new(&[0u8; 32], change_amount, change_randomness)?; 222 ( 223 Some(change_record.commitment().clone()), 224 Some(change_record), 225 ) 226 } else { 227 (None, None) 228 }; 229 230 // Generate proof data 231 let proof_data = Self::generate_proof_data(&input, order)?; 232 233 let proof = SaleProof { 234 input_commitment: input.record.commitment().clone(), 235 nullifier, 236 change_commitment, 237 sale_amount: order.delta_amount, 238 order_hash: order.order_hash(), 239 proof_data, 240 }; 241 242 Ok((proof, change_record)) 243 } 244 245 /// Generate proof data 246 fn generate_proof_data(input: &SaleInput, order: &SaleOrder) -> Result<Vec<u8>> { 247 let mut hasher = Sha256::new(); 248 hasher.update(b"DELTA_SALE_PROOF"); 249 hasher.update(input.record.commitment().as_bytes()); 250 hasher.update(order.order_hash()); 251 hasher.update(input.record_amount.to_le_bytes()); 252 253 let hash = hasher.finalize(); 254 Ok(hash.to_vec()) 255 } 256 } 257 258 // ============================================================================ 259 // Tests 260 // ============================================================================ 261 262 #[cfg(test)] 263 mod tests { 264 use super::*; 265 266 fn create_test_record(amount: u64, secret: u64) -> DeltaTokenRecord { 267 let owner = [0u8; 32]; 268 DeltaTokenRecord::new(&owner, amount, secret).unwrap() 269 } 270 271 #[test] 272 fn test_sale_order_creation() { 273 let order = SaleOrder::new(100_000_000, 50_000_000, 1000, 1).unwrap(); 274 assert_eq!(order.delta_amount, 100_000_000); 275 assert!(!order.is_expired(999)); 276 assert!(order.is_expired(1001)); 277 } 278 279 #[test] 280 fn test_full_sale() { 281 let record = create_test_record(100_000_000, 12345); 282 let order = SaleOrder::new(100_000_000, 50_000_000, 1000, 1).unwrap(); 283 284 let input = SaleInput { 285 record, 286 owner_secret: 12345, 287 record_amount: 100_000_000, 288 }; 289 290 let (proof, change) = SaleCircuit::prove(input, &order).unwrap(); 291 292 assert!(proof.verify()); 293 assert!(change.is_none()); // Full sale, no change 294 assert_eq!(proof.sale_amount(), 100_000_000); 295 } 296 297 #[test] 298 fn test_partial_sale() { 299 let record = create_test_record(200_000_000, 12345); 300 let order = SaleOrder::new(100_000_000, 50_000_000, 1000, 1).unwrap(); 301 302 let input = SaleInput { 303 record, 304 owner_secret: 12345, 305 record_amount: 200_000_000, 306 }; 307 308 let (proof, change) = SaleCircuit::prove(input, &order).unwrap(); 309 310 assert!(proof.verify()); 311 assert!(change.is_some()); // Partial sale, has change 312 assert_eq!(proof.sale_amount(), 100_000_000); 313 } 314 315 #[test] 316 fn test_insufficient_balance_fails() { 317 let record = create_test_record(50_000_000, 12345); 318 let order = SaleOrder::new(100_000_000, 50_000_000, 1000, 1).unwrap(); 319 320 let input = SaleInput { 321 record, 322 owner_secret: 12345, 323 record_amount: 50_000_000, 324 }; 325 326 let result = SaleCircuit::prove(input, &order); 327 assert!(result.is_err()); 328 } 329 }