Zk instructions pass (#22851)

* zk-token-sdk: re-organize transcript

* zk-token-sdk: add pod ElGamal group encryption

* zk-token-sdk: add transcript domain separators for sigma proofs

* zk-token-sdk: clean up transfer tx decryption

* zk-token-sdk: resolve encoding issues for transfer

* zk-token-sdk: fix transfer test

* zk-token-sdk: clean up transcript for close account and withdraw instructions

* zk-token-sdk: add transfer with fee instruction

* zk-token-sdk: add transfer with fee instruction

* zk-token-sdk: add pod for cryptographic structs needed for fee

* zk-token-sdk: add pod for fee sigma proof

* zk-token-sdk: fix test for transfer with fee instruction

* zk-token-sdk: add range proof verification for transfer with fee

* zk-token-sdk: add transfer amount decryption for transfer-with-fee

* zk-token-sdk: add proof generation error for instruction

* zk-token-sdk: cargo fmt and clippy

* zk-token-sdk: fix bpf build
This commit is contained in:
samkim-crypto 2022-02-01 14:11:28 -05:00 committed by GitHub
parent c631a3e0e4
commit 65f8f43665
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1514 additions and 537 deletions

View File

@ -23,6 +23,7 @@ use {
curve25519_dalek::{
ristretto::{CompressedRistretto, RistrettoPoint},
scalar::Scalar,
traits::Identity,
},
serde::{Deserialize, Serialize},
solana_sdk::{
@ -49,7 +50,7 @@ use {
};
/// Algorithm handle for the twisted ElGamal encryption scheme
struct ElGamal;
pub struct ElGamal;
impl ElGamal {
/// Generates an ElGamal keypair.
///
@ -93,6 +94,7 @@ impl ElGamal {
/// On input a public key, message, and Pedersen opening, the function
/// returns the corresponding ElGamal ciphertext.
#[cfg(not(target_arch = "bpf"))]
fn encrypt_with<T: Into<Scalar>>(
amount: T,
public: &ElGamalPubkey,
@ -104,10 +106,22 @@ impl ElGamal {
ElGamalCiphertext { commitment, handle }
}
/// On input a message, the function returns a twisted ElGamal ciphertext where the associated
/// Pedersen opening is always zero. Since the opening is zero, any twisted ElGamal ciphertext
/// of this form is a valid ciphertext under any ElGamal public key.
#[cfg(not(target_arch = "bpf"))]
pub fn encode<T: Into<Scalar>>(amount: T) -> ElGamalCiphertext {
let commitment = Pedersen::encode(amount);
let handle = DecryptHandle(RistrettoPoint::identity());
ElGamalCiphertext { commitment, handle }
}
/// On input a secret key and a ciphertext, the function returns the decrypted message.
///
/// The output of this function is of type `DiscreteLog`. To recover, the originally encrypted
/// message, use `DiscreteLog::decode`.
#[cfg(not(target_arch = "bpf"))]
fn decrypt(secret: &ElGamalSecretKey, ciphertext: &ElGamalCiphertext) -> DiscreteLog {
DiscreteLog {
generator: *G,
@ -117,6 +131,7 @@ impl ElGamal {
/// On input a secret key and a ciphertext, the function returns the decrypted message
/// interpretted as type `u32`.
#[cfg(not(target_arch = "bpf"))]
fn decrypt_u32(secret: &ElGamalSecretKey, ciphertext: &ElGamalCiphertext) -> Option<u32> {
let discrete_log_instance = Self::decrypt(secret, ciphertext);
discrete_log_instance.decode_u32()
@ -124,6 +139,7 @@ impl ElGamal {
/// On input a secret key, a ciphertext, and a pre-computed hashmap, the function returns the
/// decrypted message interpretted as type `u32`.
#[cfg(not(target_arch = "bpf"))]
fn decrypt_u32_online(
secret: &ElGamalSecretKey,
ciphertext: &ElGamalCiphertext,

View File

@ -52,6 +52,13 @@ impl Pedersen {
PedersenCommitment(RistrettoPoint::multiscalar_mul(&[x, *r], &[*G, *H]))
}
/// On input a message, the function returns a Pedersen commitment with zero as the opening.
///
/// This function is deterministic.
pub fn encode<T: Into<Scalar>>(amount: T) -> PedersenCommitment {
PedersenCommitment(amount.into() * &(*G))
}
}
/// Pedersen opening type.

View File

@ -5,6 +5,8 @@ use thiserror::Error;
// TODO: clean up errors for encryption
#[derive(Error, Clone, Debug, Eq, PartialEq)]
pub enum ProofError {
#[error("proof generation failed")]
Generation,
#[error("proof failed to verify")]
Verification,
#[error("range proof failed to verify")]
@ -41,8 +43,8 @@ impl From<EqualityProofError> for ProofError {
}
}
impl From<FeeProofError> for ProofError {
fn from(_err: FeeProofError) -> Self {
impl From<FeeSigmaProofError> for ProofError {
fn from(_err: FeeSigmaProofError) -> Self {
Self::FeeProof
}
}

View File

@ -26,10 +26,10 @@ use {
#[repr(C)]
pub struct CloseAccountData {
/// The source account ElGamal pubkey
pub elgamal_pubkey: pod::ElGamalPubkey, // 32 bytes
pub pubkey: pod::ElGamalPubkey, // 32 bytes
/// The source account available balance in encrypted form
pub balance: pod::ElGamalCiphertext, // 64 bytes
pub ciphertext: pod::ElGamalCiphertext, // 64 bytes
/// Proof that the source account available balance is zero
pub proof: CloseAccountProof, // 64 bytes
@ -37,23 +37,33 @@ pub struct CloseAccountData {
#[cfg(not(target_arch = "bpf"))]
impl CloseAccountData {
pub fn new(source_keypair: &ElGamalKeypair, balance: ElGamalCiphertext) -> Self {
let proof = CloseAccountProof::new(source_keypair, &balance);
pub fn new(
keypair: &ElGamalKeypair,
ciphertext: &ElGamalCiphertext,
) -> Result<Self, ProofError> {
let pod_pubkey = pod::ElGamalPubkey((&keypair.public).to_bytes());
let pod_ciphertext = pod::ElGamalCiphertext(ciphertext.to_bytes());
CloseAccountData {
elgamal_pubkey: source_keypair.public.into(),
balance: balance.into(),
let mut transcript = CloseAccountProof::transcript_new(&pod_pubkey, &pod_ciphertext);
let proof = CloseAccountProof::new(keypair, ciphertext, &mut transcript);
Ok(CloseAccountData {
pubkey: pod_pubkey,
ciphertext: pod_ciphertext,
proof,
}
})
}
}
#[cfg(not(target_arch = "bpf"))]
impl Verifiable for CloseAccountData {
fn verify(&self) -> Result<(), ProofError> {
let elgamal_pubkey = self.elgamal_pubkey.try_into()?;
let balance = self.balance.try_into()?;
self.proof.verify(&elgamal_pubkey, &balance)
let mut transcript = CloseAccountProof::transcript_new(&self.pubkey, &self.ciphertext);
let pubkey = self.pubkey.try_into()?;
let ciphertext = self.ciphertext.try_into()?;
self.proof.verify(&pubkey, &ciphertext, &mut transcript)
}
}
@ -69,18 +79,24 @@ pub struct CloseAccountProof {
#[allow(non_snake_case)]
#[cfg(not(target_arch = "bpf"))]
impl CloseAccountProof {
fn transcript_new() -> Transcript {
Transcript::new(b"CloseAccountProof")
fn transcript_new(
pubkey: &pod::ElGamalPubkey,
ciphertext: &pod::ElGamalCiphertext,
) -> Transcript {
let mut transcript = Transcript::new(b"CloseAccountProof");
transcript.append_pubkey(b"pubkey", pubkey);
transcript.append_ciphertext(b"ciphertext", ciphertext);
transcript
}
pub fn new(source_keypair: &ElGamalKeypair, balance: &ElGamalCiphertext) -> Self {
let mut transcript = Self::transcript_new();
// TODO: Add ciphertext to transcript
// add a domain separator to record the start of the protocol
transcript.close_account_proof_domain_sep();
let proof = ZeroBalanceProof::new(source_keypair, balance, &mut transcript);
pub fn new(
keypair: &ElGamalKeypair,
ciphertext: &ElGamalCiphertext,
transcript: &mut Transcript,
) -> Self {
let proof = ZeroBalanceProof::new(keypair, ciphertext, transcript);
CloseAccountProof {
proof: proof.into(),
@ -89,72 +105,33 @@ impl CloseAccountProof {
pub fn verify(
&self,
elgamal_pubkey: &ElGamalPubkey,
balance: &ElGamalCiphertext,
pubkey: &ElGamalPubkey,
ciphertext: &ElGamalCiphertext,
transcript: &mut Transcript,
) -> Result<(), ProofError> {
let mut transcript = Self::transcript_new();
// add a domain separator to record the start of the protocol
transcript.close_account_proof_domain_sep();
// verify zero balance proof
let proof: ZeroBalanceProof = self.proof.try_into()?;
proof.verify(elgamal_pubkey, balance, &mut transcript)?;
proof.verify(pubkey, ciphertext, transcript)?;
Ok(())
}
}
#[cfg(test)]
mod test {
use {
super::*,
crate::encryption::{
elgamal::{DecryptHandle, ElGamalKeypair},
pedersen::{Pedersen, PedersenOpening},
},
};
use super::*;
#[test]
fn test_close_account_correctness() {
let source_keypair = ElGamalKeypair::new_rand();
let keypair = ElGamalKeypair::new_rand();
// general case: encryption of 0
let balance = source_keypair.public.encrypt(0_u64);
let proof = CloseAccountProof::new(&source_keypair, &balance);
assert!(proof.verify(&source_keypair.public, &balance).is_ok());
let ciphertext = keypair.public.encrypt(0_u64);
let close_account_data = CloseAccountData::new(&keypair, &ciphertext).unwrap();
assert!(close_account_data.verify().is_ok());
// general case: encryption of > 0
let balance = source_keypair.public.encrypt(1_u64);
let proof = CloseAccountProof::new(&source_keypair, &balance);
assert!(proof.verify(&source_keypair.public, &balance).is_err());
// // edge case: all zero ciphertext - such ciphertext should always be a valid encryption of 0
let zeroed_ct: ElGamalCiphertext = pod::ElGamalCiphertext::zeroed().try_into().unwrap();
let proof = CloseAccountProof::new(&source_keypair, &zeroed_ct);
assert!(proof.verify(&source_keypair.public, &zeroed_ct).is_ok());
// edge cases: only C or D is zero - such ciphertext is always invalid
let zeroed_comm = Pedersen::with(0_u64, &PedersenOpening::default());
let handle = balance.handle;
let zeroed_comm_ciphertext = ElGamalCiphertext {
commitment: zeroed_comm,
handle,
};
let proof = CloseAccountProof::new(&source_keypair, &zeroed_comm_ciphertext);
assert!(proof
.verify(&source_keypair.public, &zeroed_comm_ciphertext)
.is_err());
let zeroed_handle_ciphertext = ElGamalCiphertext {
commitment: balance.commitment,
handle: DecryptHandle::default(),
};
let proof = CloseAccountProof::new(&source_keypair, &zeroed_handle_ciphertext);
assert!(proof
.verify(&source_keypair.public, &zeroed_handle_ciphertext)
.is_err());
let ciphertext = keypair.public.encrypt(1_u64);
let close_account_data = CloseAccountData::new(&keypair, &ciphertext).unwrap();
assert!(close_account_data.verify().is_err());
}
}

View File

@ -1,14 +1,24 @@
mod close_account;
mod transfer;
mod withdraw;
pub mod close_account;
pub mod transfer;
pub mod transfer_with_fee;
pub mod withdraw;
#[cfg(not(target_arch = "bpf"))]
use crate::errors::ProofError;
pub use {
close_account::CloseAccountData,
transfer::{TransferCommitments, TransferData, TransferPubkeys},
withdraw::WithdrawData,
use {
crate::{
encryption::{
elgamal::ElGamalCiphertext,
pedersen::{PedersenCommitment, PedersenOpening},
},
errors::ProofError,
},
curve25519_dalek::scalar::Scalar,
};
pub use {close_account::CloseAccountData, transfer::TransferData, withdraw::WithdrawData};
/// Constant for 2^32
#[cfg(not(target_arch = "bpf"))]
const TWO_32: u64 = 4294967296;
#[cfg(not(target_arch = "bpf"))]
pub trait Verifiable {
@ -22,3 +32,36 @@ pub enum Role {
Dest,
Auditor,
}
/// Split u64 number into two u32 numbers
#[cfg(not(target_arch = "bpf"))]
pub fn split_u64_into_u32(amount: u64) -> (u32, u32) {
let lo = amount as u32;
let hi = (amount >> 32) as u32;
(lo, hi)
}
#[cfg(not(target_arch = "bpf"))]
fn combine_u32_ciphertexts(
ciphertext_lo: &ElGamalCiphertext,
ciphertext_hi: &ElGamalCiphertext,
) -> ElGamalCiphertext {
ciphertext_lo + &(ciphertext_hi * &Scalar::from(TWO_32))
}
#[cfg(not(target_arch = "bpf"))]
pub fn combine_u32_commitments(
comm_lo: &PedersenCommitment,
comm_hi: &PedersenCommitment,
) -> PedersenCommitment {
comm_lo + comm_hi * &Scalar::from(TWO_32)
}
#[cfg(not(target_arch = "bpf"))]
pub fn combine_u32_openings(
opening_lo: &PedersenOpening,
opening_hi: &PedersenOpening,
) -> PedersenOpening {
opening_lo + opening_hi * &Scalar::from(TWO_32)
}

View File

@ -13,188 +13,197 @@ use {
pedersen::{Pedersen, PedersenCommitment, PedersenOpening},
},
errors::ProofError,
instruction::{Role, Verifiable},
instruction::{combine_u32_ciphertexts, split_u64_into_u32, Role, Verifiable, TWO_32},
range_proof::RangeProof,
sigma_proofs::{equality_proof::EqualityProof, validity_proof::AggregatedValidityProof},
transcript::TranscriptProtocol,
},
curve25519_dalek::scalar::Scalar,
arrayref::{array_ref, array_refs},
merlin::Transcript,
std::convert::TryInto,
};
#[derive(Clone)]
#[repr(C)]
#[cfg(not(target_arch = "bpf"))]
pub struct TransferAmountEncryption {
pub commitment: PedersenCommitment,
pub source: DecryptHandle,
pub dest: DecryptHandle,
pub auditor: DecryptHandle,
}
#[cfg(not(target_arch = "bpf"))]
impl TransferAmountEncryption {
pub fn new(
amount: u32,
pubkey_source: &ElGamalPubkey,
pubkey_dest: &ElGamalPubkey,
pubkey_auditor: &ElGamalPubkey,
) -> (Self, PedersenOpening) {
let (commitment, opening) = Pedersen::new(amount);
let transfer_amount_encryption = Self {
commitment,
source: pubkey_source.decrypt_handle(&opening),
dest: pubkey_dest.decrypt_handle(&opening),
auditor: pubkey_auditor.decrypt_handle(&opening),
};
(transfer_amount_encryption, opening)
}
pub fn to_bytes(&self) -> [u8; 128] {
let mut bytes = [0u8; 128];
bytes[..32].copy_from_slice(&self.commitment.to_bytes());
bytes[32..64].copy_from_slice(&self.source.to_bytes());
bytes[64..96].copy_from_slice(&self.dest.to_bytes());
bytes[96..128].copy_from_slice(&self.auditor.to_bytes());
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, ProofError> {
let bytes = array_ref![bytes, 0, 128];
let (commitment, source, dest, auditor) = array_refs![bytes, 32, 32, 32, 32];
let commitment =
PedersenCommitment::from_bytes(commitment).ok_or(ProofError::Verification)?;
let source = DecryptHandle::from_bytes(source).ok_or(ProofError::Verification)?;
let dest = DecryptHandle::from_bytes(dest).ok_or(ProofError::Verification)?;
let auditor = DecryptHandle::from_bytes(auditor).ok_or(ProofError::Verification)?;
Ok(Self {
commitment,
source,
dest,
auditor,
})
}
}
#[derive(Clone, Copy, Pod, Zeroable)]
#[repr(C)]
pub struct TransferData {
/// The encrypted transfer amount
pub encrypted_transfer_amount: EncryptedTransferAmount,
/// Group encryption of the low 32 bits of the transfer amount
pub ciphertext_lo: pod::TransferAmountEncryption,
/// Group encryption of the high 32 bits of the transfer amount
pub ciphertext_hi: pod::TransferAmountEncryption,
/// The public encryption keys associated with the transfer: source, dest, and auditor
pub transfer_public_keys: TransferPubkeys, // 128 bytes
pub transfer_pubkeys: pod::TransferPubkeys,
/// The final spendable ciphertext after the transfer
pub new_spendable_ct: pod::ElGamalCiphertext, // 64 bytes
pub ciphertext_new_source: pod::ElGamalCiphertext,
// 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 {
(spendable_balance, ciphertext_old_source): (u64, &ElGamalCiphertext),
keypair_source: &ElGamalKeypair,
(pubkey_dest, pubkey_auditor): (&ElGamalPubkey, &ElGamalPubkey),
) -> Result<Self, ProofError> {
// 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.commitment;
let spendable_handle = spendable_balance_ciphertext.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 {
commitment: new_spendable_comm,
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,
let (ciphertext_lo, opening_lo) = TransferAmountEncryption::new(
amount_lo,
&keypair_source.public,
pubkey_dest,
pubkey_auditor,
);
let (ciphertext_hi, opening_hi) = TransferAmountEncryption::new(
amount_hi,
&keypair_source.public,
pubkey_dest,
pubkey_auditor,
);
Self {
encrypted_transfer_amount,
new_spendable_ct: new_spendable_ct.into(),
transfer_public_keys,
// subtract transfer amount from the spendable ciphertext
let new_spendable_balance = spendable_balance
.checked_sub(transfer_amount)
.ok_or(ProofError::Generation)?;
let transfer_amount_lo_source = ElGamalCiphertext {
commitment: ciphertext_lo.commitment,
handle: ciphertext_lo.source,
};
let transfer_amount_hi_source = ElGamalCiphertext {
commitment: ciphertext_hi.commitment,
handle: ciphertext_hi.source,
};
let ciphertext_new_source = ciphertext_old_source
- combine_u32_ciphertexts(&transfer_amount_lo_source, &transfer_amount_hi_source);
// generate transcript and append all public inputs
let pod_transfer_pubkeys =
pod::TransferPubkeys::new(&keypair_source.public, pubkey_dest, pubkey_auditor);
let pod_ciphertext_lo: pod::TransferAmountEncryption = ciphertext_lo.into();
let pod_ciphertext_hi: pod::TransferAmountEncryption = ciphertext_hi.into();
let pod_ciphertext_new_source: pod::ElGamalCiphertext = ciphertext_new_source.into();
let mut transcript = TransferProof::transcript_new(
&pod_transfer_pubkeys,
&pod_ciphertext_lo,
&pod_ciphertext_hi,
&pod_ciphertext_new_source,
);
let proof = TransferProof::new(
(amount_lo, amount_hi),
keypair_source,
(pubkey_dest, pubkey_auditor),
&opening_lo,
&opening_hi,
(new_spendable_balance, &ciphertext_new_source),
&mut transcript,
);
Ok(Self {
ciphertext_lo: pod_ciphertext_lo,
ciphertext_hi: pod_ciphertext_hi,
transfer_pubkeys: pod_transfer_pubkeys,
ciphertext_new_source: pod_ciphertext_new_source,
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 ciphertext_lo: TransferAmountEncryption = self.ciphertext_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()?;
let handle_lo = match role {
Role::Source => ciphertext_lo.source,
Role::Dest => ciphertext_lo.dest,
Role::Auditor => ciphertext_lo.auditor,
};
Ok(ElGamalCiphertext {
commitment: transfer_comm_lo,
handle: decryption_handle_lo,
commitment: ciphertext_lo.commitment,
handle: handle_lo,
})
}
/// 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 ciphertext_hi: TransferAmountEncryption = self.ciphertext_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()?;
let handle_hi = match role {
Role::Source => ciphertext_hi.source,
Role::Dest => ciphertext_hi.dest,
Role::Auditor => ciphertext_hi.auditor,
};
Ok(ElGamalCiphertext {
commitment: transfer_comm_hi,
handle: decryption_handle_hi,
commitment: ciphertext_hi.commitment,
handle: handle_hi,
})
}
@ -222,17 +231,25 @@ impl TransferData {
#[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,
};
// generate transcript and append all public inputs
let mut transcript = TransferProof::transcript_new(
&self.transfer_pubkeys,
&self.ciphertext_lo,
&self.ciphertext_hi,
&self.ciphertext_new_source,
);
let ciphertext_lo = self.ciphertext_lo.try_into()?;
let ciphertext_hi = self.ciphertext_hi.try_into()?;
let transfer_pubkeys = self.transfer_pubkeys.try_into()?;
let new_spendable_ciphertext = self.ciphertext_new_source.try_into()?;
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,
&ciphertext_lo,
&ciphertext_hi,
&transfer_pubkeys,
&new_spendable_ciphertext,
&mut transcript,
)
}
}
@ -242,7 +259,7 @@ impl Verifiable for TransferData {
#[repr(C)]
pub struct TransferProof {
/// New Pedersen commitment for the remaining balance in source
pub source_commitment: pod::PedersenCommitment,
pub commitment_new_source: pod::PedersenCommitment,
/// Associated equality proof
pub equality_proof: pod::EqualityProof,
@ -257,143 +274,118 @@ pub struct TransferProof {
#[allow(non_snake_case)]
#[cfg(not(target_arch = "bpf"))]
impl TransferProof {
fn transcript_new() -> Transcript {
Transcript::new(b"TransferProof")
fn transcript_new(
transfer_pubkeys: &pod::TransferPubkeys,
ciphertext_lo: &pod::TransferAmountEncryption,
ciphertext_hi: &pod::TransferAmountEncryption,
ciphertext_new_source: &pod::ElGamalCiphertext,
) -> Transcript {
let mut transcript = Transcript::new(b"transfer-proof");
transcript.append_message(b"transfer-pubkeys", &transfer_pubkeys.0);
transcript.append_message(b"ciphertext-lo", &ciphertext_lo.0);
transcript.append_message(b"ciphertext-hi", &ciphertext_hi.0);
transcript.append_message(b"ciphertext-new-source", &ciphertext_new_source.0);
transcript
}
#[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,
(transfer_amount_lo, transfer_amount_hi): (u32, u32),
keypair_source: &ElGamalKeypair,
(pubkey_dest, pubkey_auditor): (&ElGamalPubkey, &ElGamalPubkey),
opening_lo: &PedersenOpening,
opening_hi: &PedersenOpening,
(source_new_balance, ciphertext_new_source): (u64, &ElGamalCiphertext),
transcript: &mut Transcript,
) -> 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);
let (commitment_new_source, opening_source) = 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.commitment.get_point();
let D_EG = source_new_balance_ct.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 pod_commitment_new_source: pod::PedersenCommitment = commitment_new_source.into();
transcript.append_commitment(b"commitment-new-source", &pod_commitment_new_source);
// generate equality_proof
let equality_proof = EqualityProof::new(
source_keypair,
source_new_balance_ct,
keypair_source,
ciphertext_new_source,
source_new_balance,
&source_open,
&mut transcript,
&opening_source,
transcript,
);
// generate ciphertext validity proof
let validity_proof = AggregatedValidityProof::new(
(dest_pk, auditor_pk),
transfer_amt,
openings,
&mut transcript,
(pubkey_dest, pubkey_auditor),
(transfer_amount_lo, transfer_amount_hi),
(opening_lo, opening_hi),
transcript,
);
// generate the range proof
let range_proof = RangeProof::new(
vec![source_new_balance, transfer_amt.0, transfer_amt.1],
vec![
source_new_balance,
transfer_amount_lo as u64,
transfer_amount_hi as u64,
],
vec![64, 32, 32],
vec![&source_open, openings.0, openings.1],
&mut transcript,
vec![&opening_source, opening_lo, opening_hi],
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"),
commitment_new_source: pod_commitment_new_source,
equality_proof: equality_proof.into(),
validity_proof: validity_proof.into(),
range_proof: range_proof.try_into().expect("range proof: length error"),
}
}
pub fn verify(
self,
amount_comms: &TransferCommitments,
decryption_handles_lo: &TransferDecryptHandles,
decryption_handles_hi: &TransferDecryptHandles,
new_spendable_ct: &pod::ElGamalCiphertext,
transfer_public_keys: &TransferPubkeys,
&self,
ciphertext_lo: &TransferAmountEncryption,
ciphertext_hi: &TransferAmountEncryption,
transfer_pubkeys: &TransferPubkeys,
new_spendable_ciphertext: &ElGamalCiphertext,
transcript: &mut Transcript,
) -> Result<(), ProofError> {
let mut transcript = Self::transcript_new();
transcript.append_commitment(b"commitment-new-source", &self.commitment_new_source);
let commitment: PedersenCommitment = self.source_commitment.try_into()?;
let commitment: PedersenCommitment = self.commitment_new_source.try_into()?;
let equality_proof: EqualityProof = self.equality_proof.try_into()?;
let aggregated_validity_proof: AggregatedValidityProof = 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.commitment.get_point();
let D_EG = new_spendable_ct.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)?;
equality_proof.verify(
&transfer_pubkeys.source,
new_spendable_ciphertext,
&commitment,
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: DecryptHandle = decryption_handles_lo.dest.try_into()?;
let handle_hi_dest: DecryptHandle = decryption_handles_hi.dest.try_into()?;
let handle_lo_auditor: DecryptHandle = decryption_handles_lo.auditor.try_into()?;
let handle_hi_auditor: DecryptHandle = decryption_handles_hi.auditor.try_into()?;
// TODO: validity proof
// verify validity proof
aggregated_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,
(&transfer_pubkeys.dest, &transfer_pubkeys.auditor),
(&ciphertext_lo.commitment, &ciphertext_hi.commitment),
(&ciphertext_lo.dest, &ciphertext_hi.dest),
(&ciphertext_lo.auditor, &ciphertext_hi.auditor),
transcript,
)?;
// verify range proof
let commitment_new_source = self.commitment_new_source.try_into()?;
range_proof.verify(
vec![
&self.source_commitment.into(),
&amount_comms.lo.into(),
&amount_comms.hi.into(),
&commitment_new_source,
&ciphertext_lo.commitment,
&ciphertext_hi.commitment,
],
vec![64_usize, 32_usize, 32_usize],
&mut transcript,
transcript,
)?;
Ok(())
@ -401,85 +393,52 @@ impl TransferProof {
}
/// The ElGamal public keys needed for a transfer
#[derive(Clone, Copy, Pod, Zeroable)]
#[derive(Clone)]
#[repr(C)]
#[cfg(not(target_arch = "bpf"))]
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::DecryptHandle, // 32 bytes
pub dest: pod::DecryptHandle, // 32 bytes
pub auditor: pod::DecryptHandle, // 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::DecryptHandle,
/// The decryption handle for fee collector ElGamal pubkey
pub decrypt_handle_fee_collector: pod::DecryptHandle,
}
/// 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)
pub source: ElGamalPubkey,
pub dest: ElGamalPubkey,
pub auditor: ElGamalPubkey,
}
#[cfg(not(target_arch = "bpf"))]
pub fn combine_u32_handles(handle_lo: DecryptHandle, handle_hi: DecryptHandle) -> DecryptHandle {
handle_lo + handle_hi * Scalar::from(TWO_32)
impl TransferPubkeys {
// TODO: use constructor instead
pub fn to_bytes(&self) -> [u8; 96] {
let mut bytes = [0u8; 96];
bytes[..32].copy_from_slice(&self.source.to_bytes());
bytes[32..64].copy_from_slice(&self.dest.to_bytes());
bytes[64..96].copy_from_slice(&self.auditor.to_bytes());
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, ProofError> {
let bytes = array_ref![bytes, 0, 96];
let (source, dest, auditor) = array_refs![bytes, 32, 32, 32];
let source = ElGamalPubkey::from_bytes(source).ok_or(ProofError::Verification)?;
let dest = ElGamalPubkey::from_bytes(dest).ok_or(ProofError::Verification)?;
let auditor = ElGamalPubkey::from_bytes(auditor).ok_or(ProofError::Verification)?;
Ok(Self {
source,
dest,
auditor,
})
}
}
/*
pub fn combine_u32_ciphertexts(ct_lo: ElGamalCiphertext, ct_hi: ElGamalCiphertext) -> ElGamalCiphertext {
ct_lo + ct_hi * Scalar::from(TWO_32)
}*/
#[cfg(not(target_arch = "bpf"))]
impl pod::TransferPubkeys {
pub fn new(source: &ElGamalPubkey, dest: &ElGamalPubkey, auditor: &ElGamalPubkey) -> Self {
let mut bytes = [0u8; 96];
bytes[..32].copy_from_slice(&source.to_bytes());
bytes[32..64].copy_from_slice(&dest.to_bytes());
bytes[64..96].copy_from_slice(&auditor.to_bytes());
Self(bytes)
}
}
#[cfg(test)]
mod test {
@ -494,7 +453,7 @@ mod test {
// create source account spendable ciphertext
let spendable_balance: u64 = 77;
let spendable_ct = source_keypair.public.encrypt(spendable_balance);
let spendable_ciphertext = source_keypair.public.encrypt(spendable_balance);
// transfer amount
let transfer_amount: u64 = 55;
@ -502,12 +461,11 @@ mod test {
// create transfer data
let transfer_data = TransferData::new(
transfer_amount,
spendable_balance,
spendable_ct,
(spendable_balance, &spendable_ciphertext),
&source_keypair,
dest_pk,
auditor_pk,
);
(&dest_pk, &auditor_pk),
)
.unwrap();
assert!(transfer_data.verify().is_ok());
}
@ -529,7 +487,7 @@ mod test {
// create source account spendable ciphertext
let spendable_balance: u64 = 77;
let spendable_ct = source_keypair.public.encrypt(spendable_balance);
let spendable_ciphertext = source_keypair.public.encrypt(spendable_balance);
// transfer amount
let transfer_amount: u64 = 55;
@ -537,12 +495,11 @@ mod test {
// create transfer data
let transfer_data = TransferData::new(
transfer_amount,
spendable_balance,
spendable_ct,
(spendable_balance, &spendable_ciphertext),
&source_keypair,
dest_pk,
auditor_pk,
);
(&dest_pk, &auditor_pk),
)
.unwrap();
assert_eq!(
transfer_data

View File

@ -0,0 +1,690 @@
use {
crate::zk_token_elgamal::pod,
bytemuck::{Pod, Zeroable},
};
#[cfg(not(target_arch = "bpf"))]
use {
crate::{
encryption::{
discrete_log::*,
elgamal::{
DecryptHandle, ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey, ElGamalSecretKey,
},
pedersen::{Pedersen, PedersenCommitment, PedersenOpening},
},
errors::ProofError,
instruction::{
combine_u32_ciphertexts, combine_u32_commitments, combine_u32_openings,
split_u64_into_u32, transfer::TransferAmountEncryption, Role, Verifiable, TWO_32,
},
range_proof::RangeProof,
sigma_proofs::{
equality_proof::EqualityProof,
fee_proof::FeeSigmaProof,
validity_proof::{AggregatedValidityProof, ValidityProof},
},
transcript::TranscriptProtocol,
},
arrayref::{array_ref, array_refs},
curve25519_dalek::scalar::Scalar,
merlin::Transcript,
std::convert::TryInto,
subtle::{ConditionallySelectable, ConstantTimeGreater},
};
#[cfg(not(target_arch = "bpf"))]
const FEE_DENOMINATOR: u64 = 10000;
#[cfg(not(target_arch = "bpf"))]
lazy_static::lazy_static! {
pub static ref COMMITMENT_FEE_DENOMINATOR: PedersenCommitment = Pedersen::encode(FEE_DENOMINATOR);
}
// #[derive(Clone, Copy, Pod, Zeroable)]
#[derive(Clone, Copy, Pod, Zeroable)]
#[repr(C)]
pub struct TransferWithFeeData {
/// Group encryption of the low 32 bites of the transfer amount
pub ciphertext_lo: pod::TransferAmountEncryption,
/// Group encryption of the high 32 bits of the transfer amount
pub ciphertext_hi: pod::TransferAmountEncryption,
/// The public encryption keys associated with the transfer: source, dest, and auditor
pub transfer_with_fee_pubkeys: pod::TransferWithFeePubkeys,
/// The final spendable ciphertext after the transfer,
pub ciphertext_new_source: pod::ElGamalCiphertext,
// transfer fee encryption
pub ciphertext_fee: pod::FeeEncryption,
// fee parameters
pub fee_parameters: pod::FeeParameters,
// transfer fee proof
pub proof: TransferWithFeeProof,
}
#[cfg(not(target_arch = "bpf"))]
impl TransferWithFeeData {
pub fn new(
transfer_amount: u64,
(spendable_balance, ciphertext_old_source): (u64, &ElGamalCiphertext),
keypair_source: &ElGamalKeypair,
(pubkey_dest, pubkey_auditor): (&ElGamalPubkey, &ElGamalPubkey),
fee_parameters: FeeParameters,
pubkey_fee_collector: &ElGamalPubkey,
) -> Result<Self, ProofError> {
// split and encrypt transfer amount
let (amount_lo, amount_hi) = split_u64_into_u32(transfer_amount);
let (ciphertext_lo, opening_lo) = TransferAmountEncryption::new(
amount_lo,
&keypair_source.public,
pubkey_dest,
pubkey_auditor,
);
let (ciphertext_hi, opening_hi) = TransferAmountEncryption::new(
amount_hi,
&keypair_source.public,
pubkey_dest,
pubkey_auditor,
);
// subtract transfer amount from the spendable ciphertext
let new_spendable_balance = spendable_balance
.checked_sub(transfer_amount)
.ok_or(ProofError::Generation)?;
let transfer_amount_lo_source = ElGamalCiphertext {
commitment: ciphertext_lo.commitment,
handle: ciphertext_lo.source,
};
let transfer_amount_hi_source = ElGamalCiphertext {
commitment: ciphertext_hi.commitment,
handle: ciphertext_hi.source,
};
let ciphertext_new_source = ciphertext_old_source
- combine_u32_ciphertexts(&transfer_amount_lo_source, &transfer_amount_hi_source);
// calculate and encrypt fee
let (fee_amount, delta_fee) =
calculate_fee(transfer_amount, fee_parameters.fee_rate_basis_points);
let below_max = u64::ct_gt(&fee_parameters.maximum_fee, &fee_amount);
let fee_to_encrypt =
u64::conditional_select(&fee_parameters.maximum_fee, &fee_amount, below_max);
// u64::conditional_select(&fee_amount, &fee_parameters.maximum_fee, below_max);
let (ciphertext_fee, opening_fee) =
FeeEncryption::new(fee_to_encrypt, pubkey_dest, pubkey_fee_collector);
// generate transcript and append all public inputs
let pod_transfer_with_fee_pubkeys = pod::TransferWithFeePubkeys::new(
&keypair_source.public,
pubkey_dest,
pubkey_auditor,
pubkey_fee_collector,
);
let pod_ciphertext_lo = pod::TransferAmountEncryption(ciphertext_lo.to_bytes());
let pod_ciphertext_hi = pod::TransferAmountEncryption(ciphertext_hi.to_bytes());
let pod_ciphertext_new_source: pod::ElGamalCiphertext = ciphertext_new_source.into();
let pod_ciphertext_fee = pod::FeeEncryption(ciphertext_fee.to_bytes());
let mut transcript = TransferWithFeeProof::transcript_new(
&pod_transfer_with_fee_pubkeys,
&pod_ciphertext_lo,
&pod_ciphertext_hi,
&pod_ciphertext_fee,
);
let proof = TransferWithFeeProof::new(
(amount_lo, &ciphertext_lo, &opening_lo),
(amount_hi, &ciphertext_hi, &opening_hi),
keypair_source,
(pubkey_dest, pubkey_auditor),
(new_spendable_balance, &ciphertext_new_source),
(fee_amount, &ciphertext_fee, &opening_fee),
delta_fee,
pubkey_fee_collector,
fee_parameters,
&mut transcript,
);
Ok(Self {
ciphertext_lo: pod_ciphertext_lo,
ciphertext_hi: pod_ciphertext_hi,
transfer_with_fee_pubkeys: pod_transfer_with_fee_pubkeys,
ciphertext_new_source: pod_ciphertext_new_source,
ciphertext_fee: pod_ciphertext_fee,
fee_parameters: fee_parameters.into(),
proof,
})
}
/// Extracts the lo ciphertexts associated with a transfer-with-fee data
fn ciphertext_lo(&self, role: Role) -> Result<ElGamalCiphertext, ProofError> {
let ciphertext_lo: TransferAmountEncryption = self.ciphertext_lo.try_into()?;
let handle_lo = match role {
Role::Source => ciphertext_lo.source,
Role::Dest => ciphertext_lo.dest,
Role::Auditor => ciphertext_lo.auditor,
};
Ok(ElGamalCiphertext {
commitment: ciphertext_lo.commitment,
handle: handle_lo,
})
}
/// Extracts the lo ciphertexts associated with a transfer-with-fee data
fn ciphertext_hi(&self, role: Role) -> Result<ElGamalCiphertext, ProofError> {
let ciphertext_hi: TransferAmountEncryption = self.ciphertext_hi.try_into()?;
let handle_hi = match role {
Role::Source => ciphertext_hi.source,
Role::Dest => ciphertext_hi.dest,
Role::Auditor => ciphertext_hi.auditor,
};
Ok(ElGamalCiphertext {
commitment: ciphertext_hi.commitment,
handle: handle_hi,
})
}
/// Decrypts transfer amount from transfer-with-fee 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 TransferWithFeeData {
fn verify(&self) -> Result<(), ProofError> {
let mut transcript = TransferWithFeeProof::transcript_new(
&self.transfer_with_fee_pubkeys,
&self.ciphertext_lo,
&self.ciphertext_hi,
&self.ciphertext_fee,
);
let ciphertext_lo = self.ciphertext_lo.try_into()?;
let ciphertext_hi = self.ciphertext_hi.try_into()?;
let transfer_with_fee_pubkeys = self.transfer_with_fee_pubkeys.try_into()?;
let new_spendable_ciphertext = self.ciphertext_new_source.try_into()?;
let ciphertext_fee = self.ciphertext_fee.try_into()?;
let fee_parameters = self.fee_parameters.into();
self.proof.verify(
&ciphertext_lo,
&ciphertext_hi,
&transfer_with_fee_pubkeys,
&new_spendable_ciphertext,
&ciphertext_fee,
fee_parameters,
&mut transcript,
)
}
}
// #[derive(Clone, Copy, Pod, Zeroable)]
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
pub struct TransferWithFeeProof {
pub commitment_new_source: pod::PedersenCommitment,
pub commitment_claimed: pod::PedersenCommitment,
pub equality_proof: pod::EqualityProof,
pub ciphertext_amount_validity_proof: pod::AggregatedValidityProof,
pub fee_sigma_proof: pod::FeeSigmaProof,
pub ciphertext_fee_validity_proof: pod::ValidityProof,
pub range_proof: pod::RangeProof256,
}
#[allow(non_snake_case)]
#[cfg(not(target_arch = "bpf"))]
impl TransferWithFeeProof {
fn transcript_new(
transfer_with_fee_pubkeys: &pod::TransferWithFeePubkeys,
ciphertext_lo: &pod::TransferAmountEncryption,
ciphertext_hi: &pod::TransferAmountEncryption,
ciphertext_fee: &pod::FeeEncryption,
) -> Transcript {
let mut transcript = Transcript::new(b"FeeProof");
transcript.append_message(b"transfer-with-fee-pubkeys", &transfer_with_fee_pubkeys.0);
transcript.append_message(b"ciphertext-lo", &ciphertext_lo.0);
transcript.append_message(b"ciphertext-hi", &ciphertext_hi.0);
transcript.append_message(b"ciphertext-fee", &ciphertext_fee.0);
transcript
}
#[allow(clippy::too_many_arguments)]
#[allow(clippy::many_single_char_names)]
pub fn new(
transfer_amount_lo_data: (u32, &TransferAmountEncryption, &PedersenOpening),
transfer_amount_hi_data: (u32, &TransferAmountEncryption, &PedersenOpening),
keypair_source: &ElGamalKeypair,
(pubkey_dest, pubkey_auditor): (&ElGamalPubkey, &ElGamalPubkey),
(source_new_balance, ciphertext_new_source): (u64, &ElGamalCiphertext),
(fee_amount, ciphertext_fee, opening_fee): (u64, &FeeEncryption, &PedersenOpening),
delta_fee: u64,
pubkey_fee_collector: &ElGamalPubkey,
fee_parameters: FeeParameters,
transcript: &mut Transcript,
) -> Self {
let (transfer_amount_lo, ciphertext_lo, opening_lo) = transfer_amount_lo_data;
let (transfer_amount_hi, ciphertext_hi, opening_hi) = transfer_amount_hi_data;
// generate a Pedersen commitment for the remaining balance in source
let (commitment_new_source, opening_source) = Pedersen::new(source_new_balance);
let (commitment_claimed, opening_claimed) = Pedersen::new(delta_fee);
let pod_commitment_new_source: pod::PedersenCommitment = commitment_new_source.into();
let pod_commitment_claimed: pod::PedersenCommitment = commitment_claimed.into();
transcript.append_commitment(b"commitment-new-source", &pod_commitment_new_source);
transcript.append_commitment(b"commitment-claimed", &pod_commitment_claimed);
// generate equality_proof
let equality_proof = EqualityProof::new(
keypair_source,
ciphertext_new_source,
source_new_balance,
&opening_source,
transcript,
);
// generate ciphertext validity proof
let ciphertext_amount_validity_proof = AggregatedValidityProof::new(
(pubkey_dest, pubkey_auditor),
(transfer_amount_lo, transfer_amount_hi),
(opening_lo, opening_hi),
transcript,
);
let (commitment_delta, opening_delta) = compute_delta_commitment_and_opening(
(&ciphertext_lo.commitment, opening_lo),
(&ciphertext_hi.commitment, opening_hi),
(&ciphertext_fee.commitment, opening_fee),
fee_parameters.fee_rate_basis_points,
);
let fee_sigma_proof = FeeSigmaProof::new(
(fee_amount, &ciphertext_fee.commitment, opening_fee),
(delta_fee, &commitment_delta, &opening_delta),
(&commitment_claimed, &opening_claimed),
fee_parameters.maximum_fee,
transcript,
);
let ciphertext_fee_validity_proof = ValidityProof::new(
(pubkey_dest, pubkey_fee_collector),
fee_amount,
opening_fee,
transcript,
);
let opening_claimed_negated = &PedersenOpening::default() - &opening_claimed;
let range_proof = RangeProof::new(
vec![
source_new_balance,
transfer_amount_lo as u64,
transfer_amount_hi as u64,
delta_fee,
FEE_DENOMINATOR - delta_fee,
],
vec![
64, 32, 32, 64, // double check
64,
],
vec![
&opening_source,
opening_lo,
opening_hi,
&opening_claimed,
&opening_claimed_negated,
],
transcript,
);
Self {
commitment_new_source: pod_commitment_new_source,
commitment_claimed: pod_commitment_claimed,
equality_proof: equality_proof.into(),
ciphertext_amount_validity_proof: ciphertext_amount_validity_proof.into(),
fee_sigma_proof: fee_sigma_proof.into(),
ciphertext_fee_validity_proof: ciphertext_fee_validity_proof.into(),
range_proof: range_proof.try_into().expect("range proof: length error"),
}
}
pub fn verify(
&self,
ciphertext_lo: &TransferAmountEncryption,
ciphertext_hi: &TransferAmountEncryption,
transfer_with_fee_pubkeys: &TransferWithFeePubkeys,
new_spendable_ciphertext: &ElGamalCiphertext,
ciphertext_fee: &FeeEncryption,
fee_parameters: FeeParameters,
transcript: &mut Transcript,
) -> Result<(), ProofError> {
transcript.append_commitment(b"commitment-new-source", &self.commitment_new_source);
transcript.append_commitment(b"commitment-claimed", &self.commitment_claimed);
let commitment_new_source: PedersenCommitment = self.commitment_new_source.try_into()?;
let commitment_claimed: PedersenCommitment = self.commitment_claimed.try_into()?;
let equality_proof: EqualityProof = self.equality_proof.try_into()?;
let ciphertext_amount_validity_proof: AggregatedValidityProof =
self.ciphertext_amount_validity_proof.try_into()?;
let fee_sigma_proof: FeeSigmaProof = self.fee_sigma_proof.try_into()?;
let ciphertext_fee_validity_proof: ValidityProof =
self.ciphertext_fee_validity_proof.try_into()?;
let range_proof: RangeProof = self.range_proof.try_into()?;
// verify equality proof
equality_proof.verify(
&transfer_with_fee_pubkeys.source,
new_spendable_ciphertext,
&commitment_new_source,
transcript,
)?;
// verify that the transfer amount is encrypted correctly
ciphertext_amount_validity_proof.verify(
(
&transfer_with_fee_pubkeys.dest,
&transfer_with_fee_pubkeys.auditor,
),
(&ciphertext_lo.commitment, &ciphertext_hi.commitment),
(&ciphertext_lo.dest, &ciphertext_hi.dest),
(&ciphertext_lo.auditor, &ciphertext_hi.auditor),
transcript,
)?;
// verify fee sigma proof
let commitment_delta = compute_delta_commitment(
&ciphertext_lo.commitment,
&ciphertext_hi.commitment,
&ciphertext_fee.commitment,
fee_parameters.fee_rate_basis_points,
);
fee_sigma_proof.verify(
&ciphertext_fee.commitment,
&commitment_delta,
&commitment_claimed,
fee_parameters.maximum_fee,
transcript,
)?;
ciphertext_fee_validity_proof.verify(
&ciphertext_fee.commitment,
(
&transfer_with_fee_pubkeys.dest,
&transfer_with_fee_pubkeys.fee_collector,
),
(&ciphertext_fee.dest, &ciphertext_fee.fee_collector),
transcript,
)?;
let commitment_claimed_negated = &(*COMMITMENT_FEE_DENOMINATOR) - &commitment_claimed;
range_proof.verify(
vec![
&commitment_new_source,
&ciphertext_lo.commitment,
&ciphertext_hi.commitment,
&commitment_claimed,
&commitment_claimed_negated,
],
vec![64, 32, 32, 64, 64],
transcript,
)?;
Ok(())
}
}
/// The ElGamal public keys needed for a transfer with fee
#[derive(Clone)]
#[repr(C)]
#[cfg(not(target_arch = "bpf"))]
pub struct TransferWithFeePubkeys {
pub source: ElGamalPubkey,
pub dest: ElGamalPubkey,
pub auditor: ElGamalPubkey,
pub fee_collector: ElGamalPubkey,
}
#[cfg(not(target_arch = "bpf"))]
impl TransferWithFeePubkeys {
pub fn to_bytes(&self) -> [u8; 128] {
let mut bytes = [0u8; 128];
bytes[..32].copy_from_slice(&self.source.to_bytes());
bytes[32..64].copy_from_slice(&self.dest.to_bytes());
bytes[64..96].copy_from_slice(&self.auditor.to_bytes());
bytes[96..128].copy_from_slice(&self.fee_collector.to_bytes());
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, ProofError> {
let bytes = array_ref![bytes, 0, 128];
let (source, dest, auditor, fee_collector) = array_refs![bytes, 32, 32, 32, 32];
let source = ElGamalPubkey::from_bytes(source).ok_or(ProofError::Verification)?;
let dest = ElGamalPubkey::from_bytes(dest).ok_or(ProofError::Verification)?;
let auditor = ElGamalPubkey::from_bytes(auditor).ok_or(ProofError::Verification)?;
let fee_collector =
ElGamalPubkey::from_bytes(fee_collector).ok_or(ProofError::Verification)?;
Ok(Self {
source,
dest,
auditor,
fee_collector,
})
}
}
#[cfg(not(target_arch = "bpf"))]
impl pod::TransferWithFeePubkeys {
pub fn new(
source: &ElGamalPubkey,
dest: &ElGamalPubkey,
auditor: &ElGamalPubkey,
fee_collector: &ElGamalPubkey,
) -> Self {
let mut bytes = [0u8; 128];
bytes[..32].copy_from_slice(&source.to_bytes());
bytes[32..64].copy_from_slice(&dest.to_bytes());
bytes[64..96].copy_from_slice(&auditor.to_bytes());
bytes[96..128].copy_from_slice(&fee_collector.to_bytes());
Self(bytes)
}
}
#[derive(Clone)]
#[repr(C)]
#[cfg(not(target_arch = "bpf"))]
pub struct FeeEncryption {
pub commitment: PedersenCommitment,
pub dest: DecryptHandle,
pub fee_collector: DecryptHandle,
}
#[cfg(not(target_arch = "bpf"))]
impl FeeEncryption {
pub fn new(
amount: u64,
pubkey_dest: &ElGamalPubkey,
pubkey_fee_collector: &ElGamalPubkey,
) -> (Self, PedersenOpening) {
let (commitment, opening) = Pedersen::new(amount);
let fee_encryption = Self {
commitment,
dest: pubkey_dest.decrypt_handle(&opening),
fee_collector: pubkey_fee_collector.decrypt_handle(&opening),
};
(fee_encryption, opening)
}
pub fn to_bytes(&self) -> [u8; 96] {
let mut bytes = [0u8; 96];
bytes[..32].copy_from_slice(&self.commitment.to_bytes());
bytes[32..64].copy_from_slice(&self.dest.to_bytes());
bytes[64..96].copy_from_slice(&self.fee_collector.to_bytes());
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, ProofError> {
let bytes = array_ref![bytes, 0, 96];
let (commitment, dest, fee_collector) = array_refs![bytes, 32, 32, 32];
let commitment =
PedersenCommitment::from_bytes(commitment).ok_or(ProofError::Verification)?;
let dest = DecryptHandle::from_bytes(dest).ok_or(ProofError::Verification)?;
let fee_collector =
DecryptHandle::from_bytes(fee_collector).ok_or(ProofError::Verification)?;
Ok(Self {
commitment,
dest,
fee_collector,
})
}
}
#[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,
}
#[cfg(not(target_arch = "bpf"))]
impl FeeParameters {
pub fn to_bytes(&self) -> [u8; 10] {
let mut bytes = [0u8; 10];
bytes[..2].copy_from_slice(&self.fee_rate_basis_points.to_le_bytes());
bytes[2..10].copy_from_slice(&self.maximum_fee.to_le_bytes());
bytes
}
pub fn from_bytes(bytes: &[u8]) -> Self {
let bytes = array_ref![bytes, 0, 10];
let (fee_rate_basis_points, maximum_fee) = array_refs![bytes, 2, 8];
Self {
fee_rate_basis_points: u16::from_le_bytes(*fee_rate_basis_points),
maximum_fee: u64::from_le_bytes(*maximum_fee),
}
}
}
#[cfg(not(target_arch = "bpf"))]
fn calculate_fee(transfer_amount: u64, fee_rate_basis_points: u16) -> (u64, u64) {
let fee_scaled = (transfer_amount as u128) * (fee_rate_basis_points as u128);
let fee = (fee_scaled / FEE_DENOMINATOR as u128) as u64;
let rem = (fee_scaled % FEE_DENOMINATOR as u128) as u64;
if rem == 0 {
(fee, rem)
} else {
(fee + 1, rem)
}
}
#[cfg(not(target_arch = "bpf"))]
fn compute_delta_commitment_and_opening(
(commitment_lo, opening_lo): (&PedersenCommitment, &PedersenOpening),
(commitment_hi, opening_hi): (&PedersenCommitment, &PedersenOpening),
(commitment_fee, opening_fee): (&PedersenCommitment, &PedersenOpening),
fee_rate_basis_points: u16,
) -> (PedersenCommitment, PedersenOpening) {
let fee_rate_scalar = Scalar::from(fee_rate_basis_points);
let commitment_delta = commitment_fee * Scalar::from(FEE_DENOMINATOR)
- &(&combine_u32_commitments(commitment_lo, commitment_hi) * &fee_rate_scalar);
let opening_delta = opening_fee * Scalar::from(FEE_DENOMINATOR)
- &(&combine_u32_openings(opening_lo, opening_hi) * &fee_rate_scalar);
(commitment_delta, opening_delta)
}
#[cfg(not(target_arch = "bpf"))]
fn compute_delta_commitment(
commitment_lo: &PedersenCommitment,
commitment_hi: &PedersenCommitment,
commitment_fee: &PedersenCommitment,
fee_rate_basis_points: u16,
) -> PedersenCommitment {
let fee_rate_scalar = Scalar::from(fee_rate_basis_points);
commitment_fee * Scalar::from(FEE_DENOMINATOR)
- &(&combine_u32_commitments(commitment_lo, commitment_hi) * &fee_rate_scalar)
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_fee_correctness() {
let keypair_source = ElGamalKeypair::new_rand();
let pubkey_dest = ElGamalKeypair::new_rand().public;
let pubkey_auditor = ElGamalKeypair::new_rand().public;
let pubkey_fee_collector = ElGamalKeypair::new_rand().public;
let spendable_balance: u64 = 120;
let spendable_ciphertext = keypair_source.public.encrypt(spendable_balance);
let transfer_amount: u64 = 100;
let fee_parameters = FeeParameters {
fee_rate_basis_points: 100,
maximum_fee: 3,
};
let fee_data = TransferWithFeeData::new(
transfer_amount,
(spendable_balance, &spendable_ciphertext),
&keypair_source,
(&pubkey_dest, &pubkey_auditor),
fee_parameters,
&pubkey_fee_collector,
)
.unwrap();
assert!(fee_data.verify().is_ok());
}
}

View File

@ -6,8 +6,8 @@ use {
use {
crate::{
encryption::{
elgamal::{ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey},
pedersen::{Pedersen, PedersenCommitment, PedersenOpening},
elgamal::{ElGamal, ElGamalCiphertext, ElGamalKeypair, ElGamalPubkey},
pedersen::{Pedersen, PedersenCommitment},
},
errors::ProofError,
instruction::Verifiable,
@ -30,11 +30,11 @@ use {
#[repr(C)]
pub struct WithdrawData {
/// The source account ElGamal pubkey
pub elgamal_pubkey: pod::ElGamalPubkey, // 32 bytes
pub pubkey: pod::ElGamalPubkey, // 32 bytes
/// The source account available balance *after* the withdraw (encrypted by
/// `source_pk`
pub final_balance_ct: pod::ElGamalCiphertext, // 64 bytes
pub final_ciphertext: pod::ElGamalCiphertext, // 64 bytes
/// Range proof
pub proof: WithdrawProof, // 736 bytes
@ -44,38 +44,43 @@ impl WithdrawData {
#[cfg(not(target_arch = "bpf"))]
pub fn new(
amount: u64,
source_keypair: &ElGamalKeypair,
keypair: &ElGamalKeypair,
current_balance: u64,
current_balance_ct: ElGamalCiphertext,
) -> Self {
current_ciphertext: &ElGamalCiphertext,
) -> Result<Self, ProofError> {
// subtract withdraw amount from current balance
//
// panics if current_balance < amount
let final_balance = current_balance - amount;
// errors if current_balance < amount
let final_balance = current_balance
.checked_sub(amount)
.ok_or(ProofError::Generation)?;
// encode withdraw amount as an ElGamal ciphertext and subtract it from
// current source balance
let amount_encoded = source_keypair
.public
.encrypt_with(amount, &PedersenOpening::default());
let final_balance_ct = current_balance_ct - amount_encoded;
let final_ciphertext = current_ciphertext - &ElGamal::encode(amount);
let proof = WithdrawProof::new(source_keypair, final_balance, &final_balance_ct);
let pod_pubkey = pod::ElGamalPubkey((&keypair.public).to_bytes());
let pod_final_ciphertext: pod::ElGamalCiphertext = final_ciphertext.into();
let mut transcript = WithdrawProof::transcript_new(&pod_pubkey, &pod_final_ciphertext);
let proof = WithdrawProof::new(keypair, final_balance, &final_ciphertext, &mut transcript);
Self {
elgamal_pubkey: source_keypair.public.into(),
final_balance_ct: final_balance_ct.into(),
Ok(Self {
pubkey: pod_pubkey,
final_ciphertext: pod_final_ciphertext,
proof,
}
})
}
}
#[cfg(not(target_arch = "bpf"))]
impl Verifiable for WithdrawData {
fn verify(&self) -> Result<(), ProofError> {
let elgamal_pubkey = self.elgamal_pubkey.try_into()?;
let final_balance_ct = self.final_balance_ct.try_into()?;
self.proof.verify(&elgamal_pubkey, &final_balance_ct)
let mut transcript = WithdrawProof::transcript_new(&self.pubkey, &self.final_ciphertext);
let elgamal_pubkey = self.pubkey.try_into()?;
let final_balance_ciphertext = self.final_ciphertext.try_into()?;
self.proof
.verify(&elgamal_pubkey, &final_balance_ciphertext, &mut transcript)
}
}
@ -98,53 +103,44 @@ pub struct WithdrawProof {
#[allow(non_snake_case)]
#[cfg(not(target_arch = "bpf"))]
impl WithdrawProof {
fn transcript_new() -> Transcript {
Transcript::new(b"WithdrawProof")
fn transcript_new(
pubkey: &pod::ElGamalPubkey,
ciphertext: &pod::ElGamalCiphertext,
) -> Transcript {
let mut transcript = Transcript::new(b"WithdrawProof");
transcript.append_pubkey(b"pubkey", pubkey);
transcript.append_ciphertext(b"ciphertext", ciphertext);
transcript
}
pub fn new(
source_keypair: &ElGamalKeypair,
keypair: &ElGamalKeypair,
final_balance: u64,
final_balance_ct: &ElGamalCiphertext,
final_ciphertext: &ElGamalCiphertext,
transcript: &mut Transcript,
) -> Self {
let mut transcript = Self::transcript_new();
// add a domain separator to record the start of the protocol
transcript.withdraw_proof_domain_sep();
// generate a Pedersen commitment for `final_balance`
let (commitment, opening) = Pedersen::new(final_balance);
let pod_commitment: pod::PedersenCommitment = commitment.into();
// extract the relevant scalar and Ristretto points from the inputs
let P_EG = source_keypair.public.get_point();
let C_EG = final_balance_ct.commitment.get_point();
let D_EG = final_balance_ct.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());
transcript.append_commitment(b"commitment", &pod_commitment);
// generate equality_proof
let equality_proof = EqualityProof::new(
source_keypair,
final_balance_ct,
keypair,
final_ciphertext,
final_balance,
&opening,
&mut transcript,
transcript,
);
let range_proof = RangeProof::new(
vec![final_balance],
vec![64],
vec![&opening],
&mut transcript,
);
let range_proof =
RangeProof::new(vec![final_balance], vec![64], vec![&opening], transcript);
WithdrawProof {
commitment: commitment.into(),
commitment: pod_commitment,
equality_proof: equality_proof.try_into().expect("equality proof"),
range_proof: range_proof.try_into().expect("range proof"),
}
@ -152,43 +148,25 @@ impl WithdrawProof {
pub fn verify(
&self,
source_pk: &ElGamalPubkey,
final_balance_ct: &ElGamalCiphertext,
pubkey: &ElGamalPubkey,
final_ciphertext: &ElGamalCiphertext,
transcript: &mut Transcript,
) -> Result<(), ProofError> {
let mut transcript = Self::transcript_new();
transcript.append_commitment(b"commitment", &self.commitment);
let commitment: PedersenCommitment = self.commitment.try_into()?;
let equality_proof: EqualityProof = self.equality_proof.try_into()?;
let range_proof: RangeProof = self.range_proof.try_into()?;
// add a domain separator to record the start of the protocol
transcript.withdraw_proof_domain_sep();
// extract the relevant scalar and Ristretto points from the inputs
let P_EG = source_pk.get_point();
let C_EG = final_balance_ct.commitment.get_point();
let D_EG = final_balance_ct.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, final_balance_ct, &commitment, &mut transcript)?;
equality_proof.verify(pubkey, final_ciphertext, &commitment, transcript)?;
// verify range proof
//
// TODO: double compressing here - consider modifying range proof input type to `PedersenCommitment`
range_proof.verify(
vec![&commitment.get_point().compress()],
vec![64_usize],
&mut transcript,
)?;
range_proof.verify(vec![&commitment], vec![64_usize], transcript)?;
Ok(())
}
@ -201,29 +179,31 @@ mod test {
#[test]
fn test_withdraw_correctness() {
// generate and verify proof for the proper setting
let elgamal_keypair = ElGamalKeypair::new_rand();
let keypair = ElGamalKeypair::new_rand();
let current_balance: u64 = 77;
let current_balance_ct = elgamal_keypair.public.encrypt(current_balance);
let current_ciphertext = keypair.public.encrypt(current_balance);
let withdraw_amount: u64 = 55;
let data = WithdrawData::new(
withdraw_amount,
&elgamal_keypair,
&keypair,
current_balance,
current_balance_ct,
);
&current_ciphertext,
)
.unwrap();
assert!(data.verify().is_ok());
// generate and verify proof with wrong balance
let wrong_balance: u64 = 99;
let data = WithdrawData::new(
withdraw_amount,
&elgamal_keypair,
&keypair,
wrong_balance,
current_balance_ct,
);
&current_ciphertext,
)
.unwrap();
assert!(data.verify().is_err());
}
}

View File

@ -31,7 +31,8 @@ mod sigma_proofs;
#[cfg(not(target_arch = "bpf"))]
mod transcript;
mod instruction;
// TODO: re-organize visibility
pub mod instruction;
pub mod zk_token_elgamal;
pub mod zk_token_proof_instruction;
pub mod zk_token_proof_program;

View File

@ -1,6 +1,6 @@
#[cfg(not(target_arch = "bpf"))]
use {
crate::encryption::pedersen::{Pedersen, PedersenOpening},
crate::encryption::pedersen::{Pedersen, PedersenCommitment, PedersenOpening},
curve25519_dalek::traits::MultiscalarMul,
rand::rngs::OsRng,
subtle::{Choice, ConditionallySelectable},
@ -220,7 +220,7 @@ impl RangeProof {
#[allow(clippy::many_single_char_names)]
pub fn verify(
&self,
comms: Vec<&CompressedRistretto>,
comms: Vec<&PedersenCommitment>,
bit_lengths: Vec<usize>,
transcript: &mut Transcript,
) -> Result<(), RangeProofError> {
@ -308,7 +308,7 @@ impl RangeProof {
.chain(self.ipp_proof.R_vec.iter().map(|R| R.decompress()))
.chain(bp_gens.G(nm).map(|&x| Some(x)))
.chain(bp_gens.H(nm).map(|&x| Some(x)))
.chain(comms.iter().map(|V| V.decompress())),
.chain(comms.iter().map(|V| Some(*V.get_point()))),
)
.ok_or(RangeProofError::MultiscalarMul)?;
@ -403,11 +403,7 @@ mod tests {
let proof = RangeProof::new(vec![55], vec![32], vec![&open], &mut transcript_create);
assert!(proof
.verify(
vec![&comm.get_point().compress()],
vec![32],
&mut transcript_verify
)
.verify(vec![&comm], vec![32], &mut transcript_verify)
.is_ok());
}
@ -427,13 +423,9 @@ mod tests {
&mut transcript_create,
);
let comm_1_point = comm_1.get_point().compress();
let comm_2_point = comm_2.get_point().compress();
let comm_3_point = comm_3.get_point().compress();
assert!(proof
.verify(
vec![&comm_1_point, &comm_2_point, &comm_3_point],
vec![&comm_1, &comm_2, &comm_3],
vec![64, 32, 32],
&mut transcript_verify,
)

View File

@ -69,6 +69,8 @@ impl EqualityProof {
opening: &PedersenOpening,
transcript: &mut Transcript,
) -> Self {
transcript.equality_proof_domain_sep();
// extract the relevant scalar and Ristretto points from the inputs
let P_EG = elgamal_keypair.public.get_point();
let D_EG = ciphertext.handle.get_point();
@ -127,6 +129,8 @@ impl EqualityProof {
commitment: &PedersenCommitment,
transcript: &mut Transcript,
) -> Result<(), EqualityProofError> {
transcript.equality_proof_domain_sep();
// extract the relevant scalar and Ristretto points from the inputs
let P_EG = elgamal_pubkey.get_point();
let C_EG = ciphertext.commitment.get_point();

View File

@ -57,7 +57,7 @@ impl From<TranscriptError> for ZeroBalanceProofError {
}
#[derive(Error, Clone, Debug, Eq, PartialEq)]
pub enum FeeProofError {
pub enum FeeSigmaProofError {
#[error("the required algebraic relation does not hold")]
AlgebraicRelation,
#[error("malformed proof")]
@ -68,7 +68,7 @@ pub enum FeeProofError {
Transcript,
}
impl From<TranscriptError> for FeeProofError {
impl From<TranscriptError> for FeeSigmaProofError {
fn from(_err: TranscriptError) -> Self {
Self::Transcript
}

View File

@ -8,7 +8,8 @@ use {
rand::rngs::OsRng,
};
use {
crate::{sigma_proofs::errors::FeeProofError, transcript::TranscriptProtocol},
crate::{sigma_proofs::errors::FeeSigmaProofError, transcript::TranscriptProtocol},
arrayref::{array_ref, array_refs},
curve25519_dalek::{
ristretto::{CompressedRistretto, RistrettoPoint},
scalar::Scalar,
@ -73,25 +74,25 @@ impl FeeSigmaProof {
&mut transcript_fee_below_max,
);
let above_max = u64::ct_gt(&max_fee, &fee_amount);
let below_max = u64::ct_gt(&max_fee, &fee_amount);
// conditionally assign transcript; transcript is not conditionally selectable
if bool::from(above_max) {
*transcript = transcript_fee_above_max;
} else {
if bool::from(below_max) {
*transcript = transcript_fee_below_max;
} else {
*transcript = transcript_fee_above_max;
}
Self {
fee_max_proof: FeeMaxProof::conditional_select(
&proof_fee_above_max.fee_max_proof,
&proof_fee_below_max.fee_max_proof,
above_max,
below_max,
),
fee_equality_proof: FeeEqualityProof::conditional_select(
&proof_fee_above_max.fee_equality_proof,
&proof_fee_below_max.fee_equality_proof,
above_max,
below_max,
),
}
}
@ -259,7 +260,7 @@ impl FeeSigmaProof {
commitment_claimed: &PedersenCommitment,
max_fee: u64,
transcript: &mut Transcript,
) -> Result<(), FeeProofError> {
) -> Result<(), FeeSigmaProofError> {
// extract the relevant scalar and Ristretto points from the input
let m = Scalar::from(max_fee);
@ -275,19 +276,19 @@ impl FeeSigmaProof {
.fee_max_proof
.Y_max_proof
.decompress()
.ok_or(FeeProofError::Format)?;
.ok_or(FeeSigmaProofError::Format)?;
let z_max = self.fee_max_proof.z_max_proof;
let Y_delta_real = self
.fee_equality_proof
.Y_delta
.decompress()
.ok_or(FeeProofError::Format)?;
.ok_or(FeeSigmaProofError::Format)?;
let Y_claimed = self
.fee_equality_proof
.Y_claimed
.decompress()
.ok_or(FeeProofError::Format)?;
.ok_or(FeeSigmaProofError::Format)?;
let z_x = self.fee_equality_proof.z_x;
let z_delta_real = self.fee_equality_proof.z_delta;
let z_claimed = self.fee_equality_proof.z_claimed;
@ -333,9 +334,56 @@ impl FeeSigmaProof {
if check.is_identity() {
Ok(())
} else {
Err(FeeProofError::AlgebraicRelation)
Err(FeeSigmaProofError::AlgebraicRelation)
}
}
pub fn to_bytes(&self) -> [u8; 256] {
let mut buf = [0_u8; 256];
buf[..32].copy_from_slice(self.fee_max_proof.Y_max_proof.as_bytes());
buf[32..64].copy_from_slice(self.fee_max_proof.z_max_proof.as_bytes());
buf[64..96].copy_from_slice(self.fee_max_proof.c_max_proof.as_bytes());
buf[96..128].copy_from_slice(self.fee_equality_proof.Y_delta.as_bytes());
buf[128..160].copy_from_slice(self.fee_equality_proof.Y_claimed.as_bytes());
buf[160..192].copy_from_slice(self.fee_equality_proof.z_x.as_bytes());
buf[192..224].copy_from_slice(self.fee_equality_proof.z_delta.as_bytes());
buf[224..256].copy_from_slice(self.fee_equality_proof.z_claimed.as_bytes());
buf
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, FeeSigmaProofError> {
let bytes = array_ref![bytes, 0, 256];
let (Y_max_proof, z_max_proof, c_max_proof, Y_delta, Y_claimed, z_x, z_delta, z_claimed) =
array_refs![bytes, 32, 32, 32, 32, 32, 32, 32, 32];
let Y_max_proof = CompressedRistretto::from_slice(Y_max_proof);
let z_max_proof =
Scalar::from_canonical_bytes(*z_max_proof).ok_or(FeeSigmaProofError::Format)?;
let c_max_proof =
Scalar::from_canonical_bytes(*c_max_proof).ok_or(FeeSigmaProofError::Format)?;
let Y_delta = CompressedRistretto::from_slice(Y_delta);
let Y_claimed = CompressedRistretto::from_slice(Y_claimed);
let z_x = Scalar::from_canonical_bytes(*z_x).ok_or(FeeSigmaProofError::Format)?;
let z_delta = Scalar::from_canonical_bytes(*z_delta).ok_or(FeeSigmaProofError::Format)?;
let z_claimed =
Scalar::from_canonical_bytes(*z_claimed).ok_or(FeeSigmaProofError::Format)?;
Ok(Self {
fee_max_proof: FeeMaxProof {
Y_max_proof,
z_max_proof,
c_max_proof,
},
fee_equality_proof: FeeEqualityProof {
Y_delta,
Y_claimed,
z_x,
z_delta,
z_claimed,
},
})
}
}
/// The fee max proof.
@ -491,4 +539,46 @@ mod test {
)
.is_ok());
}
#[test]
fn test_fee_delta_is_zero() {
let transfer_amount: u64 = 100;
let max_fee: u64 = 3;
let rate_fee: u16 = 100; // 1.00%
let amount_fee: u64 = 1;
let delta: u64 = 0; // 1*10000 - 100*100
let (commitment_transfer, opening_transfer) = Pedersen::new(transfer_amount);
let (commitment_fee, opening_fee) = Pedersen::new(amount_fee);
let scalar_rate = Scalar::from(rate_fee);
let commitment_delta =
&(&commitment_fee * &Scalar::from(10000_u64)) - &(&commitment_transfer * &scalar_rate);
let opening_delta =
&(&opening_fee * &Scalar::from(10000_u64)) - &(&opening_transfer * &scalar_rate);
let (commitment_claimed, opening_claimed) = Pedersen::new(delta);
let mut transcript_prover = Transcript::new(b"test");
let mut transcript_verifier = Transcript::new(b"test");
let proof = FeeSigmaProof::new(
(amount_fee, &commitment_fee, &opening_fee),
(delta, &commitment_delta, &opening_delta),
(&commitment_claimed, &opening_claimed),
max_fee,
&mut transcript_prover,
);
assert!(proof
.verify(
&commitment_fee,
&commitment_delta,
&commitment_claimed,
max_fee,
&mut transcript_verifier,
)
.is_ok());
}
}

View File

@ -62,11 +62,13 @@ impl ValidityProof {
/// * `opening` - The opening associated with the Pedersen commitment
/// * `transcript` - The transcript that does the bookkeeping for the Fiat-Shamir heuristic
pub fn new<T: Into<Scalar>>(
(pubkey_dest, pubkey_auditor): (&ElGamalPubkey, &ElGamalPubkey),
(pubkey_dest, pubkey_auditor): (&ElGamalPubkey, &ElGamalPubkey), // TODO: rename pubkey_auditor
amount: T,
opening: &PedersenOpening,
transcript: &mut Transcript,
) -> Self {
transcript.validity_proof_domain_sep();
// extract the relevant scalar and Ristretto points from the inputs
let P_dest = pubkey_dest.get_point();
let P_auditor = pubkey_auditor.get_point();
@ -120,6 +122,8 @@ impl ValidityProof {
(handle_dest, handle_auditor): (&DecryptHandle, &DecryptHandle),
transcript: &mut Transcript,
) -> Result<(), ValidityProofError> {
transcript.validity_proof_domain_sep();
// include Y_0, Y_1, Y_2 to transcript and extract challenges
transcript.validate_and_append_point(b"Y_0", &self.Y_0)?;
transcript.validate_and_append_point(b"Y_1", &self.Y_1)?;
@ -235,6 +239,8 @@ impl AggregatedValidityProof {
(opening_lo, opening_hi): (&PedersenOpening, &PedersenOpening),
transcript: &mut Transcript,
) -> Self {
transcript.aggregated_validity_proof_domain_sep();
let t = transcript.challenge_scalar(b"t");
let aggregated_message = amount_lo.into() + amount_hi.into() * t;
@ -263,6 +269,8 @@ impl AggregatedValidityProof {
(handle_lo_auditor, handle_hi_auditor): (&DecryptHandle, &DecryptHandle),
transcript: &mut Transcript,
) -> Result<(), ValidityProofError> {
transcript.aggregated_validity_proof_domain_sep();
let t = transcript.challenge_scalar(b"t");
let aggregated_commitment = commitment_lo + commitment_hi * t;

View File

@ -1,5 +1,5 @@
use {
crate::errors::TranscriptError,
crate::{errors::TranscriptError, zk_token_elgamal::pod},
curve25519_dalek::{ristretto::CompressedRistretto, scalar::Scalar, traits::IsIdentity},
merlin::Transcript,
};
@ -19,9 +19,6 @@ pub trait TranscriptProtocol {
/// Append a domain separator for close account proof.
fn close_account_proof_domain_sep(&mut self);
/// Append a domain separator for update account public key proof.
fn update_account_public_key_proof_domain_sep(&mut self);
/// Append a domain separator for withdraw proof.
fn withdraw_proof_domain_sep(&mut self);
@ -34,6 +31,33 @@ pub trait TranscriptProtocol {
/// Append a `point` with the given `label`.
fn append_point(&mut self, label: &'static [u8], point: &CompressedRistretto);
/// Append an ElGamal pubkey with the given `label`.
fn append_pubkey(&mut self, label: &'static [u8], point: &pod::ElGamalPubkey);
/// Append an ElGamal ciphertext with the given `label`.
fn append_ciphertext(&mut self, label: &'static [u8], point: &pod::ElGamalCiphertext);
/// Append a Pedersen commitment with the given `label`.
fn append_commitment(&mut self, label: &'static [u8], point: &pod::PedersenCommitment);
/// Append an ElGamal decryption handle with the given `label`.
fn append_handle(&mut self, label: &'static [u8], point: &pod::DecryptHandle);
/// Append a domain separator for equality proof.
fn equality_proof_domain_sep(&mut self);
/// Append a domain separator for zero-balance proof.
fn zero_balance_proof_domain_sep(&mut self);
/// Append a domain separator for validity proof.
fn validity_proof_domain_sep(&mut self);
/// Append a domain separator for aggregated validity proof.
fn aggregated_validity_proof_domain_sep(&mut self);
/// Append a domain separator for fee sigma proof.
fn fee_sigma_proof_domain_sep(&mut self);
/// Check that a point is not the identity, then append it to the
/// transcript. Otherwise, return an error.
fn validate_and_append_point(
@ -66,10 +90,6 @@ impl TranscriptProtocol for Transcript {
self.append_message(b"dom-sep", b"CloseAccountProof");
}
fn update_account_public_key_proof_domain_sep(&mut self) {
self.append_message(b"dom-sep", b"UpdateAccountPublicKeyProof");
}
fn withdraw_proof_domain_sep(&mut self) {
self.append_message(b"dom-sep", b"WithdrawProof");
}
@ -105,4 +125,40 @@ impl TranscriptProtocol for Transcript {
Scalar::from_bytes_mod_order_wide(&buf)
}
fn append_pubkey(&mut self, label: &'static [u8], pubkey: &pod::ElGamalPubkey) {
self.append_message(label, &pubkey.0);
}
fn append_ciphertext(&mut self, label: &'static [u8], ciphertext: &pod::ElGamalCiphertext) {
self.append_message(label, &ciphertext.0);
}
fn append_commitment(&mut self, label: &'static [u8], commitment: &pod::PedersenCommitment) {
self.append_message(label, &commitment.0);
}
fn append_handle(&mut self, label: &'static [u8], handle: &pod::DecryptHandle) {
self.append_message(label, &handle.0);
}
fn equality_proof_domain_sep(&mut self) {
self.append_message(b"dom-sep", b"equality-proof")
}
fn zero_balance_proof_domain_sep(&mut self) {
self.append_message(b"dom-sep", b"zero-balance-proof")
}
fn validity_proof_domain_sep(&mut self) {
self.append_message(b"dom-sep", b"validity-proof")
}
fn aggregated_validity_proof_domain_sep(&mut self) {
self.append_message(b"dom-sep", b"aggregated-validity-proof")
}
fn fee_sigma_proof_domain_sep(&mut self) {
self.append_message(b"dom-sep", b"fee-sigma-proof")
}
}

View File

@ -21,10 +21,15 @@ mod target_arch {
pedersen::PedersenCommitment,
},
errors::ProofError,
instruction::{
transfer::{TransferAmountEncryption, TransferPubkeys},
transfer_with_fee::{FeeEncryption, FeeParameters, TransferWithFeePubkeys},
},
range_proof::{errors::RangeProofError, RangeProof},
sigma_proofs::{
equality_proof::EqualityProof,
errors::*,
fee_proof::FeeSigmaProof,
validity_proof::{AggregatedValidityProof, ValidityProof},
zero_balance_proof::ZeroBalanceProof,
},
@ -202,6 +207,20 @@ mod target_arch {
}
}
impl From<FeeSigmaProof> for pod::FeeSigmaProof {
fn from(proof: FeeSigmaProof) -> Self {
Self(proof.to_bytes())
}
}
impl TryFrom<pod::FeeSigmaProof> for FeeSigmaProof {
type Error = FeeSigmaProofError;
fn try_from(pod: pod::FeeSigmaProof) -> Result<Self, Self::Error> {
Self::from_bytes(&pod.0)
}
}
impl TryFrom<RangeProof> for pod::RangeProof64 {
type Error = RangeProofError;
@ -260,6 +279,104 @@ mod target_arch {
Self::from_bytes(&pod.0)
}
}
#[cfg(not(target_arch = "bpf"))]
impl TryFrom<RangeProof> for pod::RangeProof256 {
type Error = RangeProofError;
fn try_from(proof: RangeProof) -> Result<Self, Self::Error> {
if proof.ipp_proof.serialized_size() != 576 {
return Err(RangeProofError::Format);
}
let mut buf = [0_u8; 800];
buf[..32].copy_from_slice(proof.A.as_bytes());
buf[32..64].copy_from_slice(proof.S.as_bytes());
buf[64..96].copy_from_slice(proof.T_1.as_bytes());
buf[96..128].copy_from_slice(proof.T_2.as_bytes());
buf[128..160].copy_from_slice(proof.t_x.as_bytes());
buf[160..192].copy_from_slice(proof.t_x_blinding.as_bytes());
buf[192..224].copy_from_slice(proof.e_blinding.as_bytes());
buf[224..800].copy_from_slice(&proof.ipp_proof.to_bytes());
Ok(pod::RangeProof256(buf))
}
}
impl TryFrom<pod::RangeProof256> for RangeProof {
type Error = RangeProofError;
fn try_from(pod: pod::RangeProof256) -> Result<Self, Self::Error> {
Self::from_bytes(&pod.0)
}
}
impl From<TransferPubkeys> for pod::TransferPubkeys {
fn from(keys: TransferPubkeys) -> Self {
Self(keys.to_bytes())
}
}
impl TryFrom<pod::TransferPubkeys> for TransferPubkeys {
type Error = ProofError;
fn try_from(pod: pod::TransferPubkeys) -> Result<Self, Self::Error> {
Self::from_bytes(&pod.0)
}
}
impl From<TransferWithFeePubkeys> for pod::TransferWithFeePubkeys {
fn from(keys: TransferWithFeePubkeys) -> Self {
Self(keys.to_bytes())
}
}
impl TryFrom<pod::TransferWithFeePubkeys> for TransferWithFeePubkeys {
type Error = ProofError;
fn try_from(pod: pod::TransferWithFeePubkeys) -> Result<Self, Self::Error> {
Self::from_bytes(&pod.0)
}
}
impl From<TransferAmountEncryption> for pod::TransferAmountEncryption {
fn from(ciphertext: TransferAmountEncryption) -> Self {
Self(ciphertext.to_bytes())
}
}
impl TryFrom<pod::TransferAmountEncryption> for TransferAmountEncryption {
type Error = ProofError;
fn try_from(pod: pod::TransferAmountEncryption) -> Result<Self, Self::Error> {
Self::from_bytes(&pod.0)
}
}
impl From<FeeEncryption> for pod::FeeEncryption {
fn from(ciphertext: FeeEncryption) -> Self {
Self(ciphertext.to_bytes())
}
}
impl TryFrom<pod::FeeEncryption> for FeeEncryption {
type Error = ProofError;
fn try_from(pod: pod::FeeEncryption) -> Result<Self, Self::Error> {
Self::from_bytes(&pod.0)
}
}
impl From<FeeParameters> for pod::FeeParameters {
fn from(parameters: FeeParameters) -> Self {
Self(parameters.to_bytes())
}
}
impl From<pod::FeeParameters> for FeeParameters {
fn from(pod: pod::FeeParameters) -> Self {
Self::from_bytes(&pod.0)
}
}
}
#[cfg(target_arch = "bpf")]
@ -288,11 +405,7 @@ mod tests {
let proof_deserialized: RangeProof = proof_serialized.try_into().unwrap();
assert!(proof_deserialized
.verify(
vec![&comm.get_point().compress()],
vec![64],
&mut transcript_verify
)
.verify(vec![&comm], vec![64], &mut transcript_verify)
.is_ok());
// should fail to serialize to pod::RangeProof128
@ -317,16 +430,12 @@ mod tests {
&mut transcript_create,
);
let comm_1_point = comm_1.get_point().compress();
let comm_2_point = comm_2.get_point().compress();
let comm_3_point = comm_3.get_point().compress();
let proof_serialized: pod::RangeProof128 = proof.try_into().unwrap();
let proof_deserialized: RangeProof = proof_serialized.try_into().unwrap();
assert!(proof_deserialized
.verify(
vec![&comm_1_point, &comm_2_point, &comm_3_point],
vec![&comm_1, &comm_2, &comm_3],
vec![64, 32, 32],
&mut transcript_verify,
)

View File

@ -95,6 +95,11 @@ pub struct ZeroBalanceProof(pub [u8; 96]);
unsafe impl Zeroable for ZeroBalanceProof {}
unsafe impl Pod for ZeroBalanceProof {}
/// Serialization of fee sigma proof
#[derive(Clone, Copy, Pod, Zeroable)]
#[repr(transparent)]
pub struct FeeSigmaProof(pub [u8; 256]);
/// Serialization of range proofs for 64-bit numbers (for `Withdraw` instruction)
#[derive(Clone, Copy)]
#[repr(transparent)]
@ -115,6 +120,16 @@ pub struct RangeProof128(pub [u8; 736]);
unsafe impl Zeroable for RangeProof128 {}
unsafe impl Pod for RangeProof128 {}
/// Serialization of range proofs for 128-bit numbers (for `TransferRangeProof` instruction)
#[derive(Clone, Copy)]
#[repr(transparent)]
pub struct RangeProof256(pub [u8; 800]);
// `PodRangeProof256` is a Pod and Zeroable.
// Add the marker traits manually because `bytemuck` only adds them for some `u8` arrays
unsafe impl Zeroable for RangeProof256 {}
unsafe impl Pod for RangeProof256 {}
/// Serialization for AeCiphertext
#[derive(Clone, Copy, PartialEq)]
#[repr(transparent)]
@ -136,3 +151,33 @@ impl Default for AeCiphertext {
Self::zeroed()
}
}
// TODO: refactor this code into the instruction module
#[derive(Clone, Copy)]
#[repr(transparent)]
pub struct TransferPubkeys(pub [u8; 96]);
unsafe impl Zeroable for TransferPubkeys {}
unsafe impl Pod for TransferPubkeys {}
#[derive(Clone, Copy, Pod, Zeroable)]
#[repr(transparent)]
pub struct TransferWithFeePubkeys(pub [u8; 128]);
#[derive(Clone, Copy, Pod, Zeroable)]
#[repr(transparent)]
pub struct TransferAmountEncryption(pub [u8; 128]);
#[derive(Clone, Copy)]
#[repr(transparent)]
pub struct FeeEncryption(pub [u8; 96]);
unsafe impl Zeroable for FeeEncryption {}
unsafe impl Pod for FeeEncryption {}
#[derive(Clone, Copy)]
#[repr(transparent)]
pub struct FeeParameters(pub [u8; 10]);
unsafe impl Zeroable for FeeParameters {}
unsafe impl Pod for FeeParameters {}