From 59b92ca0f72ca3705dd6933b304897c91edc81c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Bene=C5=A1?= Date: Wed, 10 Jul 2024 22:23:59 +0200 Subject: [PATCH] fix: using different generators in private refund (#7414) Fixes #7320 --- .../aztec-nr/aztec/src/generators.nr | 20 ++++ noir-projects/aztec-nr/aztec/src/lib.nr | 1 + .../src/types/token_note.nr | 100 +++++++++--------- 3 files changed, 73 insertions(+), 48 deletions(-) create mode 100644 noir-projects/aztec-nr/aztec/src/generators.nr diff --git a/noir-projects/aztec-nr/aztec/src/generators.nr b/noir-projects/aztec-nr/aztec/src/generators.nr new file mode 100644 index 00000000000..02eae3307fb --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/generators.nr @@ -0,0 +1,20 @@ +use dep::protocol_types::point::Point; + +// A set of generators generated with `derive_generators(...)` function from noir::std +global Ga1 = Point { x: 0x30426e64aee30e998c13c8ceecda3a77807dbead52bc2f3bf0eae851b4b710c1, y: 0x113156a068f603023240c96b4da5474667db3b8711c521c748212a15bc034ea6, is_infinite: false }; +global Ga2 = Point { x: 0x2825c79cc6a5cbbeef7d6a8f1b6a12b312aa338440aefeb4396148c89147c049, y: 0x129bfd1da54b7062d6b544e7e36b90736350f6fba01228c41c72099509f5701e, is_infinite: false }; +global Ga3 = Point { x: 0x0edb1e293c3ce91bfc04e3ceaa50d2c541fa9d091c72eb403efb1cfa2cb3357f, y: 0x1341d675fa030ece3113ad53ca34fd13b19b6e9762046734f414824c4d6ade35, is_infinite: false }; + +mod test { + use crate::generators::{Ga1, Ga2, Ga3}; + use dep::protocol_types::point::Point; + use std::hash::derive_generators; + + #[test] +fn test_generators() { + let generators: [Point; 3] = derive_generators("aztec_nr_generators".as_bytes(), 0); + assert_eq(generators[0], Ga1); + assert_eq(generators[1], Ga2); + assert_eq(generators[2], Ga3); + } +} diff --git a/noir-projects/aztec-nr/aztec/src/lib.nr b/noir-projects/aztec-nr/aztec/src/lib.nr index d4b1abf35e7..af7f03c1f51 100644 --- a/noir-projects/aztec-nr/aztec/src/lib.nr +++ b/noir-projects/aztec-nr/aztec/src/lib.nr @@ -1,5 +1,6 @@ mod context; mod deploy; +mod generators; mod hash; mod history; mod initializer; diff --git a/noir-projects/noir-contracts/contracts/private_token_contract/src/types/token_note.nr b/noir-projects/noir-contracts/contracts/private_token_contract/src/types/token_note.nr index 30ef7add279..99c61356e14 100644 --- a/noir-projects/noir-contracts/contracts/private_token_contract/src/types/token_note.nr +++ b/noir-projects/noir-contracts/contracts/private_token_contract/src/types/token_note.nr @@ -2,7 +2,8 @@ use dep::aztec::{ prelude::{AztecAddress, NoteHeader, NoteInterface, PrivateContext}, protocol_types::{constants::GENERATOR_INDEX__NOTE_NULLIFIER, point::Point, scalar::Scalar, hash::poseidon2_hash}, note::utils::compute_note_hash_for_consumption, oracle::unsafe_rand::unsafe_rand, - keys::getters::get_nsk_app, note::note_getter_options::PropertySelector + keys::getters::get_nsk_app, note::note_getter_options::PropertySelector, + generators::{Ga1 as G_amt, Ga2 as G_npk, Ga3 as G_rnd} }; use dep::std::field::bn254::decompose; use dep::std::embedded_curve_ops::multi_scalar_mul; @@ -32,8 +33,6 @@ trait PrivatelyRefundable { global TOKEN_NOTE_LEN: Field = 3; // 3 plus a header. global TOKEN_NOTE_BYTES_LEN: Field = 3 * 32 + 64; -// Grumpkin generator point. -global G1 = Point { x: 1, y: 17631683881184975370165255887551781615748388533673675138860, is_infinite: false }; #[aztec(note)] struct TokenNote { @@ -75,11 +74,11 @@ impl NoteInterface for TokenNote { fn compute_note_content_hash(self) -> Field { let (npk_lo, npk_hi) = decompose(self.npk_m_hash); let (random_lo, random_hi) = decompose(self.randomness); - // We compute the note content hash as an x-coordinate of `G ^ (amount + npk_m_hash + randomness)` instead + // We compute the note content hash as an x-coordinate of `G_amt * amount + G_npk * npk_m_hash + G_rnd * randomness` instead // of using pedersen or poseidon2 because it allows us to privately add and subtract from amount in public // by leveraging homomorphism. multi_scalar_mul( - [G1, G1, G1], + [G_amt, G_npk, G_rnd], [Scalar { lo: self.amount.to_integer(), hi: 0 @@ -126,56 +125,58 @@ impl OwnedNote for TokenNote { * these are going to be eventually turned into notes: * one for the user, and one for the fee payer. * - * So you can think of these (x,y) points as "partial notes": they encode part of the internals of the notes. + * So you can think of these (x, y) points as "partial notes": they encode part of the internals of the notes. * * This is because the compute_note_content_hash function above defines the content hash to be * the x-coordinate of a point defined as: * - * amount * G + npk * G + randomness * G - * = (amount + npk + randomness) * G + * G_amt * amount + G_npk * npk_m_hash + G_rnd * randomness * - * where G is a generator point. Interesting point here is that we actually need to convert + * where G_amt, G_npk and G_rnd are generator points. Interesting point here is that we actually need to convert * - amount - * - npk + * - npk_m_hash * - randomness * from grumpkin Field elements * (which have a modulus of 21888242871839275222246405745257275088548364400416034343698204186575808495617) * into a grumpkin scalar * (which have a modulus of 21888242871839275222246405745257275088696311157297823662689037894645226208583) * - * The intuition for this is that the Field elements define the domain of the x,y coordinates for points on the curves, - * but the number of points on the curve is actually greater than the size of that domain. + * The intuition for this is that the Field elements define the domain of the x, y coordinates for points on + * the curves, but the number of points on the curve is actually greater than the size of that domain. * - * (Consider, e.g. if the curve were defined over a field of 10 elements, and each x coord had two corresponding y for +/-) + * (Consider, e.g. if the curve were defined over a field of 10 elements, and each x coord had two corresponding + * y for +/-) * * For a bit more info, see * https://hackmd.io/@aztec-network/ByzgNxBfd#2-Grumpkin---A-curve-on-top-of-BN-254-for-SNARK-efficient-group-operations * * - * Anyway, if we have a secret scalar n := amount + npk + randomness, and then we reveal a point n * G, there is no efficient way to - * deduce what n is. This is the discrete log problem. + * Anyway, if we have a secret scalar s, and then we reveal a point s * G (G being a generator), there is no efficient + * way to deduce what s is. This is the discrete log problem. * * However we can still perform addition/subtraction on points! That is why we generate those two points, which are: - * incomplete_fee_payer_point := (fee_payer_npk + fee_payer_randomness) * G - * incomplete_user_point := (user_npk + funded_amount + user_randomness) * G + * incomplete_fee_payer_point := G_npk * fee_payer_npk + G_amt * fee_payer_randomness + * incomplete_user_point := G_npk * user_npk + G_amt * funded_amount + G_rnd * user_randomness * - * where `funded_amount` is the total amount in tokens that the sponsored user initially supplied, from which the transaction fee will be subtracted. + * where `funded_amount` is the total amount in tokens that the sponsored user initially supplied, from which + * the transaction fee will be subtracted. * - * So we pass those points into the teardown function (here) and compute a third point corresponding to the transaction fee as just + * So we pass those points into the teardown function (here) and compute a third point corresponding to the transaction + * fee as just: * - * fee_point := transaction_fee * G + * fee_point := G_amt * transaction_fee * * Then we arrive at the final points via addition/subtraction of that transaction fee point: * - * fee_payer_point := incomplete_fee_payer_point + fee_point - * = (fee_payer_npk + fee_payer_randomness) * G + transaction_fee * G - * = (fee_payer_npk + fee_payer_randomness + transaction_fee) * G + * fee_payer_point := incomplete_fee_payer_point + fee_point = + * = (G_npk * fee_payer_npk + G_rnd * fee_payer_randomness) + G_amt * transaction_fee = + * = G_amt * transaction_fee + G_npk * fee_payer_npk + G_rnd * fee_payer_randomness * - * user_point := incomplete_user_point - fee_point - * = (user_npk + funded_amount + user_randomness) * G - transaction_fee * G - * = (user_npk + user_randomness + (funded_amount - transaction_fee)) * G + * user_point := incomplete_user_point - fee_point = + * = (G_amt * funded_amount + G_npk * user_npk + G_rnd + user_randomness) - G_amt * transaction_fee = + * = G_amt * (funded_amount - transaction_fee) + G_npk * user_npk + G_rnd + user_randomness * - * When we return the x-coordinate of those points, it identically matches the note_content_hash of (and therefore *is*) notes like: + * The x-coordinate of points above identically matches the note_content_hash of (and therefore *is*) notes like: * { * amount: (funded_amount - transaction_fee), * npk_m_hash: user_npk, @@ -184,26 +185,29 @@ impl OwnedNote for TokenNote { * * Why do we need different randomness for the user and the fee payer notes? * --> This is because if the randomness values were the same we could fingerprint the user by doing the following: - * 1) randomness_influence = incomplete_fee_payer_point - G * fee_payer_npk = - * = (fee_payer_npk + randomness) * G - G * fee_payer_npk = randomness * G - * 2) user_fingerprint = incomplete_user_point - G * funded_amount - randomness_influence = - * = (user_npk + funded_amount + randomness) * G - funded_amount * G - randomness * G = - * = user_npk * G + * 1) randomness_influence = incomplete_fee_payer_point - G_npk * fee_payer_npk = + * = (G_npk * fee_payer_npk + G_rnd * randomness) - G_npk * fee_payer_npk = + * = G_rnd * randomness + * 2) user_fingerprint = incomplete_user_point - G_amt * funded_amount - randomness_influence = + * = (G_npk * user_npk + G_amt * funded_amount + G_rnd * randomness) - G_amt * funded_amount + * - G_rnd * randomness = + * = G_npk * user_npk * 3) Then the second time the user would use this fee paying contract we would recover the same fingerprint and * link that the 2 transactions were made by the same user. Given that it's expected that only a limited set - * of fee paying contracts will be used and they will be known searching for fingerprints by trying different + * of fee paying contracts will be used and they will be known, searching for fingerprints by trying different * fee payer npk values of these known contracts is a feasible attack. */ impl PrivatelyRefundable for TokenNote { fn generate_refund_points(fee_payer_npk_m_hash: Field, user_npk_m_hash: Field, funded_amount: Field, user_randomness: Field, fee_payer_randomness: Field) -> (Point, Point) { - // 1. To be able to multiply generators with randomness and npk_m_hash using barretneberg's (BB) blackbox function we - // first need to convert the fields to high and low limbs. + // 1. To be able to multiply generators with randomness and npk_m_hash using barretneberg's (BB) blackbox + // function we first need to convert the fields to high and low limbs. let (fee_payer_randomness_lo, fee_payer_randomness_hi) = decompose(fee_payer_randomness); let (fee_payer_npk_m_hash_lo, fee_payer_npk_m_hash_hi) = decompose(fee_payer_npk_m_hash); - // 2. Now that we have correct representationsn of fee payer and randomness we can compute `G ^ (fee_payer_npk + randomness)` + // 2. Now that we have correct representationsn of fee payer and randomness we can compute + // `G_npk * fee_payer_npk + G_rnd * randomness`. let incomplete_fee_payer_point = multi_scalar_mul( - [G1, G1], + [G_npk, G_rnd], [Scalar { lo: fee_payer_npk_m_hash_lo, hi: fee_payer_npk_m_hash_hi @@ -216,28 +220,28 @@ impl PrivatelyRefundable for TokenNote { // 3. We do the necessary conversion for values relevant for the sponsored user point. let (user_randomness_lo, user_randomness_hi) = decompose(user_randomness); - // TODO(#7324): representing user with their npk_m_hash here does not work with key rotation - let (user_lo, user_hi) = decompose(user_npk_m_hash); + // TODO(#7324), TODO(#7323): using npk_m_hash here is vulnerable in 2 ways described in the linked issues. + let (user_npk_lo, user_npk_hi) = decompose(user_npk_m_hash); let (funded_amount_lo, funded_amount_hi) = decompose(funded_amount); - // 4. We compute `G ^ (user_npk_m_hash + funded_amount + randomness)` + // 4. We compute `G_amt * funded_amount + G_npk * user_npk_m_hash + G_rnd * randomness`. let incomplete_user_point = multi_scalar_mul( - [G1, G1, G1], + [G_amt, G_npk, G_rnd], [Scalar { - lo: user_lo, - hi: user_hi - }, - Scalar { lo: funded_amount_lo, hi: funded_amount_hi }, + Scalar { + lo: user_npk_lo, + hi: user_npk_hi + }, Scalar { lo: user_randomness_lo, hi: user_randomness_hi }] ); - // 5. At last we represent the points as Points and return them. + // 5. At last we return the points. (incomplete_fee_payer_point, incomplete_user_point) } @@ -245,9 +249,9 @@ impl PrivatelyRefundable for TokenNote { // 1. We convert the transaction fee to high and low limbs to be able to use BB API. let (transaction_fee_lo, transaction_fee_hi) = decompose(transaction_fee); - // 2. We compute the fee point as `G ^ transaction_fee` + // 2. We compute the fee point as `G_amt * transaction_fee` let fee_point = multi_scalar_mul( - [G1], + [G_amt], [Scalar { lo: transaction_fee_lo, hi: transaction_fee_hi,