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

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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<AztecAddress>,
admin: SharedImmutable<AztecAddress>,
settings: SharedImmutable<Settings>,
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Did this change as it will allow us to drop the gate count by 4k once we optimize SharedImmutable to perfom only 1 merkle proof.

}

#[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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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<SETTINGS_LENGTH> for Settings {
fn serialize(self: Self) -> [Field; SETTINGS_LENGTH] {
[self.other_asset.to_field(), self.admin.to_field()]
}
}

impl Deserialize<SETTINGS_LENGTH> for Settings {
fn deserialize(fields: [Field; SETTINGS_LENGTH]) -> Self {
Settings {
other_asset: AztecAddress::from_field(fields[0]),
admin: AztecAddress::from_field(fields[1]),
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(...)`).
Expand Down
38 changes: 18 additions & 20 deletions yarn-project/end-to-end/src/e2e_fees/private_refunds.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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: {
Expand All @@ -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]);

Expand All @@ -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,
),
);

Expand All @@ -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(
Expand All @@ -121,25 +119,25 @@ 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!],
);
});

// TODO(#7694): Remove this test once the lacking feature in TXE is implemented.
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(
Expand All @@ -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.
),
},
}),
Expand Down Expand Up @@ -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,
) {}

/**
Expand All @@ -221,7 +219,7 @@ class PrivateRefundPaymentMethod implements FeePaymentMethod {
async getFunctionCalls(gasSettings: GasSettings): Promise<FunctionCall[]> {
// 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,
Expand Down