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

Commit 1808765

Browse files
[confidential-transfer] Add transfer split proof generation (#6915)
* add `confidential-transfer-proofs` * add `encryption` module * add transfer proof generation and extraction * cargo fmt * allow `arithmetic_side_effects` on ciphertexts and openings * include `solana-zk-sdk` in patches * refactor into two separate crates * clean up tests * remove unnecessary `target_os = solana` * refactor tests into a separate crate
1 parent d6a5ecf commit 1808765

File tree

16 files changed

+596
-2
lines changed

16 files changed

+596
-2
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@ members = [
6060
"token/transfer-hook/cli",
6161
"token/transfer-hook/example",
6262
"token/transfer-hook/interface",
63+
"token/confidential-transfer/proof-extraction",
64+
"token/confidential-transfer/proof-generation",
65+
"token/confidential-transfer/proof-tests",
6366
"token/client",
6467
"utils/cgen",
6568
"utils/test-client",

patch.crates-io.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ crates_map+=("solana-transaction-status transaction-status")
110110
crates_map+=("solana-udp-client udp-client")
111111
crates_map+=("solana-version version")
112112
crates_map+=("solana-zk-token-sdk zk-token-sdk")
113+
crates_map+=("solana-zk-sdk zk-sdk")
113114

114115
patch_crates=()
115116
for map_entry in "${crates_map[@]}"; do
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[package]
2+
name = "spl-token-confidential-transfer-proof-extraction"
3+
version = "0.1.0"
4+
description = "Solana Program Library Confidential Transfer Proof Extraction"
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+
solana-zk-sdk = "2.0.0"
12+
thiserror = "1.0.61"
13+
14+
[lib]
15+
crate-type = ["cdylib", "lib"]
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
use solana_zk_sdk::encryption::pod::grouped_elgamal::PodGroupedElGamalCiphertext3Handles;
2+
3+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
4+
#[repr(C)]
5+
pub struct PodTransferAmountCiphertext(pub(crate) PodGroupedElGamalCiphertext3Handles);
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
use thiserror::Error;
2+
3+
#[derive(Error, Clone, Debug, Eq, PartialEq)]
4+
pub enum TokenProofExtractionError {
5+
#[error("ElGamal pubkey mismatch")]
6+
ElGamalPubkeyMismatch,
7+
#[error("Pedersen commitment mismatch")]
8+
PedersenCommitmentMismatch,
9+
#[error("Range proof length mismatch")]
10+
RangeProofLengthMismatch,
11+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
pub mod encryption;
2+
pub mod errors;
3+
pub mod transfer;
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
use {
2+
crate::{encryption::PodTransferAmountCiphertext, errors::TokenProofExtractionError},
3+
solana_zk_sdk::{
4+
encryption::pod::elgamal::{PodElGamalCiphertext, PodElGamalPubkey},
5+
zk_elgamal_proof_program::proof_data::{
6+
BatchedGroupedCiphertext3HandlesValidityProofContext, BatchedRangeProofContext,
7+
CiphertextCommitmentEqualityProofContext,
8+
},
9+
},
10+
};
11+
12+
/// The transfer public keys associated with a transfer.
13+
pub struct TransferPubkeys {
14+
/// Source ElGamal public key
15+
pub source: PodElGamalPubkey,
16+
/// Destination ElGamal public key
17+
pub destination: PodElGamalPubkey,
18+
/// Auditor ElGamal public key
19+
pub auditor: PodElGamalPubkey,
20+
}
21+
22+
/// The proof context information needed to process a [Transfer] instruction.
23+
pub struct TransferProofContext {
24+
/// Ciphertext containing the low 16 bits of the transafer amount
25+
pub ciphertext_lo: PodTransferAmountCiphertext,
26+
/// Ciphertext containing the high 32 bits of the transafer amount
27+
pub ciphertext_hi: PodTransferAmountCiphertext,
28+
/// The transfer public keys associated with a transfer
29+
pub transfer_pubkeys: TransferPubkeys,
30+
/// The new source available balance ciphertext
31+
pub new_source_ciphertext: PodElGamalCiphertext,
32+
}
33+
34+
impl TransferProofContext {
35+
pub fn verify_and_extract(
36+
equality_proof_context: &CiphertextCommitmentEqualityProofContext,
37+
ciphertext_validity_proof_context: &BatchedGroupedCiphertext3HandlesValidityProofContext,
38+
range_proof_context: &BatchedRangeProofContext,
39+
) -> Result<Self, TokenProofExtractionError> {
40+
// The equality proof context consists of the source ElGamal public key, the new
41+
// source available balance ciphertext, and the new source available
42+
// commitment. The public key and ciphertext should be returned as parts
43+
// of `TransferProofContextInfo` and the commitment should be checked
44+
// with range proof for consistency.
45+
let CiphertextCommitmentEqualityProofContext {
46+
pubkey: source_pubkey_from_equality_proof,
47+
ciphertext: new_source_ciphertext,
48+
commitment: new_source_commitment,
49+
} = equality_proof_context;
50+
51+
// The ciphertext validity proof context consists of the destination ElGamal
52+
// public key, auditor ElGamal public key, and the transfer amount
53+
// ciphertexts. All of these fields should be returned as part of
54+
// `TransferProofContextInfo`. In addition, the commitments pertaining
55+
// to the transfer amount ciphertexts should be checked with range proof for
56+
// consistency.
57+
let BatchedGroupedCiphertext3HandlesValidityProofContext {
58+
first_pubkey: source_pubkey_from_validity_proof,
59+
second_pubkey: destination_pubkey,
60+
third_pubkey: auditor_pubkey,
61+
grouped_ciphertext_lo: transfer_amount_ciphertext_lo,
62+
grouped_ciphertext_hi: transfer_amount_ciphertext_hi,
63+
} = ciphertext_validity_proof_context;
64+
65+
// The range proof context consists of the Pedersen commitments and bit-lengths
66+
// for which the range proof is proved. The commitments must consist of
67+
// three commitments pertaining to the new source available balance, the
68+
// low bits of the transfer amount, and high bits of the transfer
69+
// amount. These commitments must be checked for bit lengths `64`, `16`,
70+
// and `32`.
71+
let BatchedRangeProofContext {
72+
commitments: range_proof_commitments,
73+
bit_lengths: range_proof_bit_lengths,
74+
} = range_proof_context;
75+
76+
// check that the source pubkey is consistent between equality and ciphertext
77+
// validity proofs
78+
if source_pubkey_from_equality_proof != source_pubkey_from_validity_proof {
79+
return Err(TokenProofExtractionError::ElGamalPubkeyMismatch);
80+
}
81+
82+
// check that the range proof was created for the correct set of Pedersen
83+
// commitments
84+
let transfer_amount_commitment_lo = transfer_amount_ciphertext_lo.extract_commitment();
85+
let transfer_amount_commitment_hi = transfer_amount_ciphertext_hi.extract_commitment();
86+
87+
let expected_commitments = [
88+
*new_source_commitment,
89+
transfer_amount_commitment_lo,
90+
transfer_amount_commitment_hi,
91+
];
92+
93+
if !range_proof_commitments
94+
.iter()
95+
.zip(expected_commitments.iter())
96+
.all(|(proof_commitment, expected_commitment)| proof_commitment == expected_commitment)
97+
{
98+
return Err(TokenProofExtractionError::PedersenCommitmentMismatch);
99+
}
100+
101+
// check that the range proof was created for the correct number of bits
102+
const REMAINING_BALANCE_BIT_LENGTH: u8 = 64;
103+
const TRANSFER_AMOUNT_LO_BIT_LENGTH: u8 = 16;
104+
const TRANSFER_AMOUNT_HI_BIT_LENGTH: u8 = 32;
105+
const PADDING_BIT_LENGTH: u8 = 16;
106+
let expected_bit_lengths = [
107+
REMAINING_BALANCE_BIT_LENGTH,
108+
TRANSFER_AMOUNT_LO_BIT_LENGTH,
109+
TRANSFER_AMOUNT_HI_BIT_LENGTH,
110+
PADDING_BIT_LENGTH,
111+
]
112+
.iter();
113+
114+
if !range_proof_bit_lengths
115+
.iter()
116+
.zip(expected_bit_lengths)
117+
.all(|(proof_len, expected_len)| proof_len == expected_len)
118+
{
119+
return Err(TokenProofExtractionError::RangeProofLengthMismatch);
120+
}
121+
122+
let transfer_pubkeys = TransferPubkeys {
123+
source: *source_pubkey_from_equality_proof,
124+
destination: *destination_pubkey,
125+
auditor: *auditor_pubkey,
126+
};
127+
128+
let context_info = TransferProofContext {
129+
ciphertext_lo: PodTransferAmountCiphertext(*transfer_amount_ciphertext_lo),
130+
ciphertext_hi: PodTransferAmountCiphertext(*transfer_amount_ciphertext_hi),
131+
transfer_pubkeys,
132+
new_source_ciphertext: *new_source_ciphertext,
133+
};
134+
135+
Ok(context_info)
136+
}
137+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[package]
2+
name = "spl-token-confidential-transfer-proof-generation"
3+
version = "0.1.0"
4+
description = "Solana Program Library Confidential Transfer Proof Generation"
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+
curve25519-dalek = "3.2.1"
12+
solana-zk-sdk = "2.0.0"
13+
thiserror = "1.0.61"
14+
15+
[dev-dependencies]
16+
17+
[lib]
18+
crate-type = ["cdylib", "lib"]
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
use solana_zk_sdk::encryption::{
2+
elgamal::{DecryptHandle, ElGamalPubkey},
3+
grouped_elgamal::{GroupedElGamal, GroupedElGamalCiphertext},
4+
pedersen::{PedersenCommitment, PedersenOpening},
5+
};
6+
7+
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
8+
#[repr(C)]
9+
pub struct TransferAmountCiphertext(pub(crate) GroupedElGamalCiphertext<3>);
10+
11+
impl TransferAmountCiphertext {
12+
pub fn new(
13+
amount: u64,
14+
source_pubkey: &ElGamalPubkey,
15+
destination_pubkey: &ElGamalPubkey,
16+
auditor_pubkey: &ElGamalPubkey,
17+
) -> (Self, PedersenOpening) {
18+
let opening = PedersenOpening::new_rand();
19+
let grouped_ciphertext = GroupedElGamal::<3>::encrypt_with(
20+
[source_pubkey, destination_pubkey, auditor_pubkey],
21+
amount,
22+
&opening,
23+
);
24+
25+
(Self(grouped_ciphertext), opening)
26+
}
27+
28+
pub fn get_commitment(&self) -> &PedersenCommitment {
29+
&self.0.commitment
30+
}
31+
32+
pub fn get_source_handle(&self) -> &DecryptHandle {
33+
// `TransferAmountCiphertext` is a wrapper for `GroupedElGamalCiphertext<3>`,
34+
// which holds exactly three decryption handles.
35+
self.0.handles.first().unwrap()
36+
}
37+
38+
pub fn get_destination_handle(&self) -> &DecryptHandle {
39+
// `TransferAmountCiphertext` is a wrapper for `GroupedElGamalCiphertext<3>`,
40+
// which holds exactly three decryption handles.
41+
self.0.handles.get(1).unwrap()
42+
}
43+
44+
pub fn get_auditor_handle(&self) -> &DecryptHandle {
45+
// `TransferAmountCiphertext` is a wrapper for `GroupedElGamalCiphertext<3>`,
46+
// which holds exactly three decryption handles.
47+
self.0.handles.get(2).unwrap()
48+
}
49+
}

0 commit comments

Comments
 (0)