Skip to content

Commit

Permalink
[confidential-transfer] Add transfer split proof generation (solana-l…
Browse files Browse the repository at this point in the history
…abs#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
  • Loading branch information
samkim-crypto authored Jun 27, 2024
1 parent d6a5ecf commit 1808765
Show file tree
Hide file tree
Showing 16 changed files with 596 additions and 2 deletions.
32 changes: 30 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,9 @@ members = [
"token/transfer-hook/cli",
"token/transfer-hook/example",
"token/transfer-hook/interface",
"token/confidential-transfer/proof-extraction",
"token/confidential-transfer/proof-generation",
"token/confidential-transfer/proof-tests",
"token/client",
"utils/cgen",
"utils/test-client",
Expand Down
1 change: 1 addition & 0 deletions patch.crates-io.sh
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ crates_map+=("solana-transaction-status transaction-status")
crates_map+=("solana-udp-client udp-client")
crates_map+=("solana-version version")
crates_map+=("solana-zk-token-sdk zk-token-sdk")
crates_map+=("solana-zk-sdk zk-sdk")

patch_crates=()
for map_entry in "${crates_map[@]}"; do
Expand Down
15 changes: 15 additions & 0 deletions token/confidential-transfer/proof-extraction/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
[package]
name = "spl-token-confidential-transfer-proof-extraction"
version = "0.1.0"
description = "Solana Program Library Confidential Transfer Proof Extraction"
authors = ["Solana Labs Maintainers <maintainers@solanalabs.com>"]
repository = "https://github.com/solana-labs/solana-program-library"
license = "Apache-2.0"
edition = "2021"

[dependencies]
solana-zk-sdk = "2.0.0"
thiserror = "1.0.61"

[lib]
crate-type = ["cdylib", "lib"]
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
use solana_zk_sdk::encryption::pod::grouped_elgamal::PodGroupedElGamalCiphertext3Handles;

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(C)]
pub struct PodTransferAmountCiphertext(pub(crate) PodGroupedElGamalCiphertext3Handles);
11 changes: 11 additions & 0 deletions token/confidential-transfer/proof-extraction/src/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use thiserror::Error;

#[derive(Error, Clone, Debug, Eq, PartialEq)]
pub enum TokenProofExtractionError {
#[error("ElGamal pubkey mismatch")]
ElGamalPubkeyMismatch,
#[error("Pedersen commitment mismatch")]
PedersenCommitmentMismatch,
#[error("Range proof length mismatch")]
RangeProofLengthMismatch,
}
3 changes: 3 additions & 0 deletions token/confidential-transfer/proof-extraction/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub mod encryption;
pub mod errors;
pub mod transfer;
137 changes: 137 additions & 0 deletions token/confidential-transfer/proof-extraction/src/transfer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
use {
crate::{encryption::PodTransferAmountCiphertext, errors::TokenProofExtractionError},
solana_zk_sdk::{
encryption::pod::elgamal::{PodElGamalCiphertext, PodElGamalPubkey},
zk_elgamal_proof_program::proof_data::{
BatchedGroupedCiphertext3HandlesValidityProofContext, BatchedRangeProofContext,
CiphertextCommitmentEqualityProofContext,
},
},
};

/// The transfer public keys associated with a transfer.
pub struct TransferPubkeys {
/// Source ElGamal public key
pub source: PodElGamalPubkey,
/// Destination ElGamal public key
pub destination: PodElGamalPubkey,
/// Auditor ElGamal public key
pub auditor: PodElGamalPubkey,
}

/// The proof context information needed to process a [Transfer] instruction.
pub struct TransferProofContext {
/// Ciphertext containing the low 16 bits of the transafer amount
pub ciphertext_lo: PodTransferAmountCiphertext,
/// Ciphertext containing the high 32 bits of the transafer amount
pub ciphertext_hi: PodTransferAmountCiphertext,
/// The transfer public keys associated with a transfer
pub transfer_pubkeys: TransferPubkeys,
/// The new source available balance ciphertext
pub new_source_ciphertext: PodElGamalCiphertext,
}

impl TransferProofContext {
pub fn verify_and_extract(
equality_proof_context: &CiphertextCommitmentEqualityProofContext,
ciphertext_validity_proof_context: &BatchedGroupedCiphertext3HandlesValidityProofContext,
range_proof_context: &BatchedRangeProofContext,
) -> Result<Self, TokenProofExtractionError> {
// The equality proof context consists of the source ElGamal public key, the new
// source available balance ciphertext, and the new source available
// commitment. The public key and ciphertext should be returned as parts
// of `TransferProofContextInfo` and the commitment should be checked
// with range proof for consistency.
let CiphertextCommitmentEqualityProofContext {
pubkey: source_pubkey_from_equality_proof,
ciphertext: new_source_ciphertext,
commitment: new_source_commitment,
} = equality_proof_context;

// The ciphertext validity proof context consists of the destination ElGamal
// public key, auditor ElGamal public key, and the transfer amount
// ciphertexts. All of these fields should be returned as part of
// `TransferProofContextInfo`. In addition, the commitments pertaining
// to the transfer amount ciphertexts should be checked with range proof for
// consistency.
let BatchedGroupedCiphertext3HandlesValidityProofContext {
first_pubkey: source_pubkey_from_validity_proof,
second_pubkey: destination_pubkey,
third_pubkey: auditor_pubkey,
grouped_ciphertext_lo: transfer_amount_ciphertext_lo,
grouped_ciphertext_hi: transfer_amount_ciphertext_hi,
} = ciphertext_validity_proof_context;

// The range proof context consists of the Pedersen commitments and bit-lengths
// for which the range proof is proved. The commitments must consist of
// three commitments pertaining to the new source available balance, the
// low bits of the transfer amount, and high bits of the transfer
// amount. These commitments must be checked for bit lengths `64`, `16`,
// and `32`.
let BatchedRangeProofContext {
commitments: range_proof_commitments,
bit_lengths: range_proof_bit_lengths,
} = range_proof_context;

// check that the source pubkey is consistent between equality and ciphertext
// validity proofs
if source_pubkey_from_equality_proof != source_pubkey_from_validity_proof {
return Err(TokenProofExtractionError::ElGamalPubkeyMismatch);
}

// check that the range proof was created for the correct set of Pedersen
// commitments
let transfer_amount_commitment_lo = transfer_amount_ciphertext_lo.extract_commitment();
let transfer_amount_commitment_hi = transfer_amount_ciphertext_hi.extract_commitment();

let expected_commitments = [
*new_source_commitment,
transfer_amount_commitment_lo,
transfer_amount_commitment_hi,
];

if !range_proof_commitments
.iter()
.zip(expected_commitments.iter())
.all(|(proof_commitment, expected_commitment)| proof_commitment == expected_commitment)
{
return Err(TokenProofExtractionError::PedersenCommitmentMismatch);
}

// check that the range proof was created for the correct number of bits
const REMAINING_BALANCE_BIT_LENGTH: u8 = 64;
const TRANSFER_AMOUNT_LO_BIT_LENGTH: u8 = 16;
const TRANSFER_AMOUNT_HI_BIT_LENGTH: u8 = 32;
const PADDING_BIT_LENGTH: u8 = 16;
let expected_bit_lengths = [
REMAINING_BALANCE_BIT_LENGTH,
TRANSFER_AMOUNT_LO_BIT_LENGTH,
TRANSFER_AMOUNT_HI_BIT_LENGTH,
PADDING_BIT_LENGTH,
]
.iter();

if !range_proof_bit_lengths
.iter()
.zip(expected_bit_lengths)
.all(|(proof_len, expected_len)| proof_len == expected_len)
{
return Err(TokenProofExtractionError::RangeProofLengthMismatch);
}

let transfer_pubkeys = TransferPubkeys {
source: *source_pubkey_from_equality_proof,
destination: *destination_pubkey,
auditor: *auditor_pubkey,
};

let context_info = TransferProofContext {
ciphertext_lo: PodTransferAmountCiphertext(*transfer_amount_ciphertext_lo),
ciphertext_hi: PodTransferAmountCiphertext(*transfer_amount_ciphertext_hi),
transfer_pubkeys,
new_source_ciphertext: *new_source_ciphertext,
};

Ok(context_info)
}
}
18 changes: 18 additions & 0 deletions token/confidential-transfer/proof-generation/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "spl-token-confidential-transfer-proof-generation"
version = "0.1.0"
description = "Solana Program Library Confidential Transfer Proof Generation"
authors = ["Solana Labs Maintainers <maintainers@solanalabs.com>"]
repository = "https://github.com/solana-labs/solana-program-library"
license = "Apache-2.0"
edition = "2021"

[dependencies]
curve25519-dalek = "3.2.1"
solana-zk-sdk = "2.0.0"
thiserror = "1.0.61"

[dev-dependencies]

[lib]
crate-type = ["cdylib", "lib"]
49 changes: 49 additions & 0 deletions token/confidential-transfer/proof-generation/src/encryption.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
use solana_zk_sdk::encryption::{
elgamal::{DecryptHandle, ElGamalPubkey},
grouped_elgamal::{GroupedElGamal, GroupedElGamalCiphertext},
pedersen::{PedersenCommitment, PedersenOpening},
};

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[repr(C)]
pub struct TransferAmountCiphertext(pub(crate) GroupedElGamalCiphertext<3>);

impl TransferAmountCiphertext {
pub fn new(
amount: u64,
source_pubkey: &ElGamalPubkey,
destination_pubkey: &ElGamalPubkey,
auditor_pubkey: &ElGamalPubkey,
) -> (Self, PedersenOpening) {
let opening = PedersenOpening::new_rand();
let grouped_ciphertext = GroupedElGamal::<3>::encrypt_with(
[source_pubkey, destination_pubkey, auditor_pubkey],
amount,
&opening,
);

(Self(grouped_ciphertext), opening)
}

pub fn get_commitment(&self) -> &PedersenCommitment {
&self.0.commitment
}

pub fn get_source_handle(&self) -> &DecryptHandle {
// `TransferAmountCiphertext` is a wrapper for `GroupedElGamalCiphertext<3>`,
// which holds exactly three decryption handles.
self.0.handles.first().unwrap()
}

pub fn get_destination_handle(&self) -> &DecryptHandle {
// `TransferAmountCiphertext` is a wrapper for `GroupedElGamalCiphertext<3>`,
// which holds exactly three decryption handles.
self.0.handles.get(1).unwrap()
}

pub fn get_auditor_handle(&self) -> &DecryptHandle {
// `TransferAmountCiphertext` is a wrapper for `GroupedElGamalCiphertext<3>`,
// which holds exactly three decryption handles.
self.0.handles.get(2).unwrap()
}
}
11 changes: 11 additions & 0 deletions token/confidential-transfer/proof-generation/src/errors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use {solana_zk_sdk::zk_elgamal_proof_program::errors::ProofGenerationError, thiserror::Error};

#[derive(Error, Clone, Debug, Eq, PartialEq)]
pub enum TokenProofGenerationError {
#[error("inner proof generation failed")]
ProofGeneration(#[from] ProofGenerationError),
#[error("not enough funds in account")]
NotEnoughFunds,
#[error("illegal amount bit length")]
IllegalAmountBitLength,
}
Loading

0 comments on commit 1808765

Please sign in to comment.