penumbra_shielded_pool/output/
proof.rsuse base64::prelude::*;
use std::str::FromStr;
use anyhow::Result;
use ark_groth16::r1cs_to_qap::LibsnarkReduction;
use ark_r1cs_std::uint8::UInt8;
use ark_serialize::{CanonicalDeserialize, CanonicalSerialize};
use decaf377::{Bls12_377, Fq, Fr};
use decaf377_fmd as fmd;
use decaf377_ka as ka;
use ark_ff::ToConstraintField;
use ark_groth16::{Groth16, PreparedVerifyingKey, Proof, ProvingKey};
use ark_r1cs_std::prelude::*;
use ark_relations::r1cs::{ConstraintSynthesizer, ConstraintSystemRef};
use ark_snark::SNARK;
use penumbra_keys::{keys::Diversifier, Address};
use penumbra_proto::{penumbra::core::component::shielded_pool::v1 as pb, DomainType};
use penumbra_tct::r1cs::StateCommitmentVar;
use crate::{note, Note, Rseed};
use penumbra_asset::{
balance,
balance::{commitment::BalanceCommitmentVar, BalanceVar},
Value,
};
use penumbra_proof_params::{DummyWitness, VerifyingKeyExt, GROTH16_PROOF_LENGTH_BYTES};
#[derive(Clone, Debug)]
pub struct OutputProofPublic {
pub balance_commitment: balance::Commitment,
pub note_commitment: note::StateCommitment,
}
#[derive(Clone, Debug)]
pub struct OutputProofPrivate {
pub note: Note,
pub balance_blinding: Fr,
}
#[cfg(test)]
fn check_satisfaction(public: &OutputProofPublic, private: &OutputProofPrivate) -> Result<()> {
use penumbra_asset::Balance;
if private.note.diversified_generator() == decaf377::Element::default() {
anyhow::bail!("diversified generator is identity");
}
let balance_commitment =
(-Balance::from(private.note.value())).commit(private.balance_blinding);
if balance_commitment != public.balance_commitment {
anyhow::bail!("balance commitment did not match public input");
}
let note_commitment = private.note.commit();
if note_commitment != public.note_commitment {
anyhow::bail!("note commitment did not match public input");
}
Ok(())
}
#[cfg(test)]
fn check_circuit_satisfaction(
public: OutputProofPublic,
private: OutputProofPrivate,
) -> Result<()> {
use ark_relations::r1cs::{self, ConstraintSystem};
let cs = ConstraintSystem::new_ref();
let circuit = OutputCircuit { public, private };
cs.set_optimization_goal(r1cs::OptimizationGoal::Constraints);
circuit
.generate_constraints(cs.clone())
.expect("can generate constraints from circuit");
cs.finalize();
if !cs.is_satisfied()? {
anyhow::bail!("constraints are not satisfied");
}
Ok(())
}
#[derive(Clone, Debug)]
pub struct OutputCircuit {
public: OutputProofPublic,
private: OutputProofPrivate,
}
impl OutputCircuit {
fn new(public: OutputProofPublic, private: OutputProofPrivate) -> Self {
Self { public, private }
}
}
impl ConstraintSynthesizer<Fq> for OutputCircuit {
fn generate_constraints(self, cs: ConstraintSystemRef<Fq>) -> ark_relations::r1cs::Result<()> {
let note_var = note::NoteVar::new_witness(cs.clone(), || Ok(self.private.note.clone()))?;
let balance_blinding_arr: [u8; 32] = self.private.balance_blinding.to_bytes();
let balance_blinding_vars = UInt8::new_witness_vec(cs.clone(), &balance_blinding_arr)?;
let claimed_note_commitment =
StateCommitmentVar::new_input(cs.clone(), || Ok(self.public.note_commitment))?;
let claimed_balance_commitment =
BalanceCommitmentVar::new_input(cs.clone(), || Ok(self.public.balance_commitment))?;
let balance_commitment =
BalanceVar::from_negative_value_var(note_var.value()).commit(balance_blinding_vars)?;
balance_commitment.enforce_equal(&claimed_balance_commitment)?;
let note_commitment = note_var.commit()?;
note_commitment.enforce_equal(&claimed_note_commitment)?;
Ok(())
}
}
impl DummyWitness for OutputCircuit {
fn with_dummy_witness() -> Self {
let diversifier_bytes = [1u8; 16];
let pk_d_bytes = decaf377::Element::GENERATOR.vartime_compress().0;
let clue_key_bytes = [1; 32];
let diversifier = Diversifier(diversifier_bytes);
let address = Address::from_components(
diversifier,
ka::Public(pk_d_bytes),
fmd::ClueKey(clue_key_bytes),
)
.expect("generated 1 address");
let note = Note::from_parts(
address,
Value::from_str("1upenumbra").expect("valid value"),
Rseed([1u8; 32]),
)
.expect("can make a note");
let balance_blinding = Fr::from(1u64);
let public = OutputProofPublic {
note_commitment: note.commit(),
balance_commitment: balance::Commitment(decaf377::Element::GENERATOR),
};
let private = OutputProofPrivate {
note,
balance_blinding,
};
OutputCircuit { public, private }
}
}
#[derive(Clone, Debug)]
pub struct OutputProof([u8; GROTH16_PROOF_LENGTH_BYTES]);
impl OutputProof {
#![allow(clippy::too_many_arguments)]
pub fn prove(
blinding_r: Fq,
blinding_s: Fq,
pk: &ProvingKey<Bls12_377>,
public: OutputProofPublic,
private: OutputProofPrivate,
) -> anyhow::Result<Self> {
let circuit = OutputCircuit::new(public, private);
let proof = Groth16::<Bls12_377, LibsnarkReduction>::create_proof_with_reduction(
circuit, pk, blinding_r, blinding_s,
)
.map_err(|err| anyhow::anyhow!(err))?;
let mut proof_bytes = [0u8; GROTH16_PROOF_LENGTH_BYTES];
Proof::serialize_compressed(&proof, &mut proof_bytes[..]).expect("can serialize Proof");
Ok(Self(proof_bytes))
}
#[tracing::instrument(level="debug", skip(self, vk), fields(self = ?BASE64_STANDARD.encode(self.clone().encode_to_vec()), vk = ?vk.debug_id()))]
pub fn verify(
&self,
vk: &PreparedVerifyingKey<Bls12_377>,
public: OutputProofPublic,
) -> anyhow::Result<()> {
let proof =
Proof::deserialize_compressed_unchecked(&self.0[..]).map_err(|e| anyhow::anyhow!(e))?;
let mut public_inputs = Vec::new();
public_inputs.extend(
public
.note_commitment
.0
.to_field_elements()
.ok_or_else(|| anyhow::anyhow!("note commitment is not a valid field element"))?,
);
public_inputs.extend(
public
.balance_commitment
.0
.to_field_elements()
.ok_or_else(|| {
anyhow::anyhow!("balance commitment is not a valid field element")
})?,
);
tracing::trace!(?public_inputs);
let start = std::time::Instant::now();
let proof_result = Groth16::<Bls12_377, LibsnarkReduction>::verify_with_processed_vk(
vk,
public_inputs.as_slice(),
&proof,
)
.map_err(|err| anyhow::anyhow!(err))?;
tracing::debug!(?proof_result, elapsed = ?start.elapsed());
proof_result
.then_some(())
.ok_or_else(|| anyhow::anyhow!("output proof did not verify"))
}
}
impl DomainType for OutputProof {
type Proto = pb::ZkOutputProof;
}
impl From<OutputProof> for pb::ZkOutputProof {
fn from(proof: OutputProof) -> Self {
pb::ZkOutputProof {
inner: proof.0.to_vec(),
}
}
}
impl TryFrom<pb::ZkOutputProof> for OutputProof {
type Error = anyhow::Error;
fn try_from(proto: pb::ZkOutputProof) -> Result<Self, Self::Error> {
Ok(OutputProof(proto.inner[..].try_into()?))
}
}
#[cfg(test)]
mod tests {
use super::*;
use decaf377::{Fq, Fr};
use penumbra_asset::{asset, Balance, Value};
use penumbra_keys::keys::{Bip44Path, SeedPhrase, SpendKey};
use penumbra_num::Amount;
use proptest::prelude::*;
use crate::{note, Note};
fn fq_strategy() -> BoxedStrategy<Fq> {
any::<[u8; 32]>()
.prop_map(|bytes| Fq::from_le_bytes_mod_order(&bytes[..]))
.boxed()
}
fn fr_strategy() -> BoxedStrategy<Fr> {
any::<[u8; 32]>()
.prop_map(|bytes| Fr::from_le_bytes_mod_order(&bytes[..]))
.boxed()
}
prop_compose! {
fn arb_valid_output_statement()(seed_phrase_randomness in any::<[u8; 32]>(), rseed_randomness in any::<[u8; 32]>(), amount in any::<u64>(), balance_blinding in fr_strategy(), asset_id64 in any::<u64>(), address_index in any::<u32>()) -> (OutputProofPublic, OutputProofPrivate) {
let seed_phrase = SeedPhrase::from_randomness(&seed_phrase_randomness);
let sk_recipient = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
let fvk_recipient = sk_recipient.full_viewing_key();
let ivk_recipient = fvk_recipient.incoming();
let (dest, _dtk_d) = ivk_recipient.payment_address(address_index.into());
let value_to_send = Value {
amount: Amount::from(amount),
asset_id: asset::Id(Fq::from(asset_id64)),
};
let note = Note::from_parts(
dest,
value_to_send,
Rseed(rseed_randomness),
).expect("should be able to create note");
let note_commitment = note.commit();
let balance_commitment = (-Balance::from(value_to_send)).commit(balance_blinding);
let public = OutputProofPublic { balance_commitment, note_commitment };
let private = OutputProofPrivate { note, balance_blinding};
(public, private)
}
}
proptest! {
#[test]
fn output_proof_happy_path((public, private) in arb_valid_output_statement()) {
assert!(check_satisfaction(&public, &private).is_ok());
assert!(check_circuit_satisfaction(public, private).is_ok());
}
}
prop_compose! {
fn arb_invalid_output_note_commitment_integrity()(seed_phrase_randomness in any::<[u8; 32]>(), rseed_randomness in any::<[u8; 32]>(), amount in any::<u64>(), balance_blinding in fr_strategy(), asset_id64 in any::<u64>(), address_index in any::<u32>(), incorrect_note_blinding in fq_strategy()) -> (OutputProofPublic, OutputProofPrivate) {
let seed_phrase = SeedPhrase::from_randomness(&seed_phrase_randomness);
let sk_recipient = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
let fvk_recipient = sk_recipient.full_viewing_key();
let ivk_recipient = fvk_recipient.incoming();
let (dest, _dtk_d) = ivk_recipient.payment_address(address_index.into());
let value_to_send = Value {
amount: Amount::from(amount),
asset_id: asset::Id(Fq::from(asset_id64)),
};
let note = Note::from_parts(
dest,
value_to_send,
Rseed(rseed_randomness),
).expect("should be able to create note");
let balance_commitment = (-Balance::from(value_to_send)).commit(balance_blinding);
let incorrect_note_commitment = note::commitment(
incorrect_note_blinding,
value_to_send,
note.diversified_generator(),
note.transmission_key_s(),
note.clue_key(),
);
let bad_public = OutputProofPublic { balance_commitment, note_commitment: incorrect_note_commitment };
let private = OutputProofPrivate { note, balance_blinding};
(bad_public, private)
}
}
proptest! {
#[test]
fn output_proof_verification_fails_note_commitment_integrity((public, private) in arb_invalid_output_note_commitment_integrity()) {
assert!(check_satisfaction(&public, &private).is_err());
assert!(check_circuit_satisfaction(public, private).is_err());
}
}
prop_compose! {
fn arb_invalid_output_balance_commitment_integrity()(seed_phrase_randomness in any::<[u8; 32]>(), rseed_randomness in any::<[u8; 32]>(), amount in any::<u64>(), balance_blinding in fr_strategy(), asset_id64 in any::<u64>(), address_index in any::<u32>(), incorrect_v_blinding in fr_strategy()) -> (OutputProofPublic, OutputProofPrivate) {
let seed_phrase = SeedPhrase::from_randomness(&seed_phrase_randomness);
let sk_recipient = SpendKey::from_seed_phrase_bip44(seed_phrase, &Bip44Path::new(0));
let fvk_recipient = sk_recipient.full_viewing_key();
let ivk_recipient = fvk_recipient.incoming();
let (dest, _dtk_d) = ivk_recipient.payment_address(address_index.into());
let value_to_send = Value {
amount: Amount::from(amount),
asset_id: asset::Id(Fq::from(asset_id64)),
};
let note = Note::from_parts(
dest,
value_to_send,
Rseed(rseed_randomness),
).expect("should be able to create note");
let note_commitment = note.commit();
let incorrect_balance_commitment = (-Balance::from(value_to_send)).commit(incorrect_v_blinding);
let bad_public = OutputProofPublic { balance_commitment: incorrect_balance_commitment, note_commitment };
let private = OutputProofPrivate { note, balance_blinding};
(bad_public, private)
}
}
proptest! {
#[test]
fn output_proof_verification_fails_balance_commitment_integrity((public, private) in arb_invalid_output_balance_commitment_integrity()) {
assert!(check_satisfaction(&public, &private).is_err());
assert!(check_circuit_satisfaction(public, private).is_err());
}
}
}