[zk-token-sdk] Add fee sigma proof verification instruction (#32291)

* add fee sigma proof verification instruction

* update proof program processor for fee sigma proof

* update bench for fee sigma proof

* cargo fmt

* clippy

* fix target arch

* add comments to describe fee, delta, and claimed instruction data field
This commit is contained in:
samkim-crypto 2023-06-28 09:49:56 +09:00 committed by GitHub
parent 87c1b67d53
commit 4bc8f90182
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 366 additions and 3 deletions

2
Cargo.lock generated
View File

@ -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",

View File

@ -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 }

View File

@ -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::<ProofContextState<FeeSigmaProofContext>>(),
&success_proof_data,
&fail_proof_data,
)
.await;
test_close_context_state(
ProofInstruction::VerifyFeeSigma,
size_of::<ProofContextState<FeeSigmaProofContext>>(),
&success_proof_data,
)
.await;
}
async fn test_verify_proof_without_context<T, U>(
proof_instruction: ProofInstruction,
success_proof_data: &T,

View File

@ -19,6 +19,7 @@ solana-zk-token-sdk = { workspace = true }
[dev-dependencies]
criterion = { workspace = true }
curve25519-dalek = { workspace = true }
[[bench]]
name = "verify_proofs"

View File

@ -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);

View File

@ -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::<FeeSigmaProofData, FeeSigmaProofContext>(invoke_context)
}
}
});

View File

@ -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<Self, ProofError> {
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<FeeSigmaProofContext> 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());
}
}

View File

@ -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<T: Pod> {

View File

@ -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.