Skip to content
This repository was archived by the owner on Mar 11, 2025. It is now read-only.

Commit 3a260d6

Browse files
[confidential-transfer] Add confidential transfer ciphertext arithmetic crate (#7026)
* add confidential transfer ciphertext arithmetic crate * cargo fmt * add comment on `ristretto_to_elgamal_ciphertext` function
1 parent 493aa06 commit 3a260d6

File tree

4 files changed

+368
-0
lines changed

4 files changed

+368
-0
lines changed

Cargo.lock

Lines changed: 12 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ members = [
6060
"token/transfer-hook/cli",
6161
"token/transfer-hook/example",
6262
"token/transfer-hook/interface",
63+
"token/confidential-transfer/ciphertext-arithmetic",
6364
"token/confidential-transfer/proof-extraction",
6465
"token/confidential-transfer/proof-generation",
6566
"token/confidential-transfer/proof-tests",
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
[package]
2+
name = "spl-token-confidential-transfer-ciphertext-arithmetic"
3+
version = "0.1.0"
4+
description = "Solana Program Library Confidential Transfer Ciphertext Arithmetic"
5+
authors = ["Solana Labs Maintainers <[email protected]>"]
6+
repository = "https://github.com/solana-labs/solana-program-library"
7+
license = "Apache-2.0"
8+
edition = "2021"
9+
10+
[dependencies]
11+
base64 = "0.22.1"
12+
bytemuck = "1.16.1"
13+
solana-curve25519 = "2.0.0"
14+
solana-zk-sdk = "2.0.0"
15+
16+
[dev-dependencies]
17+
spl-token-confidential-transfer-proof-generation = { version = "0.1.0", path = "../proof-generation" }
18+
curve25519-dalek = "3.2.1"
19+
20+
[lib]
21+
crate-type = ["cdylib", "lib"]
Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
use {
2+
base64::{engine::general_purpose::STANDARD, Engine},
3+
bytemuck::bytes_of,
4+
solana_curve25519::{
5+
ristretto::{add_ristretto, multiply_ristretto, subtract_ristretto, PodRistrettoPoint},
6+
scalar::PodScalar,
7+
},
8+
solana_zk_sdk::encryption::pod::elgamal::PodElGamalCiphertext,
9+
std::str::FromStr,
10+
};
11+
12+
const SHIFT_BITS: usize = 16;
13+
14+
const G: PodRistrettoPoint = PodRistrettoPoint([
15+
226, 242, 174, 10, 106, 188, 78, 113, 168, 132, 169, 97, 197, 0, 81, 95, 88, 227, 11, 106, 165,
16+
130, 221, 141, 182, 166, 89, 69, 224, 141, 45, 118,
17+
]);
18+
19+
/// Add two ElGamal ciphertexts
20+
pub fn add(
21+
left_ciphertext: &PodElGamalCiphertext,
22+
right_ciphertext: &PodElGamalCiphertext,
23+
) -> Option<PodElGamalCiphertext> {
24+
let (left_commitment, left_handle) = elgamal_ciphertext_to_ristretto(left_ciphertext);
25+
let (right_commitment, right_handle) = elgamal_ciphertext_to_ristretto(right_ciphertext);
26+
27+
let result_commitment = add_ristretto(&left_commitment, &right_commitment)?;
28+
let result_handle = add_ristretto(&left_handle, &right_handle)?;
29+
30+
Some(ristretto_to_elgamal_ciphertext(
31+
&result_commitment,
32+
&result_handle,
33+
))
34+
}
35+
36+
/// Multiply an ElGamal ciphertext by a scalar
37+
pub fn multiply(
38+
scalar: &PodScalar,
39+
ciphertext: &PodElGamalCiphertext,
40+
) -> Option<PodElGamalCiphertext> {
41+
let (commitment, handle) = elgamal_ciphertext_to_ristretto(ciphertext);
42+
43+
let result_commitment = multiply_ristretto(scalar, &commitment)?;
44+
let result_handle = multiply_ristretto(scalar, &handle)?;
45+
46+
Some(ristretto_to_elgamal_ciphertext(
47+
&result_commitment,
48+
&result_handle,
49+
))
50+
}
51+
52+
/// Compute `left_ciphertext + (right_ciphertext_lo + 2^16 *
53+
/// right_ciphertext_hi)`
54+
pub fn add_with_lo_hi(
55+
left_ciphertext: &PodElGamalCiphertext,
56+
right_ciphertext_lo: &PodElGamalCiphertext,
57+
right_ciphertext_hi: &PodElGamalCiphertext,
58+
) -> Option<PodElGamalCiphertext> {
59+
let shift_scalar = u64_to_scalar(1_u64 << SHIFT_BITS);
60+
let shifted_right_ciphertext_hi = multiply(&shift_scalar, right_ciphertext_hi)?;
61+
let combined_right_ciphertext = add(right_ciphertext_lo, &shifted_right_ciphertext_hi)?;
62+
add(left_ciphertext, &combined_right_ciphertext)
63+
}
64+
65+
/// Subtract two ElGamal ciphertexts
66+
pub fn subtract(
67+
left_ciphertext: &PodElGamalCiphertext,
68+
right_ciphertext: &PodElGamalCiphertext,
69+
) -> Option<PodElGamalCiphertext> {
70+
let (left_commitment, left_handle) = elgamal_ciphertext_to_ristretto(left_ciphertext);
71+
let (right_commitment, right_handle) = elgamal_ciphertext_to_ristretto(right_ciphertext);
72+
73+
let result_commitment = subtract_ristretto(&left_commitment, &right_commitment)?;
74+
let result_handle = subtract_ristretto(&left_handle, &right_handle)?;
75+
76+
Some(ristretto_to_elgamal_ciphertext(
77+
&result_commitment,
78+
&result_handle,
79+
))
80+
}
81+
82+
/// Compute `left_ciphertext - (right_ciphertext_lo + 2^16 *
83+
/// right_ciphertext_hi)`
84+
pub fn subtract_with_lo_hi(
85+
left_ciphertext: &PodElGamalCiphertext,
86+
right_ciphertext_lo: &PodElGamalCiphertext,
87+
right_ciphertext_hi: &PodElGamalCiphertext,
88+
) -> Option<PodElGamalCiphertext> {
89+
let shift_scalar = u64_to_scalar(1_u64 << SHIFT_BITS);
90+
let shifted_right_ciphertext_hi = multiply(&shift_scalar, right_ciphertext_hi)?;
91+
let combined_right_ciphertext = add(right_ciphertext_lo, &shifted_right_ciphertext_hi)?;
92+
subtract(left_ciphertext, &combined_right_ciphertext)
93+
}
94+
95+
/// Add a constant amount to a ciphertext
96+
pub fn add_to(ciphertext: &PodElGamalCiphertext, amount: u64) -> Option<PodElGamalCiphertext> {
97+
let amount_scalar = u64_to_scalar(amount);
98+
let amount_point = multiply_ristretto(&amount_scalar, &G)?;
99+
100+
let (commitment, handle) = elgamal_ciphertext_to_ristretto(ciphertext);
101+
102+
let result_commitment = add_ristretto(&commitment, &amount_point)?;
103+
104+
Some(ristretto_to_elgamal_ciphertext(&result_commitment, &handle))
105+
}
106+
107+
/// Subtract a constant amount to a ciphertext
108+
pub fn subtract_from(
109+
ciphertext: &PodElGamalCiphertext,
110+
amount: u64,
111+
) -> Option<PodElGamalCiphertext> {
112+
let amount_scalar = u64_to_scalar(amount);
113+
let amount_point = multiply_ristretto(&amount_scalar, &G)?;
114+
115+
let (commitment, handle) = elgamal_ciphertext_to_ristretto(ciphertext);
116+
117+
let result_commitment = subtract_ristretto(&commitment, &amount_point)?;
118+
119+
Some(ristretto_to_elgamal_ciphertext(&result_commitment, &handle))
120+
}
121+
122+
/// Convert a `u64` amount into a curve25519 scalar
123+
fn u64_to_scalar(amount: u64) -> PodScalar {
124+
let mut amount_bytes = [0u8; 32];
125+
amount_bytes[..8].copy_from_slice(&amount.to_le_bytes());
126+
PodScalar(amount_bytes)
127+
}
128+
129+
/// Convert a `PodElGamalCiphertext` into a tuple of commitment and decrypt
130+
/// handle `PodRistrettoPoint`
131+
fn elgamal_ciphertext_to_ristretto(
132+
ciphertext: &PodElGamalCiphertext,
133+
) -> (PodRistrettoPoint, PodRistrettoPoint) {
134+
let ciphertext_bytes = bytes_of(ciphertext); // must be of length 64 by type
135+
let commitment_bytes = ciphertext_bytes[..32].try_into().unwrap();
136+
let handle_bytes = ciphertext_bytes[32..64].try_into().unwrap();
137+
(
138+
PodRistrettoPoint(commitment_bytes),
139+
PodRistrettoPoint(handle_bytes),
140+
)
141+
}
142+
143+
/// Convert a pair of `PodRistrettoPoint` to a `PodElGamalCiphertext`
144+
/// interpretting the first as the commitment and the second as the handle
145+
fn ristretto_to_elgamal_ciphertext(
146+
commitment: &PodRistrettoPoint,
147+
handle: &PodRistrettoPoint,
148+
) -> PodElGamalCiphertext {
149+
let mut ciphertext_bytes = [0u8; 64];
150+
ciphertext_bytes[..32].copy_from_slice(bytes_of(commitment));
151+
ciphertext_bytes[32..64].copy_from_slice(bytes_of(handle));
152+
// Unfortunately, the `solana-zk-sdk` does not exporse a constructor interface
153+
// to construct `PodRistrettoPoint` from bytes. As a work-around, encode the
154+
// bytes as base64 string and then convert the string to a
155+
// `PodElGamalCiphertext`.
156+
let ciphertext_string = STANDARD.encode(ciphertext_bytes);
157+
FromStr::from_str(&ciphertext_string).unwrap()
158+
}
159+
160+
#[cfg(test)]
161+
mod tests {
162+
use {
163+
super::*,
164+
bytemuck::Zeroable,
165+
curve25519_dalek::scalar::Scalar,
166+
solana_zk_sdk::encryption::{
167+
elgamal::{ElGamalCiphertext, ElGamalKeypair},
168+
pedersen::{Pedersen, PedersenOpening},
169+
pod::{elgamal::PodDecryptHandle, pedersen::PodPedersenCommitment},
170+
},
171+
spl_token_confidential_transfer_proof_generation::try_split_u64,
172+
};
173+
174+
const TWO_16: u64 = 65536;
175+
176+
#[test]
177+
fn test_zero_ct() {
178+
let spendable_balance = PodElGamalCiphertext::zeroed();
179+
let spendable_ct: ElGamalCiphertext = spendable_balance.try_into().unwrap();
180+
181+
// spendable_ct should be an encryption of 0 for any public key when
182+
// `PedersenOpen::default()` is used
183+
let keypair = ElGamalKeypair::new_rand();
184+
let public = keypair.pubkey();
185+
let balance: u64 = 0;
186+
assert_eq!(
187+
spendable_ct,
188+
public.encrypt_with(balance, &PedersenOpening::default())
189+
);
190+
191+
// homomorphism should work like any other ciphertext
192+
let open = PedersenOpening::new_rand();
193+
let transfer_amount_ciphertext = public.encrypt_with(55_u64, &open);
194+
let transfer_amount_pod: PodElGamalCiphertext = transfer_amount_ciphertext.into();
195+
196+
let sum = add(&spendable_balance, &transfer_amount_pod).unwrap();
197+
198+
let expected: PodElGamalCiphertext = public.encrypt_with(55_u64, &open).into();
199+
assert_eq!(expected, sum);
200+
}
201+
202+
#[test]
203+
fn test_add_to() {
204+
let spendable_balance = PodElGamalCiphertext::zeroed();
205+
206+
let added_ciphertext = add_to(&spendable_balance, 55).unwrap();
207+
208+
let keypair = ElGamalKeypair::new_rand();
209+
let public = keypair.pubkey();
210+
let expected: PodElGamalCiphertext = public
211+
.encrypt_with(55_u64, &PedersenOpening::default())
212+
.into();
213+
214+
assert_eq!(expected, added_ciphertext);
215+
}
216+
217+
#[test]
218+
fn test_subtract_from() {
219+
let amount = 77_u64;
220+
let keypair = ElGamalKeypair::new_rand();
221+
let public = keypair.pubkey();
222+
let open = PedersenOpening::new_rand();
223+
let encrypted_amount: PodElGamalCiphertext = public.encrypt_with(amount, &open).into();
224+
225+
let subtracted_ciphertext = subtract_from(&encrypted_amount, 55).unwrap();
226+
227+
let expected: PodElGamalCiphertext = public.encrypt_with(22_u64, &open).into();
228+
229+
assert_eq!(expected, subtracted_ciphertext);
230+
}
231+
232+
#[test]
233+
fn test_transfer_arithmetic() {
234+
// transfer amount
235+
let transfer_amount: u64 = 55;
236+
let (amount_lo, amount_hi) = try_split_u64(transfer_amount, 16).unwrap();
237+
238+
// generate public keys
239+
let source_keypair = ElGamalKeypair::new_rand();
240+
let source_pubkey = source_keypair.pubkey();
241+
242+
let destination_keypair = ElGamalKeypair::new_rand();
243+
let destination_pubkey = destination_keypair.pubkey();
244+
245+
let auditor_keypair = ElGamalKeypair::new_rand();
246+
let auditor_pubkey = auditor_keypair.pubkey();
247+
248+
// commitments associated with TransferRangeProof
249+
let (commitment_lo, opening_lo) = Pedersen::new(amount_lo);
250+
let (commitment_hi, opening_hi) = Pedersen::new(amount_hi);
251+
252+
let commitment_lo: PodPedersenCommitment = commitment_lo.into();
253+
let commitment_hi: PodPedersenCommitment = commitment_hi.into();
254+
255+
// decryption handles associated with TransferValidityProof
256+
let source_handle_lo: PodDecryptHandle = source_pubkey.decrypt_handle(&opening_lo).into();
257+
let destination_handle_lo: PodDecryptHandle =
258+
destination_pubkey.decrypt_handle(&opening_lo).into();
259+
let _auditor_handle_lo: PodDecryptHandle =
260+
auditor_pubkey.decrypt_handle(&opening_lo).into();
261+
262+
let source_handle_hi: PodDecryptHandle = source_pubkey.decrypt_handle(&opening_hi).into();
263+
let destination_handle_hi: PodDecryptHandle =
264+
destination_pubkey.decrypt_handle(&opening_hi).into();
265+
let _auditor_handle_hi: PodDecryptHandle =
266+
auditor_pubkey.decrypt_handle(&opening_hi).into();
267+
268+
// source spendable and recipient pending
269+
let source_opening = PedersenOpening::new_rand();
270+
let destination_opening = PedersenOpening::new_rand();
271+
272+
let source_spendable_ciphertext: PodElGamalCiphertext =
273+
source_pubkey.encrypt_with(77_u64, &source_opening).into();
274+
let destination_pending_ciphertext: PodElGamalCiphertext = destination_pubkey
275+
.encrypt_with(77_u64, &destination_opening)
276+
.into();
277+
278+
// program arithmetic for the source account
279+
let commitment_lo_point = PodRistrettoPoint(bytes_of(&commitment_lo).try_into().unwrap());
280+
let source_handle_lo_point =
281+
PodRistrettoPoint(bytes_of(&source_handle_lo).try_into().unwrap());
282+
283+
let commitment_hi_point = PodRistrettoPoint(bytes_of(&commitment_hi).try_into().unwrap());
284+
let source_handle_hi_point =
285+
PodRistrettoPoint(bytes_of(&source_handle_hi).try_into().unwrap());
286+
287+
let source_ciphertext_lo =
288+
ristretto_to_elgamal_ciphertext(&commitment_lo_point, &source_handle_lo_point);
289+
let source_ciphertext_hi =
290+
ristretto_to_elgamal_ciphertext(&commitment_hi_point, &source_handle_hi_point);
291+
292+
let final_source_spendable = subtract_with_lo_hi(
293+
&source_spendable_ciphertext,
294+
&source_ciphertext_lo,
295+
&source_ciphertext_hi,
296+
)
297+
.unwrap();
298+
299+
let final_source_opening =
300+
source_opening - (opening_lo.clone() + opening_hi.clone() * Scalar::from(TWO_16));
301+
let expected_source: PodElGamalCiphertext = source_pubkey
302+
.encrypt_with(22_u64, &final_source_opening)
303+
.into();
304+
assert_eq!(expected_source, final_source_spendable);
305+
306+
// program arithmetic for the destination account
307+
let destination_handle_lo_point =
308+
PodRistrettoPoint(bytes_of(&destination_handle_lo).try_into().unwrap());
309+
let destination_handle_hi_point =
310+
PodRistrettoPoint(bytes_of(&destination_handle_hi).try_into().unwrap());
311+
312+
let destination_ciphertext_lo =
313+
ristretto_to_elgamal_ciphertext(&commitment_lo_point, &destination_handle_lo_point);
314+
let destination_ciphertext_hi =
315+
ristretto_to_elgamal_ciphertext(&commitment_hi_point, &destination_handle_hi_point);
316+
317+
let final_destination_pending_ciphertext = add_with_lo_hi(
318+
&destination_pending_ciphertext,
319+
&destination_ciphertext_lo,
320+
&destination_ciphertext_hi,
321+
)
322+
.unwrap();
323+
324+
let final_destination_opening =
325+
destination_opening + (opening_lo + opening_hi * Scalar::from(TWO_16));
326+
let expected_destination_ciphertext: PodElGamalCiphertext = destination_pubkey
327+
.encrypt_with(132_u64, &final_destination_opening)
328+
.into();
329+
assert_eq!(
330+
expected_destination_ciphertext,
331+
final_destination_pending_ciphertext
332+
);
333+
}
334+
}

0 commit comments

Comments
 (0)