Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

circuit: Make TxInputNote fields public #224

Merged
merged 2 commits into from
Jun 26, 2024
Merged
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
325 changes: 155 additions & 170 deletions circuits/src/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,20 @@ use crate::sender_enc;
/// suitable for being introduced in the transfer circuit
#[derive(Debug, Clone)]
pub struct TxInputNote<const H: usize> {
pub(crate) merkle_opening: Opening<(), H>,
pub(crate) note: Note,
pub(crate) note_pk_p: JubJubAffine,
pub(crate) value: u64,
pub(crate) value_blinder: JubJubScalar,
pub(crate) nullifier: BlsScalar,
pub(crate) signature: SignatureDouble,
/// the merkle opening for the note
pub merkle_opening: Opening<(), H>,
/// the input note
pub note: Note,
/// the note-public-key prime
pub note_pk_p: JubJubAffine,
/// the value associated to the note
pub value: u64,
/// the value blinder used to obfuscate the value
pub value_blinder: JubJubScalar,
/// the nullifier used to spend the note
pub nullifier: BlsScalar,
/// the signature of the payload-hash
pub signature: SignatureDouble,
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -153,158 +160,6 @@ impl TxOutputNote {
}
}

/// Transaction gadget proving the following properties in ZK for a generic
/// `I` [`TxInputNote`] and [`OUTPUT_NOTES`] (2) [`TxOutputNote`]:
///
/// 1. Membership: every [`TxInputNote`] is included in the Merkle tree of
/// notes.
/// 2. Ownership: the sender holds the note secret key for every
/// [`TxInputNote`].
/// 3. Nullification: the nullifier is calculated correctly.
/// 4. Minting: the value commitment for every [`TxOutputNote`] is computed
/// correctly.
/// 5. Balance integrity: the sum of the values of all [`TxInputNote`] is equal
/// to the sum of the values of all [`TxOutputNote`] + the gas fee + a
/// deposit, where a deposit refers to funds being transfered to a contract.
///
/// The gadget appends the following public input values to the circuit:
/// - `root`
/// - `[nullifier; I]`
/// - `[output_value_commitment; 2]`
/// - `max_fee`
/// - `deposit`
fn nullify_gadget<const H: usize, const I: usize>(
composer: &mut Composer,
payload_hash: &Witness,
root: &BlsScalar,
tx_input_notes: &[TxInputNote<H>; I],
tx_output_notes: &[TxOutputNote; OUTPUT_NOTES],
max_fee: u64,
deposit: u64,
) -> Result<(), Error> {
let root_pi = composer.append_public(*root);

let mut tx_input_notes_sum = Composer::ZERO;

// NULLIFY ALL TX INPUT NOTES
for tx_input_note in tx_input_notes {
// APPEND THE WITNESSES TO THE CIRCUIT
let w_tx_input_note = tx_input_note.append_to_circuit(composer);

// VERIFY THE DOUBLE KEY SCHNORR SIGNATURE
gadgets::verify_signature_double(
composer,
w_tx_input_note.signature_u,
w_tx_input_note.signature_r,
w_tx_input_note.signature_r_p,
w_tx_input_note.note_pk,
w_tx_input_note.note_pk_p,
*payload_hash,
)?;

// COMPUTE AND ASSERT THE NULLIFIER
let nullifier = HashGadget::digest(
composer,
Domain::Other,
&[
*w_tx_input_note.note_pk_p.x(),
*w_tx_input_note.note_pk_p.y(),
w_tx_input_note.pos,
],
)[0];
composer.assert_equal(nullifier, w_tx_input_note.nullifier);

// PERFORM A RANGE CHECK ([0, 2^64 - 1]) ON THE VALUE OF THE NOTE
composer.component_range::<32>(w_tx_input_note.value);

// SUM UP ALL THE TX INPUT NOTE VALUES
let constraint = Constraint::new()
.left(1)
.a(tx_input_notes_sum)
.right(1)
.b(w_tx_input_note.value);
tx_input_notes_sum = composer.gate_add(constraint);

// COMMIT TO THE VALUE OF THE NOTE
let pc_1 = composer
.component_mul_generator(w_tx_input_note.value, GENERATOR)?;
let pc_2 = composer.component_mul_generator(
w_tx_input_note.value_blinder,
GENERATOR_NUMS,
)?;
let value_commitment = composer.component_add_point(pc_1, pc_2);

// COMPUTE THE NOTE HASH
let note_hash = HashGadget::digest(
composer,
Domain::Other,
&[
w_tx_input_note.note_type,
*value_commitment.x(),
*value_commitment.y(),
*w_tx_input_note.note_pk.x(),
*w_tx_input_note.note_pk.y(),
w_tx_input_note.pos,
],
)[0];

// VERIFY THE MERKLE OPENING
let root =
opening_gadget(composer, &tx_input_note.merkle_opening, note_hash);
composer.assert_equal(root, root_pi);
}

let mut tx_output_sum = Composer::ZERO;

// COMMIT TO ALL TX OUTPUT NOTES
for tx_output_note in tx_output_notes {
// APPEND THE WITNESSES TO THE CIRCUIT
let value = composer.append_witness(tx_output_note.value);
let expected_value_commitment =
composer.append_public_point(tx_output_note.value_commitment);
let value_blinder =
composer.append_witness(tx_output_note.value_blinder);

// PERFORM A RANGE CHECK ([0, 2^64 - 1]) ON THE VALUE OF THE NOTE
composer.component_range::<32>(value);

// SUM UP ALL THE TX OUTPUT NOTE VALUES
let constraint =
Constraint::new().left(1).a(tx_output_sum).right(1).b(value);
tx_output_sum = composer.gate_add(constraint);

// COMMIT TO THE VALUE OF THE NOTE
let pc_1 = composer.component_mul_generator(value, GENERATOR)?;
let pc_2 =
composer.component_mul_generator(value_blinder, GENERATOR_NUMS)?;
let computed_value_commitment =
composer.component_add_point(pc_1, pc_2);

composer.assert_equal_point(
expected_value_commitment,
computed_value_commitment,
);
}

let max_fee = composer.append_public(max_fee);
let deposit = composer.append_public(deposit);

// SUM UP THE DEPOSIT AND THE MAX FEE
let constraint = Constraint::new()
.left(1)
.a(tx_output_sum)
.right(1)
.b(max_fee)
.fourth(1)
.d(deposit);
tx_output_sum = composer.gate_add(constraint);

// VERIFY BALANCE
composer.assert_equal(tx_input_notes_sum, tx_output_sum);

Ok(())
}

/// Declaration of the transaction circuit calling the [`gadget`].
#[derive(Debug)]
pub struct TxCircuit<const H: usize, const I: usize> {
Expand Down Expand Up @@ -411,6 +266,22 @@ impl<const H: usize, const I: usize> TxCircuit<H, I> {
}

impl<const H: usize, const I: usize> Circuit for TxCircuit<H, I> {
/// Transaction gadget proving the following properties in ZK for a generic
/// `I` [`TxInputNote`] and [`OUTPUT_NOTES`] [`TxOutputNote`]:
///
/// 1. Membership: every [`TxInputNote`] is included in the Merkle tree of
/// notes.
/// 2. Ownership: the sender holds the note secret key for every
/// [`TxInputNote`].
/// 3. Nullification: the nullifier is calculated correctly.
/// 4. Minting: the value commitment for every [`TxOutputNote`] is computed
/// correctly.
/// 5. Balance integrity: the sum of the values of all [`TxInputNote`] is
/// equal to the sum of the values of all [`TxOutputNote`] + the gas fee
/// + a deposit, where a deposit refers to funds being transfered to a
/// contract.
/// 6. Sender-data: Verify that the sender was encrypted correctly.
///
/// The circuit has the following public inputs:
/// - `payload_hash`
/// - `root`
Expand All @@ -425,18 +296,132 @@ impl<const H: usize, const I: usize> Circuit for TxCircuit<H, I> {
// Make the payload hash a public input of the circuit
let payload_hash = composer.append_public(self.payload_hash);

// Nullify all the tx input notes
nullify_gadget::<H, I>(
composer,
&payload_hash,
&self.root,
&self.tx_input_notes,
&self.tx_output_notes,
self.max_fee,
self.deposit,
)?;
// Append the root as public input
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

which is the advantage of adding this here and changing the already peer-reviewed comments?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ease of work: the other file was a real pain to debug and I changed only the comments that directly refer to the steps 1 - 6 in the documentation of the function.
I also thought that it is good to have the circuit implementation be more close to the circuit outlined in the specs with clear markers where what things are checked (e.g. membership, ownership, etc)

let root_pi = composer.append_public(self.root);

let mut tx_input_notes_sum = Composer::ZERO;

// Check membership, ownership and nullification of all input notes
for tx_input_note in &self.tx_input_notes {
let w_tx_input_note = tx_input_note.append_to_circuit(composer);

// Verify: 2. Ownership
gadgets::verify_signature_double(
composer,
w_tx_input_note.signature_u,
w_tx_input_note.signature_r,
w_tx_input_note.signature_r_p,
w_tx_input_note.note_pk,
w_tx_input_note.note_pk_p,
payload_hash,
)?;

// Verify: 3. Nullification
let nullifier = HashGadget::digest(
composer,
Domain::Other,
&[
*w_tx_input_note.note_pk_p.x(),
*w_tx_input_note.note_pk_p.y(),
w_tx_input_note.pos,
],
)[0];
composer.assert_equal(nullifier, w_tx_input_note.nullifier);

// Perform a range check ([0, 2^64 - 1]) on the value of the note
composer.component_range::<32>(w_tx_input_note.value);

// Sum up all the tx input note values
let constraint = Constraint::new()
.left(1)
.a(tx_input_notes_sum)
.right(1)
.b(w_tx_input_note.value);
tx_input_notes_sum = composer.gate_add(constraint);

// Commit to the value of the note
let pc_1 = composer
.component_mul_generator(w_tx_input_note.value, GENERATOR)?;
let pc_2 = composer.component_mul_generator(
w_tx_input_note.value_blinder,
GENERATOR_NUMS,
)?;
let value_commitment = composer.component_add_point(pc_1, pc_2);

// Compute the note hash
let note_hash = HashGadget::digest(
composer,
Domain::Other,
&[
w_tx_input_note.note_type,
*value_commitment.x(),
*value_commitment.y(),
*w_tx_input_note.note_pk.x(),
*w_tx_input_note.note_pk.y(),
w_tx_input_note.pos,
],
)[0];

// Verify: 1. Membership
let root = opening_gadget(
composer,
&tx_input_note.merkle_opening,
note_hash,
);
composer.assert_equal(root, root_pi);
}

let mut tx_output_sum = Composer::ZERO;

// Commit to all tx output notes
for tx_output_note in &self.tx_output_notes {
// Append the witnesses to the circuit
let value = composer.append_witness(tx_output_note.value);
let expected_value_commitment =
composer.append_public_point(tx_output_note.value_commitment);
let value_blinder =
composer.append_witness(tx_output_note.value_blinder);

// Perform a range check ([0, 2^64 - 1]) on the value OF THE NOTE
composer.component_range::<32>(value);

// Sum up all the tx output note values
let constraint =
Constraint::new().left(1).a(tx_output_sum).right(1).b(value);
tx_output_sum = composer.gate_add(constraint);

// Commit to the value of the note
let pc_1 = composer.component_mul_generator(value, GENERATOR)?;
let pc_2 = composer
.component_mul_generator(value_blinder, GENERATOR_NUMS)?;
let computed_value_commitment =
composer.component_add_point(pc_1, pc_2);

// Verify: 4. Minting
composer.assert_equal_point(
expected_value_commitment,
computed_value_commitment,
);
}

// Append max_fee and deposit as public inputs
let max_fee = composer.append_public(self.max_fee);
let deposit = composer.append_public(self.deposit);

// Add the deposit and the max fee to the sum of the output-values
let constraint = Constraint::new()
.left(1)
.a(tx_output_sum)
.right(1)
.b(max_fee)
.fourth(1)
.d(deposit);
tx_output_sum = composer.gate_add(constraint);

// Verify: 5. Balance integrity
composer.assert_equal(tx_input_notes_sum, tx_output_sum);

// Prove correctness of the sender keys encryption
// Verify: 6. Sender-data
sender_enc::gadget(
composer,
self.sender_pk,
Expand Down