This repository was archived by the owner on Mar 11, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
[confidential-transfer] Add confidential transfer ciphertext arithmetic crate #7026
Merged
samkim-crypto
merged 3 commits into
solana-labs:master
from
samkim-crypto:ciphertext-arithmetic
Jul 19, 2024
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
21 changes: 21 additions & 0 deletions
21
token/confidential-transfer/ciphertext-arithmetic/Cargo.toml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
[package] | ||
name = "spl-token-confidential-transfer-ciphertext-arithmetic" | ||
version = "0.1.0" | ||
description = "Solana Program Library Confidential Transfer Ciphertext Arithmetic" | ||
authors = ["Solana Labs Maintainers <[email protected]>"] | ||
repository = "https://github.com/solana-labs/solana-program-library" | ||
license = "Apache-2.0" | ||
edition = "2021" | ||
|
||
[dependencies] | ||
base64 = "0.22.1" | ||
bytemuck = "1.16.1" | ||
solana-curve25519 = "2.0.0" | ||
solana-zk-sdk = "2.0.0" | ||
|
||
[dev-dependencies] | ||
spl-token-confidential-transfer-proof-generation = { version = "0.1.0", path = "../proof-generation" } | ||
curve25519-dalek = "3.2.1" | ||
|
||
[lib] | ||
crate-type = ["cdylib", "lib"] |
334 changes: 334 additions & 0 deletions
334
token/confidential-transfer/ciphertext-arithmetic/src/lib.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,334 @@ | ||
use { | ||
base64::{engine::general_purpose::STANDARD, Engine}, | ||
bytemuck::bytes_of, | ||
solana_curve25519::{ | ||
ristretto::{add_ristretto, multiply_ristretto, subtract_ristretto, PodRistrettoPoint}, | ||
scalar::PodScalar, | ||
}, | ||
solana_zk_sdk::encryption::pod::elgamal::PodElGamalCiphertext, | ||
std::str::FromStr, | ||
}; | ||
|
||
const SHIFT_BITS: usize = 16; | ||
|
||
const G: PodRistrettoPoint = PodRistrettoPoint([ | ||
226, 242, 174, 10, 106, 188, 78, 113, 168, 132, 169, 97, 197, 0, 81, 95, 88, 227, 11, 106, 165, | ||
130, 221, 141, 182, 166, 89, 69, 224, 141, 45, 118, | ||
]); | ||
|
||
/// Add two ElGamal ciphertexts | ||
pub fn add( | ||
left_ciphertext: &PodElGamalCiphertext, | ||
right_ciphertext: &PodElGamalCiphertext, | ||
) -> Option<PodElGamalCiphertext> { | ||
let (left_commitment, left_handle) = elgamal_ciphertext_to_ristretto(left_ciphertext); | ||
let (right_commitment, right_handle) = elgamal_ciphertext_to_ristretto(right_ciphertext); | ||
|
||
let result_commitment = add_ristretto(&left_commitment, &right_commitment)?; | ||
let result_handle = add_ristretto(&left_handle, &right_handle)?; | ||
|
||
Some(ristretto_to_elgamal_ciphertext( | ||
&result_commitment, | ||
&result_handle, | ||
)) | ||
} | ||
|
||
/// Multiply an ElGamal ciphertext by a scalar | ||
pub fn multiply( | ||
scalar: &PodScalar, | ||
ciphertext: &PodElGamalCiphertext, | ||
) -> Option<PodElGamalCiphertext> { | ||
let (commitment, handle) = elgamal_ciphertext_to_ristretto(ciphertext); | ||
|
||
let result_commitment = multiply_ristretto(scalar, &commitment)?; | ||
let result_handle = multiply_ristretto(scalar, &handle)?; | ||
|
||
Some(ristretto_to_elgamal_ciphertext( | ||
&result_commitment, | ||
&result_handle, | ||
)) | ||
} | ||
|
||
/// Compute `left_ciphertext + (right_ciphertext_lo + 2^16 * | ||
/// right_ciphertext_hi)` | ||
pub fn add_with_lo_hi( | ||
left_ciphertext: &PodElGamalCiphertext, | ||
right_ciphertext_lo: &PodElGamalCiphertext, | ||
right_ciphertext_hi: &PodElGamalCiphertext, | ||
) -> Option<PodElGamalCiphertext> { | ||
let shift_scalar = u64_to_scalar(1_u64 << SHIFT_BITS); | ||
let shifted_right_ciphertext_hi = multiply(&shift_scalar, right_ciphertext_hi)?; | ||
let combined_right_ciphertext = add(right_ciphertext_lo, &shifted_right_ciphertext_hi)?; | ||
add(left_ciphertext, &combined_right_ciphertext) | ||
} | ||
|
||
/// Subtract two ElGamal ciphertexts | ||
pub fn subtract( | ||
left_ciphertext: &PodElGamalCiphertext, | ||
right_ciphertext: &PodElGamalCiphertext, | ||
) -> Option<PodElGamalCiphertext> { | ||
let (left_commitment, left_handle) = elgamal_ciphertext_to_ristretto(left_ciphertext); | ||
let (right_commitment, right_handle) = elgamal_ciphertext_to_ristretto(right_ciphertext); | ||
|
||
let result_commitment = subtract_ristretto(&left_commitment, &right_commitment)?; | ||
let result_handle = subtract_ristretto(&left_handle, &right_handle)?; | ||
|
||
Some(ristretto_to_elgamal_ciphertext( | ||
&result_commitment, | ||
&result_handle, | ||
)) | ||
} | ||
|
||
/// Compute `left_ciphertext - (right_ciphertext_lo + 2^16 * | ||
/// right_ciphertext_hi)` | ||
pub fn subtract_with_lo_hi( | ||
left_ciphertext: &PodElGamalCiphertext, | ||
right_ciphertext_lo: &PodElGamalCiphertext, | ||
right_ciphertext_hi: &PodElGamalCiphertext, | ||
) -> Option<PodElGamalCiphertext> { | ||
let shift_scalar = u64_to_scalar(1_u64 << SHIFT_BITS); | ||
let shifted_right_ciphertext_hi = multiply(&shift_scalar, right_ciphertext_hi)?; | ||
let combined_right_ciphertext = add(right_ciphertext_lo, &shifted_right_ciphertext_hi)?; | ||
subtract(left_ciphertext, &combined_right_ciphertext) | ||
} | ||
|
||
/// Add a constant amount to a ciphertext | ||
pub fn add_to(ciphertext: &PodElGamalCiphertext, amount: u64) -> Option<PodElGamalCiphertext> { | ||
let amount_scalar = u64_to_scalar(amount); | ||
let amount_point = multiply_ristretto(&amount_scalar, &G)?; | ||
|
||
let (commitment, handle) = elgamal_ciphertext_to_ristretto(ciphertext); | ||
|
||
let result_commitment = add_ristretto(&commitment, &amount_point)?; | ||
|
||
Some(ristretto_to_elgamal_ciphertext(&result_commitment, &handle)) | ||
} | ||
|
||
/// Subtract a constant amount to a ciphertext | ||
pub fn subtract_from( | ||
ciphertext: &PodElGamalCiphertext, | ||
amount: u64, | ||
) -> Option<PodElGamalCiphertext> { | ||
let amount_scalar = u64_to_scalar(amount); | ||
let amount_point = multiply_ristretto(&amount_scalar, &G)?; | ||
|
||
let (commitment, handle) = elgamal_ciphertext_to_ristretto(ciphertext); | ||
|
||
let result_commitment = subtract_ristretto(&commitment, &amount_point)?; | ||
|
||
Some(ristretto_to_elgamal_ciphertext(&result_commitment, &handle)) | ||
} | ||
|
||
/// Convert a `u64` amount into a curve25519 scalar | ||
fn u64_to_scalar(amount: u64) -> PodScalar { | ||
let mut amount_bytes = [0u8; 32]; | ||
amount_bytes[..8].copy_from_slice(&amount.to_le_bytes()); | ||
PodScalar(amount_bytes) | ||
} | ||
|
||
/// Convert a `PodElGamalCiphertext` into a tuple of commitment and decrypt | ||
/// handle `PodRistrettoPoint` | ||
fn elgamal_ciphertext_to_ristretto( | ||
ciphertext: &PodElGamalCiphertext, | ||
) -> (PodRistrettoPoint, PodRistrettoPoint) { | ||
let ciphertext_bytes = bytes_of(ciphertext); // must be of length 64 by type | ||
let commitment_bytes = ciphertext_bytes[..32].try_into().unwrap(); | ||
let handle_bytes = ciphertext_bytes[32..64].try_into().unwrap(); | ||
( | ||
PodRistrettoPoint(commitment_bytes), | ||
PodRistrettoPoint(handle_bytes), | ||
) | ||
} | ||
|
||
/// Convert a pair of `PodRistrettoPoint` to a `PodElGamalCiphertext` | ||
/// interpretting the first as the commitment and the second as the handle | ||
fn ristretto_to_elgamal_ciphertext( | ||
commitment: &PodRistrettoPoint, | ||
handle: &PodRistrettoPoint, | ||
) -> PodElGamalCiphertext { | ||
let mut ciphertext_bytes = [0u8; 64]; | ||
ciphertext_bytes[..32].copy_from_slice(bytes_of(commitment)); | ||
ciphertext_bytes[32..64].copy_from_slice(bytes_of(handle)); | ||
// Unfortunately, the `solana-zk-sdk` does not exporse a constructor interface | ||
// to construct `PodRistrettoPoint` from bytes. As a work-around, encode the | ||
// bytes as base64 string and then convert the string to a | ||
// `PodElGamalCiphertext`. | ||
let ciphertext_string = STANDARD.encode(ciphertext_bytes); | ||
FromStr::from_str(&ciphertext_string).unwrap() | ||
} | ||
|
||
#[cfg(test)] | ||
mod tests { | ||
use { | ||
super::*, | ||
bytemuck::Zeroable, | ||
curve25519_dalek::scalar::Scalar, | ||
solana_zk_sdk::encryption::{ | ||
elgamal::{ElGamalCiphertext, ElGamalKeypair}, | ||
pedersen::{Pedersen, PedersenOpening}, | ||
pod::{elgamal::PodDecryptHandle, pedersen::PodPedersenCommitment}, | ||
}, | ||
spl_token_confidential_transfer_proof_generation::try_split_u64, | ||
}; | ||
|
||
const TWO_16: u64 = 65536; | ||
|
||
#[test] | ||
fn test_zero_ct() { | ||
let spendable_balance = PodElGamalCiphertext::zeroed(); | ||
let spendable_ct: ElGamalCiphertext = spendable_balance.try_into().unwrap(); | ||
|
||
// spendable_ct should be an encryption of 0 for any public key when | ||
// `PedersenOpen::default()` is used | ||
let keypair = ElGamalKeypair::new_rand(); | ||
let public = keypair.pubkey(); | ||
let balance: u64 = 0; | ||
assert_eq!( | ||
spendable_ct, | ||
public.encrypt_with(balance, &PedersenOpening::default()) | ||
); | ||
|
||
// homomorphism should work like any other ciphertext | ||
let open = PedersenOpening::new_rand(); | ||
let transfer_amount_ciphertext = public.encrypt_with(55_u64, &open); | ||
let transfer_amount_pod: PodElGamalCiphertext = transfer_amount_ciphertext.into(); | ||
|
||
let sum = add(&spendable_balance, &transfer_amount_pod).unwrap(); | ||
|
||
let expected: PodElGamalCiphertext = public.encrypt_with(55_u64, &open).into(); | ||
assert_eq!(expected, sum); | ||
} | ||
|
||
#[test] | ||
fn test_add_to() { | ||
let spendable_balance = PodElGamalCiphertext::zeroed(); | ||
|
||
let added_ciphertext = add_to(&spendable_balance, 55).unwrap(); | ||
|
||
let keypair = ElGamalKeypair::new_rand(); | ||
let public = keypair.pubkey(); | ||
let expected: PodElGamalCiphertext = public | ||
.encrypt_with(55_u64, &PedersenOpening::default()) | ||
.into(); | ||
|
||
assert_eq!(expected, added_ciphertext); | ||
} | ||
|
||
#[test] | ||
fn test_subtract_from() { | ||
let amount = 77_u64; | ||
let keypair = ElGamalKeypair::new_rand(); | ||
let public = keypair.pubkey(); | ||
let open = PedersenOpening::new_rand(); | ||
let encrypted_amount: PodElGamalCiphertext = public.encrypt_with(amount, &open).into(); | ||
|
||
let subtracted_ciphertext = subtract_from(&encrypted_amount, 55).unwrap(); | ||
|
||
let expected: PodElGamalCiphertext = public.encrypt_with(22_u64, &open).into(); | ||
|
||
assert_eq!(expected, subtracted_ciphertext); | ||
} | ||
|
||
#[test] | ||
fn test_transfer_arithmetic() { | ||
// transfer amount | ||
let transfer_amount: u64 = 55; | ||
let (amount_lo, amount_hi) = try_split_u64(transfer_amount, 16).unwrap(); | ||
|
||
// generate public keys | ||
let source_keypair = ElGamalKeypair::new_rand(); | ||
let source_pubkey = source_keypair.pubkey(); | ||
|
||
let destination_keypair = ElGamalKeypair::new_rand(); | ||
let destination_pubkey = destination_keypair.pubkey(); | ||
|
||
let auditor_keypair = ElGamalKeypair::new_rand(); | ||
let auditor_pubkey = auditor_keypair.pubkey(); | ||
|
||
// commitments associated with TransferRangeProof | ||
let (commitment_lo, opening_lo) = Pedersen::new(amount_lo); | ||
let (commitment_hi, opening_hi) = Pedersen::new(amount_hi); | ||
|
||
let commitment_lo: PodPedersenCommitment = commitment_lo.into(); | ||
let commitment_hi: PodPedersenCommitment = commitment_hi.into(); | ||
|
||
// decryption handles associated with TransferValidityProof | ||
let source_handle_lo: PodDecryptHandle = source_pubkey.decrypt_handle(&opening_lo).into(); | ||
let destination_handle_lo: PodDecryptHandle = | ||
destination_pubkey.decrypt_handle(&opening_lo).into(); | ||
let _auditor_handle_lo: PodDecryptHandle = | ||
auditor_pubkey.decrypt_handle(&opening_lo).into(); | ||
|
||
let source_handle_hi: PodDecryptHandle = source_pubkey.decrypt_handle(&opening_hi).into(); | ||
let destination_handle_hi: PodDecryptHandle = | ||
destination_pubkey.decrypt_handle(&opening_hi).into(); | ||
let _auditor_handle_hi: PodDecryptHandle = | ||
auditor_pubkey.decrypt_handle(&opening_hi).into(); | ||
|
||
// source spendable and recipient pending | ||
let source_opening = PedersenOpening::new_rand(); | ||
let destination_opening = PedersenOpening::new_rand(); | ||
|
||
let source_spendable_ciphertext: PodElGamalCiphertext = | ||
source_pubkey.encrypt_with(77_u64, &source_opening).into(); | ||
let destination_pending_ciphertext: PodElGamalCiphertext = destination_pubkey | ||
.encrypt_with(77_u64, &destination_opening) | ||
.into(); | ||
|
||
// program arithmetic for the source account | ||
let commitment_lo_point = PodRistrettoPoint(bytes_of(&commitment_lo).try_into().unwrap()); | ||
let source_handle_lo_point = | ||
PodRistrettoPoint(bytes_of(&source_handle_lo).try_into().unwrap()); | ||
|
||
let commitment_hi_point = PodRistrettoPoint(bytes_of(&commitment_hi).try_into().unwrap()); | ||
let source_handle_hi_point = | ||
PodRistrettoPoint(bytes_of(&source_handle_hi).try_into().unwrap()); | ||
|
||
let source_ciphertext_lo = | ||
ristretto_to_elgamal_ciphertext(&commitment_lo_point, &source_handle_lo_point); | ||
let source_ciphertext_hi = | ||
ristretto_to_elgamal_ciphertext(&commitment_hi_point, &source_handle_hi_point); | ||
|
||
let final_source_spendable = subtract_with_lo_hi( | ||
&source_spendable_ciphertext, | ||
&source_ciphertext_lo, | ||
&source_ciphertext_hi, | ||
) | ||
.unwrap(); | ||
|
||
let final_source_opening = | ||
source_opening - (opening_lo.clone() + opening_hi.clone() * Scalar::from(TWO_16)); | ||
let expected_source: PodElGamalCiphertext = source_pubkey | ||
.encrypt_with(22_u64, &final_source_opening) | ||
.into(); | ||
assert_eq!(expected_source, final_source_spendable); | ||
|
||
// program arithmetic for the destination account | ||
let destination_handle_lo_point = | ||
PodRistrettoPoint(bytes_of(&destination_handle_lo).try_into().unwrap()); | ||
let destination_handle_hi_point = | ||
PodRistrettoPoint(bytes_of(&destination_handle_hi).try_into().unwrap()); | ||
|
||
let destination_ciphertext_lo = | ||
ristretto_to_elgamal_ciphertext(&commitment_lo_point, &destination_handle_lo_point); | ||
let destination_ciphertext_hi = | ||
ristretto_to_elgamal_ciphertext(&commitment_hi_point, &destination_handle_hi_point); | ||
|
||
let final_destination_pending_ciphertext = add_with_lo_hi( | ||
&destination_pending_ciphertext, | ||
&destination_ciphertext_lo, | ||
&destination_ciphertext_hi, | ||
) | ||
.unwrap(); | ||
|
||
let final_destination_opening = | ||
destination_opening + (opening_lo + opening_hi * Scalar::from(TWO_16)); | ||
let expected_destination_ciphertext: PodElGamalCiphertext = destination_pubkey | ||
.encrypt_with(132_u64, &final_destination_opening) | ||
.into(); | ||
assert_eq!( | ||
expected_destination_ciphertext, | ||
final_destination_pending_ciphertext | ||
); | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is it worth adding the constructor in future work? This seems a bit strange
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes, we should add the constructor on the zk-sdk side. I'l create an issue on the SPL side to use the new constructor once it lands.