Files
solana/zk-token-sdk/src/instruction/transfer.rs
2022-01-06 11:18:06 -05:00

562 lines
19 KiB
Rust

use {
crate::zk_token_elgamal::pod,
bytemuck::{Pod, Zeroable},
};
#[cfg(not(target_arch = "bpf"))]
use {
crate::{
encryption::{
discrete_log::*,
elgamal::{ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey, ElGamalSecretKey},
pedersen::{Pedersen, PedersenCommitment, PedersenDecryptHandle, PedersenOpening},
},
errors::ProofError,
instruction::{Role, Verifiable},
range_proof::RangeProof,
sigma_proofs::{equality_proof::EqualityProof, validity_proof::ValidityProof},
transcript::TranscriptProtocol,
},
curve25519_dalek::scalar::Scalar,
merlin::Transcript,
std::convert::TryInto,
};
#[derive(Clone, Copy, Pod, Zeroable)]
#[repr(C)]
pub struct TransferData {
/// The encrypted transfer amount
pub encrypted_transfer_amount: EncryptedTransferAmount,
/// The public encryption keys associated with the transfer: source, dest, and auditor
pub transfer_public_keys: TransferPubkeys, // 128 bytes
/// The final spendable ciphertext after the transfer
pub new_spendable_ct: pod::ElGamalCiphertext, // 64 bytes
// pub fee: EncryptedTransferFee,
/// Zero-knowledge proofs for Transfer
pub proof: TransferProof,
}
#[derive(Clone, Copy)]
#[repr(C)]
pub struct FeeParameters {
/// Fee rate expressed as basis points of the transfer amount, i.e. increments of 0.01%
pub fee_rate_basis_points: u16,
/// Maximum fee assessed on transfers, expressed as an amount of tokens
pub maximum_fee: u64,
}
#[allow(dead_code)]
fn calculate_fee(transfer_amount: u64, fee_parameters: FeeParameters) -> u64 {
// TODO: temporary way to calculate fees for now. Should account for overflows/compiler
// optimizations
let fee = (transfer_amount * (fee_parameters.fee_rate_basis_points as u64)) / 10000;
if fee % 10000 > 0 {
fee + 1
} else {
fee
}
}
#[cfg(not(target_arch = "bpf"))]
impl TransferData {
#[allow(clippy::too_many_arguments)]
pub fn new(
// amount of the transfer
transfer_amount: u64,
// available balance in the source account as u64
spendable_balance: u64,
// available balance in the source account as ElGamalCiphertext
spendable_balance_ciphertext: ElGamalCiphertext,
// source account ElGamal keypair
source_keypair: &ElGamalKeypair,
// destination account ElGamal pubkey
dest_pk: ElGamalPubkey,
// auditor ElGamal pubkey
auditor_pk: ElGamalPubkey,
// // fee collector ElGamal pubkey
// fee_collector_pk: ElGamalPubkey,
// // fee rate and cap value
// fee_parameters: FeeParameters,
) -> Self {
// split and encrypt transfer amount
let (amount_lo, amount_hi) = split_u64_into_u32(transfer_amount);
let (comm_lo, open_lo) = Pedersen::new(amount_lo);
let (comm_hi, open_hi) = Pedersen::new(amount_hi);
let handle_source_lo = source_keypair.public.decrypt_handle(&open_lo);
let handle_dest_lo = dest_pk.decrypt_handle(&open_lo);
let handle_auditor_lo = auditor_pk.decrypt_handle(&open_lo);
let handle_source_hi = source_keypair.public.decrypt_handle(&open_hi);
let handle_dest_hi = dest_pk.decrypt_handle(&open_hi);
let handle_auditor_hi = auditor_pk.decrypt_handle(&open_hi);
// organize transfer amount commitments and decrypt handles
let decrypt_handles_lo = TransferDecryptHandles {
source: handle_source_lo.into(),
dest: handle_dest_lo.into(),
auditor: handle_auditor_lo.into(),
};
let decrypt_handles_hi = TransferDecryptHandles {
source: handle_source_hi.into(),
dest: handle_dest_hi.into(),
auditor: handle_auditor_hi.into(),
};
let encrypted_transfer_amount = EncryptedTransferAmount {
amount_comm_lo: comm_lo.into(),
amount_comm_hi: comm_hi.into(),
decrypt_handles_lo,
decrypt_handles_hi,
};
// group public keys for transfer
let transfer_public_keys = TransferPubkeys {
source_pk: source_keypair.public.into(),
dest_pk: dest_pk.into(),
auditor_pk: auditor_pk.into(),
};
// subtract transfer amount from the spendable ciphertext
let spendable_comm = spendable_balance_ciphertext.message_comm;
let spendable_handle = spendable_balance_ciphertext.decrypt_handle;
let new_spendable_balance = spendable_balance - transfer_amount;
let new_spendable_comm = spendable_comm - combine_u32_comms(comm_lo, comm_hi);
let new_spendable_handle =
spendable_handle - combine_u32_handles(handle_source_lo, handle_source_hi);
let new_spendable_ct = ElGamalCiphertext {
message_comm: new_spendable_comm,
decrypt_handle: new_spendable_handle,
};
// range_proof and validity_proof should be generated together
let proof = TransferProof::new(
source_keypair,
&dest_pk,
&auditor_pk,
(amount_lo as u64, amount_hi as u64),
(&open_lo, &open_hi),
new_spendable_balance,
&new_spendable_ct,
);
Self {
encrypted_transfer_amount,
new_spendable_ct: new_spendable_ct.into(),
transfer_public_keys,
proof,
}
}
/// Extracts the lo ciphertexts associated with a transfer data
fn ciphertext_lo(&self, role: Role) -> Result<ElGamalCiphertext, ProofError> {
let transfer_comm_lo: PedersenCommitment =
self.encrypted_transfer_amount.amount_comm_lo.try_into()?;
let decryption_handle_lo = match role {
Role::Source => self.encrypted_transfer_amount.decrypt_handles_lo.source,
Role::Dest => self.encrypted_transfer_amount.decrypt_handles_lo.dest,
Role::Auditor => self.encrypted_transfer_amount.decrypt_handles_lo.auditor,
}
.try_into()?;
Ok((transfer_comm_lo, decryption_handle_lo).into())
}
/// Extracts the lo ciphertexts associated with a transfer data
fn ciphertext_hi(&self, role: Role) -> Result<ElGamalCiphertext, ProofError> {
let transfer_comm_hi: PedersenCommitment =
self.encrypted_transfer_amount.amount_comm_hi.try_into()?;
let decryption_handle_hi = match role {
Role::Source => self.encrypted_transfer_amount.decrypt_handles_hi.source,
Role::Dest => self.encrypted_transfer_amount.decrypt_handles_hi.dest,
Role::Auditor => self.encrypted_transfer_amount.decrypt_handles_hi.auditor,
}
.try_into()?;
Ok((transfer_comm_hi, decryption_handle_hi).into())
}
/// Decrypts transfer amount from transfer data
///
/// TODO: This function should run in constant time. Use `subtle::Choice` for the if statement
/// and make sure that the function does not terminate prematurely due to errors
///
/// TODO: Define specific error type for decryption error
pub fn decrypt_amount(&self, role: Role, sk: &ElGamalSecretKey) -> Result<u64, ProofError> {
let ciphertext_lo = self.ciphertext_lo(role)?;
let ciphertext_hi = self.ciphertext_hi(role)?;
let amount_lo = ciphertext_lo.decrypt_u32_online(sk, &DECODE_U32_PRECOMPUTATION_FOR_G);
let amount_hi = ciphertext_hi.decrypt_u32_online(sk, &DECODE_U32_PRECOMPUTATION_FOR_G);
if let (Some(amount_lo), Some(amount_hi)) = (amount_lo, amount_hi) {
Ok((amount_lo as u64) + (TWO_32 * amount_hi as u64))
} else {
Err(ProofError::Verification)
}
}
}
#[cfg(not(target_arch = "bpf"))]
impl Verifiable for TransferData {
fn verify(&self) -> Result<(), ProofError> {
let transfer_commitments = TransferCommitments {
lo: self.encrypted_transfer_amount.amount_comm_lo,
hi: self.encrypted_transfer_amount.amount_comm_hi,
};
self.proof.verify(
&transfer_commitments,
&self.encrypted_transfer_amount.decrypt_handles_lo,
&self.encrypted_transfer_amount.decrypt_handles_hi,
&self.new_spendable_ct,
&self.transfer_public_keys,
)
}
}
#[allow(non_snake_case)]
#[derive(Clone, Copy, Pod, Zeroable)]
#[repr(C)]
pub struct TransferProof {
/// New Pedersen commitment for the remaining balance in source
pub source_commitment: pod::PedersenCommitment,
/// Associated equality proof
pub equality_proof: pod::EqualityProof,
/// Associated ciphertext validity proof
pub validity_proof: pod::ValidityProof,
// Associated range proof
pub range_proof: pod::RangeProof128,
}
#[allow(non_snake_case)]
#[cfg(not(target_arch = "bpf"))]
impl TransferProof {
fn transcript_new() -> Transcript {
Transcript::new(b"TransferProof")
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::many_single_char_names)]
pub fn new(
source_keypair: &ElGamalKeypair,
dest_pk: &ElGamalPubkey,
auditor_pk: &ElGamalPubkey,
transfer_amt: (u64, u64),
openings: (&PedersenOpening, &PedersenOpening),
source_new_balance: u64,
source_new_balance_ct: &ElGamalCiphertext,
) -> Self {
let mut transcript = Self::transcript_new();
// add a domain separator to record the start of the protocol
transcript.transfer_proof_domain_sep();
// generate a Pedersen commitment for the remaining balance in source
let (source_commitment, source_open) = Pedersen::new(source_new_balance);
// extract the relevant scalar and Ristretto points from the inputs
let P_EG = source_keypair.public.get_point();
let C_EG = source_new_balance_ct.message_comm.get_point();
let D_EG = source_new_balance_ct.decrypt_handle.get_point();
let C_Ped = source_commitment.get_point();
// append all current state to the transcript
transcript.append_point(b"P_EG", &P_EG.compress());
transcript.append_point(b"C_EG", &C_EG.compress());
transcript.append_point(b"D_EG", &D_EG.compress());
transcript.append_point(b"C_Ped", &C_Ped.compress());
// let c = transcript.challenge_scalar(b"c");
// println!("{:?}", c);
// generate equality_proof
let equality_proof = EqualityProof::new(
source_keypair,
source_new_balance_ct,
source_new_balance,
&source_open,
&mut transcript,
);
// generate ciphertext validity proof
let validity_proof =
ValidityProof::new(dest_pk, auditor_pk, transfer_amt, openings, &mut transcript);
// generate the range proof
let range_proof = RangeProof::new(
vec![source_new_balance, transfer_amt.0, transfer_amt.1],
vec![64, 32, 32],
vec![&source_open, openings.0, openings.1],
&mut transcript,
);
Self {
source_commitment: source_commitment.into(),
equality_proof: equality_proof.try_into().expect("equality proof"),
validity_proof: validity_proof.try_into().expect("validity proof"),
range_proof: range_proof.try_into().expect("range proof"),
}
}
pub fn verify(
self,
amount_comms: &TransferCommitments,
decryption_handles_lo: &TransferDecryptHandles,
decryption_handles_hi: &TransferDecryptHandles,
new_spendable_ct: &pod::ElGamalCiphertext,
transfer_public_keys: &TransferPubkeys,
) -> Result<(), ProofError> {
let mut transcript = Self::transcript_new();
let commitment: PedersenCommitment = self.source_commitment.try_into()?;
let equality_proof: EqualityProof = self.equality_proof.try_into()?;
let validity_proof: ValidityProof = self.validity_proof.try_into()?;
let range_proof: RangeProof = self.range_proof.try_into()?;
// add a domain separator to record the start of the protocol
transcript.transfer_proof_domain_sep();
// extract the relevant scalar and Ristretto points from the inputs
let source_pk: ElGamalPubkey = transfer_public_keys.source_pk.try_into()?;
let new_spendable_ct: ElGamalCiphertext = (*new_spendable_ct).try_into()?;
let P_EG = source_pk.get_point();
let C_EG = new_spendable_ct.message_comm.get_point();
let D_EG = new_spendable_ct.decrypt_handle.get_point();
let C_Ped = commitment.get_point();
// append all current state to the transcript
transcript.append_point(b"P_EG", &P_EG.compress());
transcript.append_point(b"C_EG", &C_EG.compress());
transcript.append_point(b"D_EG", &D_EG.compress());
transcript.append_point(b"C_Ped", &C_Ped.compress());
// verify equality proof
//
// TODO: we can also consider verifying equality and range proof in a batch
equality_proof.verify(&source_pk, &new_spendable_ct, &commitment, &mut transcript)?;
// TODO: record destination and auditor public keys to transcript
let dest_elgamal_pubkey: ElGamalPubkey = transfer_public_keys.dest_pk.try_into()?;
let auditor_elgamal_pubkey: ElGamalPubkey = transfer_public_keys.auditor_pk.try_into()?;
let amount_comm_lo: PedersenCommitment = amount_comms.lo.try_into()?;
let amount_comm_hi: PedersenCommitment = amount_comms.hi.try_into()?;
let handle_lo_dest: PedersenDecryptHandle = decryption_handles_lo.dest.try_into()?;
let handle_hi_dest: PedersenDecryptHandle = decryption_handles_hi.dest.try_into()?;
let handle_lo_auditor: PedersenDecryptHandle = decryption_handles_lo.auditor.try_into()?;
let handle_hi_auditor: PedersenDecryptHandle = decryption_handles_hi.auditor.try_into()?;
// TODO: validity proof
validity_proof.verify(
&dest_elgamal_pubkey,
&auditor_elgamal_pubkey,
(&amount_comm_lo, &amount_comm_hi),
(&handle_lo_dest, &handle_hi_dest),
(&handle_lo_auditor, &handle_hi_auditor),
&mut transcript,
)?;
// verify range proof
range_proof.verify(
vec![
&self.source_commitment.into(),
&amount_comms.lo.into(),
&amount_comms.hi.into(),
],
vec![64_usize, 32_usize, 32_usize],
&mut transcript,
)?;
Ok(())
}
}
/// The ElGamal public keys needed for a transfer
#[derive(Clone, Copy, Pod, Zeroable)]
#[repr(C)]
pub struct TransferPubkeys {
pub source_pk: pod::ElGamalPubkey, // 32 bytes
pub dest_pk: pod::ElGamalPubkey, // 32 bytes
pub auditor_pk: pod::ElGamalPubkey, // 32 bytes
}
#[derive(Clone, Copy, Pod, Zeroable)]
#[repr(C)]
pub struct EncryptedTransferAmount {
pub amount_comm_lo: pod::PedersenCommitment,
pub amount_comm_hi: pod::PedersenCommitment,
/// The decryption handles that allow decryption of the lo-bits of the transfer amount
pub decrypt_handles_lo: TransferDecryptHandles,
/// The decryption handles that allow decryption of the hi-bits of the transfer amount
pub decrypt_handles_hi: TransferDecryptHandles,
}
/// The decryption handles needed for a transfer
#[derive(Clone, Copy, Pod, Zeroable)]
#[repr(C)]
pub struct TransferDecryptHandles {
pub source: pod::PedersenDecryptHandle, // 32 bytes
pub dest: pod::PedersenDecryptHandle, // 32 bytes
pub auditor: pod::PedersenDecryptHandle, // 32 bytes
}
#[derive(Clone, Copy, Pod, Zeroable)]
#[repr(C)]
pub struct TransferCommitments {
pub lo: pod::PedersenCommitment,
pub hi: pod::PedersenCommitment,
}
#[derive(Clone, Copy, Pod, Zeroable)]
#[repr(C)]
pub struct EncryptedTransferFee {
/// The transfer fee commitment
pub fee_comm: pod::PedersenCommitment,
/// The decryption handle for destination ElGamal pubkey
pub decrypt_handle_dest: pod::PedersenDecryptHandle,
/// The decryption handle for fee collector ElGamal pubkey
pub decrypt_handle_fee_collector: pod::PedersenDecryptHandle,
}
/// Split u64 number into two u32 numbers
#[cfg(not(target_arch = "bpf"))]
pub fn split_u64_into_u32(amt: u64) -> (u32, u32) {
let lo = amt as u32;
let hi = (amt >> 32) as u32;
(lo, hi)
}
/// Constant for 2^32
#[cfg(not(target_arch = "bpf"))]
const TWO_32: u64 = 4294967296;
#[cfg(not(target_arch = "bpf"))]
pub fn combine_u32_comms(
comm_lo: PedersenCommitment,
comm_hi: PedersenCommitment,
) -> PedersenCommitment {
comm_lo + comm_hi * Scalar::from(TWO_32)
}
#[cfg(not(target_arch = "bpf"))]
pub fn combine_u32_handles(
handle_lo: PedersenDecryptHandle,
handle_hi: PedersenDecryptHandle,
) -> PedersenDecryptHandle {
handle_lo + handle_hi * Scalar::from(TWO_32)
}
/*
pub fn combine_u32_ciphertexts(ct_lo: ElGamalCiphertext, ct_hi: ElGamalCiphertext) -> ElGamalCiphertext {
ct_lo + ct_hi * Scalar::from(TWO_32)
}*/
#[cfg(test)]
mod test {
use {super::*, crate::encryption::elgamal::ElGamalKeypair};
#[test]
fn test_transfer_correctness() {
// ElGamalKeypair keys for source, destination, and auditor accounts
let source_keypair = ElGamalKeypair::default();
let dest_pk = ElGamalKeypair::default().public;
let auditor_pk = ElGamalKeypair::default().public;
// create source account spendable ciphertext
let spendable_balance: u64 = 77;
let spendable_ct = source_keypair.public.encrypt(spendable_balance);
// transfer amount
let transfer_amount: u64 = 55;
// create transfer data
let transfer_data = TransferData::new(
transfer_amount,
spendable_balance,
spendable_ct,
&source_keypair,
dest_pk,
auditor_pk,
);
assert!(transfer_data.verify().is_ok());
}
#[test]
fn test_source_dest_ciphertext() {
// ElGamalKeypair keys for source, destination, and auditor accounts
let source_keypair = ElGamalKeypair::default();
let ElGamalKeypair {
public: dest_pk,
secret: dest_sk,
} = ElGamalKeypair::default();
let ElGamalKeypair {
public: auditor_pk,
secret: auditor_sk,
} = ElGamalKeypair::default();
// create source account spendable ciphertext
let spendable_balance: u64 = 77;
let spendable_ct = source_keypair.public.encrypt(spendable_balance);
// transfer amount
let transfer_amount: u64 = 55;
// create transfer data
let transfer_data = TransferData::new(
transfer_amount,
spendable_balance,
spendable_ct,
&source_keypair,
dest_pk,
auditor_pk,
);
assert_eq!(
transfer_data
.decrypt_amount(Role::Source, &source_keypair.secret)
.unwrap(),
55_u64,
);
assert_eq!(
transfer_data.decrypt_amount(Role::Dest, &dest_sk).unwrap(),
55_u64,
);
assert_eq!(
transfer_data
.decrypt_amount(Role::Auditor, &auditor_sk)
.unwrap(),
55_u64,
);
}
}