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

Commit b257efc

Browse files
committed
add confidential transfer ciphertext arithmetic crate
1 parent cfaa453 commit b257efc

File tree

4 files changed

+360
-0
lines changed

4 files changed

+360
-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: 326 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,326 @@
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 * right_ciphertext_hi)`
53+
pub fn add_with_lo_hi(
54+
left_ciphertext: &PodElGamalCiphertext,
55+
right_ciphertext_lo: &PodElGamalCiphertext,
56+
right_ciphertext_hi: &PodElGamalCiphertext,
57+
) -> Option<PodElGamalCiphertext> {
58+
let shift_scalar = u64_to_scalar(1_u64 << SHIFT_BITS);
59+
let shifted_right_ciphertext_hi = multiply(&shift_scalar, right_ciphertext_hi)?;
60+
let combined_right_ciphertext = add(right_ciphertext_lo, &shifted_right_ciphertext_hi)?;
61+
add(left_ciphertext, &combined_right_ciphertext)
62+
}
63+
64+
/// Subtract two ElGamal ciphertexts
65+
pub fn subtract(
66+
left_ciphertext: &PodElGamalCiphertext,
67+
right_ciphertext: &PodElGamalCiphertext,
68+
) -> Option<PodElGamalCiphertext> {
69+
let (left_commitment, left_handle) = elgamal_ciphertext_to_ristretto(left_ciphertext);
70+
let (right_commitment, right_handle) = elgamal_ciphertext_to_ristretto(right_ciphertext);
71+
72+
let result_commitment = subtract_ristretto(&left_commitment, &right_commitment)?;
73+
let result_handle = subtract_ristretto(&left_handle, &right_handle)?;
74+
75+
Some(ristretto_to_elgamal_ciphertext(
76+
&result_commitment,
77+
&result_handle,
78+
))
79+
}
80+
81+
/// Compute `left_ciphertext - (right_ciphertext_lo + 2^16 * right_ciphertext_hi)`
82+
pub fn subtract_with_lo_hi(
83+
left_ciphertext: &PodElGamalCiphertext,
84+
right_ciphertext_lo: &PodElGamalCiphertext,
85+
right_ciphertext_hi: &PodElGamalCiphertext,
86+
) -> Option<PodElGamalCiphertext> {
87+
let shift_scalar = u64_to_scalar(1_u64 << SHIFT_BITS);
88+
let shifted_right_ciphertext_hi = multiply(&shift_scalar, right_ciphertext_hi)?;
89+
let combined_right_ciphertext = add(right_ciphertext_lo, &shifted_right_ciphertext_hi)?;
90+
subtract(left_ciphertext, &combined_right_ciphertext)
91+
}
92+
93+
/// Add a constant amount to a ciphertext
94+
pub fn add_to(ciphertext: &PodElGamalCiphertext, amount: u64) -> Option<PodElGamalCiphertext> {
95+
let amount_scalar = u64_to_scalar(amount);
96+
let amount_point = multiply_ristretto(&amount_scalar, &G)?;
97+
98+
let (commitment, handle) = elgamal_ciphertext_to_ristretto(ciphertext);
99+
100+
let result_commitment = add_ristretto(&commitment, &amount_point)?;
101+
102+
Some(ristretto_to_elgamal_ciphertext(&result_commitment, &handle))
103+
}
104+
105+
/// Subtract a constant amount to a ciphertext
106+
pub fn subtract_from(
107+
ciphertext: &PodElGamalCiphertext,
108+
amount: u64,
109+
) -> Option<PodElGamalCiphertext> {
110+
let amount_scalar = u64_to_scalar(amount);
111+
let amount_point = multiply_ristretto(&amount_scalar, &G)?;
112+
113+
let (commitment, handle) = elgamal_ciphertext_to_ristretto(ciphertext);
114+
115+
let result_commitment = subtract_ristretto(&commitment, &amount_point)?;
116+
117+
Some(ristretto_to_elgamal_ciphertext(&result_commitment, &handle))
118+
}
119+
120+
/// Convert a `u64` amount into a curve25519 scalar
121+
fn u64_to_scalar(amount: u64) -> PodScalar {
122+
let mut amount_bytes = [0u8; 32];
123+
amount_bytes[..8].copy_from_slice(&amount.to_le_bytes());
124+
PodScalar(amount_bytes)
125+
}
126+
127+
/// Convert a `PodElGamalCiphertext` into a tuple of commitment and decrypt handle
128+
/// `PodRistrettoPoint`
129+
fn elgamal_ciphertext_to_ristretto(
130+
ciphertext: &PodElGamalCiphertext,
131+
) -> (PodRistrettoPoint, PodRistrettoPoint) {
132+
let ciphertext_bytes = bytes_of(ciphertext); // must be of length 64 by type
133+
let commitment_bytes = ciphertext_bytes[..32].try_into().unwrap();
134+
let handle_bytes = ciphertext_bytes[32..64].try_into().unwrap();
135+
(
136+
PodRistrettoPoint(commitment_bytes),
137+
PodRistrettoPoint(handle_bytes),
138+
)
139+
}
140+
141+
fn ristretto_to_elgamal_ciphertext(
142+
commitment: &PodRistrettoPoint,
143+
handle: &PodRistrettoPoint,
144+
) -> PodElGamalCiphertext {
145+
let mut ciphertext_bytes = [0u8; 64];
146+
ciphertext_bytes[..32].copy_from_slice(bytes_of(commitment));
147+
ciphertext_bytes[32..64].copy_from_slice(bytes_of(handle));
148+
let ciphertext_string = STANDARD.encode(ciphertext_bytes);
149+
FromStr::from_str(&ciphertext_string).unwrap()
150+
}
151+
152+
#[cfg(test)]
153+
mod tests {
154+
use {
155+
super::*,
156+
bytemuck::Zeroable,
157+
curve25519_dalek::scalar::Scalar,
158+
solana_zk_sdk::encryption::{
159+
elgamal::{ElGamalCiphertext, ElGamalKeypair},
160+
pedersen::{Pedersen, PedersenOpening},
161+
pod::{elgamal::PodDecryptHandle, pedersen::PodPedersenCommitment},
162+
},
163+
spl_token_confidential_transfer_proof_generation::try_split_u64,
164+
};
165+
166+
const TWO_16: u64 = 65536;
167+
168+
#[test]
169+
fn test_zero_ct() {
170+
let spendable_balance = PodElGamalCiphertext::zeroed();
171+
let spendable_ct: ElGamalCiphertext = spendable_balance.try_into().unwrap();
172+
173+
// spendable_ct should be an encryption of 0 for any public key when
174+
// `PedersenOpen::default()` is used
175+
let keypair = ElGamalKeypair::new_rand();
176+
let public = keypair.pubkey();
177+
let balance: u64 = 0;
178+
assert_eq!(
179+
spendable_ct,
180+
public.encrypt_with(balance, &PedersenOpening::default())
181+
);
182+
183+
// homomorphism should work like any other ciphertext
184+
let open = PedersenOpening::new_rand();
185+
let transfer_amount_ciphertext = public.encrypt_with(55_u64, &open);
186+
let transfer_amount_pod: PodElGamalCiphertext = transfer_amount_ciphertext.into();
187+
188+
let sum = add(&spendable_balance, &transfer_amount_pod).unwrap();
189+
190+
let expected: PodElGamalCiphertext = public.encrypt_with(55_u64, &open).into();
191+
assert_eq!(expected, sum);
192+
}
193+
194+
#[test]
195+
fn test_add_to() {
196+
let spendable_balance = PodElGamalCiphertext::zeroed();
197+
198+
let added_ciphertext = add_to(&spendable_balance, 55).unwrap();
199+
200+
let keypair = ElGamalKeypair::new_rand();
201+
let public = keypair.pubkey();
202+
let expected: PodElGamalCiphertext = public
203+
.encrypt_with(55_u64, &PedersenOpening::default())
204+
.into();
205+
206+
assert_eq!(expected, added_ciphertext);
207+
}
208+
209+
#[test]
210+
fn test_subtract_from() {
211+
let amount = 77_u64;
212+
let keypair = ElGamalKeypair::new_rand();
213+
let public = keypair.pubkey();
214+
let open = PedersenOpening::new_rand();
215+
let encrypted_amount: PodElGamalCiphertext = public.encrypt_with(amount, &open).into();
216+
217+
let subtracted_ciphertext = subtract_from(&encrypted_amount, 55).unwrap();
218+
219+
let expected: PodElGamalCiphertext = public.encrypt_with(22_u64, &open).into();
220+
221+
assert_eq!(expected, subtracted_ciphertext);
222+
}
223+
224+
#[test]
225+
fn test_transfer_arithmetic() {
226+
// transfer amount
227+
let transfer_amount: u64 = 55;
228+
let (amount_lo, amount_hi) = try_split_u64(transfer_amount, 16).unwrap();
229+
230+
// generate public keys
231+
let source_keypair = ElGamalKeypair::new_rand();
232+
let source_pubkey = source_keypair.pubkey();
233+
234+
let destination_keypair = ElGamalKeypair::new_rand();
235+
let destination_pubkey = destination_keypair.pubkey();
236+
237+
let auditor_keypair = ElGamalKeypair::new_rand();
238+
let auditor_pubkey = auditor_keypair.pubkey();
239+
240+
// commitments associated with TransferRangeProof
241+
let (commitment_lo, opening_lo) = Pedersen::new(amount_lo);
242+
let (commitment_hi, opening_hi) = Pedersen::new(amount_hi);
243+
244+
let commitment_lo: PodPedersenCommitment = commitment_lo.into();
245+
let commitment_hi: PodPedersenCommitment = commitment_hi.into();
246+
247+
// decryption handles associated with TransferValidityProof
248+
let source_handle_lo: PodDecryptHandle = source_pubkey.decrypt_handle(&opening_lo).into();
249+
let destination_handle_lo: PodDecryptHandle =
250+
destination_pubkey.decrypt_handle(&opening_lo).into();
251+
let _auditor_handle_lo: PodDecryptHandle =
252+
auditor_pubkey.decrypt_handle(&opening_lo).into();
253+
254+
let source_handle_hi: PodDecryptHandle = source_pubkey.decrypt_handle(&opening_hi).into();
255+
let destination_handle_hi: PodDecryptHandle =
256+
destination_pubkey.decrypt_handle(&opening_hi).into();
257+
let _auditor_handle_hi: PodDecryptHandle =
258+
auditor_pubkey.decrypt_handle(&opening_hi).into();
259+
260+
// source spendable and recipient pending
261+
let source_opening = PedersenOpening::new_rand();
262+
let destination_opening = PedersenOpening::new_rand();
263+
264+
let source_spendable_ciphertext: PodElGamalCiphertext =
265+
source_pubkey.encrypt_with(77_u64, &source_opening).into();
266+
let destination_pending_ciphertext: PodElGamalCiphertext = destination_pubkey
267+
.encrypt_with(77_u64, &destination_opening)
268+
.into();
269+
270+
// program arithmetic for the source account
271+
let commitment_lo_point = PodRistrettoPoint(bytes_of(&commitment_lo).try_into().unwrap());
272+
let source_handle_lo_point =
273+
PodRistrettoPoint(bytes_of(&source_handle_lo).try_into().unwrap());
274+
275+
let commitment_hi_point = PodRistrettoPoint(bytes_of(&commitment_hi).try_into().unwrap());
276+
let source_handle_hi_point =
277+
PodRistrettoPoint(bytes_of(&source_handle_hi).try_into().unwrap());
278+
279+
let source_ciphertext_lo =
280+
ristretto_to_elgamal_ciphertext(&commitment_lo_point, &source_handle_lo_point);
281+
let source_ciphertext_hi =
282+
ristretto_to_elgamal_ciphertext(&commitment_hi_point, &source_handle_hi_point);
283+
284+
let final_source_spendable = subtract_with_lo_hi(
285+
&source_spendable_ciphertext,
286+
&source_ciphertext_lo,
287+
&source_ciphertext_hi,
288+
)
289+
.unwrap();
290+
291+
let final_source_opening =
292+
source_opening - (opening_lo.clone() + opening_hi.clone() * Scalar::from(TWO_16));
293+
let expected_source: PodElGamalCiphertext = source_pubkey
294+
.encrypt_with(22_u64, &final_source_opening)
295+
.into();
296+
assert_eq!(expected_source, final_source_spendable);
297+
298+
// program arithmetic for the destination account
299+
let destination_handle_lo_point =
300+
PodRistrettoPoint(bytes_of(&destination_handle_lo).try_into().unwrap());
301+
let destination_handle_hi_point =
302+
PodRistrettoPoint(bytes_of(&destination_handle_hi).try_into().unwrap());
303+
304+
let destination_ciphertext_lo =
305+
ristretto_to_elgamal_ciphertext(&commitment_lo_point, &destination_handle_lo_point);
306+
let destination_ciphertext_hi =
307+
ristretto_to_elgamal_ciphertext(&commitment_hi_point, &destination_handle_hi_point);
308+
309+
let final_destination_pending_ciphertext = add_with_lo_hi(
310+
&destination_pending_ciphertext,
311+
&destination_ciphertext_lo,
312+
&destination_ciphertext_hi,
313+
)
314+
.unwrap();
315+
316+
let final_destination_opening =
317+
destination_opening + (opening_lo + opening_hi * Scalar::from(TWO_16));
318+
let expected_destination_ciphertext: PodElGamalCiphertext = destination_pubkey
319+
.encrypt_with(132_u64, &final_destination_opening)
320+
.into();
321+
assert_eq!(
322+
expected_destination_ciphertext,
323+
final_destination_pending_ciphertext
324+
);
325+
}
326+
}

0 commit comments

Comments
 (0)