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 deleted file mode 100644 index 738950c0a531..000000000000 --- a/noir-projects/noir-contracts/contracts/private_fpc_contract/src/lib.nr +++ /dev/null @@ -1,15 +0,0 @@ -use dep::aztec::protocol_types::abis::log_hash::LogHash; -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_randomness_as_unencrypted_log(context: &mut PrivateContext, randomness: Field) { - let counter = context.next_counter(); - 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(), 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 645f6ab15e5c..7753e5cd49d3 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 @@ -1,35 +1,39 @@ -mod lib; +mod settings; contract PrivateFPC { - use dep::aztec::{protocol_types::{address::AztecAddress, hash::poseidon2_hash}, state_vars::SharedImmutable}; + use dep::aztec::{protocol_types::{address::AztecAddress, hash::compute_siloed_nullifier}, state_vars::SharedImmutable}; use dep::token_with_refunds::TokenWithRefunds; - use crate::lib::emit_randomness_as_unencrypted_log; + use crate::settings::Settings; #[aztec(storage)] struct Storage { - other_asset: SharedImmutable, - admin: SharedImmutable, + settings: SharedImmutable, } #[aztec(public)] #[aztec(initializer)] fn constructor(other_asset: AztecAddress, admin: AztecAddress) { - storage.other_asset.initialize(other_asset); - storage.admin.initialize(admin); + let settings = Settings { other_asset, admin }; + storage.settings.initialize(settings); } #[aztec(private)] fn fund_transaction_privately(amount: Field, asset: AztecAddress, user_randomness: Field) { - assert(asset == storage.other_asset.read_private()); + // TODO: Once SharedImmutable performs only 1 merkle proof here, we'll save ~4k gates + let settings = storage.settings.read_private(); + + assert(asset == settings.other_asset); // We use different randomness for fee payer to prevent a potential privacy leak (see description // of `setup_refund(...)` function in TokenWithRefunds for details. - let fee_payer_randomness = poseidon2_hash([user_randomness]); - // We emit fee payer randomness to ensure FPC admin can reconstruct their fee note - emit_randomness_as_unencrypted_log(&mut context, fee_payer_randomness); + let fee_payer_randomness = compute_siloed_nullifier(context.this_address(), user_randomness); + // We emit fee payer randomness as nullifier to ensure FPC admin can reconstruct their fee note - note that + // protocol circuits will perform the siloing as was done above and hence the final nullifier will be correct + // fee payer randomness. + context.push_nullifier(user_randomness); TokenWithRefunds::at(asset).setup_refund( - storage.admin.read_private(), + settings.admin, context.msg_sender(), amount, user_randomness, diff --git a/noir-projects/noir-contracts/contracts/private_fpc_contract/src/settings.nr b/noir-projects/noir-contracts/contracts/private_fpc_contract/src/settings.nr new file mode 100644 index 000000000000..4d4fa24c18f7 --- /dev/null +++ b/noir-projects/noir-contracts/contracts/private_fpc_contract/src/settings.nr @@ -0,0 +1,23 @@ +use dep::aztec::protocol_types::{address::AztecAddress, traits::{Serialize, Deserialize}}; + +global SETTINGS_LENGTH = 2; + +struct Settings { + other_asset: AztecAddress, + admin: AztecAddress, +} + +impl Serialize for Settings { + fn serialize(self: Self) -> [Field; SETTINGS_LENGTH] { + [self.other_asset.to_field(), self.admin.to_field()] + } +} + +impl Deserialize for Settings { + fn deserialize(fields: [Field; SETTINGS_LENGTH]) -> Self { + Settings { + other_asset: AztecAddress::from_field(fields[0]), + admin: AztecAddress::from_field(fields[1]), + } + } +} diff --git a/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/main.nr b/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/main.nr index a246e4d88744..0e89f3a101ec 100644 --- a/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/token_with_refunds_contract/src/main.nr @@ -545,7 +545,14 @@ contract TokenWithRefunds { // 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. - storage.balances.sub(user, U128::from_integer(funded_amount)).emit(encode_and_encrypt_note_with_keys(&mut context, user_ovpk, user_ivpk, user)); + let change = subtract_balance( + &mut context, + storage, + user, + U128::from_integer(funded_amount), + INITIAL_TRANSFER_CALL_MAX_NOTES + ); + storage.balances.add(user, change).emit(encode_and_encrypt_note_with_keys_unconstrained(&mut context, user_ovpk, user_ivpk, user)); // 4. We create the partial notes for the fee payer and the user. // --> Called "partial" because they don't have the amount set yet (that will be done in `complete_refund(...)`). 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 a382cd116bec..6f198f665399 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 @@ -8,9 +8,8 @@ import { type Wallet, } from '@aztec/aztec.js'; import { Fr, type GasSettings } from '@aztec/circuits.js'; -import { deriveStorageSlotInMap } from '@aztec/circuits.js/hash'; +import { deriveStorageSlotInMap, siloNullifier } from '@aztec/circuits.js/hash'; import { FunctionSelector, FunctionType } from '@aztec/foundation/abi'; -import { poseidon2Hash } from '@aztec/foundation/crypto'; import { type PrivateFPCContract, TokenWithRefundsContract } from '@aztec/noir-contracts.js'; import { expectMapping } from '../fixtures/utils.js'; @@ -57,10 +56,10 @@ describe('e2e_fees/private_refunds', () => { it('can do private payments and refunds', async () => { // 1. We generate randomness for Alice and derive randomness for Bob. const aliceRandomness = Fr.random(); // Called user_randomness in contracts - const bobRandomness = poseidon2Hash([aliceRandomness]); // Called fee_payer_randomness in contracts + const bobRandomness = siloNullifier(privateFPC.address, aliceRandomness); // Called fee_payer_randomness in contracts // 2. We call arbitrary `private_get_name(...)` function to check that the fee refund flow works. - const tx = await tokenWithRefunds.methods + const { txHash, transactionFee, debugInfo } = await tokenWithRefunds.methods .private_get_name() .send({ fee: { @@ -75,19 +74,18 @@ describe('e2e_fees/private_refunds', () => { ), }, }) - .wait(); + .wait({ debug: true }); - expect(tx.transactionFee).toBeGreaterThan(0); + expect(transactionFee).toBeGreaterThan(0); - // 3. We check that randomness for Bob was correctly emitted as an unencrypted log (Bobs needs it to reconstruct his note). - const resp = await aliceWallet.getUnencryptedLogs({ txHash: tx.txHash }); - const bobRandomnessFromLog = Fr.fromBuffer(resp.logs[0].log.data); + // 3. We check that randomness for Bob was correctly emitted as a nullifier (Bobs needs it to reconstruct his note). + const bobRandomnessFromLog = debugInfo?.nullifiers[1]; expect(bobRandomnessFromLog).toEqual(bobRandomness); // 4. Now we compute the contents of the note containing the refund for Alice. The refund note value is simply // the fee limit minus 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!)); + const refundNoteValue = t.gasSettings.getFeeLimit().sub(new Fr(transactionFee!)); const aliceNpkMHash = t.aliceWallet.getCompleteAddress().publicKeys.masterNullifierPublicKey.hash(); const aliceRefundNote = new Note([refundNoteValue, aliceNpkMHash, aliceRandomness]); @@ -102,7 +100,7 @@ describe('e2e_fees/private_refunds', () => { tokenWithRefunds.address, deriveStorageSlotInMap(TokenWithRefundsContract.storage.balances.slot, t.aliceAddress), TokenWithRefundsContract.notes.TokenNote.id, - tx.txHash, + txHash, ), ); @@ -111,7 +109,7 @@ describe('e2e_fees/private_refunds', () => { // 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 bobNpkMHash = t.bobWallet.getCompleteAddress().publicKeys.masterNullifierPublicKey.hash(); - const bobFeeNote = new Note([new Fr(tx.transactionFee!), bobNpkMHash, bobRandomness]); + const bobFeeNote = new Note([new Fr(transactionFee!), bobNpkMHash, bobRandomness]); // 7. 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( @@ -121,17 +119,17 @@ describe('e2e_fees/private_refunds', () => { tokenWithRefunds.address, deriveStorageSlotInMap(TokenWithRefundsContract.storage.balances.slot, t.bobAddress), TokenWithRefundsContract.notes.TokenNote.id, - tx.txHash, + txHash, ), ); // 8. 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!]); + await expectMapping(t.getGasBalanceFn, [privateFPC.address], [initialFPCGasBalance - transactionFee!]); // ... and that the transaction fee was correctly transferred from Alice to Bob. await expectMapping( t.getTokenWithRefundsBalanceFn, [aliceAddress, t.bobAddress], - [initialAliceBalance - tx.transactionFee!, initialBobBalance + tx.transactionFee!], + [initialAliceBalance - transactionFee!, initialBobBalance + transactionFee!], ); }); @@ -139,7 +137,7 @@ describe('e2e_fees/private_refunds', () => { it('insufficient funded amount is correctly handled', async () => { // 1. We generate randomness for Alice and derive randomness for Bob. const aliceRandomness = Fr.random(); // Called user_randomness in contracts - const bobRandomness = poseidon2Hash([aliceRandomness]); // Called fee_payer_randomness in contracts + const bobRandomness = siloNullifier(privateFPC.address, aliceRandomness); // Called fee_payer_randomness in contracts // 2. We call arbitrary `private_get_name(...)` function to check that the fee refund flow works. await expect( @@ -153,7 +151,7 @@ describe('e2e_fees/private_refunds', () => { aliceRandomness, bobRandomness, t.bobWallet.getAddress(), // Bob is the recipient of the fee notes. - true, // We set max fee/funded amount to zero to trigger the error. + true, // We set max fee/funded amount to 1 to trigger the error. ), }, }), @@ -195,10 +193,10 @@ class PrivateRefundPaymentMethod implements FeePaymentMethod { private feeRecipient: AztecAddress, /** - * If true, the max fee will be set to 0. + * If true, the max fee will be set to 1. * TODO(#7694): Remove this param once the lacking feature in TXE is implemented. */ - private setMaxFeeToZero = false, + private setMaxFeeToOne = false, ) {} /** @@ -221,7 +219,7 @@ class PrivateRefundPaymentMethod implements FeePaymentMethod { async getFunctionCalls(gasSettings: GasSettings): Promise { // We assume 1:1 exchange rate between fee juice and token. But in reality you would need to convert feeLimit // (maxFee) to be in token denomination. - const maxFee = this.setMaxFeeToZero ? Fr.ZERO : gasSettings.getFeeLimit(); + const maxFee = this.setMaxFeeToOne ? Fr.ONE : gasSettings.getFeeLimit(); await this.wallet.createAuthWit({ caller: this.paymentContract,