From 3a5dd504dd9aff3cda6943559fc2ef2dbecc31ca Mon Sep 17 00:00:00 2001 From: josh crites Date: Mon, 26 Aug 2024 15:34:47 -0400 Subject: [PATCH 1/6] add partial notes doc --- .../aztec/concepts/storage/partial_notes.md | 103 ++++++++++++++++++ .../contracts/token_contract/src/main.nr | 6 +- .../token_contract/src/types/token_note.nr | 2 + 3 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 docs/docs/aztec/concepts/storage/partial_notes.md diff --git a/docs/docs/aztec/concepts/storage/partial_notes.md b/docs/docs/aztec/concepts/storage/partial_notes.md new file mode 100644 index 00000000000..908e5f367a0 --- /dev/null +++ b/docs/docs/aztec/concepts/storage/partial_notes.md @@ -0,0 +1,103 @@ +--- +title: Partial Notes +description: Describes how partial notes are used in Aztec +tags: [notes, storage] +--- + +Partial notes are a concept that allow users to commit to an encrypted value, and allow a counterparty to update that value without knowing the specific details of the encrypted value. To do this, we can leverage the homomorphic properties of the pedersen commitment. + +Why is this useful? + +Consider the case where a user wants to pay for a transaction fee, using a [fee-payment contract](../../../protocol-specs/gas-and-fees/index.md) and they want to do this privately. They can't be certain what the transaction fee will be because the state of the network will have progressed by the time the transaction is processed by the sequencer, and transaction fees are dynamic. So the user can commit to a value for the transaction fee, publicly post this commitment, the fee payer can update the public commitment, deducting the final cost of the transaction from the commitment and returning the unused value to the user. + +So, in general, the user is: + +- doing some computation in private +- encrypting/compressing that computation with a point +- passing that point as an argument to a public function + +And the fee payer is: + +- updating that point in public +- treating/emitting the result(s) as a note hash(es) + +The idea of committing to a value and allowing a counterparty to update that value without knowing the specific details of the encrypted value is a powerful concept that can be used in many different applications. For example, this could be used for updating timestamp values in private, without revealing the exact timestamp, which could be useful for many defi applications. + +## Private Fee Payment Example + +Alice wants to use a fee-payment contract for fee abstraction, and wants to use private balances. That is, she wants to pay the FPC (fee-payment contract) some amount in an arbitrary token privately (e.g. bananas), and have the FPC pay the `transaction_fee`. + +Alice also wants to get her refund privately in the same token (e.g. bananas). + +The trouble is that the FPC doesn't know if Alice is going to run public functions, in which case it doesn't know what refund is due until the end of public execution. + +And we can't use the normal flow to create a transaction fee refund note for Alice, since that demands we have Alice's address in public. + +So we define a new type of note with its `compute_note_content_hash` defined as the x-coordinate of the following point: + +$$ +\text{amount}*G_{amount} + \text{address}*G_{address} + \text{randomness}*G_{randomness} +$$ + +Suppose Alice is willing to pay up to a set amount in bananas for her transaction. (Note, this amount gets passed into public so that when `transaction_fee` is known the FPC can verify that it isn't losing money. Wallets are expected to choose common values here, e.g. powers of 10). + +Then we can subtract the set amount from Alice's balance of private bananas, and create a point in private like: + +$$ +P_a' := \text{funded amount}*G_{amount} + \text{alice address}*G_{address} + \text{rand}_a*G_{randomness} +$$ + +Where did that $\text{rand}_a$ come from? Well from Alice of course. So we can't trust it is random. So we hash it with Alice's address and emit it as a nullifier. + +We also need to create a point for the owner of the FPC (whom we call Bob) to receive the transaction fee, which will also need randomness. + +So we compute $\text{rand}_b := h(\text{rand}_a, \text{msg_sender})$, and emit that as a nullifier (to make sure it is unique) and as an unencrypted log (so Bob can recreate his refund note after the transaction). + +$$ +P_b' := \text{bob address}*G_{address} + \text{rand}_b*G_{randomness} +$$ + +Here, the $P'$s "partially encode" the notes that we are _going to create_ for Alice and Bob. So we can use points as "Partial Notes". + +We pass these points to public, and at the end of public execution, we compute another point $P_{fee} := (\text{transaction fee}) * G_{amount}$. + +Then, we arrive at the point that corresponds to the complete note by + +$$ +P_a := P_a'-P_{fee} = (\text{funded amount} - \text{transaction fee})*G_{amount} + \text{alice address}*G_{address} +\text{rand}_a*G_{randomness} +$$ + +$$ +P_b := P_b'+P_{fee} = (\text{transaction fee})*G_{amount} + \text{bob address}*G_{address} +\text{rand}_b*G_{randomness} +$$ + +Then we just emit `P_a.x` and `P_b.x` as a note hashes in the proper slot, and we're done! Alice can reconstruct the expected note preimage since she knows the expected refund and randomness and add it to her PXE. + +How do we know the proper slot? By storing all the notes in a `PrivateSet` as opposed to a `Map`. So all the notes have the same storage slot. +See the very bottom of this doc for a link to a suggestion from Mike, that might enable the more familiar paradigm of mappings and storage slots. + +### Private Fee Payment Implementation + +[`NoteInterface.nr`](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/noir-projects/aztec-nr/aztec/src/note/note_interface.nr) implements `compute_note_hiding_point`, which takes a note and computes the point "hides" it. + +This is implemented in the example token contract: + +#include_code compute_note_hiding_point noir-projects/noir-contracts/contracts/token_contract/src/types/token_note.nr rust + +Those `G_x` are generators that generated [here](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/noir-projects/noir-projects/aztec-nr/aztec/src/generators.nr). Anyone can use them for separating different fields in a "partial note". + +We can see the complete implementation of creating and completing partial notes in an Aztec contract in the `setup_refund` and `complete_refund` functions. + +#### `setup_refund` + +#include_code setup_refund noir-projects/noir-contracts/contracts/token_contract/src/contract/token_contract.nr rust + +The `setup_refund` function sets the `complete_refund` function to be called at the end of the public function execution (`set_public_teardown_function`). This ensures that the partial notes will be completed and the fee payer will be paid and the user refund will be issued. + +#### `complete_refund` + +#include_code complete_refund noir-projects/noir-contracts/contracts/token_contract/src/contract/token_contract.nr rust + +## Future work + +This pattern of making public commitments to notes that can be modified by another party, privately, can be generalized to work with different kinds of applications. The Aztec labs team is working on adding libraries and tooling to make this easier to implement in your own contracts. 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 97e0cf031b6..f5dae9fb707 100644 --- a/noir-projects/noir-contracts/contracts/token_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/token_contract/src/main.nr @@ -503,6 +503,7 @@ contract Token { /// /// `fee_payer_point` and `user_point` above are public information because they are passed as args to the public /// `complete_refund(...)` function. + // docs:start:setup_refund #[aztec(private)] fn setup_refund( fee_payer: AztecAddress, // Address of the entity which will receive the fee note. @@ -521,7 +522,7 @@ contract Token { let user_npk_m_hash = user_keys.npk_m.hash(); // 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 + // (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. let change = subtract_balance( &mut context, @@ -573,9 +574,11 @@ contract Token { ] ); } + // docs:end:setup_refund // TODO(#7728): even though the funded_amount should be a U128, we can't have that type in a contract interface due // to serialization issues. + // docs:start:complete_refund #[aztec(public)] #[aztec(internal)] fn complete_refund( @@ -614,6 +617,7 @@ contract Token { context.push_note_hash(user_note_hash); // --> Once the tx is settled user and fee recipient can add the notes to their pixies. } + // docs:end:complete_refund /// Internal /// diff --git a/noir-projects/noir-contracts/contracts/token_contract/src/types/token_note.nr b/noir-projects/noir-contracts/contracts/token_contract/src/types/token_note.nr index 6c557043be1..1b83782f9e6 100644 --- a/noir-projects/noir-contracts/contracts/token_contract/src/types/token_note.nr +++ b/noir-projects/noir-contracts/contracts/token_contract/src/types/token_note.nr @@ -47,6 +47,7 @@ impl NoteInterface for TokenNote { poseidon2_hash_with_separator([note_hash_for_nullify, secret],GENERATOR_INDEX__NOTE_NULLIFIER) } + // docs:start:compute_note_hiding_point fn compute_note_hiding_point(self) -> Point { // We use the unsafe version because the multi_scalar_mul will constrain the scalars. let amount_scalar = from_field_unsafe(self.amount.to_integer()); @@ -62,6 +63,7 @@ impl NoteInterface for TokenNote { [amount_scalar, npk_m_hash_scalar, randomness_scalar, slot_scalar] ) } + // docs:end:compute_note_hiding_point } impl TokenNote { From fee7fa569790d1577b384b07e6ef1a18cb2f1960 Mon Sep 17 00:00:00 2001 From: josh crites Date: Tue, 27 Aug 2024 08:55:07 -0400 Subject: [PATCH 2/6] fix paths --- docs/docs/aztec/concepts/storage/partial_notes.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/docs/aztec/concepts/storage/partial_notes.md b/docs/docs/aztec/concepts/storage/partial_notes.md index 908e5f367a0..caf292c23e0 100644 --- a/docs/docs/aztec/concepts/storage/partial_notes.md +++ b/docs/docs/aztec/concepts/storage/partial_notes.md @@ -90,13 +90,13 @@ We can see the complete implementation of creating and completing partial notes #### `setup_refund` -#include_code setup_refund noir-projects/noir-contracts/contracts/token_contract/src/contract/token_contract.nr rust +#include_code setup_refund noir-projects/noir-contracts/contracts/token_contract/src/main.nr rust The `setup_refund` function sets the `complete_refund` function to be called at the end of the public function execution (`set_public_teardown_function`). This ensures that the partial notes will be completed and the fee payer will be paid and the user refund will be issued. #### `complete_refund` -#include_code complete_refund noir-projects/noir-contracts/contracts/token_contract/src/contract/token_contract.nr rust +#include_code complete_refund noir-projects/noir-contracts/contracts/token_contract/src/main.nr rust ## Future work From 1c1344393509337abb79637a9c8851f338bb5a9a Mon Sep 17 00:00:00 2001 From: benesjan Date: Wed, 28 Aug 2024 09:52:42 +0000 Subject: [PATCH 3/6] WIP --- cspell.json | 1 + .../aztec/concepts/storage/partial_notes.md | 49 ++++++++++++++++--- .../private_fpc_contract/src/main.nr | 1 + .../src/e2e_fees/private_refunds.test.ts | 2 + 4 files changed, 47 insertions(+), 6 deletions(-) diff --git a/cspell.json b/cspell.json index c84e2e3e1ff..c6cbe0fd523 100644 --- a/cspell.json +++ b/cspell.json @@ -117,6 +117,7 @@ "hasher", "headstart", "herskind", + "homomorphic", "ierc", "indexeddb", "initialise", diff --git a/docs/docs/aztec/concepts/storage/partial_notes.md b/docs/docs/aztec/concepts/storage/partial_notes.md index caf292c23e0..e397f2a7bef 100644 --- a/docs/docs/aztec/concepts/storage/partial_notes.md +++ b/docs/docs/aztec/concepts/storage/partial_notes.md @@ -4,8 +4,43 @@ description: Describes how partial notes are used in Aztec tags: [notes, storage] --- -Partial notes are a concept that allow users to commit to an encrypted value, and allow a counterparty to update that value without knowing the specific details of the encrypted value. To do this, we can leverage the homomorphic properties of the pedersen commitment. - +Partial notes are a concept that allow users to commit to an encrypted value, and allow a counterparty to update that value without knowing the specific details of the encrypted value. +To do this, we leverage the following properties of elliptic curve operations: + +1. `x_1 * G + x_2 * G` equals `(x_1 + x_2) * G` and +2. `f(x) = x * G` being a one-way function. + +Property 1 allows us to be continually adding to a point on elliptic curve and property 2 allows us to pass the point to a public realm without revealing anything about the point preimage. + +Before getting to partial notes let's recap what is the flow of standard notes. + +## Note lifecycle recap +The standard note flow is as follows: +1. Create a note in your contract, +2. compute the note hash/commitment, +3. emit the note hash, +4. emit the note (note hash preimage) as an encrypted note log, +5. sequencer picks up the transaction, includes it in a block (note hash gets included in a note hash tree) and submits the block on-chain, +6. nodes following the network pick up the new block, update its internal state and if they have accounts attached they try decrypting all the encrypted note logs, +7. if a node succeeds in decrypting a log it stores the note in its database, +8. later on when we want to spend a note, a contract obtains it via oracle and stores a note hash read request within the function context (note hash read request contains a newly computed note hash), +9. based on the note and a nullifier secret key a nullifier is computed and emitted, +10. protocol circuits check that the note is a valid note by checking that the note hash read request corresponds to a real note in the note hash tree and that the new nullifier does not yet exist in the nullifier tree, +11. if the conditions in point 10. are satisfied the nullifier is inserted into the nullifier tree and the note is at the end of its life. + +Now let's do the same for partial notes. + +## Partial notes life cycle +1. Create a partial/unfinished note in a private function of your contract --> partial here means that the values within the note are not yet considered finalized (e.g. `amount` in a `TokenNote`), +2. compute a note hiding point of the partial note using a multi scalar multiplication on an elliptic curve. For `TokenNote` this would be done as `G_amt * amount0 + G_npk * npk_m_hash + G_rnd * randomness + G_slot * slot`, +3. pass the note hiding point to a public function, +4. in a public function determine the value you want to add to the note (e.g. adding a value to an amount) and add it to the note hiding point (e.g. `NOTE_HIDING_POINT + G_amt * amount`), +5. get the note hash by finalizing the note hiding point (the note hash is the x coordinate of the point), +6. emit the note hash, +7. manually construct the note in your application and add it to your node (PXE) --> this currently has to be done manually and not automatically via encrypted note logs because we have not yet implemented partial notes delivery (tracked in [issue #8238](https://github.com/AztecProtocol/aztec-packages/issues/8238)) +8. from this point on the partial note is equal normal note and hence the rest of the life cycle is the same. + +## Use cases Why is this useful? Consider the case where a user wants to pay for a transaction fee, using a [fee-payment contract](../../../protocol-specs/gas-and-fees/index.md) and they want to do this privately. They can't be certain what the transaction fee will be because the state of the network will have progressed by the time the transaction is processed by the sequencer, and transaction fees are dynamic. So the user can commit to a value for the transaction fee, publicly post this commitment, the fee payer can update the public commitment, deducting the final cost of the transaction from the commitment and returning the unused value to the user. @@ -23,7 +58,7 @@ And the fee payer is: The idea of committing to a value and allowing a counterparty to update that value without knowing the specific details of the encrypted value is a powerful concept that can be used in many different applications. For example, this could be used for updating timestamp values in private, without revealing the exact timestamp, which could be useful for many defi applications. -## Private Fee Payment Example +### Private Fee Payment Example Alice wants to use a fee-payment contract for fee abstraction, and wants to use private balances. That is, she wants to pay the FPC (fee-payment contract) some amount in an arbitrary token privately (e.g. bananas), and have the FPC pay the `transaction_fee`. @@ -47,11 +82,13 @@ $$ P_a' := \text{funded amount}*G_{amount} + \text{alice address}*G_{address} + \text{rand}_a*G_{randomness} $$ -Where did that $\text{rand}_a$ come from? Well from Alice of course. So we can't trust it is random. So we hash it with Alice's address and emit it as a nullifier. - We also need to create a point for the owner of the FPC (whom we call Bob) to receive the transaction fee, which will also need randomness. -So we compute $\text{rand}_b := h(\text{rand}_a, \text{msg_sender})$, and emit that as a nullifier (to make sure it is unique) and as an unencrypted log (so Bob can recreate his refund note after the transaction). +So in the contract we compute $\text{rand}_b := h(\text{rand}_a, \text{msg_sender})$. + +:::warning +We need to use different randomness for Bob's note here to avoid potential privacy leak (see [description](https://github.com/AztecProtocol/aztec-packages/blob/#include_aztec_version/noir-projects/noir-contracts/contracts/token_contract/src/main.nr#L491) of `setup_refund` function) +::: $$ P_b' := \text{bob address}*G_{address} + \text{rand}_b*G_{randomness} 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 56e589c9a45..c8716b0f9d4 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 @@ -30,6 +30,7 @@ contract PrivateFPC { // 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. + // TODO(#8238): Implement proper note delivery context.push_nullifier(user_randomness); Token::at(asset).setup_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 417f83c4773..33f8cafae20 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 @@ -93,6 +93,7 @@ describe('e2e_fees/private_refunds', () => { // 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. + // TODO(#8238): Implement proper note delivery await t.aliceWallet.addNote( new ExtendedNote( aliceRefundNote, @@ -112,6 +113,7 @@ describe('e2e_fees/private_refunds', () => { 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. + // TODO(#8238): Implement proper note delivery await t.bobWallet.addNote( new ExtendedNote( bobFeeNote, From 2340fb213a2ba59b40dbbc6c75f6217e09e2d8c0 Mon Sep 17 00:00:00 2001 From: benesjan Date: Wed, 28 Aug 2024 11:46:25 +0000 Subject: [PATCH 4/6] last touches --- .../aztec/concepts/storage/partial_notes.md | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/docs/docs/aztec/concepts/storage/partial_notes.md b/docs/docs/aztec/concepts/storage/partial_notes.md index e397f2a7bef..9e48bfaedb1 100644 --- a/docs/docs/aztec/concepts/storage/partial_notes.md +++ b/docs/docs/aztec/concepts/storage/partial_notes.md @@ -4,7 +4,7 @@ description: Describes how partial notes are used in Aztec tags: [notes, storage] --- -Partial notes are a concept that allow users to commit to an encrypted value, and allow a counterparty to update that value without knowing the specific details of the encrypted value. +Partial notes are a concept that allows users to commit to an encrypted value, and allows a counterparty to update that value without knowing the specific details of the encrypted value. To do this, we leverage the following properties of elliptic curve operations: 1. `x_1 * G + x_2 * G` equals `(x_1 + x_2) * G` and @@ -15,9 +15,11 @@ Property 1 allows us to be continually adding to a point on elliptic curve and p Before getting to partial notes let's recap what is the flow of standard notes. ## Note lifecycle recap + The standard note flow is as follows: + 1. Create a note in your contract, -2. compute the note hash/commitment, +2. compute the note hash, 3. emit the note hash, 4. emit the note (note hash preimage) as an encrypted note log, 5. sequencer picks up the transaction, includes it in a block (note hash gets included in a note hash tree) and submits the block on-chain, @@ -31,6 +33,7 @@ The standard note flow is as follows: Now let's do the same for partial notes. ## Partial notes life cycle + 1. Create a partial/unfinished note in a private function of your contract --> partial here means that the values within the note are not yet considered finalized (e.g. `amount` in a `TokenNote`), 2. compute a note hiding point of the partial note using a multi scalar multiplication on an elliptic curve. For `TokenNote` this would be done as `G_amt * amount0 + G_npk * npk_m_hash + G_rnd * randomness + G_slot * slot`, 3. pass the note hiding point to a public function, @@ -38,9 +41,10 @@ Now let's do the same for partial notes. 5. get the note hash by finalizing the note hiding point (the note hash is the x coordinate of the point), 6. emit the note hash, 7. manually construct the note in your application and add it to your node (PXE) --> this currently has to be done manually and not automatically via encrypted note logs because we have not yet implemented partial notes delivery (tracked in [issue #8238](https://github.com/AztecProtocol/aztec-packages/issues/8238)) -8. from this point on the partial note is equal normal note and hence the rest of the life cycle is the same. +8. from this point on the flow of partial notes is the same as for normal notes. ## Use cases + Why is this useful? Consider the case where a user wants to pay for a transaction fee, using a [fee-payment contract](../../../protocol-specs/gas-and-fees/index.md) and they want to do this privately. They can't be certain what the transaction fee will be because the state of the network will have progressed by the time the transaction is processed by the sequencer, and transaction fees are dynamic. So the user can commit to a value for the transaction fee, publicly post this commitment, the fee payer can update the public commitment, deducting the final cost of the transaction from the commitment and returning the unused value to the user. @@ -68,10 +72,10 @@ The trouble is that the FPC doesn't know if Alice is going to run public functio And we can't use the normal flow to create a transaction fee refund note for Alice, since that demands we have Alice's address in public. -So we define a new type of note with its `compute_note_content_hash` defined as the x-coordinate of the following point: +So we define a new type of note with its `compute_note_hiding_point` defined as: $$ -\text{amount}*G_{amount} + \text{address}*G_{address} + \text{randomness}*G_{randomness} +\text{amount}*G_{amount} + \text{address}*G_{address} + \text{randomness}*G_{randomness} + \text{slot}*G_{slot} $$ Suppose Alice is willing to pay up to a set amount in bananas for her transaction. (Note, this amount gets passed into public so that when `transaction_fee` is known the FPC can verify that it isn't losing money. Wallets are expected to choose common values here, e.g. powers of 10). @@ -79,7 +83,7 @@ Suppose Alice is willing to pay up to a set amount in bananas for her transactio Then we can subtract the set amount from Alice's balance of private bananas, and create a point in private like: $$ -P_a' := \text{funded amount}*G_{amount} + \text{alice address}*G_{address} + \text{rand}_a*G_{randomness} +P_a' := \text{alice address}*G_{address} + \text{rand}_a*G_{randomness} + \text{Alice note slot}*G_{slot} $$ We also need to create a point for the owner of the FPC (whom we call Bob) to receive the transaction fee, which will also need randomness. @@ -91,27 +95,25 @@ We need to use different randomness for Bob's note here to avoid potential priva ::: $$ -P_b' := \text{bob address}*G_{address} + \text{rand}_b*G_{randomness} +P_b' := \text{bob address}*G_{address} + \text{rand}_b*G_{randomness} + \text{Bob note slot}*G_{slot} $$ Here, the $P'$s "partially encode" the notes that we are _going to create_ for Alice and Bob. So we can use points as "Partial Notes". -We pass these points to public, and at the end of public execution, we compute another point $P_{fee} := (\text{transaction fee}) * G_{amount}$. +We pass these points and the funded amount to public, and at the end of public execution, we compute tx fee point $P_{fee} := (\text{transaction fee}) * G_{amount}$ and refund point $P_{refund} := (\text{funded_amount - transaction_fee}) * G_{amount}$ Then, we arrive at the point that corresponds to the complete note by $$ -P_a := P_a'-P_{fee} = (\text{funded amount} - \text{transaction fee})*G_{amount} + \text{alice address}*G_{address} +\text{rand}_a*G_{randomness} +P_a := P_a'+P_{refund} = (\text{funded amount} - \text{transaction fee})*G_{amount} + \text{alice address}*G_{address} +\text{rand}_a*G_{randomness} + \text{Alice note slot}*G_{slot} $$ $$ -P_b := P_b'+P_{fee} = (\text{transaction fee})*G_{amount} + \text{bob address}*G_{address} +\text{rand}_b*G_{randomness} +P_b := P_b'+P_{fee} = (\text{transaction fee})*G_{amount} + \text{bob address}*G_{address} +\text{rand}_b*G_{randomness} + \text{Bob note slot}*G_{slot} $$ -Then we just emit `P_a.x` and `P_b.x` as a note hashes in the proper slot, and we're done! Alice can reconstruct the expected note preimage since she knows the expected refund and randomness and add it to her PXE. - -How do we know the proper slot? By storing all the notes in a `PrivateSet` as opposed to a `Map`. So all the notes have the same storage slot. -See the very bottom of this doc for a link to a suggestion from Mike, that might enable the more familiar paradigm of mappings and storage slots. +Then we just emit `P_a.x` and `P_b.x` as a note hashes, and we're done! +(Now Alice and Bob need to manually add the notes to their PXEs since [issue #8238](https://github.com/AztecProtocol/aztec-packages/issues/8238) remains to be implemented.) ### Private Fee Payment Implementation From 7a7716b43b31317f9c3bb8b93d8c9342e2ec4191 Mon Sep 17 00:00:00 2001 From: benesjan Date: Wed, 28 Aug 2024 12:33:08 +0000 Subject: [PATCH 5/6] desc fix --- .../contracts/token_contract/src/main.nr | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) 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 f5dae9fb707..8e99df297d0 100644 --- a/noir-projects/noir-contracts/contracts/token_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/token_contract/src/main.nr @@ -491,18 +491,22 @@ contract Token { /// We need to use different randomness for the user and for the fee payer notes because if the randomness values /// were the same we could fingerprint the user by doing the following: /// 1) randomness_influence = fee_payer_point - G_npk * fee_payer_npk = - /// = (G_npk * fee_payer_npk + G_rnd * randomness) - G_npk * fee_payer_npk = + /// = (G_npk * fee_payer_npk + G_rnd * randomness + G_slot * fee_payer_slot) + /// - G_npk * fee_payer_npk - G_slot * fee_payer_slot = /// = G_rnd * randomness /// 2) user_fingerprint = user_point - randomness_influence = - /// = (G_npk * user_npk + G_rnd * randomness) - G_rnd * randomness = - /// = G_npk * user_npk + /// = (G_npk * user_npk + G_rnd * randomness + G_slot * user_slot) - G_rnd * randomness = + /// = G_npk * user_npk + G_slot * user_slot /// 3) Then the second time the user would use this fee paying contract we would recover the same fingerprint /// and link that the 2 transactions were made by the same user. Given that it's expected that only /// a limited set of fee paying contracts will be used and they will be known, searching for fingerprints - /// by trying different fee payer npk values of these known contracts is a feasible attack. + /// by trying different fee payers is a feasible attack. /// - /// `fee_payer_point` and `user_point` above are public information because they are passed as args to the public - /// `complete_refund(...)` function. + /// Note 1: fee_payer_npk is publicly available in a key registry contract under a fee_payer address. So if we have + /// a known set of fee payer contract addresses getting fee_payer_npk and fee_payer_slot is trivial (slot + /// is derived in a `Map<...>` as a hash of balances map slot and a fee payer address). + /// Note 2: fee_payer_point and user_point above are public information because they are passed as args to + /// the public `complete_refund(...)` function. // docs:start:setup_refund #[aztec(private)] fn setup_refund( From 63450709b5f008c76b1ba963ab18e9a84ac38748 Mon Sep 17 00:00:00 2001 From: josh crites Date: Wed, 28 Aug 2024 16:10:56 -0400 Subject: [PATCH 6/6] cats feedback --- .../aztec/concepts/storage/partial_notes.md | 53 ++++++++++--------- 1 file changed, 27 insertions(+), 26 deletions(-) diff --git a/docs/docs/aztec/concepts/storage/partial_notes.md b/docs/docs/aztec/concepts/storage/partial_notes.md index 9e48bfaedb1..2ea605e50df 100644 --- a/docs/docs/aztec/concepts/storage/partial_notes.md +++ b/docs/docs/aztec/concepts/storage/partial_notes.md @@ -5,6 +5,26 @@ tags: [notes, storage] --- Partial notes are a concept that allows users to commit to an encrypted value, and allows a counterparty to update that value without knowing the specific details of the encrypted value. + +## Use cases + +Why is this useful? + +Consider the case where a user wants to pay for a transaction fee, using a [fee-payment contract](../../../protocol-specs/gas-and-fees/index.md) and they want to do this privately. They can't be certain what the transaction fee will be because the state of the network will have progressed by the time the transaction is processed by the sequencer, and transaction fees are dynamic. So the user can commit to a value for the transaction fee, publicly post this commitment, the fee payer can update the public commitment, deducting the final cost of the transaction from the commitment and returning the unused value to the user. + +So, in general, the user is: + +- doing some computation in private +- encrypting/compressing that computation with a point +- passing that point as an argument to a public function + +And the fee payer is: + +- updating that point in public +- treating/emitting the result(s) as a note hash(es) + +The idea of committing to a value and allowing a counterparty to update that value without knowing the specific details of the encrypted value is a powerful concept that can be used in many different applications. For example, this could be used for updating timestamp values in private, without revealing the exact timestamp, which could be useful for many defi applications. + To do this, we leverage the following properties of elliptic curve operations: 1. `x_1 * G + x_2 * G` equals `(x_1 + x_2) * G` and @@ -23,8 +43,8 @@ The standard note flow is as follows: 3. emit the note hash, 4. emit the note (note hash preimage) as an encrypted note log, 5. sequencer picks up the transaction, includes it in a block (note hash gets included in a note hash tree) and submits the block on-chain, -6. nodes following the network pick up the new block, update its internal state and if they have accounts attached they try decrypting all the encrypted note logs, -7. if a node succeeds in decrypting a log it stores the note in its database, +6. nodes and PXEs following the network pick up the new block, update its internal state and if they have accounts attached they search for relevant encrypted note logs, +7. if a users PXE finds a log it stores the note in its database, 8. later on when we want to spend a note, a contract obtains it via oracle and stores a note hash read request within the function context (note hash read request contains a newly computed note hash), 9. based on the note and a nullifier secret key a nullifier is computed and emitted, 10. protocol circuits check that the note is a valid note by checking that the note hash read request corresponds to a real note in the note hash tree and that the new nullifier does not yet exist in the nullifier tree, @@ -35,7 +55,7 @@ Now let's do the same for partial notes. ## Partial notes life cycle 1. Create a partial/unfinished note in a private function of your contract --> partial here means that the values within the note are not yet considered finalized (e.g. `amount` in a `TokenNote`), -2. compute a note hiding point of the partial note using a multi scalar multiplication on an elliptic curve. For `TokenNote` this would be done as `G_amt * amount0 + G_npk * npk_m_hash + G_rnd * randomness + G_slot * slot`, +2. compute a note hiding point of the partial note using a multi scalar multiplication on an elliptic curve. For `TokenNote` this would be done as `G_amt * amount0 + G_npk * npk_m_hash + G_rnd * randomness + G_slot * slot`, where each `G_` is a generator point for a specific field in the note, 3. pass the note hiding point to a public function, 4. in a public function determine the value you want to add to the note (e.g. adding a value to an amount) and add it to the note hiding point (e.g. `NOTE_HIDING_POINT + G_amt * amount`), 5. get the note hash by finalizing the note hiding point (the note hash is the x coordinate of the point), @@ -43,30 +63,11 @@ Now let's do the same for partial notes. 7. manually construct the note in your application and add it to your node (PXE) --> this currently has to be done manually and not automatically via encrypted note logs because we have not yet implemented partial notes delivery (tracked in [issue #8238](https://github.com/AztecProtocol/aztec-packages/issues/8238)) 8. from this point on the flow of partial notes is the same as for normal notes. -## Use cases - -Why is this useful? - -Consider the case where a user wants to pay for a transaction fee, using a [fee-payment contract](../../../protocol-specs/gas-and-fees/index.md) and they want to do this privately. They can't be certain what the transaction fee will be because the state of the network will have progressed by the time the transaction is processed by the sequencer, and transaction fees are dynamic. So the user can commit to a value for the transaction fee, publicly post this commitment, the fee payer can update the public commitment, deducting the final cost of the transaction from the commitment and returning the unused value to the user. - -So, in general, the user is: - -- doing some computation in private -- encrypting/compressing that computation with a point -- passing that point as an argument to a public function - -And the fee payer is: - -- updating that point in public -- treating/emitting the result(s) as a note hash(es) - -The idea of committing to a value and allowing a counterparty to update that value without knowing the specific details of the encrypted value is a powerful concept that can be used in many different applications. For example, this could be used for updating timestamp values in private, without revealing the exact timestamp, which could be useful for many defi applications. - ### Private Fee Payment Example -Alice wants to use a fee-payment contract for fee abstraction, and wants to use private balances. That is, she wants to pay the FPC (fee-payment contract) some amount in an arbitrary token privately (e.g. bananas), and have the FPC pay the `transaction_fee`. +Alice wants to use a fee-payment contract for fee abstraction, and wants to use private balances. That is, she wants to pay the FPC (fee-payment contract) some amount in an arbitrary token privately (e.g. a stablecoin), and have the FPC pay the `transaction_fee`. -Alice also wants to get her refund privately in the same token (e.g. bananas). +Alice also wants to get her refund privately in the same token (e.g. the stablecoin). The trouble is that the FPC doesn't know if Alice is going to run public functions, in which case it doesn't know what refund is due until the end of public execution. @@ -78,9 +79,9 @@ $$ \text{amount}*G_{amount} + \text{address}*G_{address} + \text{randomness}*G_{randomness} + \text{slot}*G_{slot} $$ -Suppose Alice is willing to pay up to a set amount in bananas for her transaction. (Note, this amount gets passed into public so that when `transaction_fee` is known the FPC can verify that it isn't losing money. Wallets are expected to choose common values here, e.g. powers of 10). +Suppose Alice is willing to pay up to a set amount in stablecoins for her transaction. (Note, this amount gets passed into public so that when `transaction_fee` is known the FPC can verify that it isn't losing money. Wallets are expected to choose common values here, e.g. powers of 10). -Then we can subtract the set amount from Alice's balance of private bananas, and create a point in private like: +Then we can subtract the set amount from Alice's balance of private stablecoins, and create a point in private like: $$ P_a' := \text{alice address}*G_{address} + \text{rand}_a*G_{randomness} + \text{Alice note slot}*G_{slot}