Rename crypto crate to sdk

This commit is contained in:
Michael Vines
2021-09-29 21:45:35 -07:00
parent 7da620f0b4
commit d01d425e4b
21 changed files with 4672 additions and 0 deletions

View File

@@ -0,0 +1,163 @@
use {
crate::pod::*,
bytemuck::{Pod, Zeroable},
};
#[cfg(not(target_arch = "bpf"))]
use {
crate::{
encryption::elgamal::{ElGamalCT, ElGamalSK},
errors::ProofError,
instruction::Verifiable,
transcript::TranscriptProtocol,
},
curve25519_dalek::{
ristretto::RistrettoPoint,
scalar::Scalar,
traits::{IsIdentity, MultiscalarMul},
},
merlin::Transcript,
rand::rngs::OsRng,
std::convert::TryInto,
};
/// This struct includes the cryptographic proof *and* the account data information needed to verify
/// the proof
///
/// - The pre-instruction should call CloseAccountData::verify_proof(&self)
/// - The actual program should check that `balance` is consistent with what is
/// currently stored in the confidential token account
///
#[derive(Clone, Copy, Pod, Zeroable)]
#[repr(C)]
pub struct CloseAccountData {
/// The source account available balance in encrypted form
pub balance: PodElGamalCT, // 64 bytes
/// Proof that the source account available balance is zero
pub proof: CloseAccountProof, // 64 bytes
}
#[cfg(not(target_arch = "bpf"))]
impl CloseAccountData {
pub fn new(source_sk: &ElGamalSK, balance: ElGamalCT) -> Self {
let proof = CloseAccountProof::new(source_sk, &balance);
CloseAccountData {
balance: balance.into(),
proof,
}
}
}
#[cfg(not(target_arch = "bpf"))]
impl Verifiable for CloseAccountData {
fn verify(&self) -> Result<(), ProofError> {
let balance = self.balance.try_into()?;
self.proof.verify(&balance)
}
}
/// This struct represents the cryptographic proof component that certifies that the encrypted
/// balance is zero
#[derive(Clone, Copy, Pod, Zeroable)]
#[repr(C)]
#[allow(non_snake_case)]
pub struct CloseAccountProof {
pub R: PodCompressedRistretto, // 32 bytes
pub z: PodScalar, // 32 bytes
}
#[allow(non_snake_case)]
#[cfg(not(target_arch = "bpf"))]
impl CloseAccountProof {
fn transcript_new() -> Transcript {
Transcript::new(b"CloseAccountProof")
}
pub fn new(source_sk: &ElGamalSK, balance: &ElGamalCT) -> Self {
let mut transcript = Self::transcript_new();
// add a domain separator to record the start of the protocol
transcript.close_account_proof_domain_sep();
// extract the relevant scalar and Ristretto points from the input
let s = source_sk.get_scalar();
let C = balance.decrypt_handle.get_point();
// generate a random masking factor that also serves as a nonce
let r = Scalar::random(&mut OsRng); // using OsRng for now
let R = (r * C).compress();
// record R on transcript and receive a challenge scalar
transcript.append_point(b"R", &R);
let c = transcript.challenge_scalar(b"c");
// compute the masked secret key
let z = c * s + r;
CloseAccountProof {
R: R.into(),
z: z.into(),
}
}
pub fn verify(&self, balance: &ElGamalCT) -> 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();
// extract the relevant scalar and Ristretto points from the input
let C = balance.message_comm.get_point();
let D = balance.decrypt_handle.get_point();
let R = self.R.into();
let z = self.z.into();
// generate a challenge scalar
//
// use `append_point` as opposed to `validate_and_append_point` as the ciphertext is
// already guaranteed to be well-formed
transcript.append_point(b"R", &R);
let c = transcript.challenge_scalar(b"c");
// decompress R or return verification error
let R = R.decompress().ok_or(ProofError::VerificationError)?;
// check the required algebraic relation
let check = RistrettoPoint::multiscalar_mul(vec![z, -c, -Scalar::one()], vec![D, C, R]);
if check.is_identity() {
Ok(())
} else {
Err(ProofError::VerificationError)
}
}
}
#[cfg(test)]
mod test {
use {super::*, crate::encryption::elgamal::ElGamal};
#[test]
fn test_close_account_correctness() {
let (source_pk, source_sk) = ElGamal::keygen();
// If account balance is 0, then the proof should succeed
let balance = source_pk.encrypt(0_u64);
let proof = CloseAccountProof::new(&source_sk, &balance);
assert!(proof.verify(&balance).is_ok());
// If account balance is not zero, then the proof verification should fail
let balance = source_pk.encrypt(55_u64);
let proof = CloseAccountProof::new(&source_sk, &balance);
assert!(proof.verify(&balance).is_err());
// A zeroed cyphertext should be considered as an account balance of 0
let zeroed_ct: ElGamalCT = PodElGamalCT::zeroed().try_into().unwrap();
let proof = CloseAccountProof::new(&source_sk, &zeroed_ct);
assert!(proof.verify(&zeroed_ct).is_ok());
}
}

View File

@@ -0,0 +1,21 @@
mod close_account;
pub mod transfer;
mod update_account_pk;
mod withdraw;
#[cfg(not(target_arch = "bpf"))]
use crate::errors::ProofError;
pub use {
close_account::CloseAccountData,
transfer::{
TransferComms, TransferData, TransferEphemeralState, TransferPubKeys,
TransferRangeProofData, TransferValidityProofData,
},
update_account_pk::UpdateAccountPkData,
withdraw::WithdrawData,
};
#[cfg(not(target_arch = "bpf"))]
pub trait Verifiable {
fn verify(&self) -> Result<(), ProofError>;
}

View File

@@ -0,0 +1,546 @@
use {
crate::pod::*,
bytemuck::{Pod, Zeroable},
};
#[cfg(not(target_arch = "bpf"))]
use {
crate::{
encryption::{
elgamal::{ElGamalCT, ElGamalPK, ElGamalSK},
pedersen::{Pedersen, PedersenBase, PedersenComm, PedersenDecHandle, PedersenOpen},
},
errors::ProofError,
instruction::Verifiable,
range_proof::RangeProof,
transcript::TranscriptProtocol,
},
curve25519_dalek::{
ristretto::{CompressedRistretto, RistrettoPoint},
scalar::Scalar,
traits::{IsIdentity, MultiscalarMul, VartimeMultiscalarMul},
},
merlin::Transcript,
rand::rngs::OsRng,
std::convert::TryInto,
};
/// Just a grouping struct for the data required for the two transfer instructions. It is
/// convenient to generate the two components jointly as they share common components.
pub struct TransferData {
pub range_proof: TransferRangeProofData,
pub validity_proof: TransferValidityProofData,
}
#[cfg(not(target_arch = "bpf"))]
impl TransferData {
pub fn new(
transfer_amount: u64,
spendable_balance: u64,
spendable_ct: ElGamalCT,
source_pk: ElGamalPK,
source_sk: &ElGamalSK,
dest_pk: ElGamalPK,
auditor_pk: ElGamalPK,
) -> Self {
// split and encrypt transfer amount
//
// encryption is a bit more involved since we are generating each components of an ElGamal
// ciphertext separately.
let (amount_lo, amount_hi) = split_u64_into_u32(transfer_amount);
let (comm_lo, open_lo) = Pedersen::commit(amount_lo);
let (comm_hi, open_hi) = Pedersen::commit(amount_hi);
let handle_source_lo = source_pk.gen_decrypt_handle(&open_lo);
let handle_dest_lo = dest_pk.gen_decrypt_handle(&open_lo);
let handle_auditor_lo = auditor_pk.gen_decrypt_handle(&open_lo);
let handle_source_hi = source_pk.gen_decrypt_handle(&open_hi);
let handle_dest_hi = dest_pk.gen_decrypt_handle(&open_hi);
let handle_auditor_hi = auditor_pk.gen_decrypt_handle(&open_hi);
// message encoding as Pedersen commitments, which will be included in range proof data
let amount_comms = TransferComms {
lo: comm_lo.into(),
hi: comm_hi.into(),
};
// decryption handles, which will be included in the validity proof data
let decryption_handles_lo = TransferHandles {
source: handle_source_lo.into(),
dest: handle_dest_lo.into(),
auditor: handle_auditor_lo.into(),
};
let decryption_handles_hi = TransferHandles {
source: handle_source_hi.into(),
dest: handle_dest_hi.into(),
auditor: handle_auditor_hi.into(),
};
// grouping of the public keys for the transfer
let transfer_public_keys = TransferPubKeys {
source_pk: source_pk.into(),
dest_pk: dest_pk.into(),
auditor_pk: auditor_pk.into(),
};
// subtract transfer amount from the spendable ciphertext
let spendable_comm = spendable_ct.message_comm;
let spendable_handle = spendable_ct.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 = ElGamalCT {
message_comm: new_spendable_comm,
decrypt_handle: new_spendable_handle,
};
// range_proof and validity_proof should be generated together
let (transfer_proofs, ephemeral_state) = TransferProofs::new(
source_sk,
&source_pk,
&dest_pk,
&auditor_pk,
(amount_lo as u64, amount_hi as u64),
&open_lo,
&open_hi,
new_spendable_balance,
&new_spendable_ct,
);
// generate data components
let range_proof = TransferRangeProofData {
amount_comms,
proof: transfer_proofs.range_proof,
ephemeral_state,
};
let validity_proof = TransferValidityProofData {
decryption_handles_lo,
decryption_handles_hi,
transfer_public_keys,
new_spendable_ct: new_spendable_ct.into(),
proof: transfer_proofs.validity_proof,
ephemeral_state,
};
TransferData {
range_proof,
validity_proof,
}
}
}
#[derive(Clone, Copy, Pod, Zeroable)]
#[repr(C)]
pub struct TransferRangeProofData {
/// The transfer amount encoded as Pedersen commitments
pub amount_comms: TransferComms, // 64 bytes
/// Proof that certifies:
/// 1. the source account has enough funds for the transfer (i.e. the final balance is a
/// 64-bit positive number)
/// 2. the transfer amount is a 64-bit positive number
pub proof: PodRangeProof128, // 736 bytes
/// Ephemeral state between the two transfer instruction data
pub ephemeral_state: TransferEphemeralState, // 128 bytes
}
#[cfg(not(target_arch = "bpf"))]
impl Verifiable for TransferRangeProofData {
fn verify(&self) -> Result<(), ProofError> {
let mut transcript = Transcript::new(b"TransferRangeProof");
// standard range proof verification
let proof: RangeProof = self.proof.try_into()?;
proof.verify_with(
vec![
&self.ephemeral_state.spendable_comm_verification.into(),
&self.amount_comms.lo.into(),
&self.amount_comms.hi.into(),
],
vec![64_usize, 32_usize, 32_usize],
Some(self.ephemeral_state.x.into()),
Some(self.ephemeral_state.z.into()),
&mut transcript,
)
}
}
#[derive(Clone, Copy, Pod, Zeroable)]
#[repr(C)]
pub struct TransferValidityProofData {
/// The decryption handles that allow decryption of the lo-bits
pub decryption_handles_lo: TransferHandles, // 96 bytes
/// The decryption handles that allow decryption of the hi-bits
pub decryption_handles_hi: TransferHandles, // 96 bytes
/// The public encryption keys associated with the transfer: source, dest, and auditor
pub transfer_public_keys: TransferPubKeys, // 96 bytes
/// The final spendable ciphertext after the transfer
pub new_spendable_ct: PodElGamalCT, // 64 bytes
/// Proof that certifies that the decryption handles are generated correctly
pub proof: ValidityProof, // 160 bytes
/// Ephemeral state between the two transfer instruction data
pub ephemeral_state: TransferEphemeralState, // 128 bytes
}
/// The joint data that is shared between the two transfer instructions.
///
/// Identical ephemeral data should be included in the two transfer instructions and this should be
/// checked by the ZK Token program.
#[derive(Clone, Copy, Pod, Zeroable, PartialEq)]
#[repr(C)]
pub struct TransferEphemeralState {
pub spendable_comm_verification: PodPedersenComm, // 32 bytes
pub x: PodScalar, // 32 bytes
pub z: PodScalar, // 32 bytes
pub t_x_blinding: PodScalar, // 32 bytes
}
#[cfg(not(target_arch = "bpf"))]
impl Verifiable for TransferValidityProofData {
fn verify(&self) -> Result<(), ProofError> {
self.proof.verify(
&self.new_spendable_ct.try_into()?,
&self.decryption_handles_lo,
&self.decryption_handles_hi,
&self.transfer_public_keys,
&self.ephemeral_state,
)
}
}
/// Just a grouping struct for the two proofs that are needed for a transfer instruction. The two
/// proofs have to be generated together as they share joint data.
pub struct TransferProofs {
pub range_proof: PodRangeProof128,
pub validity_proof: ValidityProof,
}
#[allow(non_snake_case)]
#[cfg(not(target_arch = "bpf"))]
impl TransferProofs {
#[allow(clippy::too_many_arguments)]
#[allow(clippy::many_single_char_names)]
pub fn new(
source_sk: &ElGamalSK,
source_pk: &ElGamalPK,
dest_pk: &ElGamalPK,
auditor_pk: &ElGamalPK,
transfer_amt: (u64, u64),
lo_open: &PedersenOpen,
hi_open: &PedersenOpen,
new_spendable_balance: u64,
new_spendable_ct: &ElGamalCT,
) -> (Self, TransferEphemeralState) {
// TODO: should also commit to pubkeys and commitments later
let mut transcript_validity_proof = merlin::Transcript::new(b"TransferValidityProof");
let H = PedersenBase::default().H;
let D = new_spendable_ct.decrypt_handle.get_point();
let s = source_sk.get_scalar();
// Generate proof for the new spendable ciphertext
let r_new = Scalar::random(&mut OsRng);
let y = Scalar::random(&mut OsRng);
let R = RistrettoPoint::multiscalar_mul(vec![y, r_new], vec![D, H]).compress();
transcript_validity_proof.append_point(b"R", &R);
let c = transcript_validity_proof.challenge_scalar(b"c");
let z = s + c * y;
let new_spendable_open = PedersenOpen(c * r_new);
let spendable_comm_verification =
Pedersen::commit_with(new_spendable_balance, &new_spendable_open);
// Generate proof for the transfer amounts
let t_1_blinding = PedersenOpen::random(&mut OsRng);
let t_2_blinding = PedersenOpen::random(&mut OsRng);
let u = transcript_validity_proof.challenge_scalar(b"u");
let P_joint = RistrettoPoint::multiscalar_mul(
vec![Scalar::one(), u, u * u],
vec![
source_pk.get_point(),
dest_pk.get_point(),
auditor_pk.get_point(),
],
);
let T_joint = (new_spendable_open.get_scalar() * P_joint).compress();
let T_1 = (t_1_blinding.get_scalar() * P_joint).compress();
let T_2 = (t_2_blinding.get_scalar() * P_joint).compress();
transcript_validity_proof.append_point(b"T_1", &T_1);
transcript_validity_proof.append_point(b"T_2", &T_2);
// define the validity proof
let validity_proof = ValidityProof {
R: R.into(),
z: z.into(),
T_joint: T_joint.into(),
T_1: T_1.into(),
T_2: T_2.into(),
};
// generate the range proof
let mut transcript_range_proof = Transcript::new(b"TransferRangeProof");
let (range_proof, x, z) = RangeProof::create_with(
vec![new_spendable_balance, transfer_amt.0, transfer_amt.1],
vec![64, 32, 32],
vec![&new_spendable_open, lo_open, hi_open],
&t_1_blinding,
&t_2_blinding,
&mut transcript_range_proof,
);
// define ephemeral state
let ephemeral_state = TransferEphemeralState {
spendable_comm_verification: spendable_comm_verification.into(),
x: x.into(),
z: z.into(),
t_x_blinding: range_proof.t_x_blinding.into(),
};
(
Self {
range_proof: range_proof.try_into().expect("valid range_proof"),
validity_proof,
},
ephemeral_state,
)
}
}
/// Proof components for transfer instructions.
///
/// These two components should be output by a RangeProof creation function.
#[allow(non_snake_case)]
#[derive(Clone, Copy, Pod, Zeroable)]
#[repr(C)]
pub struct ValidityProof {
// Proof component for the spendable ciphertext components: R
pub R: PodCompressedRistretto, // 32 bytes
// Proof component for the spendable ciphertext components: z
pub z: PodScalar, // 32 bytes
// Proof component for the transaction amount components: T_src
pub T_joint: PodCompressedRistretto, // 32 bytes
// Proof component for the transaction amount components: T_1
pub T_1: PodCompressedRistretto, // 32 bytes
// Proof component for the transaction amount components: T_2
pub T_2: PodCompressedRistretto, // 32 bytes
}
#[allow(non_snake_case)]
#[cfg(not(target_arch = "bpf"))]
impl ValidityProof {
pub fn verify(
self,
new_spendable_ct: &ElGamalCT,
decryption_handles_lo: &TransferHandles,
decryption_handles_hi: &TransferHandles,
transfer_public_keys: &TransferPubKeys,
ephemeral_state: &TransferEphemeralState,
) -> Result<(), ProofError> {
let mut transcript = Transcript::new(b"TransferValidityProof");
let source_pk: ElGamalPK = transfer_public_keys.source_pk.try_into()?;
let dest_pk: ElGamalPK = transfer_public_keys.dest_pk.try_into()?;
let auditor_pk: ElGamalPK = transfer_public_keys.auditor_pk.try_into()?;
// verify Pedersen commitment in the ephemeral state
let C_ephemeral: CompressedRistretto = ephemeral_state.spendable_comm_verification.into();
let C = new_spendable_ct.message_comm.get_point();
let D = new_spendable_ct.decrypt_handle.get_point();
let R = self.R.into();
let z: Scalar = self.z.into();
transcript.validate_and_append_point(b"R", &R)?;
let c = transcript.challenge_scalar(b"c");
let R = R.decompress().ok_or(ProofError::VerificationError)?;
let spendable_comm_verification =
RistrettoPoint::multiscalar_mul(vec![Scalar::one(), -z, c], vec![C, D, R]).compress();
if C_ephemeral != spendable_comm_verification {
return Err(ProofError::VerificationError);
}
// derive joint public key
let u = transcript.challenge_scalar(b"u");
let P_joint = RistrettoPoint::vartime_multiscalar_mul(
vec![Scalar::one(), u, u * u],
vec![
source_pk.get_point(),
dest_pk.get_point(),
auditor_pk.get_point(),
],
);
// check well-formedness of decryption handles
let t_x_blinding: Scalar = ephemeral_state.t_x_blinding.into();
let T_1: CompressedRistretto = self.T_1.into();
let T_2: CompressedRistretto = self.T_2.into();
let x = ephemeral_state.x.into();
let z: Scalar = ephemeral_state.z.into();
let handle_source_lo: PedersenDecHandle = decryption_handles_lo.source.try_into()?;
let handle_dest_lo: PedersenDecHandle = decryption_handles_lo.dest.try_into()?;
let handle_auditor_lo: PedersenDecHandle = decryption_handles_lo.auditor.try_into()?;
let D_joint: CompressedRistretto = self.T_joint.into();
let D_joint = D_joint.decompress().ok_or(ProofError::VerificationError)?;
let D_joint_lo = RistrettoPoint::vartime_multiscalar_mul(
vec![Scalar::one(), u, u * u],
vec![
handle_source_lo.get_point(),
handle_dest_lo.get_point(),
handle_auditor_lo.get_point(),
],
);
let handle_source_hi: PedersenDecHandle = decryption_handles_hi.source.try_into()?;
let handle_dest_hi: PedersenDecHandle = decryption_handles_hi.dest.try_into()?;
let handle_auditor_hi: PedersenDecHandle = decryption_handles_hi.auditor.try_into()?;
let D_joint_hi = RistrettoPoint::vartime_multiscalar_mul(
vec![Scalar::one(), u, u * u],
vec![
handle_source_hi.get_point(),
handle_dest_hi.get_point(),
handle_auditor_hi.get_point(),
],
);
// TODO: combine Pedersen commitment verification above to here for efficiency
// TODO: might need to add an additional proof-of-knowledge here (additional 64 byte)
let mega_check = RistrettoPoint::optional_multiscalar_mul(
vec![-t_x_blinding, x, x * x, z * z, z * z * z, z * z * z * z],
vec![
Some(P_joint),
T_1.decompress(),
T_2.decompress(),
Some(D_joint),
Some(D_joint_lo),
Some(D_joint_hi),
],
)
.ok_or(ProofError::VerificationError)?;
if mega_check.is_identity() {
Ok(())
} else {
Err(ProofError::VerificationError)
}
}
}
/// The ElGamal public keys needed for a transfer
#[derive(Clone, Copy, Pod, Zeroable)]
#[repr(C)]
pub struct TransferPubKeys {
pub source_pk: PodElGamalPK, // 32 bytes
pub dest_pk: PodElGamalPK, // 32 bytes
pub auditor_pk: PodElGamalPK, // 32 bytes
}
/// The transfer amount commitments needed for a transfer
#[derive(Clone, Copy, Pod, Zeroable)]
#[repr(C)]
pub struct TransferComms {
pub lo: PodPedersenComm, // 32 bytes
pub hi: PodPedersenComm, // 32 bytes
}
/// The decryption handles needed for a transfer
#[derive(Clone, Copy, Pod, Zeroable)]
#[repr(C)]
pub struct TransferHandles {
pub source: PodPedersenDecHandle, // 32 bytes
pub dest: PodPedersenDecHandle, // 32 bytes
pub auditor: PodPedersenDecHandle, // 32 bytes
}
/// 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: PedersenComm, comm_hi: PedersenComm) -> PedersenComm {
comm_lo + comm_hi * Scalar::from(TWO_32)
}
#[cfg(not(target_arch = "bpf"))]
pub fn combine_u32_handles(
handle_lo: PedersenDecHandle,
handle_hi: PedersenDecHandle,
) -> PedersenDecHandle {
handle_lo + handle_hi * Scalar::from(TWO_32)
}
#[cfg(not(target_arch = "bpf"))]
pub fn combine_u32_ciphertexts(ct_lo: ElGamalCT, ct_hi: ElGamalCT) -> ElGamalCT {
ct_lo + ct_hi * Scalar::from(TWO_32)
}
#[cfg(test)]
mod test {
use super::*;
use crate::encryption::elgamal::ElGamal;
#[test]
fn test_transfer_correctness() {
// ElGamal keys for source, destination, and auditor accounts
let (source_pk, source_sk) = ElGamal::keygen();
let (dest_pk, _) = ElGamal::keygen();
let (auditor_pk, _) = ElGamal::keygen();
// create source account spendable ciphertext
let spendable_balance: u64 = 77;
let spendable_ct = source_pk.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_pk,
&source_sk,
dest_pk,
auditor_pk,
);
// verify range proof
assert!(transfer_data.range_proof.verify().is_ok());
// verify ciphertext validity proof
assert!(transfer_data.validity_proof.verify().is_ok());
}
}

View File

@@ -0,0 +1,271 @@
use {
crate::pod::*,
bytemuck::{Pod, Zeroable},
};
#[cfg(not(target_arch = "bpf"))]
use {
crate::{
encryption::{
elgamal::{ElGamalCT, ElGamalPK, ElGamalSK},
pedersen::PedersenBase,
},
errors::ProofError,
instruction::Verifiable,
transcript::TranscriptProtocol,
},
curve25519_dalek::{
ristretto::RistrettoPoint,
scalar::Scalar,
traits::{IsIdentity, MultiscalarMul},
},
merlin::Transcript,
rand::rngs::OsRng,
std::convert::TryInto,
};
/// This struct includes the cryptographic proof *and* the account data information needed to verify
/// the proof
///
/// - The pre-instruction should call UpdateAccountPKData::verify(&self)
/// - The actual program should check that `current_ct` is consistent with what is
/// currently stored in the confidential token account
///
#[derive(Clone, Copy, Pod, Zeroable)]
#[repr(C)]
pub struct UpdateAccountPkData {
/// Current ElGamal encryption key
pub current_pk: PodElGamalPK, // 32 bytes
/// Current encrypted available balance
pub current_ct: PodElGamalCT, // 64 bytes
/// New ElGamal encryption key
pub new_pk: PodElGamalPK, // 32 bytes
/// New encrypted available balance
pub new_ct: PodElGamalCT, // 64 bytes
/// Proof that the current and new ciphertexts are consistent
pub proof: UpdateAccountPkProof, // 160 bytes
}
impl UpdateAccountPkData {
#[cfg(not(target_arch = "bpf"))]
pub fn new(
current_balance: u64,
current_ct: ElGamalCT,
current_pk: ElGamalPK,
current_sk: &ElGamalSK,
new_pk: ElGamalPK,
new_sk: &ElGamalSK,
) -> Self {
let new_ct = new_pk.encrypt(current_balance);
let proof =
UpdateAccountPkProof::new(current_balance, current_sk, new_sk, &current_ct, &new_ct);
Self {
current_pk: current_pk.into(),
current_ct: current_ct.into(),
new_ct: new_ct.into(),
new_pk: new_pk.into(),
proof,
}
}
}
#[cfg(not(target_arch = "bpf"))]
impl Verifiable for UpdateAccountPkData {
fn verify(&self) -> Result<(), ProofError> {
let current_ct = self.current_ct.try_into()?;
let new_ct = self.new_ct.try_into()?;
self.proof.verify(&current_ct, &new_ct)
}
}
/// This struct represents the cryptographic proof component that certifies that the current_ct and
/// new_ct encrypt equal values
#[derive(Clone, Copy, Pod, Zeroable)]
#[repr(C)]
#[allow(non_snake_case)]
pub struct UpdateAccountPkProof {
pub R_0: PodCompressedRistretto, // 32 bytes
pub R_1: PodCompressedRistretto, // 32 bytes
pub z_sk_0: PodScalar, // 32 bytes
pub z_sk_1: PodScalar, // 32 bytes
pub z_x: PodScalar, // 32 bytes
}
#[allow(non_snake_case)]
#[cfg(not(target_arch = "bpf"))]
impl UpdateAccountPkProof {
fn transcript_new() -> Transcript {
Transcript::new(b"UpdateAccountPkProof")
}
fn new(
current_balance: u64,
current_sk: &ElGamalSK,
new_sk: &ElGamalSK,
current_ct: &ElGamalCT,
new_ct: &ElGamalCT,
) -> Self {
let mut transcript = Self::transcript_new();
// add a domain separator to record the start of the protocol
transcript.update_account_public_key_proof_domain_sep();
// extract the relevant scalar and Ristretto points from the input
let s_0 = current_sk.get_scalar();
let s_1 = new_sk.get_scalar();
let x = Scalar::from(current_balance);
let D_0 = current_ct.decrypt_handle.get_point();
let D_1 = new_ct.decrypt_handle.get_point();
let G = PedersenBase::default().G;
// generate a random masking factor that also serves as a nonce
let r_sk_0 = Scalar::random(&mut OsRng);
let r_sk_1 = Scalar::random(&mut OsRng);
let r_x = Scalar::random(&mut OsRng);
let R_0 = (r_sk_0 * D_0 + r_x * G).compress();
let R_1 = (r_sk_1 * D_1 + r_x * G).compress();
// record R_0, R_1 on transcript and receive a challenge scalar
transcript.append_point(b"R_0", &R_0);
transcript.append_point(b"R_1", &R_1);
let c = transcript.challenge_scalar(b"c");
let _w = transcript.challenge_scalar(b"w"); // for consistency of transcript
// compute the masked secret keys and amount
let z_sk_0 = c * s_0 + r_sk_0;
let z_sk_1 = c * s_1 + r_sk_1;
let z_x = c * x + r_x;
UpdateAccountPkProof {
R_0: R_0.into(),
R_1: R_1.into(),
z_sk_0: z_sk_0.into(),
z_sk_1: z_sk_1.into(),
z_x: z_x.into(),
}
}
fn verify(&self, current_ct: &ElGamalCT, new_ct: &ElGamalCT) -> Result<(), ProofError> {
let mut transcript = Self::transcript_new();
// add a domain separator to record the start of the protocol
transcript.update_account_public_key_proof_domain_sep();
// extract the relevant scalar and Ristretto points from the input
let C_0 = current_ct.message_comm.get_point();
let D_0 = current_ct.decrypt_handle.get_point();
let C_1 = new_ct.message_comm.get_point();
let D_1 = new_ct.decrypt_handle.get_point();
let R_0 = self.R_0.into();
let R_1 = self.R_1.into();
let z_sk_0 = self.z_sk_0.into();
let z_sk_1: Scalar = self.z_sk_1.into();
let z_x = self.z_x.into();
let G = PedersenBase::default().G;
// generate a challenge scalar
transcript.validate_and_append_point(b"R_0", &R_0)?;
transcript.validate_and_append_point(b"R_1", &R_1)?;
let c = transcript.challenge_scalar(b"c");
let w = transcript.challenge_scalar(b"w");
// decompress R_0, R_1 or return verification error
let R_0 = R_0.decompress().ok_or(ProofError::VerificationError)?;
let R_1 = R_1.decompress().ok_or(ProofError::VerificationError)?;
// check the required algebraic relation
let check = RistrettoPoint::multiscalar_mul(
vec![
z_sk_0,
z_x,
-c,
-Scalar::one(),
w * z_sk_1,
w * z_x,
-w * c,
-w * Scalar::one(),
],
vec![D_0, G, C_0, R_0, D_1, G, C_1, R_1],
);
if check.is_identity() {
Ok(())
} else {
Err(ProofError::VerificationError)
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::encryption::elgamal::ElGamal;
#[test]
fn test_update_account_public_key_correctness() {
let (current_pk, current_sk) = ElGamal::keygen();
let (new_pk, new_sk) = ElGamal::keygen();
// If current_ct and new_ct encrypt same values, then the proof verification should succeed
let balance: u64 = 77;
let current_ct = current_pk.encrypt(balance);
let new_ct = new_pk.encrypt(balance);
let proof = UpdateAccountPkProof::new(balance, &current_sk, &new_sk, &current_ct, &new_ct);
assert!(proof.verify(&current_ct, &new_ct).is_ok());
// If current_ct and new_ct encrypt different values, then the proof verification should fail
let new_ct = new_pk.encrypt(55_u64);
let proof = UpdateAccountPkProof::new(balance, &current_sk, &new_sk, &current_ct, &new_ct);
assert!(proof.verify(&current_ct, &new_ct).is_err());
// A zeroed cipehrtext should be considered as an account balance of 0
let balance: u64 = 0;
let zeroed_ct_as_current_ct: ElGamalCT = PodElGamalCT::zeroed().try_into().unwrap();
let new_ct: ElGamalCT = new_pk.encrypt(balance);
let proof = UpdateAccountPkProof::new(
balance,
&current_sk,
&new_sk,
&zeroed_ct_as_current_ct,
&new_ct,
);
assert!(proof.verify(&zeroed_ct_as_current_ct, &new_ct).is_ok());
let current_ct: ElGamalCT = PodElGamalCT::zeroed().try_into().unwrap();
let zeroed_ct_as_new_ct: ElGamalCT = PodElGamalCT::zeroed().try_into().unwrap();
let proof = UpdateAccountPkProof::new(
balance,
&current_sk,
&new_sk,
&current_ct,
&zeroed_ct_as_new_ct,
);
assert!(proof.verify(&current_ct, &zeroed_ct_as_new_ct).is_ok());
let zeroed_ct_as_current_ct: ElGamalCT = PodElGamalCT::zeroed().try_into().unwrap();
let zeroed_ct_as_new_ct: ElGamalCT = PodElGamalCT::zeroed().try_into().unwrap();
let proof = UpdateAccountPkProof::new(
balance,
&current_sk,
&new_sk,
&zeroed_ct_as_current_ct,
&zeroed_ct_as_new_ct,
);
assert!(proof
.verify(&zeroed_ct_as_current_ct, &zeroed_ct_as_new_ct)
.is_ok());
}
}

View File

@@ -0,0 +1,207 @@
use {
crate::pod::*,
bytemuck::{Pod, Zeroable},
};
#[cfg(not(target_arch = "bpf"))]
use {
crate::{
encryption::{
elgamal::{ElGamalCT, ElGamalPK, ElGamalSK},
pedersen::{PedersenBase, PedersenOpen},
},
errors::ProofError,
instruction::Verifiable,
range_proof::RangeProof,
transcript::TranscriptProtocol,
},
curve25519_dalek::{ristretto::RistrettoPoint, scalar::Scalar, traits::MultiscalarMul},
merlin::Transcript,
rand::rngs::OsRng,
std::convert::TryInto,
};
/// This struct includes the cryptographic proof *and* the account data information needed to verify
/// the proof
///
/// - The pre-instruction should call WithdrawData::verify_proof(&self)
/// - The actual program should check that `current_ct` is consistent with what is
/// currently stored in the confidential token account TODO: update this statement
///
#[derive(Clone, Copy, Pod, Zeroable)]
#[repr(C)]
pub struct WithdrawData {
/// The source account available balance *after* the withdraw (encrypted by
/// `source_pk`
pub final_balance_ct: PodElGamalCT, // 64 bytes
/// Proof that the account is solvent
pub proof: WithdrawProof, // 736 bytes
}
impl WithdrawData {
#[cfg(not(target_arch = "bpf"))]
pub fn new(
amount: u64,
source_pk: ElGamalPK,
source_sk: &ElGamalSK,
current_balance: u64,
current_balance_ct: ElGamalCT,
) -> Self {
// subtract withdraw amount from current balance
//
// panics if current_balance < amount
let final_balance = current_balance - amount;
// encode withdraw amount as an ElGamal ciphertext and subtract it from
// current source balance
let amount_encoded = source_pk.encrypt_with(amount, &PedersenOpen::default());
let final_balance_ct = current_balance_ct - amount_encoded;
let proof = WithdrawProof::new(source_sk, final_balance, &final_balance_ct);
Self {
final_balance_ct: final_balance_ct.into(),
proof,
}
}
}
#[cfg(not(target_arch = "bpf"))]
impl Verifiable for WithdrawData {
fn verify(&self) -> Result<(), ProofError> {
let final_balance_ct = self.final_balance_ct.try_into()?;
self.proof.verify(&final_balance_ct)
}
}
/// This struct represents the cryptographic proof component that certifies the account's solvency
/// for withdrawal
#[derive(Clone, Copy, Pod, Zeroable)]
#[repr(C)]
#[allow(non_snake_case)]
pub struct WithdrawProof {
/// Wrapper for range proof: R component
pub R: PodCompressedRistretto, // 32 bytes
/// Wrapper for range proof: z component
pub z: PodScalar, // 32 bytes
/// Associated range proof
pub range_proof: PodRangeProof64, // 672 bytes
}
#[allow(non_snake_case)]
#[cfg(not(target_arch = "bpf"))]
impl WithdrawProof {
fn transcript_new() -> Transcript {
Transcript::new(b"WithdrawProof")
}
pub fn new(source_sk: &ElGamalSK, final_balance: u64, final_balance_ct: &ElGamalCT) -> Self {
let mut transcript = Self::transcript_new();
// 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 input
let H = PedersenBase::default().H;
let D = final_balance_ct.decrypt_handle.get_point();
let s = source_sk.get_scalar();
// new pedersen opening
let r_new = Scalar::random(&mut OsRng);
// generate a random masking factor that also serves as a nonce
let y = Scalar::random(&mut OsRng);
let R = RistrettoPoint::multiscalar_mul(vec![y, r_new], vec![D, H]).compress();
// record R on transcript and receive a challenge scalar
transcript.append_point(b"R", &R);
let c = transcript.challenge_scalar(b"c");
// compute the masked secret key
let z = s + c * y;
// compute the new Pedersen commitment and opening
let new_open = PedersenOpen(c * r_new);
let range_proof = RangeProof::create(
vec![final_balance],
vec![64],
vec![&new_open],
&mut transcript,
);
WithdrawProof {
R: R.into(),
z: z.into(),
range_proof: range_proof.try_into().expect("range proof"),
}
}
pub fn verify(&self, final_balance_ct: &ElGamalCT) -> Result<(), ProofError> {
let mut transcript = Self::transcript_new();
// 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 input
let C = final_balance_ct.message_comm.get_point();
let D = final_balance_ct.decrypt_handle.get_point();
let R = self.R.into();
let z: Scalar = self.z.into();
// generate a challenge scalar
transcript.validate_and_append_point(b"R", &R)?;
let c = transcript.challenge_scalar(b"c");
// decompress R or return verification error
let R = R.decompress().ok_or(ProofError::VerificationError)?;
// compute new Pedersen commitment to verify range proof with
let new_comm = RistrettoPoint::multiscalar_mul(vec![Scalar::one(), -z, c], vec![C, D, R]);
let range_proof: RangeProof = self.range_proof.try_into()?;
range_proof.verify(vec![&new_comm.compress()], vec![64_usize], &mut transcript)
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::encryption::elgamal::ElGamal;
#[test]
#[ignore]
fn test_withdraw_correctness() {
// generate and verify proof for the proper setting
let (source_pk, source_sk) = ElGamal::keygen();
let current_balance: u64 = 77;
let current_balance_ct = source_pk.encrypt(current_balance);
let withdraw_amount: u64 = 55;
let data = WithdrawData::new(
withdraw_amount,
source_pk,
&source_sk,
current_balance,
current_balance_ct,
);
assert!(data.verify().is_ok());
// generate and verify proof with wrong balance
let wrong_balance: u64 = 99;
let data = WithdrawData::new(
withdraw_amount,
source_pk,
&source_sk,
wrong_balance,
current_balance_ct,
);
assert!(data.verify().is_err());
// TODO: test for ciphertexts that encrypt numbers outside the 0, 2^64 range
}
}