Skip to content

Commit

Permalink
feat: optimizing PrivateFPC (#7980)
Browse files Browse the repository at this point in the history
  • Loading branch information
benesjan authored Aug 15, 2024
1 parent 2dbb9e8 commit d018335
Show file tree
Hide file tree
Showing 4 changed files with 53 additions and 43 deletions.

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>,
}

#[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]),
}
}
}
30 changes: 14 additions & 16 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 Down

0 comments on commit d018335

Please sign in to comment.