diff --git a/noir-projects/aztec-nr/aztec/src/note/utils.nr b/noir-projects/aztec-nr/aztec/src/note/utils.nr index 59843201ffb4..23f58f8906cf 100644 --- a/noir-projects/aztec-nr/aztec/src/note/utils.nr +++ b/noir-projects/aztec-nr/aztec/src/note/utils.nr @@ -9,14 +9,18 @@ use dep::protocol_types::{ utils::arr_copy_slice }; +pub fn compute_inner_note_hash_from_preimage(storage_slot: Field, note_content_hash: Field) -> Field { + pedersen_hash( + [storage_slot, note_content_hash], + GENERATOR_INDEX__INNER_NOTE_HASH + ) +} + fn compute_inner_note_hash(note: Note) -> Field where Note: NoteInterface { let header = note.get_header(); let note_hash = note.compute_note_content_hash(); - pedersen_hash( - [header.storage_slot, note_hash], - GENERATOR_INDEX__INNER_NOTE_HASH - ) + compute_inner_note_hash_from_preimage(header.storage_slot, note_hash) } pub fn compute_siloed_nullifier( diff --git a/noir-projects/noir-contracts/contracts/private_fpc_contract/src/lib.nr b/noir-projects/noir-contracts/contracts/private_fpc_contract/src/lib.nr index 0f8e23c71a0d..738950c0a531 100644 --- a/noir-projects/noir-contracts/contracts/private_fpc_contract/src/lib.nr +++ b/noir-projects/noir-contracts/contracts/private_fpc_contract/src/lib.nr @@ -3,13 +3,13 @@ use dep::aztec::oracle::logs::emit_unencrypted_log_private_internal; use dep::aztec::hash::compute_unencrypted_log_hash; use dep::aztec::context::PrivateContext; -fn emit_nonce_as_unencrypted_log(context: &mut PrivateContext, nonce: Field) { +fn emit_randomness_as_unencrypted_log(context: &mut PrivateContext, randomness: Field) { let counter = context.next_counter(); - let log_slice = nonce.to_be_bytes_arr(); - let log_hash = compute_unencrypted_log_hash(context.this_address(), nonce); + let log_slice = randomness.to_be_bytes_arr(); + let log_hash = compute_unencrypted_log_hash(context.this_address(), randomness); // 40 = addr (32) + raw log len (4) + processed log len (4) let len = 40 + log_slice.len().to_field(); let side_effect = LogHash { value: log_hash, counter, length: len }; context.unencrypted_logs_hashes.push(side_effect); - let _void = emit_unencrypted_log_private_internal(context.this_address(), nonce, counter); + let _void = emit_unencrypted_log_private_internal(context.this_address(), randomness, counter); } diff --git a/noir-projects/noir-contracts/contracts/private_fpc_contract/src/main.nr b/noir-projects/noir-contracts/contracts/private_fpc_contract/src/main.nr index 5476f6fdb777..c7a6d116d2ec 100644 --- a/noir-projects/noir-contracts/contracts/private_fpc_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/private_fpc_contract/src/main.nr @@ -4,7 +4,7 @@ contract PrivateFPC { use dep::aztec::protocol_types::{abis::log_hash::LogHash, address::AztecAddress}; use dep::aztec::state_vars::SharedImmutable; use dep::private_token::PrivateToken; - use crate::lib::emit_nonce_as_unencrypted_log; + use crate::lib::emit_randomness_as_unencrypted_log; #[aztec(storage)] struct Storage { @@ -20,19 +20,19 @@ contract PrivateFPC { } #[aztec(private)] - fn fund_transaction_privately(amount: Field, asset: AztecAddress, nonce: Field) { + fn fund_transaction_privately(amount: Field, asset: AztecAddress, randomness: Field) { assert(asset == storage.other_asset.read_private()); // convince the FPC we are not cheating - context.push_nullifier(nonce, 0); + context.push_nullifier(randomness, 0); // allow the FPC to reconstruct their fee note - emit_nonce_as_unencrypted_log(&mut context, nonce); + emit_randomness_as_unencrypted_log(&mut context, randomness); PrivateToken::at(asset).setup_refund( storage.admin_npk_m_hash.read_private(), context.msg_sender(), amount, - nonce + randomness ).call(&mut context); context.set_as_fee_payer(); } diff --git a/noir-projects/noir-contracts/contracts/private_token_contract/src/main.nr b/noir-projects/noir-contracts/contracts/private_token_contract/src/main.nr index c3643b1ff39b..369aa623f317 100644 --- a/noir-projects/noir-contracts/contracts/private_token_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/private_token_contract/src/main.nr @@ -6,12 +6,9 @@ mod test; contract PrivateToken { use dep::compressed_string::FieldCompressedString; use dep::aztec::{ - hash::compute_secret_hash, + note::utils::compute_inner_note_hash_from_preimage, hash::compute_secret_hash, prelude::{NoteGetterOptions, Map, PublicMutable, SharedImmutable, PrivateSet, AztecAddress}, - protocol_types::{ - abis::function_selector::FunctionSelector, hash::pedersen_hash, - constants::GENERATOR_INDEX__INNER_NOTE_HASH - }, + protocol_types::{abis::function_selector::FunctionSelector, hash::pedersen_hash}, oracle::unsafe_rand::unsafe_rand, encrypted_logs::encrypted_note_emission::{encode_and_encrypt_note, encode_and_encrypt_note_with_keys} }; @@ -160,57 +157,69 @@ contract PrivateToken { #[aztec(private)] fn setup_refund( - fee_payer_npk_m_hash: Field, - sponsored_user: AztecAddress, - funded_amount: Field, - refund_nonce: Field + fee_payer_npk_m_hash: Field, // NpkMHash of the entity which will receive the fee note. + user: AztecAddress, // A user for which we are setting up the fee refund. + funded_amount: Field, // The amount the user funded the fee payer with (represents fee limit). + randomness: Field // A randomness to mix in with the generated notes. ) { - assert_current_call_valid_authwit(&mut context, sponsored_user); + // 1. This function is called by fee paying contract (fee_payer) when setting up a refund so we need to support + // the authwit flow here and check that the user really permitted fee_payer to set up a refund on their behalf. + assert_current_call_valid_authwit(&mut context, user); + + // 2. Get all the relevant user keys let header = context.get_header(); - let sponsored_user_npk_m_hash = header.get_npk_m_hash(&mut context, sponsored_user); - let sponsored_user_ovpk = header.get_ovpk_m(&mut context, sponsored_user); - let sponsored_user_ivpk = header.get_ivpk_m(&mut context, sponsored_user); - storage.balances.sub(sponsored_user_npk_m_hash, U128::from_integer(funded_amount)).emit(encode_and_encrypt_note_with_keys(&mut context, sponsored_user_ovpk, sponsored_user_ivpk)); - let points = TokenNote::generate_refund_points( + let user_npk_m_hash = header.get_npk_m_hash(&mut context, user); + let user_ovpk = header.get_ovpk_m(&mut context, user); + let user_ivpk = header.get_ivpk_m(&mut context, user); + + // 3. Deduct the funded amount from the user's balance - this is a maximum fee a user is willing to pay + // (called fee limit in aztec spec). The difference between fee limit and the actual tx fee will be refunded + // to the user in the `complete_refund(...)` function. + // TODO(#7324): using npk_m_hash here does not work with key rotation + storage.balances.sub(user_npk_m_hash, U128::from_integer(funded_amount)).emit(encode_and_encrypt_note_with_keys(&mut context, user_ovpk, user_ivpk)); + + // 4. We generate the refund points. + let (fee_payer_point, user_point) = TokenNote::generate_refund_points( fee_payer_npk_m_hash, - sponsored_user_npk_m_hash, + user_npk_m_hash, funded_amount, - refund_nonce + randomness ); + + // 5. Set the public teardown function to `complete_refund(...)`. Public teardown is the only time when a public + // function has access to the final transaction fee, which is needed to compute the actual refund amount. context.set_public_teardown_function( context.this_address(), FunctionSelector::from_signature("complete_refund(Field,Field,Field,Field)"), - [points[0].x, points[0].y, points[1].x, points[1].y] + [fee_payer_point.x, fee_payer_point.y, user_point.x, user_point.y] ); } #[aztec(public)] #[aztec(internal)] fn complete_refund( - fpc_point_x: Field, - fpc_point_y: Field, + fee_payer_point_x: Field, + fee_payer_point_y: Field, user_point_x: Field, user_point_y: Field ) { - let fpc_point = EmbeddedCurvePoint { x: fpc_point_x, y: fpc_point_y, is_infinite: false }; + // 1. We get the final note content hashes by calling the `complete_refund` on the note. + let fee_payer_point = EmbeddedCurvePoint { x: fee_payer_point_x, y: fee_payer_point_y, is_infinite: false }; let user_point = EmbeddedCurvePoint { x: user_point_x, y: user_point_y, is_infinite: false }; let tx_fee = context.transaction_fee(); - let note_hashes = TokenNote::complete_refund(fpc_point, user_point, tx_fee); - - // `compute_inner_note_hash` manually, without constructing the note - // `3` is the storage slot of the balances - context.push_note_hash( - pedersen_hash( - [PrivateToken::storage().balances.slot, note_hashes[0]], - GENERATOR_INDEX__INNER_NOTE_HASH - ) - ); - context.push_note_hash( - pedersen_hash( - [PrivateToken::storage().balances.slot, note_hashes[1]], - GENERATOR_INDEX__INNER_NOTE_HASH - ) + let (fee_payer_note_content_hash, user_note_content_hash) = TokenNote::complete_refund(fee_payer_point, user_point, tx_fee); + + // 2. Now we "manually" compute the inner note hashes. + let fee_payer_inner_note_hash = compute_inner_note_hash_from_preimage( + PrivateToken::storage().balances.slot, + fee_payer_note_content_hash ); + let user_inner_note_hash = compute_inner_note_hash_from_preimage(PrivateToken::storage().balances.slot, user_note_content_hash); + + // 3. At last we emit the note hashes. + context.push_note_hash(fee_payer_inner_note_hash); + context.push_note_hash(user_inner_note_hash); + // --> Once the tx is settled user and fee recipient can add the notes to their pixies. } /// Internal /// 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 b389ad10fb2a..6d0a9a8cc329 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 @@ -17,16 +17,16 @@ trait OwnedNote { trait PrivatelyRefundable { fn generate_refund_points( fee_payer_npk_m_hash: Field, - sponsored_user_npk_m_hash: Field, + user_npk_m_hash: Field, funded_amount: Field, - refund_nonce: Field - ) -> [EmbeddedCurvePoint; 2]; + randomness: Field + ) -> (EmbeddedCurvePoint, EmbeddedCurvePoint); fn complete_refund( - fee_payer_point: EmbeddedCurvePoint, - sponsored_user_point: EmbeddedCurvePoint, + incomplete_fee_payer_point: EmbeddedCurvePoint, + incomplete_user_point: EmbeddedCurvePoint, transaction_fee: Field - ) -> [Field; 2]; + ) -> (Field, Field); } global TOKEN_NOTE_LEN: Field = 3; // 3 plus a header. @@ -74,6 +74,9 @@ 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 + // 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], [EmbeddedCurveScalar { @@ -115,55 +118,128 @@ impl OwnedNote for TokenNote { } } +/** + * What is happening below? + * + * First in generate_refund_points, we create two points on the grumpkin curve; + * 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. + * + * 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 + * + * where G is a generator point. Interesting point here is that we actually need to convert + * - amount + * - npk + * - 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. + * + * (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. + * + * 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 + randomness) * G + * incomplete_user_point := (user_npk + funded_amount + randomness) * G + * + * 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 + * + * fee_point := transaction_fee * G + * + * 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 + randomness) * G + transaction_fee * G + * = (fee_payer_npk + randomness + transaction_fee) * G + * + * user_point := incomplete_user_point - fee_point + * = (user_npk + funded_amount + randomness) * G - transaction_fee * G + * = (user_npk + randomness + (funded_amount - transaction_fee)) * G + * + * When we return the x-coordinate of those points, it identically matches the note_content_hash of (and therefore *is*) notes like: + * { + * amount: (funded_amount - transaction_fee), + * npk_m_hash: user_npk, + * randomness: randomness + * } + */ impl PrivatelyRefundable for TokenNote { - fn generate_refund_points(fee_payer_npk_m_hash: Field, sponsored_user_npk_m_hash: Field, funded_amount: Field, refund_nonce: Field) -> [EmbeddedCurvePoint; 2] { - let (refund_nonce_lo, refund_nonce_hi) = decompose(refund_nonce); - let (fee_payer_lo, fee_payer_hi) = decompose(fee_payer_npk_m_hash); - - let fee_payer_point = multi_scalar_mul( + fn generate_refund_points(fee_payer_npk_m_hash: Field, user_npk_m_hash: Field, funded_amount: Field, randomness: Field) -> (EmbeddedCurvePoint, EmbeddedCurvePoint) { + // 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 (randomness_lo, randomness_hi) = decompose(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)` + let incomplete_fee_payer_point = multi_scalar_mul( [G1, G1], [EmbeddedCurveScalar { - lo: fee_payer_lo, - hi: fee_payer_hi + lo: fee_payer_npk_m_hash_lo, + hi: fee_payer_npk_m_hash_hi }, EmbeddedCurveScalar { - lo: refund_nonce_lo, - hi: refund_nonce_hi + lo: randomness_lo, + hi: randomness_hi }] ); - let (sponsored_user_lo, sponsored_user_hi) = decompose(sponsored_user_npk_m_hash); + // 3. We do the necessary conversion for values relevant for the sponsored user point. + // 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); let (funded_amount_lo, funded_amount_hi) = decompose(funded_amount); - let sponsored_user_point = multi_scalar_mul( + + // 4. We compute `G ^ (user_npk_m_hash + funded_amount + randomness)` + let incomplete_user_point = multi_scalar_mul( [G1, G1, G1], [EmbeddedCurveScalar { - lo: sponsored_user_lo, - hi: sponsored_user_hi + lo: user_lo, + hi: user_hi }, EmbeddedCurveScalar { lo: funded_amount_lo, hi: funded_amount_hi }, EmbeddedCurveScalar { - lo: refund_nonce_lo, - hi: refund_nonce_hi + lo: randomness_lo, + hi: randomness_hi }] ); - [EmbeddedCurvePoint { - x: fee_payer_point[0], - y: fee_payer_point[1], - is_infinite: fee_payer_point[2] == 1 - },EmbeddedCurvePoint { - x: sponsored_user_point[0], - y: sponsored_user_point[1], - is_infinite: sponsored_user_point[2] == 1 - } ] + // 5. At last we represent the points as EmbeddedCurvePoints and return them. + (EmbeddedCurvePoint { + x: incomplete_fee_payer_point[0], + y: incomplete_fee_payer_point[1], + is_infinite: incomplete_fee_payer_point[2] == 1 + }, EmbeddedCurvePoint { + x: incomplete_user_point[0], + y: incomplete_user_point[1], + is_infinite: incomplete_user_point[2] == 1 + }) } - fn complete_refund(fee_payer_point: EmbeddedCurvePoint, sponsored_user_point: EmbeddedCurvePoint, transaction_fee: Field) -> [Field; 2] { - + fn complete_refund(incomplete_fee_payer_point: EmbeddedCurvePoint, incomplete_user_point: EmbeddedCurvePoint, transaction_fee: Field) -> (Field, Field) { + // 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` let fee_point_raw = multi_scalar_mul( [G1], [EmbeddedCurveScalar { @@ -174,78 +250,17 @@ impl PrivatelyRefundable for TokenNote { let fee_point = EmbeddedCurvePoint { x: fee_point_raw[0], y: fee_point_raw[1], - is_infinite: fee_point_raw[2] ==1 + is_infinite: fee_point_raw[2] == 1 }; - /** - What is happening here? - - Back up in generate_refund_points, we created two points on the grumpkin curve; - these are going to be eventually turned into notes: - one for the user, and one for the FPC. - - 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 the content hash to be - the x-coordinate of a point defined as: - - amount * G + npk * G + randomness * G - = (amount + npk + randomness) * G - - where G is a generator point. Interesting point here is that we actually need to convert - - amount - - npk - - 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. - - (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. - - However we can still perform addition/subtraction on points! That is why we generate those two points, which are: - fee_payer_point := (fee_payer_npk + nonce) * G - sponsored_user_point := (sponsored_user_npk + funded_amount + nonce) * G - - 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 - - fee_point := transaction_fee * G - - Then we arrive at the final points via addition/subtraction of that transaction fee point: - - completed_fpc_point := fee_payer_point + fee_point - = (fee_payer_npk + nonce) * G + transaction_fee * G - = (fee_payer_npk + nonce + transaction_fee) * G - - completed_user_point := sponsored_user_point - fee_point - = (sponsored_user_npk + funded_amount + nonce) * G - transaction_fee * G - = (sponsored_user_npk + nonce + (funded_amount - transaction_fee)) * G - - When we return the x-coordinate of those points, it identically matches the note_content_hash of (and therefore *is*) notes like: - { - amount: (funded_amount - transaction_fee), - npk_m_hash: sponsored_user_npk, - randomness: nonce - } - */ - - let completed_fpc_point = fee_payer_point + fee_point; + // 3. Now we leverage homomorphism to privately add the fee to fee payer point and subtract it from + // the sponsored user point in public. + let fee_payer_point = incomplete_fee_payer_point + fee_point; + let user_point = incomplete_user_point - fee_point; - let completed_user_point = sponsored_user_point - fee_point; - assert_eq(completed_user_point.is_infinite, false); + assert_eq(user_point.is_infinite, false); - [completed_fpc_point.x, completed_user_point.x] + // Finally we return the x-coordinates of the points which are the note content hashes. + (fee_payer_point.x, user_point.x) } } diff --git a/yarn-project/end-to-end/src/e2e_fees/account_init.test.ts b/yarn-project/end-to-end/src/e2e_fees/account_init.test.ts index d223febf6b76..35263c9a4cda 100644 --- a/yarn-project/end-to-end/src/e2e_fees/account_init.test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/account_init.test.ts @@ -63,8 +63,8 @@ describe('e2e_fees account_init', () => { async function initBalances() { [[fpcsInitialGas], [fpcsInitialPublicBananas]] = await Promise.all([ - t.gasBalances(bananaFPC.address), - t.bananaPublicBalances(bananaFPC.address), + t.getGasBalanceFn(bananaFPC.address), + t.getBananaPublicBalanceFn(bananaFPC.address), ]); } @@ -86,14 +86,14 @@ describe('e2e_fees account_init', () => { describe('account pays its own fee', () => { it('pays natively in the gas token after Alice bridges funds', async () => { await t.gasTokenContract.methods.mint_public(bobsAddress, t.INITIAL_GAS_BALANCE).send().wait(); - const [bobsInitialGas] = await t.gasBalances(bobsAddress); + const [bobsInitialGas] = await t.getGasBalanceFn(bobsAddress); expect(bobsInitialGas).toEqual(t.INITIAL_GAS_BALANCE); const paymentMethod = new NativeFeePaymentMethod(bobsAddress); const tx = await bobsAccountManager.deploy({ fee: { gasSettings, paymentMethod } }).wait(); expect(tx.transactionFee!).toBeGreaterThan(0n); - await expect(t.gasBalances(bobsAddress)).resolves.toEqual([bobsInitialGas - tx.transactionFee!]); + await expect(t.getGasBalanceFn(bobsAddress)).resolves.toEqual([bobsInitialGas - tx.transactionFee!]); }); it('pays natively in the gas token by bridging funds themselves', async () => { @@ -106,7 +106,7 @@ describe('e2e_fees account_init', () => { const paymentMethod = new NativeFeePaymentMethodWithClaim(bobsAddress, t.INITIAL_GAS_BALANCE, secret); const tx = await bobsAccountManager.deploy({ fee: { gasSettings, paymentMethod } }).wait(); expect(tx.transactionFee!).toBeGreaterThan(0n); - await expect(t.gasBalances(bobsAddress)).resolves.toEqual([t.INITIAL_GAS_BALANCE - tx.transactionFee!]); + await expect(t.getGasBalanceFn(bobsAddress)).resolves.toEqual([t.INITIAL_GAS_BALANCE - tx.transactionFee!]); }); it('pays privately through an FPC', async () => { @@ -128,13 +128,15 @@ describe('e2e_fees account_init', () => { expect(actualFee).toBeGreaterThan(0n); // the new account should have paid the full fee to the FPC - await expect(t.bananaPrivateBalances(bobsAddress)).resolves.toEqual([mintedBananas - maxFee]); + await expect(t.getBananaPrivateBalanceFn(bobsAddress)).resolves.toEqual([mintedBananas - maxFee]); // the FPC got paid through "unshield", so it's got a new public balance - await expect(t.bananaPublicBalances(bananaFPC.address)).resolves.toEqual([fpcsInitialPublicBananas + actualFee]); + await expect(t.getBananaPublicBalanceFn(bananaFPC.address)).resolves.toEqual([ + fpcsInitialPublicBananas + actualFee, + ]); // the FPC should have been the fee payer - await expect(t.gasBalances(bananaFPC.address)).resolves.toEqual([fpcsInitialGas - actualFee]); + await expect(t.getGasBalanceFn(bananaFPC.address)).resolves.toEqual([fpcsInitialGas - actualFee]); // the new account should have received a refund await t.addPendingShieldNoteToPXE(bobsAddress, maxFee - actualFee, computeSecretHash(rebateSecret), tx.txHash); @@ -145,7 +147,7 @@ describe('e2e_fees account_init', () => { .send() .wait(); - await expect(t.bananaPrivateBalances(bobsAddress)).resolves.toEqual([mintedBananas - actualFee]); + await expect(t.getBananaPrivateBalanceFn(bobsAddress)).resolves.toEqual([mintedBananas - actualFee]); }); it('pays publicly through an FPC', async () => { @@ -164,13 +166,13 @@ describe('e2e_fees account_init', () => { expect(actualFee).toBeGreaterThan(0n); // we should have paid the fee to the FPC - await expect(t.bananaPublicBalances(bobsAddress, bananaFPC.address)).resolves.toEqual([ + await expect(t.getBananaPublicBalanceFn(bobsAddress, bananaFPC.address)).resolves.toEqual([ mintedBananas - actualFee, fpcsInitialPublicBananas + actualFee, ]); // the FPC should have paid the sequencer - await expect(t.gasBalances(bananaFPC.address)).resolves.toEqual([fpcsInitialGas - actualFee]); + await expect(t.getGasBalanceFn(bananaFPC.address)).resolves.toEqual([fpcsInitialGas - actualFee]); }); }); @@ -178,7 +180,7 @@ describe('e2e_fees account_init', () => { it('pays natively in the gas token', async () => { // mint gas tokens to alice await t.gasTokenContract.methods.mint_public(aliceAddress, t.INITIAL_GAS_BALANCE).send().wait(); - const [alicesInitialGas] = await t.gasBalances(aliceAddress); + const [alicesInitialGas] = await t.getGasBalanceFn(aliceAddress); // bob generates the private keys for his account on his own const bobsPublicKeysHash = deriveKeys(bobsSecretKey).publicKeys.hash(); @@ -208,7 +210,7 @@ describe('e2e_fees account_init', () => { // alice paid in gas tokens expect(tx.transactionFee!).toBeGreaterThan(0n); - await expect(t.gasBalances(aliceAddress)).resolves.toEqual([alicesInitialGas - tx.transactionFee!]); + await expect(t.getGasBalanceFn(aliceAddress)).resolves.toEqual([alicesInitialGas - tx.transactionFee!]); // bob can now use his wallet for sending txs await bananaCoin.withWallet(bobsWallet).methods.transfer_public(bobsAddress, aliceAddress, 0n, 0n).send().wait(); diff --git a/yarn-project/end-to-end/src/e2e_fees/dapp_subscription.test.ts b/yarn-project/end-to-end/src/e2e_fees/dapp_subscription.test.ts index 41529900ae91..33e25bda2bb6 100644 --- a/yarn-project/end-to-end/src/e2e_fees/dapp_subscription.test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/dapp_subscription.test.ts @@ -67,32 +67,33 @@ describe('e2e_fees dapp_subscription', () => { beforeAll(async () => { await expectMapping( - t.gasBalances, + t.getGasBalanceFn, [aliceAddress, sequencerAddress, subscriptionContract.address, bananaFPC.address], [0n, 0n, t.INITIAL_GAS_BALANCE, t.INITIAL_GAS_BALANCE], ); await expectMapping( - t.bananaPrivateBalances, + t.getBananaPrivateBalanceFn, [aliceAddress, bobAddress, bananaFPC.address], [t.ALICE_INITIAL_BANANAS, 0n, 0n], ); await expectMapping( - t.bananaPublicBalances, + t.getBananaPublicBalanceFn, [aliceAddress, bobAddress, bananaFPC.address], [t.ALICE_INITIAL_BANANAS, 0n, 0n], ); }); beforeEach(async () => { - [initialSubscriptionContractGasBalance, initialSequencerGasBalance, initialFPCGasBalance] = (await t.gasBalances( - subscriptionContract, - sequencerAddress, + [initialSubscriptionContractGasBalance, initialSequencerGasBalance, initialFPCGasBalance] = + (await t.getGasBalanceFn(subscriptionContract, sequencerAddress, bananaFPC)) as Balances; + initialBananasPublicBalances = (await t.getBananaPublicBalanceFn(aliceAddress, bobAddress, bananaFPC)) as Balances; + initialBananasPrivateBalances = (await t.getBananaPrivateBalanceFn( + aliceAddress, + bobAddress, bananaFPC, )) as Balances; - initialBananasPublicBalances = (await t.bananaPublicBalances(aliceAddress, bobAddress, bananaFPC)) as Balances; - initialBananasPrivateBalances = (await t.bananaPrivateBalances(aliceAddress, bobAddress, bananaFPC)) as Balances; }); it('should allow Alice to subscribe by paying privately with bananas', async () => { @@ -112,7 +113,7 @@ describe('e2e_fees dapp_subscription', () => { ); await expectMapping( - t.gasBalances, + t.getGasBalanceFn, [sequencerAddress, bananaFPC.address], [initialSequencerGasBalance, initialFPCGasBalance - transactionFee!], ); @@ -140,7 +141,7 @@ describe('e2e_fees dapp_subscription', () => { ); await expectMapping( - t.gasBalances, + t.getGasBalanceFn, [sequencerAddress, bananaFPC.address], [initialSequencerGasBalance, initialFPCGasBalance - transactionFee!], ); @@ -170,7 +171,7 @@ describe('e2e_fees dapp_subscription', () => { expect(await counterContract.methods.get_counter(bobAddress).simulate()).toBe(1n); await expectMapping( - t.gasBalances, + t.getGasBalanceFn, [sequencerAddress, subscriptionContract.address], [initialSequencerGasBalance, initialSubscriptionContractGasBalance - transactionFee!], ); @@ -216,7 +217,7 @@ describe('e2e_fees dapp_subscription', () => { const expectBananasPrivateDelta = (aliceAmount: bigint, bobAmount: bigint, fpcAmount: bigint) => expectMappingDelta( initialBananasPrivateBalances, - t.bananaPrivateBalances, + t.getBananaPrivateBalanceFn, [aliceAddress, bobAddress, bananaFPC.address], [aliceAmount, bobAmount, fpcAmount], ); @@ -224,7 +225,7 @@ describe('e2e_fees dapp_subscription', () => { const expectBananasPublicDelta = (aliceAmount: bigint, bobAmount: bigint, fpcAmount: bigint) => expectMappingDelta( initialBananasPublicBalances, - t.bananaPublicBalances, + t.getBananaPublicBalanceFn, [aliceAddress, bobAddress, bananaFPC.address], [aliceAmount, bobAmount, fpcAmount], ); diff --git a/yarn-project/end-to-end/src/e2e_fees/failures.test.ts b/yarn-project/end-to-end/src/e2e_fees/failures.test.ts index dd0bb68635cf..ca5efa6ac078 100644 --- a/yarn-project/end-to-end/src/e2e_fees/failures.test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/failures.test.ts @@ -40,15 +40,15 @@ describe('e2e_fees failures', () => { const OutrageousPublicAmountAliceDoesNotHave = BigInt(1e8); const PrivateMintedAlicePrivateBananas = BigInt(1e15); - const [initialAlicePrivateBananas, initialFPCPrivateBananas] = await t.bananaPrivateBalances( + const [initialAlicePrivateBananas, initialFPCPrivateBananas] = await t.getBananaPrivateBalanceFn( aliceAddress, bananaFPC.address, ); - const [initialAlicePublicBananas, initialFPCPublicBananas] = await t.bananaPublicBalances( + const [initialAlicePublicBananas, initialFPCPublicBananas] = await t.getBananaPublicBalanceFn( aliceAddress, bananaFPC.address, ); - const [initialAliceGas, initialFPCGas] = await t.gasBalances(aliceAddress, bananaFPC.address); + const [initialAliceGas, initialFPCGas] = await t.getGasBalanceFn(aliceAddress, bananaFPC.address); await t.mintPrivateBananas(PrivateMintedAlicePrivateBananas, aliceAddress); @@ -68,16 +68,16 @@ describe('e2e_fees failures', () => { // we did not pay the fee, because we did not submit the TX await expectMapping( - t.bananaPrivateBalances, + t.getBananaPrivateBalanceFn, [aliceAddress, bananaFPC.address], [initialAlicePrivateBananas + PrivateMintedAlicePrivateBananas, initialFPCPrivateBananas], ); await expectMapping( - t.bananaPublicBalances, + t.getBananaPublicBalanceFn, [aliceAddress, bananaFPC.address], [initialAlicePublicBananas, initialFPCPublicBananas], ); - await expectMapping(t.gasBalances, [aliceAddress, bananaFPC.address], [initialAliceGas, initialFPCGas]); + await expectMapping(t.getGasBalanceFn, [aliceAddress, bananaFPC.address], [initialAliceGas, initialFPCGas]); // if we skip simulation, it includes the failed TX const rebateSecret = Fr.random(); @@ -100,7 +100,7 @@ describe('e2e_fees failures', () => { // and thus we paid the fee await expectMapping( - t.bananaPrivateBalances, + t.getBananaPrivateBalanceFn, [aliceAddress, bananaFPC.address], [ // alice paid the maximum amount in private bananas @@ -109,11 +109,15 @@ describe('e2e_fees failures', () => { ], ); await expectMapping( - t.bananaPublicBalances, + t.getBananaPublicBalanceFn, [aliceAddress, bananaFPC.address], [initialAlicePublicBananas, initialFPCPublicBananas + feeAmount], ); - await expectMapping(t.gasBalances, [aliceAddress, bananaFPC.address], [initialAliceGas, initialFPCGas - feeAmount]); + await expectMapping( + t.getGasBalanceFn, + [aliceAddress, bananaFPC.address], + [initialAliceGas, initialFPCGas - feeAmount], + ); // Alice can redeem her shield to get the rebate const refund = gasSettings.getFeeLimit().toBigInt() - feeAmount; @@ -123,7 +127,7 @@ describe('e2e_fees failures', () => { await bananaCoin.methods.redeem_shield(aliceAddress, refund, rebateSecret).send().wait(); await expectMapping( - t.bananaPrivateBalances, + t.getBananaPrivateBalanceFn, [aliceAddress, bananaFPC.address], [initialAlicePrivateBananas + PrivateMintedAlicePrivateBananas - feeAmount, initialFPCPrivateBananas], ); @@ -133,15 +137,15 @@ describe('e2e_fees failures', () => { const OutrageousPublicAmountAliceDoesNotHave = BigInt(1e15); const PublicMintedAlicePublicBananas = BigInt(1e12); - const [initialAlicePrivateBananas, initialFPCPrivateBananas] = await t.bananaPrivateBalances( + const [initialAlicePrivateBananas, initialFPCPrivateBananas] = await t.getBananaPrivateBalanceFn( aliceAddress, bananaFPC.address, ); - const [initialAlicePublicBananas, initialFPCPublicBananas] = await t.bananaPublicBalances( + const [initialAlicePublicBananas, initialFPCPublicBananas] = await t.getBananaPublicBalanceFn( aliceAddress, bananaFPC.address, ); - const [initialAliceGas, initialFPCGas, initialSequencerGas] = await t.gasBalances( + const [initialAliceGas, initialFPCGas, initialSequencerGas] = await t.getGasBalanceFn( aliceAddress, bananaFPC.address, sequencerAddress, @@ -163,17 +167,17 @@ describe('e2e_fees failures', () => { // we did not pay the fee, because we did not submit the TX await expectMapping( - t.bananaPrivateBalances, + t.getBananaPrivateBalanceFn, [aliceAddress, bananaFPC.address, sequencerAddress], [initialAlicePrivateBananas, initialFPCPrivateBananas, 0n], ); await expectMapping( - t.bananaPublicBalances, + t.getBananaPublicBalanceFn, [aliceAddress, bananaFPC.address, sequencerAddress], [initialAlicePublicBananas + PublicMintedAlicePublicBananas, initialFPCPublicBananas, 0n], ); await expectMapping( - t.gasBalances, + t.getGasBalanceFn, [aliceAddress, bananaFPC.address, sequencerAddress], [initialAliceGas, initialFPCGas, initialSequencerGas], ); @@ -195,17 +199,17 @@ describe('e2e_fees failures', () => { // and thus we paid the fee await expectMapping( - t.bananaPrivateBalances, + t.getBananaPrivateBalanceFn, [aliceAddress, bananaFPC.address, sequencerAddress], [initialAlicePrivateBananas, initialFPCPrivateBananas, 0n], ); await expectMapping( - t.bananaPublicBalances, + t.getBananaPublicBalanceFn, [aliceAddress, bananaFPC.address, sequencerAddress], [initialAlicePublicBananas + PublicMintedAlicePublicBananas - feeAmount, initialFPCPublicBananas + feeAmount, 0n], ); await expectMapping( - t.gasBalances, + t.getGasBalanceFn, [aliceAddress, bananaFPC.address, sequencerAddress], [initialAliceGas, initialFPCGas - feeAmount, initialSequencerGas], ); @@ -248,15 +252,15 @@ describe('e2e_fees failures', () => { */ const PublicMintedAlicePublicBananas = 100_000_000_000n; - const [initialAlicePrivateBananas, initialFPCPrivateBananas] = await t.bananaPrivateBalances( + const [initialAlicePrivateBananas, initialFPCPrivateBananas] = await t.getBananaPrivateBalanceFn( aliceAddress, bananaFPC.address, ); - const [initialAlicePublicBananas, initialFPCPublicBananas] = await t.bananaPublicBalances( + const [initialAlicePublicBananas, initialFPCPublicBananas] = await t.getBananaPublicBalanceFn( aliceAddress, bananaFPC.address, ); - const [initialAliceGas, initialFPCGas, initialSequencerGas] = await t.gasBalances( + const [initialAliceGas, initialFPCGas, initialSequencerGas] = await t.getGasBalanceFn( aliceAddress, bananaFPC.address, sequencerAddress, @@ -299,13 +303,13 @@ describe('e2e_fees failures', () => { expect(receipt.transactionFee).toBeGreaterThan(0n); await expectMapping( - t.bananaPrivateBalances, + t.getBananaPrivateBalanceFn, [aliceAddress, bananaFPC.address, sequencerAddress], [initialAlicePrivateBananas, initialFPCPrivateBananas, 0n], ); // Since setup went through, Alice transferred to the FPC await expectMapping( - t.bananaPublicBalances, + t.getBananaPublicBalanceFn, [aliceAddress, bananaFPC.address, sequencerAddress], [ initialAlicePublicBananas + PublicMintedAlicePublicBananas - badGas.getFeeLimit().toBigInt(), @@ -314,7 +318,7 @@ describe('e2e_fees failures', () => { ], ); await expectMapping( - t.gasBalances, + t.getGasBalanceFn, [aliceAddress, bananaFPC.address, sequencerAddress], [initialAliceGas, initialFPCGas - receipt.transactionFee!, initialSequencerGas], ); diff --git a/yarn-project/end-to-end/src/e2e_fees/fees_test.ts b/yarn-project/end-to-end/src/e2e_fees/fees_test.ts index 83fb65f5775b..ead26d72734e 100644 --- a/yarn-project/end-to-end/src/e2e_fees/fees_test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/fees_test.ts @@ -74,10 +74,10 @@ export class FeesTest { public gasBridgeTestHarness!: IGasBridgingTestHarness; public getCoinbaseBalance!: () => Promise; - public gasBalances!: BalancesFn; - public bananaPublicBalances!: BalancesFn; - public bananaPrivateBalances!: BalancesFn; - public privateTokenBalances!: BalancesFn; + public getGasBalanceFn!: BalancesFn; + public getBananaPublicBalanceFn!: BalancesFn; + public getBananaPrivateBalanceFn!: BalancesFn; + public getPrivateTokenBalanceFn!: BalancesFn; public readonly INITIAL_GAS_BALANCE = BigInt(1e15); public readonly ALICE_INITIAL_BANANAS = BigInt(1e12); @@ -210,7 +210,7 @@ export class FeesTest { async (_data, context) => { this.gasTokenContract = await GasTokenContract.at(getCanonicalGasToken().address, this.aliceWallet); - this.gasBalances = getBalancesFn('⛽', this.gasTokenContract.methods.balance_of_public, this.logger); + this.getGasBalanceFn = getBalancesFn('⛽', this.gasTokenContract.methods.balance_of_public, this.logger); const { publicClient, walletClient } = createL1Clients(context.aztecNodeConfig.rpcUrl, MNEMONIC); this.gasBridgeTestHarness = await GasPortalTestingHarnessFactory.create({ @@ -277,7 +277,11 @@ export class FeesTest { this.privateToken = await PrivateTokenContract.at(data.privateTokenAddress, this.aliceWallet); const logger = this.logger; - this.privateTokenBalances = getBalancesFn('🕵️.private', this.privateToken.methods.balance_of_private, logger); + this.getPrivateTokenBalanceFn = getBalancesFn( + '🕵️.private', + this.privateToken.methods.balance_of_private, + logger, + ); }, ); } @@ -313,8 +317,12 @@ export class FeesTest { this.bananaFPC = bananaFPC; const logger = this.logger; - this.bananaPublicBalances = getBalancesFn('🍌.public', this.bananaCoin.methods.balance_of_public, logger); - this.bananaPrivateBalances = getBalancesFn('🍌.private', this.bananaCoin.methods.balance_of_private, logger); + this.getBananaPublicBalanceFn = getBalancesFn('🍌.public', this.bananaCoin.methods.balance_of_public, logger); + this.getBananaPrivateBalanceFn = getBalancesFn( + '🍌.private', + this.bananaCoin.methods.balance_of_private, + logger, + ); this.getCoinbaseBalance = async () => { const { walletClient } = createL1Clients(context.aztecNodeConfig.rpcUrl, MNEMONIC); diff --git a/yarn-project/end-to-end/src/e2e_fees/private_payments.test.ts b/yarn-project/end-to-end/src/e2e_fees/private_payments.test.ts index a09e33052092..5c14a3bb1bfb 100644 --- a/yarn-project/end-to-end/src/e2e_fees/private_payments.test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/private_payments.test.ts @@ -69,9 +69,9 @@ describe('e2e_fees private_payment', () => { [InitialAlicePublicBananas, InitialBobPublicBananas, InitialFPCPublicBananas], [InitialAliceGas, InitialFPCGas, InitialSequencerGas], ] = await Promise.all([ - t.bananaPrivateBalances(aliceAddress, bobAddress, bananaFPC.address), - t.bananaPublicBalances(aliceAddress, bobAddress, bananaFPC.address), - t.gasBalances(aliceAddress, bananaFPC.address, sequencerAddress), + t.getBananaPrivateBalanceFn(aliceAddress, bobAddress, bananaFPC.address), + t.getBananaPublicBalanceFn(aliceAddress, bobAddress, bananaFPC.address), + t.getGasBalanceFn(aliceAddress, bananaFPC.address, sequencerAddress), ]); }); @@ -142,17 +142,17 @@ describe('e2e_fees private_payment', () => { const [feeAmount, refundAmount] = getFeeAndRefund(tx); await expectMapping( - t.bananaPrivateBalances, + t.getBananaPrivateBalanceFn, [aliceAddress, bobAddress, bananaFPC.address, sequencerAddress], [InitialAlicePrivateBananas - maxFee - transferAmount, transferAmount, InitialFPCPrivateBananas, 0n], ); await expectMapping( - t.bananaPublicBalances, + t.getBananaPublicBalanceFn, [aliceAddress, bananaFPC.address, sequencerAddress], [InitialAlicePublicBananas, InitialFPCPublicBananas + maxFee - refundAmount, 0n], ); await expectMapping( - t.gasBalances, + t.getGasBalanceFn, [aliceAddress, bananaFPC.address, sequencerAddress], [InitialAliceGas, InitialFPCGas - feeAmount, InitialSequencerGas], ); @@ -199,17 +199,17 @@ describe('e2e_fees private_payment', () => { const [feeAmount, refundAmount] = getFeeAndRefund(tx); await expectMapping( - t.bananaPrivateBalances, + t.getBananaPrivateBalanceFn, [aliceAddress, bananaFPC.address, sequencerAddress], [InitialAlicePrivateBananas - maxFee + newlyMintedBananas, InitialFPCPrivateBananas, 0n], ); await expectMapping( - t.bananaPublicBalances, + t.getBananaPublicBalanceFn, [aliceAddress, bananaFPC.address, sequencerAddress], [InitialAlicePublicBananas, InitialFPCPublicBananas + maxFee - refundAmount, 0n], ); await expectMapping( - t.gasBalances, + t.getGasBalanceFn, [aliceAddress, bananaFPC.address, sequencerAddress], [InitialAliceGas, InitialFPCGas - feeAmount, InitialSequencerGas], ); @@ -259,17 +259,17 @@ describe('e2e_fees private_payment', () => { const [feeAmount, refundAmount] = getFeeAndRefund(tx); await expectMapping( - t.bananaPrivateBalances, + t.getBananaPrivateBalanceFn, [aliceAddress, bananaFPC.address, sequencerAddress], [InitialAlicePrivateBananas - maxFee, InitialFPCPrivateBananas, 0n], ); await expectMapping( - t.bananaPublicBalances, + t.getBananaPublicBalanceFn, [aliceAddress, bananaFPC.address, sequencerAddress], [InitialAlicePublicBananas - shieldedBananas, InitialFPCPublicBananas + maxFee - refundAmount, 0n], ); await expectMapping( - t.gasBalances, + t.getGasBalanceFn, [aliceAddress, bananaFPC.address, sequencerAddress], [InitialAliceGas, InitialFPCGas - feeAmount, InitialSequencerGas], ); @@ -327,7 +327,7 @@ describe('e2e_fees private_payment', () => { const [feeAmount, refundAmount] = getFeeAndRefund(tx); await expectMapping( - t.bananaPrivateBalances, + t.getBananaPrivateBalanceFn, [aliceAddress, bobAddress, bananaFPC.address, sequencerAddress], [ InitialAlicePrivateBananas - maxFee - privateTransfer, @@ -337,12 +337,12 @@ describe('e2e_fees private_payment', () => { ], ); await expectMapping( - t.bananaPublicBalances, + t.getBananaPublicBalanceFn, [aliceAddress, bananaFPC.address, sequencerAddress], [InitialAlicePublicBananas - shieldedBananas, InitialFPCPublicBananas + maxFee - refundAmount, 0n], ); await expectMapping( - t.gasBalances, + t.getGasBalanceFn, [aliceAddress, bananaFPC.address, sequencerAddress], [InitialAliceGas, InitialFPCGas - feeAmount, InitialSequencerGas], ); @@ -362,7 +362,7 @@ describe('e2e_fees private_payment', () => { .send() .deployed(); - await expectMapping(t.gasBalances, [bankruptFPC.address], [0n]); + await expectMapping(t.getGasBalanceFn, [bankruptFPC.address], [0n]); await expect( bananaCoin.methods diff --git a/yarn-project/end-to-end/src/e2e_fees/private_refunds.test.ts b/yarn-project/end-to-end/src/e2e_fees/private_refunds.test.ts index 8ab441d8336a..f98c200d574f 100644 --- a/yarn-project/end-to-end/src/e2e_fees/private_refunds.test.ts +++ b/yarn-project/end-to-end/src/e2e_fees/private_refunds.test.ts @@ -19,9 +19,10 @@ describe('e2e_fees/private_refunds', () => { let privateToken: PrivateTokenContract; let privateFPC: PrivateFPCContract; - let InitialAlicePrivateTokens: bigint; - let InitialBobPrivateTokens: bigint; - let InitialPrivateFPCGas: bigint; + let initialAliceBalance: bigint; + // Bob is the admin of the fee paying contract + let initialBobBalance: bigint; + let initialFPCGasBalance: bigint; const t = new FeesTest('private_refunds'); @@ -40,15 +41,21 @@ describe('e2e_fees/private_refunds', () => { }); beforeEach(async () => { - [[InitialAlicePrivateTokens, InitialBobPrivateTokens], [InitialPrivateFPCGas]] = await Promise.all([ - t.privateTokenBalances(aliceAddress, t.bobAddress), - t.gasBalances(privateFPC.address), + [[initialAliceBalance, initialBobBalance], [initialFPCGasBalance]] = await Promise.all([ + t.getPrivateTokenBalanceFn(aliceAddress, t.bobAddress), + t.getGasBalanceFn(privateFPC.address), ]); }); it('can do private payments and refunds', async () => { - const bobKeyHash = t.bobWallet.getCompleteAddress().publicKeys.masterNullifierPublicKey.hash(); - const rebateNonce = new Fr(42); + // 1. We get the hash of Bob's master nullifier public key. The corresponding nullifier secret key can later on + // be used to nullify/spend the note that contains the npk_m_hash. + // TODO(#7324): The values in complete address are currently not updated after the keys are rotated so this does + // not work with key rotation as the key might be the old one and then we would fetch a new one in the contract. + const bobNpkMHash = t.bobWallet.getCompleteAddress().publicKeys.masterNullifierPublicKey.hash(); + const randomness = Fr.random(); + + // 2. We call arbitrary `private_get_name(...)` function to check that the fee refund flow works. const tx = await privateToken.methods .private_get_name() .send({ @@ -58,8 +65,8 @@ describe('e2e_fees/private_refunds', () => { privateToken.address, privateFPC.address, aliceWallet, - rebateNonce, - bobKeyHash, + randomness, + bobNpkMHash, // We use Bob's npk_m_hash in the notes that contain the transaction fee. ), }, }) @@ -67,9 +74,19 @@ describe('e2e_fees/private_refunds', () => { expect(tx.transactionFee).toBeGreaterThan(0); - const refundedNoteValue = t.gasSettings.getFeeLimit().sub(new Fr(tx.transactionFee!)); - const aliceKeyHash = t.aliceWallet.getCompleteAddress().publicKeys.masterNullifierPublicKey.hash(); - const aliceRefundNote = new Note([refundedNoteValue, aliceKeyHash, rebateNonce]); + // 3. Now we compute the contents of the note containing the refund for Alice. The refund note value is simply + // the fee limit less the final transaction fee. The other 2 fields in the note are Alice's npk_m_hash and + // the randomness. + const refundNoteValue = t.gasSettings.getFeeLimit().sub(new Fr(tx.transactionFee!)); + // TODO(#7324): The values in complete address are currently not updated after the keys are rotated so this does + // not work with key rotation as the key might be the old one and then we would fetch a new one in the contract. + const aliceNpkMHash = t.aliceWallet.getCompleteAddress().publicKeys.masterNullifierPublicKey.hash(); + const aliceRefundNote = new Note([refundNoteValue, aliceNpkMHash, randomness]); + + // 4. If the refund flow worked it should have added emitted a note hash of the note we constructed above and we + // should be able to add the note to our PXE. Just calling `pxe.addNote(...)` is enough of a check that the note + // hash was emitted because the endpoint will compute the hash and then it will try to find it in the note hash + // tree. If the note hash is not found in the tree, an error is thrown. await t.aliceWallet.addNote( new ExtendedNote( aliceRefundNote, @@ -81,7 +98,13 @@ describe('e2e_fees/private_refunds', () => { ), ); - const bobFeeNote = new Note([new Fr(tx.transactionFee!), bobKeyHash, rebateNonce]); + // 5. Now we reconstruct the note for the final fee payment. It should contain the transaction fee, Bob's + // npk_m_hash (set in the paymentMethod above) and the randomness. + // Note that FPC emits randomness as unencrypted log and the tx fee is publicly know so Bob is able to reconstruct + // his note just from on-chain data. + const bobFeeNote = new Note([new Fr(tx.transactionFee!), bobNpkMHash, randomness]); + + // 6. Once again we add the note to PXE which computes the note hash and checks that it is in the note hash tree. await t.bobWallet.addNote( new ExtendedNote( bobFeeNote, @@ -93,11 +116,13 @@ describe('e2e_fees/private_refunds', () => { ), ); - await expectMapping(t.gasBalances, [privateFPC.address], [InitialPrivateFPCGas - tx.transactionFee!]); + // 7. At last we check that the gas balance of FPC has decreased exactly by the transaction fee ... + await expectMapping(t.getGasBalanceFn, [privateFPC.address], [initialFPCGasBalance - tx.transactionFee!]); + // ... and that the transaction fee was correctly transferred from Alice to Bob. await expectMapping( - t.privateTokenBalances, + t.getPrivateTokenBalanceFn, [aliceAddress, t.bobAddress], - [InitialAlicePrivateTokens - tx.transactionFee!, InitialBobPrivateTokens + tx.transactionFee!], + [initialAliceBalance - tx.transactionFee!, initialBobBalance + tx.transactionFee!], ); }); }); @@ -119,15 +144,15 @@ class PrivateRefundPaymentMethod implements FeePaymentMethod { private wallet: Wallet, /** - * A nonce to mix in with the generated notes. + * A randomness to mix in with the generated notes. * Use this to reconstruct note preimages for the PXE. */ - private rebateNonce: Fr, + private randomness: Fr, /** - * The hash of the nullifier private key that the FPC sends notes it receives to. + * The hash of the master nullifier public key that the FPC sends notes it receives to. */ - private feeRecipientNPKMHash: Fr, + private feeRecipientNpkMHash: Fr, ) {} /** @@ -154,7 +179,7 @@ class PrivateRefundPaymentMethod implements FeePaymentMethod { caller: this.paymentContract, action: { name: 'setup_refund', - args: [this.feeRecipientNPKMHash, this.wallet.getCompleteAddress().address, maxFee, this.rebateNonce], + args: [this.feeRecipientNpkMHash, this.wallet.getCompleteAddress().address, maxFee, this.randomness], selector: FunctionSelector.fromSignature('setup_refund(Field,(Field),Field,Field)'), type: FunctionType.PRIVATE, isStatic: false, @@ -170,7 +195,7 @@ class PrivateRefundPaymentMethod implements FeePaymentMethod { selector: FunctionSelector.fromSignature('fund_transaction_privately(Field,(Field),Field)'), type: FunctionType.PRIVATE, isStatic: false, - args: [maxFee, this.asset, this.rebateNonce], + args: [maxFee, this.asset, this.randomness], returnTypes: [], }, ];