Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions circuits/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

- Reject an input `note_pk_p` with a small-order component [#313]
- Reject a sender public key whose `A` or `B` is the identity [#314]
Comment thread
HDauven marked this conversation as resolved.
- Reject an output note key equal to the identity [#314]

## [0.8.1] - 2026-06-15

Expand Down
50 changes: 42 additions & 8 deletions circuits/src/sender_enc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ pub(crate) fn gadget(
// VERIFY A SIGNATURE FOR EACH KEY 'A' AND 'B'
// The keys are prover-supplied, so their subgroup membership is
// constrained where they enter the circuit. The signature gadgets
// additionally constrain them to be non-identity.
// additionally constrain them to be non-identity, which the sender keys
// rely on.
let sender_pk_A = append_subgroup_point(composer, *sender_pk.A())?;
let sender_pk_B = append_subgroup_point(composer, *sender_pk.B())?;

Expand Down Expand Up @@ -60,13 +61,12 @@ pub(crate) fn gadget(
)?;

// ENCRYPT EACH KEY 'A' and 'B' USING EACH OUTPUT 'NPK'
// The output note-pks are public inputs. Their subgroup membership is
// constrained in-circuit rather than left to every verifier, since they
// are the ElGamal keys the sender public key is encrypted to.
let note_pk_0 = composer.append_public_point(output_npk[0])?;
let note_pk_0 = composer.assert_torsion_free_point(note_pk_0);
let note_pk_1 = composer.append_public_point(output_npk[1])?;
let note_pk_1 = composer.assert_torsion_free_point(note_pk_1);
// The output note-pks are public inputs. Their subgroup membership, and
// their difference from the identity, are constrained in-circuit rather
// than left to every verifier, since they are the ElGamal keys the sender
// public key is encrypted to.
let note_pk_0 = append_note_pk(composer, output_npk[0])?;
let note_pk_1 = append_note_pk(composer, output_npk[1])?;

let blinder_A_0 = composer.append_witness(sender_blinder[0][0]);
let blinder_B_0 = composer.append_witness(sender_blinder[0][1]);
Expand Down Expand Up @@ -134,3 +134,37 @@ fn assert_sender_enc(

Ok(())
}

/// Appends the note public key of an output as a public point, constrained to
/// the prime-order subgroup and to differ from the identity.
fn append_note_pk(
composer: &mut Composer,
note_pk: JubJubAffine,
) -> Result<TorsionFreeWitnessPoint, Error> {
let note_pk = composer.append_public_point(note_pk)?;
let note_pk = composer.assert_torsion_free_point(note_pk);
assert_non_identity(composer, note_pk);
Ok(note_pk)
}

/// Constrains a point of the prime-order subgroup to differ from the
/// identity, in one gate, as the signature gadgets of jubjub-schnorr do.
///
/// In the subgroup only the identity has `x = 0`, so proving that `x` has an
/// inverse excludes it without branching on the witness.
fn assert_non_identity(
composer: &mut Composer,
point: TorsionFreeWitnessPoint,
) {
let x = *point.x();
let x_inverse = composer[x].invert().unwrap_or(BlsScalar::zero());
let x_inverse = composer.append_witness(x_inverse);

composer.append_gate(
Constraint::new()
.mult(1)
.a(x)
.b(x_inverse)
.constant(-BlsScalar::one()),
);
}
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
172 changes: 172 additions & 0 deletions circuits/tests/identity_keys.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) DUSK NETWORK. All rights reserved.

//! Regressions for identity keys in the transaction circuit.
Comment thread
HDauven marked this conversation as resolved.
//!
//! The signature gadgets constrain both keys of the sender to differ from the
//! identity, and the circuit constrains the note keys of the outputs, which
//! the sender is encrypted to, likewise. A transaction with any of these keys
//! set to the identity is unsatisfiable. The tests give it signatures and
//! encryptions that satisfy every other equation, so that only this
//! constraint can fail.

#![cfg(feature = "plonk")]
#![allow(non_snake_case)]

use std::sync::LazyLock;

use dusk_bytes::Serializable;
use dusk_jubjub::{
GENERATOR_EXTENDED, JubJubAffine, JubJubExtended, JubJubScalar,
};
use dusk_plonk::prelude::{
Compiler, Error as PlonkError, Prover, PublicParameters, Verifier,
};
use ff::Field;
use jubjub_schnorr::Signature as SchnorrSignature;
use phoenix_circuits::TxCircuit;
use phoenix_core::{PublicKey, value_commitment};
use rand::SeedableRng;
use rand::rngs::StdRng;

mod common;
use common::*;

/// The 1-input circuit, compiled with a test SRS. Soundness does not depend
/// on the SRS.
static CIRCUIT: LazyLock<(Prover, Verifier)> = LazyLock::new(|| {
let pp =
PublicParameters::setup(1 << 16, &mut StdRng::seed_from_u64(0x0122))
.expect("failed to generate the test SRS");
Compiler::compile::<TxCircuit<HEIGHT, 1>>(&pp, LABEL)
.expect("failed to compile circuit")
});

/// A signature that verifies for the identity key under any challenge.
fn identity_key_signature(rng: &mut StdRng) -> SchnorrSignature {
let u = JubJubScalar::random(&mut *rng);
let R = JubJubAffine::from(GENERATOR_EXTENDED * u);

let mut bytes = [0u8; SchnorrSignature::SIZE];
bytes[..32].copy_from_slice(&u.to_bytes());
bytes[32..].copy_from_slice(&R.to_bytes());
SchnorrSignature::from_bytes(&bytes).expect("R is a prime-order point")
}

/// Output note information encrypting `sender_pk` to `note_pk`, the
/// encryption computed as the circuit computes it.
fn output_note(
rng: &mut StdRng,
value: u64,
note_pk: JubJubAffine,
sender_pk: &PublicKey,
sender_blinder: [JubJubScalar; 2],
) -> OutputNoteInfo {
let value_blinder = JubJubScalar::random(&mut *rng);
let encrypt = |key: &JubJubExtended, blinder: &JubJubScalar| {
let c1 = GENERATOR_EXTENDED * blinder;
let c2 = key + JubJubExtended::from(note_pk) * blinder;
(c1.into(), c2.into())
};

OutputNoteInfo {
value,
value_commitment: value_commitment(value, value_blinder),
value_blinder,
note_pk,
sender_enc: [
encrypt(sender_pk.A(), &sender_blinder[0]),
encrypt(sender_pk.B(), &sender_blinder[1]),
],
sender_blinder,
}
}

/// Proves and verifies the 1-input transaction spending the first test note,
/// with the given sender key, signatures and output note keys.
fn prove(
sender_pk: PublicKey,
signatures: (SchnorrSignature, SchnorrSignature),
output_npk: [JubJubAffine; 2],
) -> Result<(), PlonkError> {
let mut rng = StdRng::seed_from_u64(0x0122);
let (prover, verifier) = &*CIRCUIT;

let output_notes_info = [
output_note(
&mut rng,
10,
output_npk[0],
&sender_pk,
TP.sender_blinder[0],
),
output_note(
&mut rng,
5,
output_npk[1],
&sender_pk,
TP.sender_blinder[1],
),
];
let circuit = TxCircuit {
input_notes_info: [TP.input_notes_info[0].clone()],
output_notes_info,
payload_hash: TP.payload_hash,
root: TP.root,
deposit: TP.deposit,
max_fee: TP.max_fee,
sender_pk,
signatures,
};
prover
.prove(&mut rng, &circuit)
.and_then(|(proof, pi)| verifier.verify(&proof, &pi))
}

#[test]
fn identity_sender_key_rejected() {
// The same construction proves for an honest sender key.
prove(TP.sender_pk, TP.signatures, TP.output_npk)
.expect("the honest sender proves");

let identity = JubJubExtended::identity();
let (A, B) = (*TP.sender_pk.A(), *TP.sender_pk.B());
let (sig_A, sig_B) = TP.signatures;

let sig_identity = identity_key_signature(&mut StdRng::seed_from_u64(0xa));
let err = prove(
PublicKey::new(identity, B),
(sig_identity, sig_B),
TP.output_npk,
)
.expect_err("an identity sender key A must be unsatisfiable");
assert_eq!(err, PlonkError::CircuitUnsatisfied);

let sig_identity = identity_key_signature(&mut StdRng::seed_from_u64(0xb));
let err = prove(
PublicKey::new(A, identity),
(sig_A, sig_identity),
TP.output_npk,
)
.expect_err("an identity sender key B must be unsatisfiable");
assert_eq!(err, PlonkError::CircuitUnsatisfied);
}

#[test]
fn identity_output_note_key_rejected() {
// The same construction proves for honest output note keys.
prove(TP.sender_pk, TP.signatures, TP.output_npk)
.expect("the honest transaction proves");

let identity = JubJubAffine::identity();
let [npk_0, npk_1] = TP.output_npk;

for output_npk in [[identity, npk_1], [npk_0, identity]] {
let err = prove(TP.sender_pk, TP.signatures, output_npk)
.expect_err("an identity output note key must be unsatisfiable");
assert_eq!(err, PlonkError::CircuitUnsatisfied);
}
}
8 changes: 4 additions & 4 deletions circuits/tests/verifier_data.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,16 @@ const DEPLOYED_VD_4_2: &[u8] = include_bytes!(
);

const VD_1_2: &[u8] = include_bytes!(
"assets/bf184dc7e0569daef4e74bc6b681e5651b0d5a054b9cfc656d91b83d54343f71.vd"
"assets/d2d0578d0167adadf3025e5d8cc6f41e73a68d47e9ad8aa8fcddc7ee8beaab9e.vd"
);
const VD_2_2: &[u8] = include_bytes!(
"assets/456f644ee101e40eea33af55ad915f4e3c4fd04572da1b00cda57a09050080c3.vd"
"assets/cc2bffe1693b1c1c01d6cacd586bff0553a45cb11065c4ef0821581ec376d297.vd"
);
const VD_3_2: &[u8] = include_bytes!(
"assets/63cacc7adb37b19b6d10066e2d01ac2939422d9496d27f03dbc875b5458a36a5.vd"
"assets/7143e6a20dc8392f8fd0aebed4f72d407301aa7e7bd1d72ab9b852efbfbd2a07.vd"
);
const VD_4_2: &[u8] = include_bytes!(
"assets/929b3a0024cd60e3d56deb039e8a112e78caa3f23060193f21f533ad0557b8e2.vd"
"assets/7d39e8166eecde1a7f7ceb5ea268fddd282397e7c0e3d39c17d4fbb225a9f26c.vd"
);

/// Compiles the `I`-input circuit to `vd`, and proves a transaction whose
Expand Down