diff --git a/Cargo.lock b/Cargo.lock index b4ed26532..ed942f9e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7270,6 +7270,7 @@ version = "1.17.0" dependencies = [ "bytemuck", "criterion", + "curve25519-dalek", "getrandom 0.1.16", "num-derive", "num-traits", @@ -7283,6 +7284,7 @@ name = "solana-zk-token-proof-program-tests" version = "1.17.0" dependencies = [ "bytemuck", + "curve25519-dalek", "solana-program-runtime", "solana-program-test", "solana-sdk", diff --git a/programs/zk-token-proof-tests/Cargo.toml b/programs/zk-token-proof-tests/Cargo.toml index 2a8b994d4..0dcfafc5b 100644 --- a/programs/zk-token-proof-tests/Cargo.toml +++ b/programs/zk-token-proof-tests/Cargo.toml @@ -9,6 +9,7 @@ edition = { workspace = true } [dev-dependencies] bytemuck = { workspace = true, features = ["derive"] } +curve25519-dalek = { workspace = true } solana-program-runtime = { workspace = true } solana-program-test = { workspace = true } solana-sdk = { workspace = true } diff --git a/programs/zk-token-proof-tests/tests/process_transaction.rs b/programs/zk-token-proof-tests/tests/process_transaction.rs index b09cde33d..f00d68fbf 100644 --- a/programs/zk-token-proof-tests/tests/process_transaction.rs +++ b/programs/zk-token-proof-tests/tests/process_transaction.rs @@ -1,5 +1,6 @@ use { bytemuck::Pod, + curve25519_dalek::scalar::Scalar, solana_program_test::*, solana_sdk::{ instruction::InstructionError, @@ -22,7 +23,7 @@ use { std::mem::size_of, }; -const VERIFY_INSTRUCTION_TYPES: [ProofInstruction; 13] = [ +const VERIFY_INSTRUCTION_TYPES: [ProofInstruction; 14] = [ ProofInstruction::VerifyZeroBalance, ProofInstruction::VerifyWithdraw, ProofInstruction::VerifyCiphertextCiphertextEquality, @@ -36,6 +37,7 @@ const VERIFY_INSTRUCTION_TYPES: [ProofInstruction; 13] = [ ProofInstruction::VerifyCiphertextCommitmentEquality, ProofInstruction::VerifyGroupedCiphertext2HandlesValidity, ProofInstruction::VerifyBatchedGroupedCiphertext2HandlesValidity, + ProofInstruction::VerifyFeeSigma, ]; #[tokio::test] @@ -703,6 +705,75 @@ async fn test_batched_grouped_ciphertext_2_handles_validity() { .await; } +#[allow(clippy::op_ref)] +#[tokio::test] +async fn test_fee_sigma() { + let transfer_amount: u64 = 1; + let max_fee: u64 = 3; + + let fee_rate: u16 = 400; + let fee_amount: u64 = 1; + let delta_fee: u64 = 9600; + + let (transfer_commitment, transfer_opening) = Pedersen::new(transfer_amount); + let (fee_commitment, fee_opening) = Pedersen::new(fee_amount); + + let scalar_rate = Scalar::from(fee_rate); + let delta_commitment = + &fee_commitment * Scalar::from(10_000_u64) - &transfer_commitment * &scalar_rate; + let delta_opening = &fee_opening * &Scalar::from(10_000_u64) - &transfer_opening * &scalar_rate; + + let (claimed_commitment, claimed_opening) = Pedersen::new(delta_fee); + + let success_proof_data = FeeSigmaProofData::new( + &fee_commitment, + &delta_commitment, + &claimed_commitment, + &fee_opening, + &delta_opening, + &claimed_opening, + fee_amount, + delta_fee, + max_fee, + ) + .unwrap(); + + let fail_proof_data = FeeSigmaProofData::new( + &fee_commitment, + &delta_commitment, + &claimed_commitment, + &fee_opening, + &delta_opening, + &claimed_opening, + fee_amount, + 0, + max_fee, + ) + .unwrap(); + + test_verify_proof_without_context( + ProofInstruction::VerifyFeeSigma, + &success_proof_data, + &fail_proof_data, + ) + .await; + + test_verify_proof_with_context( + ProofInstruction::VerifyFeeSigma, + size_of::>(), + &success_proof_data, + &fail_proof_data, + ) + .await; + + test_close_context_state( + ProofInstruction::VerifyFeeSigma, + size_of::>(), + &success_proof_data, + ) + .await; +} + async fn test_verify_proof_without_context( proof_instruction: ProofInstruction, success_proof_data: &T, diff --git a/programs/zk-token-proof/Cargo.toml b/programs/zk-token-proof/Cargo.toml index 812d66a53..d146ef44d 100644 --- a/programs/zk-token-proof/Cargo.toml +++ b/programs/zk-token-proof/Cargo.toml @@ -19,6 +19,7 @@ solana-zk-token-sdk = { workspace = true } [dev-dependencies] criterion = { workspace = true } +curve25519-dalek = { workspace = true } [[bench]] name = "verify_proofs" diff --git a/programs/zk-token-proof/benches/verify_proofs.rs b/programs/zk-token-proof/benches/verify_proofs.rs index 26ac7cda9..65ae5f9aa 100644 --- a/programs/zk-token-proof/benches/verify_proofs.rs +++ b/programs/zk-token-proof/benches/verify_proofs.rs @@ -1,5 +1,6 @@ use { criterion::{criterion_group, criterion_main, Criterion}, + curve25519_dalek::scalar::Scalar, solana_zk_token_sdk::{ encryption::{ elgamal::ElGamalKeypair, @@ -10,8 +11,9 @@ use { transfer::FeeParameters, BatchedGroupedCiphertext2HandlesValidityProofData, BatchedRangeProofU128Data, BatchedRangeProofU256Data, BatchedRangeProofU64Data, CiphertextCiphertextEqualityProofData, CiphertextCommitmentEqualityProofData, - GroupedCiphertext2HandlesValidityProofData, PubkeyValidityData, RangeProofU64Data, - TransferData, TransferWithFeeData, WithdrawData, ZeroBalanceProofData, ZkProofData, + FeeSigmaProofData, GroupedCiphertext2HandlesValidityProofData, PubkeyValidityData, + RangeProofU64Data, TransferData, TransferWithFeeData, WithdrawData, + ZeroBalanceProofData, ZkProofData, }, }, }; @@ -188,6 +190,45 @@ fn bench_batched_grouped_ciphertext_validity(c: &mut Criterion) { }); } +#[allow(clippy::op_ref)] +fn bench_fee_sigma(c: &mut Criterion) { + let transfer_amount: u64 = 1; + let max_fee: u64 = 3; + + let fee_rate: u16 = 400; + let fee_amount: u64 = 1; + let delta_fee: u64 = 9600; + + let (transfer_commitment, transfer_opening) = Pedersen::new(transfer_amount); + let (fee_commitment, fee_opening) = Pedersen::new(fee_amount); + + let scalar_rate = Scalar::from(fee_rate); + let delta_commitment = + &fee_commitment * Scalar::from(10_000_u64) - &transfer_commitment * &scalar_rate; + let delta_opening = &fee_opening * &Scalar::from(10_000_u64) - &transfer_opening * &scalar_rate; + + let (claimed_commitment, claimed_opening) = Pedersen::new(delta_fee); + + let proof_data = FeeSigmaProofData::new( + &fee_commitment, + &delta_commitment, + &claimed_commitment, + &fee_opening, + &delta_opening, + &claimed_opening, + fee_amount, + delta_fee, + max_fee, + ) + .unwrap(); + + c.bench_function("fee_sigma", |b| { + b.iter(|| { + proof_data.verify_proof().unwrap(); + }) + }); +} + fn bench_batched_range_proof_u64(c: &mut Criterion) { let amount_1 = 255_u64; let amount_2 = 77_u64; @@ -414,5 +455,6 @@ criterion_group!( bench_batched_range_proof_u256, bench_transfer, bench_transfer_with_fee, + bench_fee_sigma, ); criterion_main!(benches); diff --git a/programs/zk-token-proof/src/lib.rs b/programs/zk-token-proof/src/lib.rs index 87665cb23..3ecd33300 100644 --- a/programs/zk-token-proof/src/lib.rs +++ b/programs/zk-token-proof/src/lib.rs @@ -267,5 +267,12 @@ declare_process_instruction!(process_instruction, 0, |invoke_context| { BatchedGroupedCiphertext2HandlesValidityProofContext, >(invoke_context) } + ProofInstruction::VerifyFeeSigma => { + invoke_context + .consume_checked(6_547) + .map_err(|_| InstructionError::ComputationalBudgetExceeded)?; + ic_msg!(invoke_context, "VerifyFeeSigma"); + process_verify_proof::(invoke_context) + } } }); diff --git a/zk-token-sdk/src/instruction/fee_sigma.rs b/zk-token-sdk/src/instruction/fee_sigma.rs new file mode 100644 index 000000000..a158e3d27 --- /dev/null +++ b/zk-token-sdk/src/instruction/fee_sigma.rs @@ -0,0 +1,217 @@ +//! The fee sigma proof instruction. +//! +//! A fee sigma proof certifies that a Pedersen commitment to a transfer fee for SPL Token 2022 is +//! well-formed. +//! +//! A formal documentation of how transfer fees and fee sigma proof are computed can be found in +//! the [`ZK Token proof`] program documentation. +//! +//! [`ZK Token proof`]: https://edge.docs.solana.com/developing/runtime-facilities/zk-token-proof + +#[cfg(not(target_os = "solana"))] +use { + crate::{ + encryption::pedersen::{PedersenCommitment, PedersenOpening}, + errors::ProofError, + sigma_proofs::fee_proof::FeeSigmaProof, + transcript::TranscriptProtocol, + }, + merlin::Transcript, + std::convert::TryInto, +}; +use { + crate::{ + instruction::{ProofType, ZkProofData}, + zk_token_elgamal::pod, + }, + bytemuck::{Pod, Zeroable}, +}; + +/// The instruction data that is needed for the `ProofInstruction::VerifyFeeSigma` instruction. +/// +/// It includes the cryptographic proof as well as the context data information needed to verify +/// the proof. +#[derive(Clone, Copy, Pod, Zeroable)] +#[repr(C)] +pub struct FeeSigmaProofData { + pub context: FeeSigmaProofContext, + + pub proof: pod::FeeSigmaProof, +} + +/// The context data needed to verify a pubkey validity proof. +/// +/// We refer to [`ZK Token proof`] for the formal details on how the fee sigma proof is computed. +/// +/// [`ZK Token proof`]: https://edge.docs.solana.com/developing/runtime-facilities/zk-token-proof +#[derive(Clone, Copy, Pod, Zeroable)] +#[repr(C)] +pub struct FeeSigmaProofContext { + /// The Pedersen commitment to the transfer fee + pub fee_commitment: pod::PedersenCommitment, + + /// The Pedersen commitment to the real delta fee. + pub delta_commitment: pod::PedersenCommitment, + + /// The Pedersen commitment to the claimed delta fee. + pub claimed_commitment: pod::PedersenCommitment, + + /// The maximum cap for a transfer fee + pub max_fee: pod::PodU64, +} + +#[cfg(not(target_os = "solana"))] +impl FeeSigmaProofData { + pub fn new( + fee_commitment: &PedersenCommitment, + delta_commitment: &PedersenCommitment, + claimed_commitment: &PedersenCommitment, + fee_opening: &PedersenOpening, + delta_opening: &PedersenOpening, + claimed_opening: &PedersenOpening, + fee_amount: u64, + delta_fee: u64, + max_fee: u64, + ) -> Result { + let pod_fee_commitment = pod::PedersenCommitment(fee_commitment.to_bytes()); + let pod_delta_commitment = pod::PedersenCommitment(delta_commitment.to_bytes()); + let pod_claimed_commitment = pod::PedersenCommitment(claimed_commitment.to_bytes()); + let pod_max_fee = max_fee.into(); + + let context = FeeSigmaProofContext { + fee_commitment: pod_fee_commitment, + delta_commitment: pod_delta_commitment, + claimed_commitment: pod_claimed_commitment, + max_fee: pod_max_fee, + }; + + let mut transcript = context.new_transcript(); + + let proof = FeeSigmaProof::new( + (fee_amount, fee_commitment, fee_opening), + (delta_fee, delta_commitment, delta_opening), + (claimed_commitment, claimed_opening), + max_fee, + &mut transcript, + ) + .into(); + + Ok(Self { context, proof }) + } +} + +impl ZkProofData for FeeSigmaProofData { + const PROOF_TYPE: ProofType = ProofType::FeeSigma; + + fn context_data(&self) -> &FeeSigmaProofContext { + &self.context + } + + #[cfg(not(target_os = "solana"))] + fn verify_proof(&self) -> Result<(), ProofError> { + let mut transcript = self.context.new_transcript(); + + let fee_commitment = self.context.fee_commitment.try_into()?; + let delta_commitment = self.context.delta_commitment.try_into()?; + let claimed_commitment = self.context.claimed_commitment.try_into()?; + let max_fee = self.context.max_fee.into(); + let proof: FeeSigmaProof = self.proof.try_into()?; + + proof + .verify( + &fee_commitment, + &delta_commitment, + &claimed_commitment, + max_fee, + &mut transcript, + ) + .map_err(|e| e.into()) + } +} + +#[cfg(not(target_os = "solana"))] +impl FeeSigmaProofContext { + fn new_transcript(&self) -> Transcript { + let mut transcript = Transcript::new(b"FeeSigmaProof"); + transcript.append_commitment(b"fee-commitment", &self.fee_commitment); + transcript.append_commitment(b"delta-commitment", &self.fee_commitment); + transcript.append_commitment(b"claimed-commitment", &self.fee_commitment); + transcript.append_u64(b"max-fee", self.max_fee.into()); + transcript + } +} + +#[cfg(test)] +mod test { + use {super::*, crate::encryption::pedersen::Pedersen, curve25519_dalek::scalar::Scalar}; + + #[test] + fn test_fee_sigma_instruction_correctness() { + // transfer fee amount is below max fee + let transfer_amount: u64 = 1; + let max_fee: u64 = 3; + + let fee_rate: u16 = 400; + let fee_amount: u64 = 1; + let delta_fee: u64 = 9600; + + let (transfer_commitment, transfer_opening) = Pedersen::new(transfer_amount); + let (fee_commitment, fee_opening) = Pedersen::new(fee_amount); + + let scalar_rate = Scalar::from(fee_rate); + let delta_commitment = + &fee_commitment * Scalar::from(10_000_u64) - &transfer_commitment * &scalar_rate; + let delta_opening = + &fee_opening * &Scalar::from(10_000_u64) - &transfer_opening * &scalar_rate; + + let (claimed_commitment, claimed_opening) = Pedersen::new(delta_fee); + + let proof_data = FeeSigmaProofData::new( + &fee_commitment, + &delta_commitment, + &claimed_commitment, + &fee_opening, + &delta_opening, + &claimed_opening, + fee_amount, + delta_fee, + max_fee, + ) + .unwrap(); + + assert!(proof_data.verify_proof().is_ok()); + + // transfer fee amount is equal to max fee + let transfer_amount: u64 = 55; + let max_fee: u64 = 3; + + let fee_rate: u16 = 555; + let fee_amount: u64 = 4; + + let (transfer_commitment, transfer_opening) = Pedersen::new(transfer_amount); + let (fee_commitment, fee_opening) = Pedersen::new(max_fee); + + let scalar_rate = Scalar::from(fee_rate); + let delta_commitment = + &fee_commitment * &Scalar::from(10000_u64) - &transfer_commitment * &scalar_rate; + let delta_opening = + &fee_opening * &Scalar::from(10000_u64) - &transfer_opening * &scalar_rate; + + let (claimed_commitment, claimed_opening) = Pedersen::new(0_u64); + + let proof_data = FeeSigmaProofData::new( + &fee_commitment, + &delta_commitment, + &claimed_commitment, + &fee_opening, + &delta_opening, + &claimed_opening, + fee_amount, + delta_fee, + max_fee, + ) + .unwrap(); + + assert!(proof_data.verify_proof().is_ok()); + } +} diff --git a/zk-token-sdk/src/instruction/mod.rs b/zk-token-sdk/src/instruction/mod.rs index a2e9fceb8..374c224f5 100644 --- a/zk-token-sdk/src/instruction/mod.rs +++ b/zk-token-sdk/src/instruction/mod.rs @@ -6,6 +6,7 @@ pub mod batched_grouped_ciphertext_validity; pub mod batched_range_proof; pub mod ciphertext_ciphertext_equality; pub mod ciphertext_commitment_equality; +pub mod fee_sigma; pub mod grouped_ciphertext_validity; pub mod pubkey_validity; pub mod range_proof; @@ -33,6 +34,7 @@ pub use { ciphertext_commitment_equality::{ CiphertextCommitmentEqualityProofContext, CiphertextCommitmentEqualityProofData, }, + fee_sigma::{FeeSigmaProofContext, FeeSigmaProofData}, grouped_ciphertext_validity::{ GroupedCiphertext2HandlesValidityProofContext, GroupedCiphertext2HandlesValidityProofData, }, @@ -64,6 +66,7 @@ pub enum ProofType { CiphertextCommitmentEquality, GroupedCiphertext2HandlesValidity, BatchedGroupedCiphertext2HandlesValidity, + FeeSigma, } pub trait ZkProofData { diff --git a/zk-token-sdk/src/zk_token_proof_instruction.rs b/zk-token-sdk/src/zk_token_proof_instruction.rs index c13fb1c58..429c3d5fb 100644 --- a/zk-token-sdk/src/zk_token_proof_instruction.rs +++ b/zk-token-sdk/src/zk_token_proof_instruction.rs @@ -305,6 +305,25 @@ pub enum ProofInstruction { /// `BatchedGroupedCiphertextValidityProofContext` /// VerifyBatchedGroupedCiphertext2HandlesValidity, + + /// Verify a fee sigma proof. + /// + /// A fee sigma proof certifies that a Pedersen commitment that encodes a transfer fee for SPL + /// Token 2022 is well-formed. + /// + /// Accounts expected by this instruction: + /// + /// * Creating a proof context account + /// 0. `[writable]` The proof context account + /// 1. `[]` The proof context account owner + /// + /// * Otherwise + /// None + /// + /// Data expected by this instruction: + /// `FeeSigmaProofData` + /// + VerifyFeeSigma, } /// Pubkeys associated with a context state account to be used as parameters to functions.