/ token / src / sale.rs
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  }