1#[cfg(not(target_os = "solana"))]
2use {
3 crate::{
4 encryption::{
5 elgamal::{ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey, ElGamalSecretKey},
6 pedersen::{Pedersen, PedersenCommitment, PedersenOpening},
7 },
8 errors::{ProofGenerationError, ProofVerificationError},
9 instruction::{
10 errors::InstructionError,
11 transfer::{
12 encryption::TransferAmountCiphertext, try_combine_lo_hi_ciphertexts, try_split_u64,
13 Role,
14 },
15 },
16 range_proof::RangeProof,
17 sigma_proofs::{
18 batched_grouped_ciphertext_validity_proof::BatchedGroupedCiphertext2HandlesValidityProof,
19 ciphertext_commitment_equality_proof::CiphertextCommitmentEqualityProof,
20 },
21 transcript::TranscriptProtocol,
22 },
23 bytemuck::bytes_of,
24 merlin::Transcript,
25 std::convert::TryInto,
26};
27use {
28 crate::{
29 instruction::{ProofType, ZkProofData},
30 zk_token_elgamal::pod,
31 },
32 bytemuck_derive::{Pod, Zeroable},
33};
34
35#[cfg(not(target_os = "solana"))]
36const TRANSFER_SOURCE_AMOUNT_BITS: usize = 64;
37#[cfg(not(target_os = "solana"))]
38const TRANSFER_AMOUNT_LO_BITS: usize = 16;
39#[cfg(not(target_os = "solana"))]
40const TRANSFER_AMOUNT_LO_NEGATED_BITS: usize = 16;
41#[cfg(not(target_os = "solana"))]
42const TRANSFER_AMOUNT_HI_BITS: usize = 32;
43
44#[cfg(not(target_os = "solana"))]
45lazy_static::lazy_static! {
46 pub static ref COMMITMENT_MAX: PedersenCommitment = Pedersen::encode((1_u64 <<
47 TRANSFER_AMOUNT_LO_NEGATED_BITS) - 1);
48}
49
50#[derive(Clone, Copy, Pod, Zeroable)]
55#[repr(C)]
56pub struct TransferData {
57 pub context: TransferProofContext,
59
60 pub proof: TransferProof,
62}
63
64#[derive(Clone, Copy, Pod, Zeroable)]
66#[repr(C)]
67pub struct TransferProofContext {
68 pub ciphertext_lo: pod::TransferAmountCiphertext, pub ciphertext_hi: pod::TransferAmountCiphertext, pub transfer_pubkeys: TransferPubkeys, pub new_source_ciphertext: pod::ElGamalCiphertext, }
80
81#[derive(Clone, Copy, Pod, Zeroable)]
83#[repr(C)]
84pub struct TransferPubkeys {
85 pub source: pod::ElGamalPubkey,
86 pub destination: pod::ElGamalPubkey,
87 pub auditor: pod::ElGamalPubkey,
88}
89
90#[cfg(not(target_os = "solana"))]
91impl TransferData {
92 #[allow(clippy::too_many_arguments)]
93 pub fn new(
94 transfer_amount: u64,
95 (spendable_balance, ciphertext_old_source): (u64, &ElGamalCiphertext),
96 source_keypair: &ElGamalKeypair,
97 (destination_pubkey, auditor_pubkey): (&ElGamalPubkey, &ElGamalPubkey),
98 ) -> Result<Self, ProofGenerationError> {
99 let (amount_lo, amount_hi) = try_split_u64(transfer_amount, TRANSFER_AMOUNT_LO_BITS)
101 .map_err(|_| ProofGenerationError::IllegalAmountBitLength)?;
102
103 let (ciphertext_lo, opening_lo) = TransferAmountCiphertext::new(
104 amount_lo,
105 source_keypair.pubkey(),
106 destination_pubkey,
107 auditor_pubkey,
108 );
109
110 let (ciphertext_hi, opening_hi) = TransferAmountCiphertext::new(
111 amount_hi,
112 source_keypair.pubkey(),
113 destination_pubkey,
114 auditor_pubkey,
115 );
116
117 let new_spendable_balance = spendable_balance
119 .checked_sub(transfer_amount)
120 .ok_or(ProofGenerationError::NotEnoughFunds)?;
121
122 let transfer_amount_lo_source = ElGamalCiphertext {
123 commitment: *ciphertext_lo.get_commitment(),
124 handle: *ciphertext_lo.get_source_handle(),
125 };
126
127 let transfer_amount_hi_source = ElGamalCiphertext {
128 commitment: *ciphertext_hi.get_commitment(),
129 handle: *ciphertext_hi.get_source_handle(),
130 };
131
132 let new_source_ciphertext = ciphertext_old_source
133 - try_combine_lo_hi_ciphertexts(
134 &transfer_amount_lo_source,
135 &transfer_amount_hi_source,
136 TRANSFER_AMOUNT_LO_BITS,
137 )
138 .map_err(|_| ProofGenerationError::IllegalAmountBitLength)?;
139
140 let pod_transfer_pubkeys = TransferPubkeys {
142 source: (*source_keypair.pubkey()).into(),
143 destination: (*destination_pubkey).into(),
144 auditor: (*auditor_pubkey).into(),
145 };
146 let pod_ciphertext_lo: pod::TransferAmountCiphertext = ciphertext_lo.into();
147 let pod_ciphertext_hi: pod::TransferAmountCiphertext = ciphertext_hi.into();
148 let pod_new_source_ciphertext: pod::ElGamalCiphertext = new_source_ciphertext.into();
149
150 let context = TransferProofContext {
151 ciphertext_lo: pod_ciphertext_lo,
152 ciphertext_hi: pod_ciphertext_hi,
153 transfer_pubkeys: pod_transfer_pubkeys,
154 new_source_ciphertext: pod_new_source_ciphertext,
155 };
156
157 let mut transcript = context.new_transcript();
158 let proof = TransferProof::new(
159 (amount_lo, amount_hi),
160 source_keypair,
161 (destination_pubkey, auditor_pubkey),
162 &opening_lo,
163 &opening_hi,
164 (new_spendable_balance, &new_source_ciphertext),
165 &mut transcript,
166 )?;
167
168 Ok(Self { context, proof })
169 }
170
171 fn ciphertext_lo(&self, role: Role) -> Result<ElGamalCiphertext, InstructionError> {
173 let ciphertext_lo: TransferAmountCiphertext = self
174 .context
175 .ciphertext_lo
176 .try_into()
177 .map_err(|_| InstructionError::Decryption)?;
178
179 let handle_lo = match role {
180 Role::Source => Some(ciphertext_lo.get_source_handle()),
181 Role::Destination => Some(ciphertext_lo.get_destination_handle()),
182 Role::Auditor => Some(ciphertext_lo.get_auditor_handle()),
183 Role::WithdrawWithheldAuthority => None,
184 };
185
186 if let Some(handle) = handle_lo {
187 Ok(ElGamalCiphertext {
188 commitment: *ciphertext_lo.get_commitment(),
189 handle: *handle,
190 })
191 } else {
192 Err(InstructionError::MissingCiphertext)
193 }
194 }
195
196 fn ciphertext_hi(&self, role: Role) -> Result<ElGamalCiphertext, InstructionError> {
198 let ciphertext_hi: TransferAmountCiphertext = self
199 .context
200 .ciphertext_hi
201 .try_into()
202 .map_err(|_| InstructionError::Decryption)?;
203
204 let handle_hi = match role {
205 Role::Source => Some(ciphertext_hi.get_source_handle()),
206 Role::Destination => Some(ciphertext_hi.get_destination_handle()),
207 Role::Auditor => Some(ciphertext_hi.get_auditor_handle()),
208 Role::WithdrawWithheldAuthority => None,
209 };
210
211 if let Some(handle) = handle_hi {
212 Ok(ElGamalCiphertext {
213 commitment: *ciphertext_hi.get_commitment(),
214 handle: *handle,
215 })
216 } else {
217 Err(InstructionError::MissingCiphertext)
218 }
219 }
220
221 pub fn decrypt_amount(
223 &self,
224 role: Role,
225 sk: &ElGamalSecretKey,
226 ) -> Result<u64, InstructionError> {
227 let ciphertext_lo = self.ciphertext_lo(role)?;
228 let ciphertext_hi = self.ciphertext_hi(role)?;
229
230 let amount_lo = ciphertext_lo.decrypt_u32(sk);
231 let amount_hi = ciphertext_hi.decrypt_u32(sk);
232
233 if let (Some(amount_lo), Some(amount_hi)) = (amount_lo, amount_hi) {
234 let two_power = 1 << TRANSFER_AMOUNT_LO_BITS;
235 Ok(amount_lo + two_power * amount_hi)
236 } else {
237 Err(InstructionError::Decryption)
238 }
239 }
240}
241
242impl ZkProofData<TransferProofContext> for TransferData {
243 const PROOF_TYPE: ProofType = ProofType::Transfer;
244
245 fn context_data(&self) -> &TransferProofContext {
246 &self.context
247 }
248
249 #[cfg(not(target_os = "solana"))]
250 fn verify_proof(&self) -> Result<(), ProofVerificationError> {
251 let mut transcript = self.context.new_transcript();
253
254 let source_pubkey = self.context.transfer_pubkeys.source.try_into()?;
255 let destination_pubkey = self.context.transfer_pubkeys.destination.try_into()?;
256 let auditor_pubkey = self.context.transfer_pubkeys.auditor.try_into()?;
257
258 let ciphertext_lo = self.context.ciphertext_lo.try_into()?;
259 let ciphertext_hi = self.context.ciphertext_hi.try_into()?;
260 let new_spendable_ciphertext = self.context.new_source_ciphertext.try_into()?;
261
262 self.proof.verify(
263 &source_pubkey,
264 &destination_pubkey,
265 &auditor_pubkey,
266 &ciphertext_lo,
267 &ciphertext_hi,
268 &new_spendable_ciphertext,
269 &mut transcript,
270 )
271 }
272}
273
274#[allow(non_snake_case)]
275#[cfg(not(target_os = "solana"))]
276impl TransferProofContext {
277 fn new_transcript(&self) -> Transcript {
278 let mut transcript = Transcript::new(b"transfer-proof");
279 transcript.append_message(b"ciphertext-lo", bytes_of(&self.ciphertext_lo));
280 transcript.append_message(b"ciphertext-hi", bytes_of(&self.ciphertext_hi));
281 transcript.append_message(b"transfer-pubkeys", bytes_of(&self.transfer_pubkeys));
282 transcript.append_message(
283 b"new-source-ciphertext",
284 bytes_of(&self.new_source_ciphertext),
285 );
286 transcript
287 }
288}
289
290#[allow(non_snake_case)]
291#[derive(Clone, Copy, Pod, Zeroable)]
292#[repr(C)]
293pub struct TransferProof {
294 pub new_source_commitment: pod::PedersenCommitment,
296
297 pub equality_proof: pod::CiphertextCommitmentEqualityProof,
299
300 pub validity_proof: pod::BatchedGroupedCiphertext2HandlesValidityProof,
302
303 pub range_proof: pod::RangeProofU128,
305}
306
307#[allow(non_snake_case)]
308#[cfg(not(target_os = "solana"))]
309impl TransferProof {
310 pub fn new(
311 (transfer_amount_lo, transfer_amount_hi): (u64, u64),
312 source_keypair: &ElGamalKeypair,
313 (destination_pubkey, auditor_pubkey): (&ElGamalPubkey, &ElGamalPubkey),
314 opening_lo: &PedersenOpening,
315 opening_hi: &PedersenOpening,
316 (source_new_balance, new_source_ciphertext): (u64, &ElGamalCiphertext),
317 transcript: &mut Transcript,
318 ) -> Result<Self, ProofGenerationError> {
319 let (new_source_commitment, source_opening) = Pedersen::new(source_new_balance);
321
322 let pod_new_source_commitment: pod::PedersenCommitment = new_source_commitment.into();
323 transcript.append_commitment(b"commitment-new-source", &pod_new_source_commitment);
324
325 let equality_proof = CiphertextCommitmentEqualityProof::new(
327 source_keypair,
328 new_source_ciphertext,
329 &source_opening,
330 source_new_balance,
331 transcript,
332 );
333
334 let validity_proof = BatchedGroupedCiphertext2HandlesValidityProof::new(
336 (destination_pubkey, auditor_pubkey),
337 (transfer_amount_lo, transfer_amount_hi),
338 (opening_lo, opening_hi),
339 transcript,
340 );
341
342 let range_proof = if TRANSFER_AMOUNT_LO_BITS == 32 {
344 RangeProof::new(
345 vec![source_new_balance, transfer_amount_lo, transfer_amount_hi],
346 vec![
347 TRANSFER_SOURCE_AMOUNT_BITS,
348 TRANSFER_AMOUNT_LO_BITS,
349 TRANSFER_AMOUNT_HI_BITS,
350 ],
351 vec![&source_opening, opening_lo, opening_hi],
352 transcript,
353 )
354 } else {
355 let transfer_amount_lo_negated =
356 (1 << TRANSFER_AMOUNT_LO_NEGATED_BITS) - 1 - transfer_amount_lo;
357 let opening_lo_negated = &PedersenOpening::default() - opening_lo;
358
359 RangeProof::new(
360 vec![
361 source_new_balance,
362 transfer_amount_lo,
363 transfer_amount_lo_negated,
364 transfer_amount_hi,
365 ],
366 vec![
367 TRANSFER_SOURCE_AMOUNT_BITS,
368 TRANSFER_AMOUNT_LO_BITS,
369 TRANSFER_AMOUNT_LO_NEGATED_BITS,
370 TRANSFER_AMOUNT_HI_BITS,
371 ],
372 vec![&source_opening, opening_lo, &opening_lo_negated, opening_hi],
373 transcript,
374 )
375 }?;
376
377 Ok(Self {
378 new_source_commitment: pod_new_source_commitment,
379 equality_proof: equality_proof.into(),
380 validity_proof: validity_proof.into(),
381 range_proof: range_proof
382 .try_into()
383 .map_err(|_| ProofGenerationError::ProofLength)?,
384 })
385 }
386
387 pub fn verify(
388 &self,
389 source_pubkey: &ElGamalPubkey,
390 destination_pubkey: &ElGamalPubkey,
391 auditor_pubkey: &ElGamalPubkey,
392 ciphertext_lo: &TransferAmountCiphertext,
393 ciphertext_hi: &TransferAmountCiphertext,
394 ciphertext_new_spendable: &ElGamalCiphertext,
395 transcript: &mut Transcript,
396 ) -> Result<(), ProofVerificationError> {
397 transcript.append_commitment(b"commitment-new-source", &self.new_source_commitment);
398
399 let commitment: PedersenCommitment = self.new_source_commitment.try_into()?;
400 let equality_proof: CiphertextCommitmentEqualityProof = self.equality_proof.try_into()?;
401 let aggregated_validity_proof: BatchedGroupedCiphertext2HandlesValidityProof =
402 self.validity_proof.try_into()?;
403 let range_proof: RangeProof = self.range_proof.try_into()?;
404
405 equality_proof.verify(
407 source_pubkey,
408 ciphertext_new_spendable,
409 &commitment,
410 transcript,
411 )?;
412
413 aggregated_validity_proof.verify(
415 (destination_pubkey, auditor_pubkey),
416 (
417 ciphertext_lo.get_commitment(),
418 ciphertext_hi.get_commitment(),
419 ),
420 (
421 ciphertext_lo.get_destination_handle(),
422 ciphertext_hi.get_destination_handle(),
423 ),
424 (
425 ciphertext_lo.get_auditor_handle(),
426 ciphertext_hi.get_auditor_handle(),
427 ),
428 transcript,
429 )?;
430
431 let new_source_commitment = self.new_source_commitment.try_into()?;
433 if TRANSFER_AMOUNT_LO_BITS == 32 {
434 range_proof.verify(
435 vec![
436 &new_source_commitment,
437 ciphertext_lo.get_commitment(),
438 ciphertext_hi.get_commitment(),
439 ],
440 vec![
441 TRANSFER_SOURCE_AMOUNT_BITS,
442 TRANSFER_AMOUNT_LO_BITS,
443 TRANSFER_AMOUNT_HI_BITS,
444 ],
445 transcript,
446 )?;
447 } else {
448 let commitment_lo_negated = &(*COMMITMENT_MAX) - ciphertext_lo.get_commitment();
449
450 range_proof.verify(
451 vec![
452 &new_source_commitment,
453 ciphertext_lo.get_commitment(),
454 &commitment_lo_negated,
455 ciphertext_hi.get_commitment(),
456 ],
457 vec![
458 TRANSFER_SOURCE_AMOUNT_BITS,
459 TRANSFER_AMOUNT_LO_BITS,
460 TRANSFER_AMOUNT_LO_NEGATED_BITS,
461 TRANSFER_AMOUNT_HI_BITS,
462 ],
463 transcript,
464 )?;
465 }
466
467 Ok(())
468 }
469}
470
471#[cfg(test)]
472mod test {
473 use {super::*, crate::encryption::elgamal::ElGamalKeypair, bytemuck::Zeroable};
474
475 #[test]
476 fn test_transfer_correctness() {
477 let source_keypair = ElGamalKeypair::new_rand();
479
480 let dest_keypair = ElGamalKeypair::new_rand();
481 let dest_pk = dest_keypair.pubkey();
482
483 let auditor_keypair = ElGamalKeypair::new_rand();
484 let auditor_pk = auditor_keypair.pubkey();
485
486 let spendable_balance: u64 = 0;
490 let spendable_ciphertext = source_keypair.pubkey().encrypt(spendable_balance);
491
492 let transfer_amount: u64 = 0;
494
495 let transfer_data = TransferData::new(
497 transfer_amount,
498 (spendable_balance, &spendable_ciphertext),
499 &source_keypair,
500 (dest_pk, auditor_pk),
501 )
502 .unwrap();
503
504 assert!(transfer_data.verify_proof().is_ok());
505
506 let spendable_balance: u64 = u64::MAX;
510 let spendable_ciphertext = source_keypair.pubkey().encrypt(spendable_balance);
511
512 let transfer_amount: u64 =
514 (1u64 << (TRANSFER_AMOUNT_LO_BITS + TRANSFER_AMOUNT_HI_BITS)) - 1;
515
516 let transfer_data = TransferData::new(
518 transfer_amount,
519 (spendable_balance, &spendable_ciphertext),
520 &source_keypair,
521 (dest_pk, auditor_pk),
522 )
523 .unwrap();
524
525 assert!(transfer_data.verify_proof().is_ok());
526
527 let spendable_balance: u64 = 77;
531 let spendable_ciphertext = source_keypair.pubkey().encrypt(spendable_balance);
532
533 let transfer_amount: u64 = 55;
535
536 let transfer_data = TransferData::new(
538 transfer_amount,
539 (spendable_balance, &spendable_ciphertext),
540 &source_keypair,
541 (dest_pk, auditor_pk),
542 )
543 .unwrap();
544
545 assert!(transfer_data.verify_proof().is_ok());
546
547 let spendable_balance: u64 = 0;
549 let spendable_ciphertext = source_keypair.pubkey().encrypt(spendable_balance);
550 let transfer_amount: u64 = 0;
551
552 let dest_pk = pod::ElGamalPubkey::zeroed().try_into().unwrap();
553 let auditor_keypair = ElGamalKeypair::new_rand();
554 let auditor_pk = auditor_keypair.pubkey();
555
556 let transfer_data = TransferData::new(
557 transfer_amount,
558 (spendable_balance, &spendable_ciphertext),
559 &source_keypair,
560 (&dest_pk, auditor_pk),
561 )
562 .unwrap();
563
564 assert!(transfer_data.verify_proof().is_err());
565 }
566
567 #[test]
568 fn test_source_dest_ciphertext() {
569 let source_keypair = ElGamalKeypair::new_rand();
571 let dest_pk = source_keypair.pubkey();
572 let dest_sk = source_keypair.secret();
573
574 let auditor_keypair = ElGamalKeypair::new_rand();
575 let auditor_pk = auditor_keypair.pubkey();
576 let auditor_sk = auditor_keypair.secret();
577
578 let spendable_balance: u64 = 770000;
580 let spendable_ciphertext = source_keypair.pubkey().encrypt(spendable_balance);
581
582 let transfer_amount: u64 = 550000;
584
585 let transfer_data = TransferData::new(
587 transfer_amount,
588 (spendable_balance, &spendable_ciphertext),
589 &source_keypair,
590 (dest_pk, auditor_pk),
591 )
592 .unwrap();
593
594 assert_eq!(
595 transfer_data
596 .decrypt_amount(Role::Source, source_keypair.secret())
597 .unwrap(),
598 550000_u64,
599 );
600
601 assert_eq!(
602 transfer_data
603 .decrypt_amount(Role::Destination, dest_sk)
604 .unwrap(),
605 550000_u64,
606 );
607
608 assert_eq!(
609 transfer_data
610 .decrypt_amount(Role::Auditor, auditor_sk)
611 .unwrap(),
612 550000_u64,
613 );
614 }
615}