From e14d3561d0e499804063094f7887c5671132a54f Mon Sep 17 00:00:00 2001 From: Alex Gherghisan Date: Mon, 4 Mar 2024 10:22:47 +0000 Subject: [PATCH] feat: pay fees and get refund privately --- .../contracts/fpc_contract/src/interfaces.nr | 34 +++++++++---- .../contracts/fpc_contract/src/main.nr | 17 +++++-- .../contracts/token_contract/src/main.nr | 8 +-- .../src/fee/private_fee_payment_method.ts | 2 +- yarn-project/end-to-end/src/e2e_fees.test.ts | 50 +++++++++++++++++-- .../src/e2e_partial_token_notes.test.ts | 12 ++--- 6 files changed, 94 insertions(+), 29 deletions(-) diff --git a/noir-projects/noir-contracts/contracts/fpc_contract/src/interfaces.nr b/noir-projects/noir-contracts/contracts/fpc_contract/src/interfaces.nr index 0c49a06111f..36522c325f2 100644 --- a/noir-projects/noir-contracts/contracts/fpc_contract/src/interfaces.nr +++ b/noir-projects/noir-contracts/contracts/fpc_contract/src/interfaces.nr @@ -11,34 +11,46 @@ impl Token { Self { address } } - pub fn transfer_public( + pub fn split_into_partial_notes_pair( self: Self, - context: PublicContext, + context: &mut PrivateContext, from: AztecAddress, to: AztecAddress, amount: Field, nonce: Field + ) -> [Field; RETURN_VALUES_LENGTH] { + context.call_private_function( + self.address, + FunctionSelector::from_signature("split_into_partial_notes_pair((Field),(Field),Field,Field)"), + [from.to_field(), to.to_field(), amount, nonce] + ) + } + + pub fn complete_partial_notes_pair( + self: Self, + context: &mut PublicContext, + partial_note_hashes: [Field; 2], + amounts: [Field; 2] ) { let _ = context.call_public_function( self.address, - FunctionSelector::from_signature("transfer_public((Field),(Field),Field,Field)"), - [from.to_field(), to.to_field(), amount, nonce] + FunctionSelector::from_signature("complete_partial_notes_pair([Field;2],[Field;2])"), + [partial_note_hashes[0], partial_note_hashes[1], amounts[0], amounts[1]] ); } - // Private - pub fn unshield( + pub fn transfer_public( self: Self, - context: &mut PrivateContext, + context: PublicContext, from: AztecAddress, to: AztecAddress, amount: Field, nonce: Field - ) -> [Field; RETURN_VALUES_LENGTH] { - context.call_private_function( + ) { + let _ = context.call_public_function( self.address, - FunctionSelector::from_signature("unshield((Field),(Field),Field,Field)"), + FunctionSelector::from_signature("transfer_public((Field),(Field),Field,Field)"), [from.to_field(), to.to_field(), amount, nonce] - ) + ); } } diff --git a/noir-projects/noir-contracts/contracts/fpc_contract/src/main.nr b/noir-projects/noir-contracts/contracts/fpc_contract/src/main.nr index 5bc8535adc4..21c8cb72547 100644 --- a/noir-projects/noir-contracts/contracts/fpc_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/fpc_contract/src/main.nr @@ -32,7 +32,7 @@ contract FPC { fn fee_entrypoint_private(amount: Field, asset: AztecAddress, nonce: Field) { assert(asset == storage.other_asset.read_private()); - let _res = Token::at(asset).unshield( + let partial_note_hashes = Token::at(asset).split_into_partial_notes_pair( &mut context, context.msg_sender(), context.this_address(), @@ -42,11 +42,22 @@ contract FPC { let _void = context.call_public_function( context.this_address(), - FunctionSelector::from_signature("pay_fee((Field),Field,(Field))"), - [context.msg_sender().to_field(), amount, asset.to_field()] + FunctionSelector::from_signature("pay_fee_with_partial_notes(Field,(Field),[Field;2])"), + [amount, asset.to_field(), partial_note_hashes[0], partial_note_hashes[1]] ); } + #[aztec(public)] + internal fn pay_fee_with_partial_notes(amount: Field, asset: AztecAddress, partial_note_hashes: [Field; 2]) { + let refund = context.call_public_function( + storage.fee_asset.read_public(), + FunctionSelector::from_signature("pay_fee(Field)"), + [amount] + )[0]; + + Token::at(asset).complete_partial_notes_pair(&mut context, partial_note_hashes, [refund, amount - refund]) + } + #[aztec(private)] fn fee_entrypoint_public(amount: Field, asset: AztecAddress, nonce: Field) { let _void = context.call_public_function( diff --git a/noir-projects/noir-contracts/contracts/token_contract/src/main.nr b/noir-projects/noir-contracts/contracts/token_contract/src/main.nr index c255ff6083d..e5a4a8bdc41 100644 --- a/noir-projects/noir-contracts/contracts/token_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/token_contract/src/main.nr @@ -390,7 +390,7 @@ contract Token { // docs:end:balance_of_public #[aztec(private)] - fn split_to_partial_notes(from: AztecAddress, to: AztecAddress, amount: Field, nonce: Field) -> [Field; 2] { + fn split_into_partial_notes_pair(from: AztecAddress, to: AztecAddress, amount: Field, nonce: Field) -> [Field; 2] { if (!from.eq(context.msg_sender())) { assert_current_call_valid_authwit(&mut context, from); } else { @@ -411,7 +411,7 @@ contract Token { context.call_public_function( context.this_address(), - FunctionSelector::from_signature("auth_partial_notes((Field))"), + FunctionSelector::from_signature("auth_partial_notes_pair((Field))"), [hash.to_field()] ); @@ -422,12 +422,12 @@ contract Token { } #[aztec(public)] - internal fn auth_partial_notes(hash: PartialNotesHash) { + internal fn auth_partial_notes_pair(hash: PartialNotesHash) { storage.partial_notes.at(hash).write(true); } #[aztec(public)] - fn complete_partial_notes(partial_note_hashes: [Field; 2], amounts: [Field; 2]) { + fn complete_partial_notes_pair(partial_note_hashes: [Field; 2], amounts: [Field; 2]) { let hash = compute_partial_notes_pair_hash( partial_note_hashes[0], partial_note_hashes[1], diff --git a/yarn-project/aztec.js/src/fee/private_fee_payment_method.ts b/yarn-project/aztec.js/src/fee/private_fee_payment_method.ts index 7539294f6a0..2245d477335 100644 --- a/yarn-project/aztec.js/src/fee/private_fee_payment_method.ts +++ b/yarn-project/aztec.js/src/fee/private_fee_payment_method.ts @@ -54,7 +54,7 @@ export class PrivateFeePaymentMethod implements FeePaymentMethod { const messageHash = computeAuthWitMessageHash(this.paymentContract, { args: [this.wallet.getAddress(), this.paymentContract, maxFee, nonce], functionData: new FunctionData( - FunctionSelector.fromSignature('unshield((Field),(Field),Field,Field)'), + FunctionSelector.fromSignature('split_into_partial_notes_pair((Field),(Field),Field,Field)'), false, true, false, diff --git a/yarn-project/end-to-end/src/e2e_fees.test.ts b/yarn-project/end-to-end/src/e2e_fees.test.ts index d6e25bb8a84..5558b70d058 100644 --- a/yarn-project/end-to-end/src/e2e_fees.test.ts +++ b/yarn-project/end-to-end/src/e2e_fees.test.ts @@ -3,12 +3,15 @@ import { ExtendedNote, Fr, FunctionSelector, + GrumpkinScalar, Note, PrivateFeePaymentMethod, TxHash, computeMessageSecretHash, + generatePublicKey, } from '@aztec/aztec.js'; import { decodeFunctionSignature } from '@aztec/foundation/abi'; +import { BufferReader } from '@aztec/foundation/serialize'; import { TokenContract as BananaCoin, FPCContract, GasTokenContract } from '@aztec/noir-contracts.js'; import { jest } from '@jest/globals'; @@ -88,9 +91,18 @@ describe('e2e_fees', () => { e2eContext.logger(`BananaCoin deployed at ${bananaCoin.address}`); - bananaFPC = await FPCContract.deploy(e2eContext.wallets[0], bananaCoin.address, gasTokenContract.address) + const fpcPrivateKey = GrumpkinScalar.random(); + bananaFPC = await FPCContract.deployWithPublicKey( + generatePublicKey(fpcPrivateKey), + e2eContext.wallets[0], + bananaCoin.address, + gasTokenContract.address, + ) .send() .deployed(); + + await e2eContext.pxe.registerAccount(fpcPrivateKey, bananaFPC.partialAddress); + e2eContext.logger(`bananaPay deployed at ${bananaFPC.address}`); await gasBridgeTestHarness.bridgeFromL1ToL2(InitialFPCGas + 1n, InitialFPCGas, bananaFPC.address); @@ -153,7 +165,7 @@ describe('e2e_fees', () => { * increase alice BC.public by RefundAmount * */ - await bananaCoin.methods + const tx = await bananaCoin.methods .mint_public(aliceAddress, MintedBananasAmount) .send({ fee: { @@ -163,15 +175,17 @@ describe('e2e_fees', () => { }) .wait(); + await completePartialTokenNotes([RefundAmount, FeeAmount], tx.txHash); + await expectMapping( bananaPrivateBalances, [aliceAddress, bananaFPC.address, sequencerAddress], - [PrivateInitialBananasAmount - MaxFee, 0n, 0n], + [PrivateInitialBananasAmount - FeeAmount, FeeAmount, 0n], ); await expectMapping( bananaPublicBalances, [aliceAddress, bananaFPC.address, sequencerAddress], - [MintedBananasAmount + RefundAmount, MaxFee - RefundAmount, 0n], + [MintedBananasAmount, 0n, 0n], ); await expectMapping( gasBalances, @@ -195,4 +209,32 @@ describe('e2e_fees', () => { ); await e2eContext.wallets[accountIndex].addNote(extendedNote); }; + + const completePartialTokenNotes = async (expectedAmounts: (bigint | number)[], completionTxHash: TxHash) => { + // TODO use a dedicated event selector for these completion events once implemented + const logs = await e2eContext.pxe.getUnencryptedLogs({ txHash: completionTxHash }); + // logs[0] contains the partial note hashes created in private + const completedEvent0 = BufferReader.asReader(logs.logs[1].log.data).readArray(2, Fr); + const completedEvent1 = BufferReader.asReader(logs.logs[2].log.data).readArray(2, Fr); + + // constant value taken from the Noir contract + const tokenNoteId = new Fr(8411110710111078111116101n); + + expect(completedEvent0).toEqual([tokenNoteId, new Fr(expectedAmounts[0])]); + expect(completedEvent1).toEqual([tokenNoteId, new Fr(expectedAmounts[1])]); + + await e2eContext.pxe.completePartialNote( + bananaCoin.address, + tokenNoteId, + [[0, new Fr(expectedAmounts[0])]], + completionTxHash, + ); + + await e2eContext.pxe.completePartialNote( + bananaCoin.address, + tokenNoteId, + [[0, new Fr(expectedAmounts[1])]], + completionTxHash, + ); + }; }); diff --git a/yarn-project/end-to-end/src/e2e_partial_token_notes.test.ts b/yarn-project/end-to-end/src/e2e_partial_token_notes.test.ts index e8f9aac4f6f..bc721f0b503 100644 --- a/yarn-project/end-to-end/src/e2e_partial_token_notes.test.ts +++ b/yarn-project/end-to-end/src/e2e_partial_token_notes.test.ts @@ -41,7 +41,7 @@ describe('e2e_partial_token_notes', () => { await expect(tokenContract.methods.balance_of_private(ctx.wallets[0].getAddress()).view()).resolves.toEqual(0n); const completeNotesTx = await tokenContract.methods - .complete_partial_notes(partialNoteHashes, [400, 600]) + .complete_partial_notes_pair(partialNoteHashes, [400, 600]) .send() .wait(); @@ -71,9 +71,9 @@ describe('e2e_partial_token_notes', () => { it('partial notes are completable only once', async () => { const partialNoteHashes = await createPartialNotes(10n); - await tokenContract.methods.complete_partial_notes(partialNoteHashes, [5n, 5n]).send().wait(); + await tokenContract.methods.complete_partial_notes_pair(partialNoteHashes, [5n, 5n]).send().wait(); await expect( - tokenContract.methods.complete_partial_notes(partialNoteHashes, [5n, 5n]).send().wait(), + tokenContract.methods.complete_partial_notes_pair(partialNoteHashes, [5n, 5n]).send().wait(), ).rejects.toThrow(/was dropped/); }); @@ -81,7 +81,7 @@ describe('e2e_partial_token_notes', () => { const partialNoteHashes = await createPartialNotes(10n); await expect( - tokenContract.methods.complete_partial_notes(partialNoteHashes, [5n, 6n]).send().wait(), + tokenContract.methods.complete_partial_notes_pair(partialNoteHashes, [5n, 6n]).send().wait(), ).rejects.toThrow(/Partial notes not authorized/); }); @@ -91,7 +91,7 @@ describe('e2e_partial_token_notes', () => { await expect( tokenContract .withWallet(ctx.wallets[1]) - .methods.complete_partial_notes(partialNoteHashes, [5n, 5n]) + .methods.complete_partial_notes_pair(partialNoteHashes, [5n, 5n]) .send() .wait(), ).rejects.toThrow(/Partial notes not authorized/); @@ -99,7 +99,7 @@ describe('e2e_partial_token_notes', () => { const createPartialNotes = async (totalAmount: bigint | number) => { const { txHash } = await tokenContract.methods - .split_to_partial_notes(ctx.wallets[0].getAddress(), ctx.wallets[1].getAddress(), totalAmount, 0) + .split_into_partial_notes_pair(ctx.wallets[0].getAddress(), ctx.wallets[1].getAddress(), totalAmount, 0) .send() .wait();