solana_zk_token_sdk/instruction/transfer/
without_fee.rs

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/// The instruction data that is needed for the `ProofInstruction::VerifyTransfer` instruction.
51///
52/// It includes the cryptographic proof as well as the context data information needed to verify
53/// the proof.
54#[derive(Clone, Copy, Pod, Zeroable)]
55#[repr(C)]
56pub struct TransferData {
57    /// The context data for the transfer proof
58    pub context: TransferProofContext,
59
60    /// Zero-knowledge proofs for Transfer
61    pub proof: TransferProof,
62}
63
64/// The context data needed to verify a transfer proof.
65#[derive(Clone, Copy, Pod, Zeroable)]
66#[repr(C)]
67pub struct TransferProofContext {
68    /// Group encryption of the low 16 bits of the transfer amount
69    pub ciphertext_lo: pod::TransferAmountCiphertext, // 128 bytes
70
71    /// Group encryption of the high 48 bits of the transfer amount
72    pub ciphertext_hi: pod::TransferAmountCiphertext, // 128 bytes
73
74    /// The public encryption keys associated with the transfer: source, dest, and auditor
75    pub transfer_pubkeys: TransferPubkeys, // 96 bytes
76
77    /// The final spendable ciphertext after the transfer
78    pub new_source_ciphertext: pod::ElGamalCiphertext, // 64 bytes
79}
80
81/// The ElGamal public keys needed for a transfer
82#[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        // split and encrypt transfer amount
100        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        // subtract transfer amount from the spendable ciphertext
118        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        // generate transcript and append all public inputs
141        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    /// Extracts the lo ciphertexts associated with a transfer data
172    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    /// Extracts the lo ciphertexts associated with a transfer data
197    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    /// Decrypts transfer amount from transfer data
222    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        // generate transcript and append all public inputs
252        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    /// New Pedersen commitment for the remaining balance in source
295    pub new_source_commitment: pod::PedersenCommitment,
296
297    /// Associated equality proof
298    pub equality_proof: pod::CiphertextCommitmentEqualityProof,
299
300    /// Associated ciphertext validity proof
301    pub validity_proof: pod::BatchedGroupedCiphertext2HandlesValidityProof,
302
303    // Associated range proof
304    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        // generate a Pedersen commitment for the remaining balance in source
320        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        // generate equality_proof
326        let equality_proof = CiphertextCommitmentEqualityProof::new(
327            source_keypair,
328            new_source_ciphertext,
329            &source_opening,
330            source_new_balance,
331            transcript,
332        );
333
334        // generate ciphertext validity proof
335        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        // generate the range proof
343        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        // verify equality proof
406        equality_proof.verify(
407            source_pubkey,
408            ciphertext_new_spendable,
409            &commitment,
410            transcript,
411        )?;
412
413        // verify validity proof
414        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        // verify range proof
432        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        // ElGamalKeypair keys for source, destination, and auditor accounts
478        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        // Case 1: transfer 0 amount
487
488        // create source account spendable ciphertext
489        let spendable_balance: u64 = 0;
490        let spendable_ciphertext = source_keypair.pubkey().encrypt(spendable_balance);
491
492        // transfer amount
493        let transfer_amount: u64 = 0;
494
495        // create transfer data
496        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        // Case 2: transfer max amount
507
508        // create source account spendable ciphertext
509        let spendable_balance: u64 = u64::MAX;
510        let spendable_ciphertext = source_keypair.pubkey().encrypt(spendable_balance);
511
512        // transfer amount
513        let transfer_amount: u64 =
514            (1u64 << (TRANSFER_AMOUNT_LO_BITS + TRANSFER_AMOUNT_HI_BITS)) - 1;
515
516        // create transfer data
517        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        // Case 3: general success case
528
529        // create source account spendable ciphertext
530        let spendable_balance: u64 = 77;
531        let spendable_ciphertext = source_keypair.pubkey().encrypt(spendable_balance);
532
533        // transfer amount
534        let transfer_amount: u64 = 55;
535
536        // create transfer data
537        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        // Case 4: destination pubkey is invalid
548        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        // ElGamalKeypair keys for source, destination, and auditor accounts
570        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        // create source account spendable ciphertext
579        let spendable_balance: u64 = 770000;
580        let spendable_ciphertext = source_keypair.pubkey().encrypt(spendable_balance);
581
582        // transfer amount
583        let transfer_amount: u64 = 550000;
584
585        // create transfer data
586        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}