From 56992ae59b76e79bd6404e3400e640b8665e69f9 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Tue, 2 Jan 2024 08:57:59 -0300 Subject: [PATCH] docs(yellow-paper): Update keys and addresses (#3707) Keeps the specification that Mike wrote (removing app-siloed incoming viewing keys), changes private message delivery, and adds new sections on precompiles, registry, sending guidelines, diversified and stealth accounts, batched and unconstrained calls, etc. --- .../docs/addresses-and-keys/_category_.json | 8 - .../diversified-and-stealth.md | 42 ++ yellow-paper/docs/addresses-and-keys/index.md | 18 + .../docs/addresses-and-keys/precompiles.md | 164 +++++++ ...addresses-and-keys.md => specification.md} | 413 ++++-------------- yellow-paper/docs/calls/batched-calls.md | 31 ++ yellow-paper/docs/calls/delegate-calls.md | 11 + yellow-paper/docs/calls/index.md | 2 +- .../docs/calls/public_private_messaging.md | 2 +- yellow-paper/docs/calls/static-calls.md | 2 +- .../docs/calls/unconstrained-calls.md | 15 + .../private-message-delivery/_category_.json | 2 +- .../encryption-and-decryption.md | 21 +- .../note-discovery.md | 18 +- .../private-message-delivery.md | 38 +- .../docs/private-message-delivery/registry.md | 80 ++++ .../send-note-guidelines.md | 67 +++ 17 files changed, 579 insertions(+), 355 deletions(-) delete mode 100644 yellow-paper/docs/addresses-and-keys/_category_.json create mode 100644 yellow-paper/docs/addresses-and-keys/diversified-and-stealth.md create mode 100644 yellow-paper/docs/addresses-and-keys/index.md create mode 100644 yellow-paper/docs/addresses-and-keys/precompiles.md rename yellow-paper/docs/addresses-and-keys/{addresses-and-keys.md => specification.md} (63%) create mode 100644 yellow-paper/docs/calls/batched-calls.md create mode 100644 yellow-paper/docs/calls/delegate-calls.md create mode 100644 yellow-paper/docs/calls/unconstrained-calls.md create mode 100644 yellow-paper/docs/private-message-delivery/registry.md create mode 100644 yellow-paper/docs/private-message-delivery/send-note-guidelines.md diff --git a/yellow-paper/docs/addresses-and-keys/_category_.json b/yellow-paper/docs/addresses-and-keys/_category_.json deleted file mode 100644 index f76775fe0ff..00000000000 --- a/yellow-paper/docs/addresses-and-keys/_category_.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "label": "Addresses & Keys", - "position": 2, - "link": { - "type": "generated-index", - "description": "addresses & keys..." - } -} diff --git a/yellow-paper/docs/addresses-and-keys/diversified-and-stealth.md b/yellow-paper/docs/addresses-and-keys/diversified-and-stealth.md new file mode 100644 index 00000000000..b39fe8e1727 --- /dev/null +++ b/yellow-paper/docs/addresses-and-keys/diversified-and-stealth.md @@ -0,0 +1,42 @@ +--- +title: Diversified and Stealth Accounts +sidebar_position: 4 +--- + +The [keys specification](./specification.md) describes derivation mechanisms for diversified and stealth public keys. However, the protocol requires users to interact with addresses. + +## Computing Addresses + +To support diversified and stealth accounts, a user may compute the deterministic address for a given account contract that is deployed using the diversified or stealth public key, so a sender can interact with the resulting address even before the account contract is deployed. + +When the user wants to access the notes that were sent to the diversified or stealth address, they can deploy the contract at their address, and control it privately from their main account. + +## Account Contract Pseudocode + +As an example implementation, account contracts for diversified and stealth accounts can be designed to require no private constructor or state, and delegate entrypoint access control to their master address. + +``` +contract DiversifiedAccount + + private fn entrypoint(payload: action[]) + assert msg_sender == get_owner_address() + execute(payload) + + private fn is_valid(message_hash: Field) + return get_owner_address().is_valid(message_hash) + + internal private get_owner_address() + let address_preimage = pxe.get_address_preimage(this) + assert hash(address_preimage) == this + return address_preimage.deployer_address +``` + + + +Given the contract does not require initialization since it has no constructor, it can be used by its owner without being actually deployed, which reduces the setup cost. + + + +## Discarded Approaches + +An alternative approach was to introduce a new type of call, a diversified call, that would allow the caller to impersonate any address they can derive from their own, for an enshrined derivation mechanism. Account contracts could use this opcode, as opposed to a regular call, to issue calls on behalf on their diversified and stealth addresses. However, this approach failed to account for calls made back to the account contracts, in particular authwit checks. It also required protocol changes, introducing a new type of call which could be difficult to reason about, and increased attack surface. The only benefit over the approach chosen is that it would require one less extra function call to hop from the user's main account contract to the diversified or stealth one. \ No newline at end of file diff --git a/yellow-paper/docs/addresses-and-keys/index.md b/yellow-paper/docs/addresses-and-keys/index.md new file mode 100644 index 00000000000..ce710146f1b --- /dev/null +++ b/yellow-paper/docs/addresses-and-keys/index.md @@ -0,0 +1,18 @@ +--- +title: Addresses and Keys +sidebar_position: 2 +--- + +Aztec has no concept of externally-owned accounts. Every address is meant to identify a smart contract in the network. Addresses are then a commitment to a contract class, a list of constructor arguments, and a set of keys. + +Keys in Aztec are used both for authorization and privacy. Authorization keys are managed by account contracts, and not mandated by the protocol. Each account contract may use different authorization keys, if at all, with different signing mechanisms. + +Privacy keys are used for note encryption, tagging, and nullifying. These are also not enforced by the protocol. However, for facilitating composability, the protocol enshrines a set of well-known encryption and tagging mechanisms, that can be leveraged by applications as they interact with accounts. + +The [specification](./specification.md) covers the main requirements for addresses and keys, along with their specification and derivation mechanisms, while the [precompiles](./precompiles.md) section describes well-known contract addresses, with implementations defined by the protocol, used for note encryption and tagging. + +Last, the [diversified and stealth accounts](./diversified-and-stealth.md) sections describe application-level recommendations for diversified and stealth accounts. + +import DocCardList from '@theme/DocCardList'; + + diff --git a/yellow-paper/docs/addresses-and-keys/precompiles.md b/yellow-paper/docs/addresses-and-keys/precompiles.md new file mode 100644 index 00000000000..1412f9d0abb --- /dev/null +++ b/yellow-paper/docs/addresses-and-keys/precompiles.md @@ -0,0 +1,164 @@ +--- +title: Precompiles +sidebar_position: 2 +--- + +Precompiled contracts, which borrow their name from Ethereum's, are contracts not deployed by users but defined at the protocol level. These contracts and their classes are assigned well-known low-number addresses and identifiers, and their implementation is subject to change via protocol upgrades. Precompiled contracts in Aztec are implemented as a set of circuits, one for each function they expose, like user-defined private contracts. Precompiles may make use of the local PXE oracle. Note that, unlike user-defined contracts, the address of a precompiled contract instance and the identifier of its class both have no known preimage. + +Rationale for precompiled contracts is to provide a set of vetted primitives for note encryption and tagging that applications can use safely. These primitives are guaranteed to be always satisfiable when called with valid arguments. This allows account contracts to choose their preferred method of encryption and tagging from any primitive in this set, and application contracts to call into them without the risk of calling into a untrusted code, which could potentially halt the execution flow via an unsatisfiable constrain. Furthermore, by exposing these primitives in a reserved set of well-known addresses, applications can be forward-compatible and incorporate new encryption and tagging methods as accounts opt into them. + +## Constants + +- `ENCRYPTION_BATCH_SIZES=[4, 16, 32]`: Defines what max batch sizes are supported in precompiled encryption methods. +- `ENCRYPTION_PRECOMPILE_ADDRESS_RANGE=0x00..0xFFFF`: Defines the range of addresses reserved for precompiles used for encryption and tagging. +- `MAX_PLAINTEXT_LENGTH`: Defines the maximum length of a plaintext to encrypt. +- `MAX_CYPHERTEXT_LENGTH`: Defines the maximum length of a returned encrypted cyphertext. +- `MAX_TAGGED_CYPHERTEXT_LENGTH`: Defines the maximum length of a returned encrypted cyphertext prefixed with a note tag. + +## Encryption and tagging precompiles + +All precompiles in the address range `ENCRYPTION_PRECOMPILE_ADDRESS_RANGE` are reserved for encryption and tagging. Application contracts can expected to call into these contracts with note plaintext, recipients, and public keys. To facilitate forward compatibility, all unassigned addresses within the range expose the functions below as no-ops, meaning that no actions will be executed when calling into them. + +All functions in these precompiles accept a `PublicKeys` struct which contains the user advertised public keys. The structure of each of the public keys included can change from one encryption method to another, with the exception of the `nullifier_key` which is always restricted to a single field element. For forward compatibility, the precompiles interface accepts a hash of the public keys, which can be expanded within each method via an oracle call. + +``` +struct PublicKeys: + nullifier_key: Field + incoming_encryption_key: PublicKey + outgoing_encryption_key: PublicKey + incoming_internal_encryption_key: PublicKey + tagging_key: PublicKey +``` + +To identify which public key to use in the encryption, precompiles also accept an enum: + +``` +enum EncryptionType: + incoming = 1 + outgoing = 2 + incoming_internal = 3 +``` + +Precompiles expose the following private functions: + +``` +validate_keys(public_keys_hash: Field): bool +``` + +Returns true if the set of public keys represented by `public_keys` is valid for this encryption and tagging mechanism. The precompile must guarantee that any of its methods must succeed if called with a set of public keys deemed as valid. This method returns `false` for undefined precompiles. + +``` +encrypt(public_keys_hash: Field, encryption_type: EncryptionType, recipient: AztecAddress, plaintext: Field[MAX_PLAINTEXT_LENGTH]): Field[MAX_CYPHERTEXT_LENGTH] +``` + +Encrypts the given plaintext using the provided public keys, and returns the encrypted cyphertext. + +``` +encrypt_and_tag(public_keys_hash: Field, encryption_type: EncryptionType, recipient: AztecAddress, plaintext: Field[MAX_PLAINTEXT_LENGTH]): Field[MAX_TAGGED_CYPHERTEXT_LENGTH] +``` + +Encrypts and tags the given plaintext using the provided public keys, and returns the encrypted note prefixed with its tag for note discovery. + +``` +encrypt_and_broadcast(public_keys_hash: Field, encryption_type: EncryptionType, recipient: AztecAddress, plaintext: Field[MAX_PLAINTEXT_LENGTH]): Field[MAX_TAGGED_CYPHERTEXT_LENGTH] +``` + +Encrypts and tags the given plaintext using the provided public keys, broadcasts them as an event, and returns the encrypted note prefixed with its tag for note discovery. This functions should be invoked via a [delegate call](../calls/delegate-calls.md), so that the broadcasted event is emitted as if it were from the caller contract. + +``` +encrypt([call_context: CallContext, public_keys_hash: Field, encryption_type: EncryptionType, recipient: AztecAddress, plaintext: Field[MAX_PLAINTEXT_LENGTH] ][N]): Field[MAX_CYPHERTEXT_LENGTH][N] +encrypt_and_tag([call_context: CallContext, public_keys_hash: Field, encryption_type: EncryptionType, recipient: AztecAddress, plaintext: Field[MAX_PLAINTEXT_LENGTH] ][N]): Field[MAX_TAGGED_CYPHERTEXT_LENGTH][N] +encrypt_and_broadcast([call_context: CallContext, public_keys_hash: Field, encryption_type: EncryptionType, recipient: AztecAddress, plaintext: Field[MAX_PLAINTEXT_LENGTH] ][N]): Field[MAX_TAGGED_CYPHERTEXT_LENGTH][N] +``` + +Batched versions of the methods above, which accept an array of `N` tuples of public keys, recipient, and plaintext to encrypt in batch. Precompiles expose instances of this method for multiple values of `N` as defined by `ENCRYPTION_BATCH_SIZES`. Values in the batch with zeroes are skipped. These functions are intended to be used in [batched calls](../calls/batched-calls.md). + +``` +decrypt(public_keys_hash: Field, encryption_type: EncryptionType, owner: AztecAddress, cyphertext: Field[MAX_CYPHERTEXT_LENGTH]): Field[MAX_PLAINTEXT_LENGTH] +``` + +Decrypts the given cyphertext, encrypted for the provided owner. Instead of receiving the decryption key, this method triggers an oracle call to fetch the private decryption key directly from the local PXE and validates it against the supplied public key, in order to avoid leaking a user secret to untrusted application code. This method is intended for provable decryption use cases. + +## Encryption strategies + +List of encryption strategies implemented by precompiles: + +### AES128 + +Uses AES128 for encryption, by generating an AES128 symmetric key and an IV from a shared secret derived from the recipient's public key and an ephemeral keypair. Requires that the recipient's keys are points in the Grumpkin curve. The output of the encryption is the concatenation of the encrypted cyphertext and the ephemeral public key. + +Pseudocode for the encryption process: + +``` +encrypt(plaintext, recipient_public_key): + ephemeral_private_key, ephemeral_public_key = grumpkin_random_keypair() + shared_secret = recipient_public_key * ephemeral_private_key + [aes_key, aes_iv] = sha256(shared_secret ++ [0x01]) + return ephemeral_public_key ++ aes_encrypt(aes_key, aes_iv, plaintext) +``` + +Pseudocode for the decryption process: + +``` +decrypt(cyphertext, recipient_private_key): + ephemeral_public_key = cyphertext[0:64] + shared_secret = ephemeral_public_key * recipient_private_key + [aes_key, aes_iv] = sha256(shared_secret ++ [0x01]) + return aes_decrypt(aes_key, aes_iv, cyphertext[64:]) +``` + + + +## Note tagging strategies + +List of note tagging strategies implemented by precompiles: + +### Trial decryption + +Trial decryption relies on the recipient to brute-force trial-decrypting every note emitted by the chain. Every note is attempted to be decrypted with the associated decryption scheme. If decryption is successful, then the note is added to the local database. This requires no note tags to be emitted along with a note. + +In AES encryption, the plaintext is prefixed with the first 8 bytes of the IV. Decryption is deemed successful if the first 8 bytes of the decrypted plaintext matches the first 8 bytes of the IV derived from the shared secret. + +This is the cheapest approach in terms of calldata cost, and the simplest to implement, but puts a significant burden on the user. Should not be used except for accounts tied to users running full nodes. + +### Delegated trial decryption + +Delegated trial decryption relies on a tag added to each note, generated used the recipient's tagging public key. The holder of the corresponding tagging private key can trial-decrypt each tag, and if decryption is successful, proceed to decrypt the contents of the note using the associated decryption scheme. + +This allows a user to share their tagging private key with a trusted service provider, who then proceeds to trial decrypt all possible note tags on their behalf. This scheme is simple for the user, but requires trust on a third party. + + + +### Tag hopping + +Tag hopping relies on establishing a one-time shared secret through a handshake between each sender-recipient pair, advertise the handshake through a trial-decrypted brute-forced channel, and then generate tags by combining the shared secret and an incremental counter. Recipients need to trial-decrypt events emitted by a canonical `Handshake` contract to detect new channels established with them, and then scan for the next tag for each open channel. Note that the handshake contract leaks whenever a new shared secret has been established, but the participants of the handshake are kept hidden. + +This method requires the recipient to be continuously trial-decrypting the handshake channel, and then scanning for a number of tags equivalent to the number of handshakes they had received. While this can get to too large amounts for particularly active addresses, it is still far more efficient than trial decryption. + +When Alice wants to send a message to Bob for the first time: + +1. Alice creates a note, and calls into Bob's encryption and tagging precompile. +2. The precompile makes an oracle call to `getSharedSecret(Alice, Bob)`. +3. Alice's PXE looks up the shared secret which doesn't exist since this is their first interaction. +4. Alice's PXE generates a random shared secret, and stores it associated Bob along with `counter=1`. +5. The precompile makes a call to the `Handshake` contract that emits the shared secret, encrypted for Bob and optionally Alice. +6. The precompile computes `new_tag = hash(alice, bob, secret, counter)`, emits it as a nullifier, and prepends it to the note cyphertext before broadcasting it. + +For all subsequent messages: + +1. Alice creates a note, and calls into Bob's encryption and tagging precompile. +2. The precompile makes an oracle call to `getSharedSecret(Alice, Bob)`. +3. Alice's PXE looks up the shared secret and returns it, along with the current value for `counter`, and locally increments `counter`. +4. The precompile computes `previous_tag = hash(alice, bob, secret, counter)`, and performs a merkle membership proof for it in the nullifier tree. This ensures that tags are incremental and cannot be skipped. +5. The precompile computes `new_tag = hash(alice, bob, secret, counter + 1)`, emits it as a nullifier, and prepends it to the note cyphertext before broadcasting it. + +## Defined precompiles + +List of precompiles defined by the protocol: + +| Address | Encryption | Note Tagging | Comments | +|---------|------------|--------------|----------| +| 0x01 | Noop | Noop | Used by accounts to explicitly signal that they cannot receive encrypted payloads. Validation method returns `true` only for an empty list of public keys. All other methods return empty. | +| 0x02 | AES128 | Trial decryption | | +| 0x03 | AES128 | Delegated trial decryption | | +| 0x04 | AES128 | Tag hopping | | diff --git a/yellow-paper/docs/addresses-and-keys/addresses-and-keys.md b/yellow-paper/docs/addresses-and-keys/specification.md similarity index 63% rename from yellow-paper/docs/addresses-and-keys/addresses-and-keys.md rename to yellow-paper/docs/addresses-and-keys/specification.md index 3c2948042fc..1fb9d5c262a 100644 --- a/yellow-paper/docs/addresses-and-keys/addresses-and-keys.md +++ b/yellow-paper/docs/addresses-and-keys/specification.md @@ -1,5 +1,7 @@ --- +title: Specification sidebar_position: 1 +description: Specification of address format in the protocol, default privacy keys format and derivation, and nullifier derivation. --- $$ @@ -19,6 +21,7 @@ $$ \gdef\address{\color{green}{address}} \gdef\codehash{\color{green}{code\_hash}} \gdef\constructorhash{\color{green}{constructor\_hash}} +\gdef\classid{\color{green}{class\id}} \gdef\nskapp{\color{red}{nsk_{app}}} @@ -44,11 +47,16 @@ $$ \gdef\Ivpkappd{\color{violet}{Ivpk_{app,d}}} \gdef\shareableIvpkappd{\color{violet}{\widetilde{Ivpk_{app,d}}}} +\gdef\Ivpkmd{\color{violet}{Ivpk_{m,d}}} +\gdef\shareableIvpkmd{\color{violet}{\widetilde{Ivpk_{m,d}}}} \gdef\ivskappstealth{\color{red}{ivsk_{app,stealth}}} \gdef\Ivpkappdstealth{\color{violet}{Ivpk_{app,d,stealth}}} \gdef\Pkappdstealth{\color{violet}{Pk_{app,d,stealth}}} +\gdef\ivskmstealth{\color{red}{ivsk_{m,stealth}}} +\gdef\Ivpkmdstealth{\color{violet}{Ivpk_{m,d,stealth}}} +\gdef\Pkmdstealth{\color{violet}{Pk_{m,d,stealth}}} \gdef\hstealth{\color{violet}{h_{stealth}}} @@ -84,39 +92,25 @@ $$ $$ -# Proposal: Aztec Keys - ## Requirements for Keys :::info Disclaimer This is a draft. These requirements need to be considered by the wider team, and might change significantly before a mainnet release. ::: -:::info Aim -This document informally illustrates our latest thinking relating to how keys could be derived for users of Aztec. - -Its main purpose is to bring people up-to-speed on our requirements / intentions, and to outline a partial solution that we've been considering ("partial", because it doesn't quite meet all of the requirements!). - -It's by no means a finished proposal, and indeed there might be an altogether better approach. - -Maybe there are better hierarchical key derivation schemes that we're not aware of, that meet all of these requirements, and perhaps give even more features! - -Hopefully, after reading (or skim-reading) this doc, you'll be caught up on what we're aiming for, how we've been trying to get there, and the problems we're still facing. +### Scenario -Much of this is heavily inspired by ZCash sapling & orchard keys. -::: +A common illustration in this document is Bob sending funds to Alice, by: -> Note: A common illustration in this document is Bob sending funds to Alice, by: -> -> - creating a "note" for her; -> - committing to the contents of that note (a "note hash"); -> - inserting that note hash into a utxo tree; -> - encrypting the contents of that note for Alice; -> - optionally encrypting the contents of that note for Bob's future reference; -> - optionally deriving an additional "tag" (a.k.a. "clue") to accompany the ciphertexts for faster note discovery; -> - broadcasting the resulting ciphertext(s) (and tag(s)); -> - optionally identifying the tags; -> - decrypting the ciphertexts; storing the note; and some time later spending (nullifying) the note. +- creating a "note" for her; +- committing to the contents of that note (a "note hash"); +- inserting that note hash into a utxo tree; +- encrypting the contents of that note for Alice; +- optionally encrypting the contents of that note for Bob's future reference; +- optionally deriving an additional "tag" (a.k.a. "clue") to accompany the ciphertexts for faster note discovery; +- broadcasting the resulting ciphertext(s) (and tag(s)); +- optionally identifying the tags; +- decrypting the ciphertexts; storing the note; and some time later spending (nullifying) the note. > Note: there is nothing to stop an app and wallet from implementing its own key derivation scheme. Nevertheless, we're designing a 'canonical' scheme that most developers and wallets can use. @@ -137,25 +131,24 @@ A tx authentication secret key is arguably the most important key to keep privat - All keys must be re-derivable from a single `seed` secret. - Users must have the option of keeping this `seed` offline, e.g. in a hardware wallet, or on a piece of paper. -- All master keys (for a particular user) must be linkable to a single "user identifier" for that user. - - Notice: we don't prescribe whether this "user identifier" must be a public key, or an "address". The below protocol suggestion has ended up with an address, but that's not to say it's required. +- All master keys (for a particular user) must be linkable to a single address for that user. - For each contract, a siloed set of all secret keys MUST be derivable. - Reason: secret keys must be siloed, so that a malicious app circuit cannot access and emit (as an unencrypted event or as args to a public function) a user's master secret keys or the secret keys of other apps. -- Master _secret_ keys must not be passed into an app circuit. +- Master _secret_ keys must not be passed into an app circuit, except for precompiles. - Reason: a malicious app could broadcast these secret keys to the world. - Siloed secret keys _of other apps_ must not be passed into an app circuit. - Reason: a malicious app could broadcast these secret keys to the world. - The PXE must prevent an app from accessing master secret keys. - The PXE must prevent an app from accessing siloed secret keys that belong to another contract address. - Note: To achieve this, the PXE simulator will need to check whether the bytecode being executed (that is requesting secret keys) actually exists at the contract address. -- There must be one and only one way to derive all (current\*) master keys, and all siloed keys, for a particular "user identifier". +- There must be one and only one way to derive all (current\*) master keys, and all siloed keys, for a particular user address. - For example, a user should not be able to derive multiple different outgoing viewing keys for a single incoming viewing key (note: this was a 'bug' that was fixed between ZCash Sapling and Orchard). - - \*"current", alludes to the possibility that the protocol might wish to support rotating of keys, but only if one and only one set of keys is derivable as "current". + - \*"current", alludes to the possibility that the user might wish to support rotating keys, but only if one and only one set of keys is derivable as "current". - All app-siloed keys can all be deterministically linked back to the user's address, without leaking important secrets to the app. -#### Some security assumptions +#### Security assumptions -- The Aztec private execution client (PXE) and the kernel circuit (a core protocol circuit) can be trusted with master secret keys (_except for_ the tx authorization secret key, whose security assumptions are abstracted-away to wallet designers). +- The Aztec private execution client (PXE), precompiled contracts (vetted application circuits), and the kernel circuit (a core protocol circuit) can be trusted with master secret keys (_except for_ the tx authorization secret key, whose security assumptions are abstracted-away to wallet designers). ### Encryption and decryption @@ -168,16 +161,10 @@ Definitions (from the point of view of a user ("yourself")): **Requirements:** -- A user can derive app-siloed incoming/outgoing viewing keys. - - Reason: a malicious app or wallet could broadcast these secret keys to the world. - - Note: You might ask: "Why would a viewing SECRET key need to be passed into an app circuit at all?". It would be for use cases which need to prove attempted decryption. - - Reason: another reason for requiring app-siloed incoming/outgoing viewing SECRET keys relates to the "auditability" requirements further down the page: _"A user can optionally share "shareable" secret keys, to enable a 3rd party to decrypt [incoming/outgoing] data... for a single app... without leaking data for other apps"_. Without siloing the viewing secret keys, view-access data wouldn't be grantable on such a granular per-app basis. -- Given just a user's address and/or master public keys, other users can encrypt state changes, actions, and messages to them, via some app smart contract. - - This implies that a _siloed_ incoming viewing _public_ key should be derivable from a _master_ viewing _public_ key. - - This implication gave rise to the bip-32-style non-hardened (normal) derivation of siloed incoming viewing keys below. +- A user can derive app-siloed incoming internal and outgoing viewing keys. + - Reason: Allows users to share app-siloed keys to trusted 3rd parties such as auditors, scoped by app. + - Incoming viewing keys are not considered for siloed derivation due to the lack of a suitable public key derivation mechanism. - A user can encrypt a record of any actions, state changes, or messages, to _themselves_, so that they may re-sync their entire history of actions from their `seed`. -- If Bob's keys-used-for-encryption are leaked, it doesn't leak the details of Bob's interactions with Alice. - - Note: I'm not sure if we want this as a requirement, given that Bob can encrypt outgoing data for himself. If Bob didn't encrypt data for himself, this would be achievable. ### Nullifier keys @@ -185,8 +172,6 @@ Derivation of a nullifier is app-specific; a nullifier is just a `field` (siloed Many private application devs will choose to inject a secret "nullifier key" into a nullifier. Such a nullifier key would be tied to a user's public identifier (e.g. their address), and that identifier would be tied to the note being nullified (e.g. the note might contain that identifier). This is a common pattern in existing privacy protocols. Injecting a secret "nullifier key" in this way serves to hide what the nullifier is nullifying, and ensures the nullifier can only be derived by one person (assuming the nullifier key isn't leaked). -The only alternative to this pattern is plume nullifiers, but there are tradeoffs, so we'll continue to provide support for non-plume nullifiers. - > Note: not all nullifiers require injection of a secret _which is tied to a user's identity in some way_. Sometimes an app will need just need a guarantee that some value will be unique, and so will insert it into the nullifier tree. **Requirements:** @@ -218,21 +203,16 @@ Some app developers will wish to give users the option of sharing private transa - "Shareable" secret keys. - A user can optionally share "shareable" secret keys, to enable a 3rd party to decrypt the following data: - - Incoming data, across all apps - - Incoming data, siloed for a single app - Outgoing data, across all apps - Outgoing data, siloed for a single app - Incoming internal data, across all apps - Incoming internal data, siloed for a single app + - Incoming data, across all apps + - Incoming data, siloed for a single app, is **not** required due to lack of a suitable derivation scheme - Shareable nullifier key. - A user can optionally share a "shareable" nullifier key, which would enable a trusted 3rd party to see _when_ a particular note hash has been nullified, but would not divulge the contents of the note, or the circumstances under which the note was nullified (as such info would only be gleanable with the shareable viewing keys). - Given one (or many) shareable keys, a 3rd part MUST NOT be able to derive any of a user's other secret keys; be they shareable or non-shareable. - Further, they must not be able to derive any relationships _between_ other keys. - -:::danger -We haven't managed to meet these ^^^ requirements, yet. -::: - - No impersonation. - The sharing of any (or all) "shareable" key(s) MUST NOT enable the trusted 3rd party to perform any actions on the network, on behalf of the user. - The sharing of a "shareable" outgoing viewing secret (and a "shareable" _internal_ incoming viewing key) MUST NOT enable the trusted 3rd party to emit encrypted events that could be perceived as "outgoing data" (or internal incoming data) originating from the user. @@ -241,20 +221,6 @@ We haven't managed to meet these ^^^ requirements, yet. - A user can choose to only give outgoing data viewing rights to a 3rd party. (Gives rise to outgoing viewing keys). - A user can choose to keep interactions with themselves private and distinct from the viewability of interactions with other parties. (Gives rise to _internal_ incoming viewing keys). -Nice to haves: - -- The ability for a user to generate multiple of a particular kind of "shareable" secret key, so that view-access can be revoked on an individual-by-individual basis. -- The ability to revoke or rotate "shareable" secret keys, without having to deploy a fresh account contract. - -### Account Contract Bytecode - -**Requirements:** - -- The (current) bytecode of an account contract must be discoverable, based on the "user identifier" (probably an address). - - Reason: Although in 'private land', the king of the hill griefing problem prevents private account contract acir from being callable, there might still be uses where a _public_ function of an account contract might wish to be called by someone. -- The (current) bytecode of an account contract must therefore be 'tied to' to the "user identifier". -- The constructor arguments of an account contract must be discoverable. - ### Sending funds before deployment **Requirements:** @@ -267,13 +233,8 @@ Nice to haves: **Requirements:** -- [Nice to have]: A "tagging" keypair that enables faster brute-force identification of owned notes. - - Note: this is useful for rapid handshake discovery, but it is an optimization, and has trade-offs (such as more data to send). -- [Nice to have]: The ability to generate a sequence of tags between Alice and Bob, in line with our latest "Tag Hopping" ideas. - -Considerations: - -- There is no enshrined tagging scheme, currently. Whether to enshrine protocol functions (to enable calls to private account contract functions) or to let apps decide on tagging schemes, is an open debate. +- A user should be able to discover which notes belong to them, without having to trial-decrypt every single note broadcasted on chain. +- Users should be able to opt-in to using new note discovery mechanisms as they are made available in the protocol. #### Tag Hopping @@ -293,21 +254,17 @@ Given that this is our best-known approach, we include some requirements relatin ### Rotating keys -This is currently being treated as 'nice to have', simply because it's difficult and causes many complexities. +- A user should be able to rotate their set of keys, without having to deploy a new account contract. + - Reason: keys can be compromised, and setting up a new identity is costly, since the user needs to migrate all their assets. Rotating encryption keys allows the user to regain privacy for all subsequent interactions while keeping their identity. + - This requirement causes a security risk when applied to nullifier keys. If a user can rotate their nullifier key, then the nullifier for any of their notes changes, so they can re-spend any note. Rotating nullifier keys requires the nullifier public key, or at least an identifier of it, to be stored as part of the note. Alternatively, this requirement can be removed for nullifier keys, which are not allowed to be rotated. -Nice to haves: + -- A user can rotate any of their keys, without having to deploy a new account contract. - -### Diversified Addresses - -This is a core feature of zcash keys. - -Requirement: +### Diversified Keys - Alice can derive a diversified address; a random-looking address which she can (interactively) provide to Bob, so that Bob may send her funds (and general notes). - Reason: By having the recipient derive a distinct payment address _per counterparty_, and then interactively provide that address to the sender, means that if two counterparties collude, they won't be able to convince the other that they both interacted with the same recipient. -- Random-looking addresses can be derived from a 'main' address, so that private -> public function calls don't reveal the true `msg_sender`. These random-looking addresses can be provably linked back to the 'main' address. +- Random-looking addresses can be derived from a 'main' address, so that private to public function calls don't reveal the true `msg_sender`. These random-looking addresses can be provably linked back to the 'main' address. > Note: both diversified and stealth addresses would meet this requirement. - Distributing many diversified addresses must not increase the amount of time needed to scan the blockchain (they must all share a single set of viewing keys). @@ -321,43 +278,6 @@ Requirement: > Note: both diversified and stealth addresses would meet this requirement. - Unlimited random-looking addresses can be non-interactively derived by a sender for a particular recipient, in such a way that the recipient can use one set of keys to decrypt state changes or change states which are 'owned' by that stealth address. -:::note -Problem: we can derive diversified/stealth _public keys_... but how to convert them into an _address_ (which would be important to have natural address-based semantics for managing state that is owned by a stealth/diversified address)? -::: - ---- - -:::note -There are lots of general encryption requirements which are not-yet listed here, such as preventing chosen plaintext/ciphertext attacks etc. -::: - -:::note -There are some more involved complications and considerations, which haven't all fully been considered. Relevant reading: - -- [Derivation of an ephemeral secret from a note plaintext](https://zips.z.cash/zip-0212) (includes commentary on an attack that can link two diversified addresses). -- [In-band secret distribution](https://zips.z.cash/protocol/protocol.pdf) - p143 of the zcash spec. - -::: - -## Is this final? - -No. - -The 'topology' of the key derivations (i.e. the way the derivations of the keys interrelate, if you were to draw a dependency graph) is not constraint-optimized. There might be a better 'layout'. - -Domain separation hasn't been considered in-depth. - -Ephemeral key re-use needs to be considered carefully. - -BIP-32-inspired derivations have been stripped-back: 256-bit hash instead of 512-bit, no chain code. This will need security analysis. - -The requirements themselves might be adjusted (which might affect this key scheme significantly). - -Not all of the requirements have been met, yet: - -- E.g. rotation of keys -- E.g. upgrading of bytecode - ## Notation - An upper-case first letter is used for elliptic curve points (all on the Grumpkin curve) (e.g. $\Ivpkm$). @@ -385,7 +305,7 @@ Not all of the requirements have been met, yet: ## Diagrams -For if you're short on time: + ![Alt text](images/addresses-and-keys/image-5.png) @@ -393,6 +313,8 @@ The red boxes are uncertainties, which are explained later in this doc. ## Master Keys +The protocol does not enforce the usage of any of the following keys, and does not enforce the keys to conform to a particular length or algorithm. Users are expected to pick a set of keys valid for the encryption and tagging precompile they choose for their account. + | Key | Derivation | Name | Where? | Comments | |---|---|---|---|---| @@ -404,84 +326,33 @@ $\ovskm$ | h(0x04, $\sk$) | outgoing viewing secret key | PXE* | The owner of th |||||| $\Npkm$ | $\nskm \cdot G$ | nullifier public key | | Only included so that other people can derive the user's address from some public information, in such a way that it's tied to the user's $\nskm$. $\Tpkm$ | $\tskm \cdot G$ | tagging public key | | The "tagging" key pair can be used to flag "this ciphertext is for you", without requiring decryption. | -$\Ivpkm$ | $\ivskm \cdot G$ | incoming viewing public key | | A 'sender' can use this public key to derive an app-siloed incoming viewing key $\Ivpkapp$, which can then be used to derive an ephemeral symmetric encryption key, to encrypt a plaintext for some recipient. The data is "incoming" from the pov of the recipient. | $\Ovpkm$ | $\ovskm \cdot G$ | outgoing viewing public key | | Only included so that other people can derive the user's address from some public information, in such a way that it's tied to the user's $\ovskm$. | > \*These keys could also be safely passed into the Kernel circuit, but there's no immediately obvious use, so "K" has been omitted, to make design intentions clearer. -## Address - - -| Key | Derivation | Name | Comments | -|---|---|---|---| -$\constructorhash$ | h(constructor\_args, salt) | constructor hash | A commitment to the constructor arguments used when deploying the user's account contract. | -$\codehash$ | h(bytecode, $\constructorhash$) | code hash | Binds the bytecode and constructor arguments together. -$\address$ | h($\Npkm$, $\Tpkm$, $\Ivpkm$, $\Ovpkm$, $\codehash$) | address | This isn't an optimized derivation. It's just one that works. | - -:::warning - -This address derivation is "ok". - -We're considering making $\Ivpkm$ (the incoming viewing key) the basis of an "address" on the network, instead. This is the main piece of information a counterparty will need, in order to communicate (encrypt) the contents of new private notes. + -Here are some options (not an exhaustive list) of what that might look like: - -![Alt text](images/addresses-and-keys/image-4.png) +## Address -Note: the uncertainty over whether to use hardened or non-hardened keys is discussed in a later red box in this document. +An address is computed as the hash of the following fields: -Note also: this diagram is intentionally simplistic. The details are elsewhere in this doc. + +| Field | Type | Description | +|----------|----------|----------| +| version | u8 | Version identifier. Initially one, bumped for any changes to the contract instance struct. | +| deployer_address | AztecAddress | Address of the deployer for this instance. | +| salt | Field | User-generated pseudorandom value for uniqueness. | +| contract_class_id | Field | Identifier of the contract class for this instance. | +| contract_args_hash | Field | Hash of the arguments to the constructor. | +| portal_contract_address | EthereumAddress | Optional address of the L1 portal contract. | +| public_keys | PublicKeys | Optional struct of public keys used for encryption and nullifying by this contract. | -::: +The `PublicKeys` struct can vary depending on the format of keys used by the address, but it is suggested it includes the master keys defined above: $\Npkm$, $\Tpkm$, $\Ivpkm$, $\Ovpkm$. ## Derive siloed keys -### Incoming viewing keys - -:::danger Dilemma - -We have two seemingly conflicting requirements (at least, we haven't-yet found a way to meet both requirements, but maybe there's some cryptography out there that helps). Those conflicting requirements are (rephrased slightly): - -- Given just a user's address and/or master public keys, other users can encrypt state changes, actions, and messages to that user, via some app smart contract. - - This implies that a _siloed_ incoming viewing _public_ key should be derivable from a _master_ viewing _public_ key. - - This implication gave rise to the bip-32-style non-hardened (normal) derivation of siloed incoming viewing keys below. -- A user can optionally share "shareable" secret keys, to enable a 3rd party to decrypt incoming/outgoing data _for a particular app_ (and only that app). - -Why are they conflicting? - -Well, they're conflicting in the sense that bip32 doesn't help us, at least! If we solve the first requirement by deriving siloed incoming viewing keys from master incoming viewing keys in the same way that bip-32 _non-hardened_ child keys are derived from their parent keys, then the child secret key cannot be shared with a 3rd party. That's because knowledge of a non-hardened child secret key can be used to derive the parent secret key, which would then enable all incoming viewing secret keys across all apps to be derived! - -The choices seem to be: - -- Find a hierarchical key derivation scheme that enables child public keys to be derived from parent private keys (so that "someone else" may derive "your" app-siloed incoming viewing public key). -- Drop one of the requirements. I.e. one of: - - Don't enable "someone else" to derive "your" app-siloed incoming viewing public key from the master key; or - - Don't give users granular control, to grant view access to 3rd parties on an app-by-app basis. - - I.e. don't enable app-siloed viewing secret keys to be shared; instead only enable master viewing secret keys to be shared (which would give view access to all app activity). - - This would result in many apps having to encrypt notes using a user's master incoming viewing secret key (for the shareability), and so for those apps, there would be no use for app-siloed keys. (Bear in mind, though, that some apps might still want non-shareable, siloed viewing keys!) - - Note: dropping this would also conflict with another requirement: the ability to prove correct decryption within an app circuit (because apps can't be trusted with master secret keys)! - -Tricky!!! - -For now, we show an example non-hardened derivation of an app-siloed incoming viewing keypair. - -::: - - -| Key | Derivation | Name | Where? | Comments | -|---|---|---|---|---| -$\happL$ | h($\address$, app\_address) | normal siloing key for app-specific keypair derivations | | An intermediate step in a BIP-32-esque "normal" (non-hardened) child key derivation.
Note: the "L" is a lingering artifact carried over from the BIP-32 notation (where a 512-bit hmac output is split into a left and a right part), but notice there is no corresponding "R"; as a protocol simplification we propose to not derive BIP-32 chain codes (note: the bip32 author reflects [here](https://lists.linuxfoundation.org/pipermail/bitcoin-dev/2020-October/018250.html) that _"from a cryptographic point of view... the chaincode is not needed"_.) | -$\happiv$ | h(0x03, $\happL$) | normal siloing key for an app-specific incoming viewing keypair | | An intermediate step in a BIP-32-esque "normal" (non-hardened) child key derivation. | -||||| -$\ivskapp$ | $\happiv + \ivskm$ | app-siloed incoming viewing secret key | PXE*,
Not App | -$\Ivpkapp$ | $\happiv \cdot G + \Ivpkm = \ivskapp \cdot G$ | app-siloed incoming viewing public key | - -> \*These keys could also be safely passed into the Kernel circuit, but there's no immediately obvious use, so "K" has been omitted, to make design intentions clearer. - ### Nullifier keys -An app-siloed outgoing viewing keypair is derived as a hardened child keypair of the master outgoing viewing keypair. - | Key | Derivation | Name | Where? | Comments | |---|---|---|---|---| @@ -490,6 +361,10 @@ $\Nkapp$ | $h(\nskapp)$ | Shareable nullifier key | PXE, K, T3P, App| If an app See the appendix for an alternative derivation suggestion. +### Incoming viewing keys + +The protocol does not support derivation of app-siloed incoming viewing keys. + ### Outgoing viewing keys An app-siloed outgoing viewing secret key is derived as a hardened child of the master outgoing viewing secret key. @@ -499,23 +374,17 @@ An app-siloed outgoing viewing secret key is derived as a hardened child of the |---|---|---|---|---| $\ovskapp$ | $h(\ovskm, \text{app\_address})$ | -> Note: these derivations are definitely subject to change. Design considerations include: -> -> - keeping the kernel circuit simple; -> - making the hashing / ecc operations performed by the kernel circuit as generally applicable (non-specialised) as possible, so they may be re-used by app (todo: validate this is worthwhile); -> - keeping the key derivation scheme simple, with few constraints. +This is a stripped-back non-hardened derivation. Such a hardened $\ovskapp$ may enter the app circuit, but $\ovskm$ must not, so the derivation of $\ovskapp$ must only be done in a trusted precompile contract. -This is a stripped-back non-hardened derivation. Such a hardened $\nskapp$ may enter the app circuit, but $\nskm$ must not, so the derivation of $\nskapp$ would need to be delegated by the app circuit to the kernel circuit (the assumption being that the kernel circuit is a core protocol circuit which can be trusted with this master secret). See [this later section](#derive-nullifier) for how such a computation would be delegated to the kernel circuit. +### Internal incoming viewing keys -## "Handshaking" (deriving a sequence of tags for tag-hopping) +Derivation of internal incoming viewing keys is equivalent to that of outgoing viewing keys. -### Deriving a sequence of tags between Alice and Bob across all apps (at the 'master key' level) +## Handshaking for tag-hopping -:::warning +Deriving a sequence of tags for tag-hopping. -This glosses over the problem of ensuring Bob always uses the next tag in the sequence, and doesn't repeat or skip tags. See Phil's docs for further discussion on this topic. - -::: +### Deriving a sequence of tags between Alice and Bob across all apps For Bob to derive a shared secret for Alice: @@ -524,7 +393,7 @@ For Bob to derive a shared secret for Alice: |---|---|---|---| $\esk_{hs}$ | $\stackrel{rand}{\leftarrow} \mathbb{F}$ | ephemeral secret key, for handshaking | $hs$ = handshake. $\Epk_{hs}$ | $\esk_{hs} \cdot G$ | Ephemeral public key, for handshaking | -$\sharedsecret_{m,tagging}^{Bob \rightarrow Alice}$ | $\esk_{hs} \cdot \Ivpkm$ | Shared secret, for tagging | Here, we're illustrating the derivation of a shared secret (for tagging) using _master_ keys.
App developers could instead choose to use an app-specific key $\Ivpkapp$. See the next section. +$\sharedsecret_{m,tagging}^{Bob \rightarrow Alice}$ | $\esk_{hs} \cdot \Ivpkm$ | Shared secret, for tagging | Here, we're illustrating the derivation of a shared secret (for tagging) using _master_ keys. Having derived a Shared Secret, Bob can now share it with Alice as follows: @@ -559,103 +428,44 @@ This tag can be used as the basis for note retreival schemes. Each time Bob send > The colour key isn't quite clear for $\tagg_{m,i}^{Bob \rightarrow Alice}$. It will be a publicly-broadcast piece of information, but no one should learn that it relates to Bob nor Alice (except perhaps some trusted 3rd party whom Alice has entrusted with her $\ivskm$). -> TODO: Prevent spam (where a malicious user could observe the emitted tag $\tagg_{m,i}^{Bob \rightarrow Alice}$, and re-emit it many times via some other app-contract). Perhaps this could be achieved by emitting the tag as a nullifier (although this would cause state bloat). - -> TODO: Bob can encrypt a record of this handshake, for himself, using his outgoing viewing key. - -### Deriving a sequence of tags between Alice and Bob for a single app (at the 'app key' level) - -For Bob to derive a shared secret for Alice: - - -| Thing | Derivation | Name | Comments | -|---|---|---|---| -$\esk_{hs}$ | $\stackrel{rand}{\leftarrow} \mathbb{F}$ | ephemeral secret key, for handshaking | $hs$ = handshake. -$\Epk_{hs}$ | $\esk_{hs} \cdot G$ | Ephemeral public key, for handshaking | -$\sharedsecret_{app,tagging}^{Bob \rightarrow Alice}$ | $\esk_{hs} \cdot \Ivpkapp$ | Shared secret, for tagging | Note: derivation of an app-specific tagging secret using $\Ivpkapp$ would enable a trusted 3rd party (if entrusted with $\ivskapp$) to identify Alice's notes more quickly, by observing the resulting $\tagg_{app,i}^{Bob \rightarrow Alice}$ values which would accompany each $\ciphertext$. - -Having derived a Shared Secret, Bob can now share it with Alice as follows: + - -| Thing | Derivation | Name | Comments | -|---|---|---|---| -$\Taghs$ | $\esk_{hs} \cdot \Tpkm$ | Handshake message identification tag | Note: the tagging public key $\Tpkm$ exists as an optimization, seeking to make brute-force message identification as fast as possible. In many cases, handshakes can be performed offchain via traditional web2 means, but in the case of on-chain handshakes, we have no preferred alternative over simply brute-force attempting to reconcile every 'Handshake message identification tag'. Note: this optimization reduces the recipient's work by 1 cpu-friendly hash per message (at the cost of 255-bits to broadcast a compressed encoding of $\Taghs$). We'll need to decide whether this is the right speed/communication trade-off.
Note also: the _master_ tagging key $\Tpkm$ is being used in this illustration, rather than some app-specific tagging key, to make this message identification process most efficient (otherwise the user would have to re-scan all handshakes for every app they use). | -$\esk$ | $\stackrel{rand}{\leftarrow} \mathbb{F}$ | ephemeral secret key, for encryption | TODO: perhaps just one ephemeral keypair could be used? | -$\Epk$ | $\esk \cdot G$ | Ephemeral public key, for encryption | -$\sharedsecret_{m,header}$ | $\esk \cdot \Ivpkm$ | Shared secret, for encrypting the ciphertext header. | The _master_ incoming viewing key is used here, to enable Alice to more-easily discover which contract address to use, and hence which app-specific $\ivskapp$ to use to ultimately derive the app-specific tag. | -$\hmencheader$ | h("?", $\sharedsecret_{m,header}$) | Incoming encryption key | -$\ciphertextheader$ | $enc_{\hmencheader}^{\Ivpkm}$(app_address) -$\payload$ | [$\Taghs$, $\Epk_{hs}$, $\Epk$, $\ciphertextheader$] | Payload | This can be broadcast via L1.
Curve points can be compressed in the payload. | - -Alice can identify she is the indended the handshake recipient as follows: - - -| Thing | Derivation | Name | Comments | -|---|---|---|---| -$\Taghs$ | $\tskm \cdot \Epk_{hs}$ | Handshake message identification tag | Alice can extract $\Taghs$ and $\Epk_{hs}$ from the $\payload$ and perform this scalar multiplication on _every_ handshake message. If the computed $\Taghs$ value matches that of the $\payload$, then the message is indented for Alice.
Clearly, handshake transactions will need to be identifiable as such (to save Alice time), e.g. by revealing the contract address of some canonical handshaking contract alongside the $\payload$.
Recall: this step is merely an optimization, to enable Alice to do a single scalar multiplication before moving on (in cases where she is not the intended recipient). | - -If Alice successfully identifies that she is the indended the handshake recipient, she can proceed with deriving the shared secret (for tagging) as follows: - - -| Thing | Derivation | Name | Comments | -|---|---|---|---| -$\sharedsecret_{m,header}$ | $\ivskm \cdot \Epk$ | Shared secret, for encrypting the ciphertext header | -$\hmencheader$ | h("?", $\sharedsecret_{m,header}$) | Incoming encryption key | -app_address | $decrypt_{\hmencheader}^{\ivskm}(\ciphertextheader)$ | -$\ivskapp$ | See derivations above. Use the decrypted app_address. | app-specific incoming viewing secret key | -$\sharedsecret_{app,tagging}^{Bob \rightarrow Alice}$ | $\ivskapp \cdot \Epk_{hs}$ | Shared secret, for tagging | | - -A sequence of tags can then be derived by both Alice and Bob as: - - -| Thing | Derivation | Name | Comments | -|---|---|---|---| -$\tagg_{app,i}^{Bob \rightarrow Alice}$ | $h(\sharedsecret_{app,tagging}^{Bob \rightarrow Alice}, i)$ | The i-th tag in the sequence. | | - -This tag can be used as the basis for note retreival schemes. Each time Bob sends Alice a $\ciphertext$ **for this particular app**, he can attach the next unused $\tagg_{app,i}^{Bob \rightarrow Alice}$ in the sequence. Alice - who is also able to derive the next $\tagg_{app,i}^{Bob \rightarrow Alice}$ in the sequence - can make privacy-preserving calls to a server, requesting the $\ciphertext$ associated with a particular $\tagg_{app,i}^{Bob \rightarrow Alice}$. - -> TODO: Bob can encrypt a record of this handshake, for himself, using his outgoing viewing key. - -### Deriving a sequence of tags from Bob to himself across all apps (at the 'master key' level) +### Deriving a sequence of tags from Bob to himself across all apps The benefit of Bob deriving a sequence of tags for himself, is that he can re-sync his _outgoing_ transaction data more quickly, if he ever needs to in future. -There are many ways to do this: +This can be done by either: -- Copy the approach used to derive a sequence of tags between Bob and Alice (but this time do it between Bob and Bob, and use Bob's outgoing keys). - - This would require a small modification, since we don't have app-siloed outgoing viewing _public_ keys (merely as an attempt to simplify the protocol...) -- Generate a very basic sequence of tags $\tagg_{app, i}^{Bob \rightarrow Bob} = h(\ovskapp, i)$ (at the app level) and $\tagg_{m, i}^{Bob \rightarrow Bob} = h(\ovskm, i)$. +- Copying the approach used to derive a sequence of tags between Bob and Alice (but this time do it between Bob and Bob, and use Bob's outgoing keys). +- Generating a very basic sequence of tags $\tagg_{app, i}^{Bob \rightarrow Bob} = h(\ovskapp, i)$ (at the app level) and $\tagg_{m, i}^{Bob \rightarrow Bob} = h(\ovskm, i)$. - Note: In the case of deriving app-specific sequences of tags, Bob might wish to also encrypt the app*address as a ciphertext header (and attach a master tag $\tagg*{m, i}^{Bob \rightarrow Bob}$), to remind himself of the apps that he should derive tags _for_. -- Lots of other approaches. -## Derive diversified address +## Derive diversified public keys -A Diversified Address can be derived from Alice's keys, to enhance Alice's transaction privacy. If Alice's counterparties' databases are compromised, it enables Alice to retain privacy from such leakages. Basically, Alice must personally derive and provide Bob and Charlie with random-looking addresses (for Alice). Because Alice is the one deriving these Diversified Addresses (they can _only_ be derived by Alice), if Bob and Charlie chose to later collude, they would not be able to convince each-other that they'd interacted with Alice. +A diversified public key can be derived from Alice's keys, to enhance Alice's transaction privacy. If Alice's counterparties' databases are compromised, it enables Alice to retain privacy from such leakages. Diversified public keys are used for generating diversified addresses. + +Basically, Alice must personally derive and provide Bob and Charlie with random-looking addresses (for Alice). Because Alice is the one deriving these Diversified Addresses (they can _only_ be derived by Alice), if Bob and Charlie chose to later collude, they would not be able to convince each-other that they'd interacted with Alice. This is not to be confused with 'Stealth Addresses', which 'flip' who derives: Bob and Charlie would each derive a random-looking Stealth Address for Alice. Alice would then discover her new Stealth Addresses through decryption. > All of the key information below is Alice's -Alice derives a 'diversified', app-specific incoming viewing public key, and sends it to Bob: +Alice derives a 'diversified' incoming viewing public key, and sends it to Bob: | Thing | Derivation | Name | Comments | |---|---|---|---| $\d$ | $\stackrel{rand}{\leftarrow} \mathbb{F}$ |diversifier | $\Gd$ | $\d \cdot G$ | diversified generator | -$\Ivpkappd$ | $\ivskapp \cdot \Gd$ | Diversified, app-siloed incoming viewing public key | - -> Note: _master_ keys can also be diversified; just replace $app$ with $m$ in the above table of definitions. Some data (such as an app address) might need to be encrypted into a 'ciphertext header' with a master key (so as to enable the recipient to efficiently discover which app a ciphertext originated from, so they may then derive the correct siloed keys to use to decrypt the ciphertext). +$\Ivpkmd$ | $\ivskm \cdot \Gd$ | Diversified incoming viewing public key | -> Notice: when $\d = 1$, $\Ivpkappd = \Ivpkapp$. Often, it will be unncessary to diversify the below data, but we keep $\d$ around for the most generality. +> Notice: when $\d = 1$, $\Ivpkmd = \Ivpkm$. Often, it will be unncessary to diversify the below data, but we keep $\d$ around for the most generality. ---- - -## Derive stealth address +## Derive stealth public keys > All of the key information below is Alice's -For Bob to derive a Stealth Address for Alice, Bob derives: +Stealth Public Keys are used for generating Stealth Addresses. For Bob to derive a Stealth Address for Alice, Bob derives: | Thing | Derivation | Name | Comments | @@ -664,9 +474,9 @@ $\d$ | Given by Alice | (Diversifier) | Remember, in most cases, $\d=1$ is suffi $\Gd$ | $\d \cdot G$ | (Diversified) generator | Remember, when $\d = 1$, $\Gd = G$. $\esk$ | $\stackrel{rand}{\leftarrow} \mathbb{F}$ | ephemeral secret key | $\Epkd$ | $\esk \cdot \Gd$ | (Diversified) Ephemeral public key | -$\sharedsecret_{app, stealth}$ | $\esk \cdot \Ivpkappd$ | Shared secret | -$\hstealth$ | h("?", $\sharedsecret_{app, stealth}$) | Stealth key | -$\Ivpkappdstealth$ | $\hstealth \cdot \Gd + \Ivpkappd$ | (Diversified) Stealth viewing public key | +$\sharedsecret_{m, stealth}$ | $\esk \cdot \Ivpkmd$ | Shared secret | +$\hstealth$ | h("?", $\sharedsecret_{m, stealth}$) | Stealth key | +$\Ivpkmdstealth$ | $\hstealth \cdot \Gd + \Ivpkmd$ | (Diversified) Stealth viewing public key | Having derived a Stealth Address for Alice, Bob can now share it with Alice as follows: @@ -687,13 +497,15 @@ Alice can learn about her new Stealth Address as follows. First, she would ident $\sharedsecret_{m,header}$ | $\ivskm \cdot \Epkd$ | Shared secret, for encrypting the ciphertext header | $\hmencheader$ | h("?", $\sharedsecret_{m,header}$) | Incoming encryption key | app_address | $decrypt_{\hmencheader}^{\ivskm}(\ciphertextheader)$ | -$\ivskapp$ | See derivations above. Use the decrypted app_address. | app-specific incoming viewing secret key | +$\ivskapp$ | See derivations above. Use the decrypted app_address. | app-specific incoming viewing secret key | $\sharedsecret_{app, stealth}$ | $\ivskapp \cdot \Epkd$ | $\hstealth$ | h("?", $\sharedsecret_{app, stealth}$) | $\ivskappstealth$ | $\hstealth + \ivskapp$ | $\Ivpkappdstealth$ | $\ivskappstealth \cdot \Gd$ | $\Pkappdstealth$ | $\Ivpkappdstealth$ | Alias: "Alice's Stealth Public Key" | + + ## Derive nullifier Let's assume a developer wants a nullifier of a note to be derived as: @@ -729,16 +541,14 @@ $\Npkm$ | $\nskm \cdot G$ | nullifier public key | If the kernel circuit succeeds in these calculations, then the $\Nkapp$ has been validated as having a known secret key, and belonging to the $\address$. -> Note: It's ugly. I don't like having to pass such a specific and obscure request to the kernel circuit. Having said that, the outgoing viewing keys also require a [very similar request](#doing-this-inside-an-app-circuit). - ![Alt text](images/addresses-and-keys/image.png) +See the [Appendix](#appendix) for an alternative approach. + ## Encrypt and tag an incoming message Bob wants to send Alice a private message, e.g. the contents of a note, which we'll refer to as the $\plaintext$. Bob and Alice are using a "tag hopping" scheme to help with note discovery. Let's assume they've already handshaked to establish a shared secret $\sharedsecret_{m,tagging}^{Bob \rightarrow Alice}$, from which a sequence of tags $\tagg_{m,i}^{Bob \rightarrow Alice}$ can be derived. -> Note: this illustration uses _master_ keys for tags, rather than app-specific keys for tags. App-specific keys for tags could be used instead, in which case a 'ciphertext header' wouldn't be needed for the 'app_address', since the address could be inferred from the tag. - | Thing | Derivation | Name | Comments | |---|---|---|---| @@ -757,6 +567,8 @@ $\happenc$ | h("?", $\sharedsecret_{app,enc}$) | Incoming data encryption key | $\ciphertext$ | $enc^{\Ivpkappdstealth}_{\happenc}(\plaintext)$ | Ciphertext | $\payload$ | [$\tagg_{m, i}^{Bob \rightarrow Alice}$, $\ciphertextheader$, $\ciphertext$, $\Epkdheader$, $\Epkd$] | Payload | + + Alice can learn about her new $\payload$ as follows. First, she would identify the transaction has intended for her, either by observing $\tagg_{m, i}^{Bob \rightarrow Alice}$ on-chain herself (and then downloading the rest of the payload which accompanies the tag), or by making a privacy-preserving request to a server, to retrieve the payload which accompanies the tag. Assuming the $\payload$ has been identified as Alice's, and retrieved by Alice, we proceed. > Given that the tag in this illustration was derived from Alice's master key, the tag itself doesn't convey which app_address to use, to derive the correct app-siloed incoming viewing secret key that would enable decryption of the ciphertext. So first Alice needs to decrypt the $\ciphertextheader$ using her master key: @@ -773,10 +585,6 @@ $\sharedsecret_{app, enc}$ | $\ivskappstealth \cdot \Epkd$ | Shared secret, for $\happenc$ | h("?", $\sharedsecret_{app, enc}$) | Ciphertext encryption key | $\plaintext$ | $decrypt_{\happenc}^{\ivskappstealth}(\ciphertext)$ | Plaintext | -## Encrypt and tag an internal incoming message - -TODO: describe internal key derivation - ## Encrypt and tag an outgoing message Bob wants to send himself a private message (e.g. a record of the outgoing notes that he's created for other people) which we'll refer to as the $\plaintext$. Let's assume Bob has derived a sequence of tags $\tagg_{m,i}^{Bob \rightarrow Alice}$ for himself (see earlier). @@ -786,11 +594,10 @@ Bob wants to send himself a private message (e.g. a record of the outgoing notes > Note: rather than copying the 'shared secret' approach of Bob sending to Alice, we can cut a corner (because Bob is the sender and recipient, and so knows his own secrets). > Note: if Bob has sent a private message to Alice, and he also wants to send himself a corresponding message: -> > - he can likely re-use the ephemeral keypairs for himself. > - he can include $\esk$ in the plaintext that he sends to himself, as a way of reducing the size of his $\ciphertext$ (since the $\esk$ will enable him to access all the information in the ciphertext that was sent to Alice). -> - TODO: can we use a shared public key to encrypt the $\ciphertextheader$, to reduce duplicated broadcasting of encryptions of the app_address? -> - E.g. derive a shared secret, hash it, and use that as a shared public key? + + > Note: the violet symbols should actually be orange here. @@ -801,7 +608,7 @@ $\d$ | Given by Alice | (Diversifier) | Remember, in most cases, $\d=1$ is suffi $\Gd$ | $\d \cdot G$ | (Diversified) generator | Remember, when $\d = 1$, $\Gd = G$. $\eskheader$ | $\stackrel{rand}{\leftarrow} \mathbb{F}$ | ephemeral secret key | $\Epkdheader$ | $\eskheader \cdot \Gd$ | (Diversified) Ephemeral public key | -$\hmencheader$ | h("?", $\ovskm$, $\Epkdheader$) | Header encryption key | Danger: this uses a master secret key $\ovskm$, which MUST NOT be given to an app, nor to an app circuit; it may only be given to the PXE or the Kernel circuit. See below for how we can enable an app to perform these encryption steps. | +$\hmencheader$ | h("?", $\ovskm$, $\Epkdheader$) | Header encryption key | This uses a master secret key $\ovskm$, which MUST NOT be given to an app nor to an app circuit. However, it can still be given to a trusted precompile, which can handle this derivation securely. | $\ciphertextheader$ | $enc_{\hmencheader}$(app\_address) | Ciphertext header encryption key | | ||||| $\esk$ | $\stackrel{rand}{\leftarrow} \mathbb{F}$ | ephemeral secret key | @@ -824,31 +631,11 @@ $\ovskapp$ | See derivations above. Use the decrypted app_address. | | $\happenc$ | h("?", $\ovskm$, $\Epkd$) | $\plaintext$ | $decrypt_{\happenc}(\ciphertext)$ | -> TODO: how does a user validate that they have successfully decrypted a ciphertext? Is this baked into ChaChaPoly1035, for example? + -### Doing this inside an app circuit - -Here's how an app circuit could constrain the app-siloed outgoing viewing secret key ($\ovskapp$) to be correct: - -The app circuit exposes, as public inputs, an "outgoing viewing key validation request": - - -| Thing | Derivation | Name | Comments | -|---|---|---|---| -`outgoing_viewing_key_validation_request` | app_address: app_address,
hardened_child_sk: $\nskapp$,
claimed_parent_pk: $\Npkm$ | - -The kernel circuit can then validate the request (having been given $\ovskm$ as a private input to the kernel circuit): - - -| Thing | Derivation | Name | Comments | -|---|---|---|---| -$\ovskapp$ | $h(\ovskm, \text{app\_address})$ | -$\Ovpkm$ | $\ovskm \cdot G$ | Outgoing viewing public key | -| | | | Copy-constrain $\ovskm$ with $\ovskm$. | - -If the kernel circuit succeeds in these calculations, then the $\ovskapp$ has been validated as the correct app-siled secret key for $\Ovpkm$. +## Encrypt and tag an internal incoming message -![Alt text](images/addresses-and-keys/image-3.png) +Internal incoming messages are handled analogously to outgoing messages, since in both cases the sender is the same as the recipient, who has access to the secret keys when encrypting and tagging the message. ## Acknowledgements @@ -858,12 +645,6 @@ Much of this is inspired by the [ZCash Sapling and Orchard specs](https://zips.z ### Alternative nullifier key derivation -:::note - -I'm less keen on this approach now, given that the requests being made to the kernel circuit for derivation of both app-siloed nullifier keys and outgoing viewing keys are very similar now. This below proposal would make the approaches quite different, which is less good, perhaps. - -::: - This option seeks to make a minor simplification to the operations requested of the kernel circuit: instead of "a hash, a scalar mul, and a copy-constraint", it replaces the request with "two scalar muls, and a copy-constraint". The argument being, perhaps it's cleaner and a more widely useful request, that apps might wish to make to the kernel circuit under other circumstances (although the copy-constraint is still ugly and specific). @@ -915,7 +696,7 @@ The _kernel_ circuit can then validate the `pokodl_request` (having been given t |---|---|---|---| $\Nkapp$ | $\nskapp \cdot H$ | $\Npkapp$ | $\nskapp \cdot G$ | -| | | | Copy-constrain $\nskapp$ with $\nskapp$.
This constraint makes the interface to the kernel circuit a bit uglier. The `pokodl_request` now needs to convey "and constrain the discrete logs of these two pokodl requests to be equal". Ewww. | +| | | | Copy-constrain $\nskapp$ with $\nskapp$.
This constraint makes the interface to the kernel circuit a bit uglier. The `pokodl_request` now needs to convey "and constrain the discrete logs of these two pokodl requests to be equal". | If the kernel circuit succeeds in these calculations, then the $\Nkapp$ has been validated as having a known secret key, and belonging to the $\address$. diff --git a/yellow-paper/docs/calls/batched-calls.md b/yellow-paper/docs/calls/batched-calls.md new file mode 100644 index 00000000000..f3619e02099 --- /dev/null +++ b/yellow-paper/docs/calls/batched-calls.md @@ -0,0 +1,31 @@ +--- +sidebar_position: 3 +--- + +# Batched calls + +Calls to private functions can be _batched_ instead of executed [synchronously](./sync-calls.md). When executing a batched call to a private function, the function is not executed on the spot, but enqueued for execution at the end of local execution. Once the private call stack has been emptied, all batched execution requests are grouped by target (contract and function selector), and executed via a single call to each target. + +Batched calls are implemented by pushing a `PrivateCallStackItem` with the flag `is_execution_request` into a `private_batched_queue` in the execution context, and require an oracle call to `batchPrivateFunctionCall` with the same arguments as other oracle function calls. + +Batched calls are processed by the private kernel circuit. On each kernel circuit iteration, if the private call stack is not empty, the kernel circuit pops and processes the topmost entry. Otherwise, if the batched queue is not empty, the kernel pops the first item, collects and deletes all other items with the same target, and calls into the target. Note that this allows batched calls to trigger synchronous calls. + +The arguments for the batched call are arranged in an array with one position for each individual call. Each position within the array is a nested array where the first element is the call context for that individual call, followed by the actual arguments of the call. A batched call is expected to return an array of `PrivateCircuitPublicInputs`, where each public input's call context matches the call context from the corresponding individual call. This allows batched delegate calls, where each individual call processed has a context of its own. This can be used to emit logs on behalf of multiple contracts within a single batched call. + + + +In pseudocode, the kernel circuit executes the following logic: + +``` +loop: + if next_call_stack_item = context.private_call_stack.pop(): + execute(next_call_stack_item.address, next_call_stack_item.function_selector, next_call_stack_item.arguments) + else if next_batched_call = context.private_batched_queue.pop(): + let calls = context.private_batched_queue.filter(call => call.target == target) + context.private_batched_queue.delete_many(calls) + execute(target.address, target.function_selector, calls.map(call => [call.call_context, ...call.arguments])) + else: + break +``` + +The rationale for batched calls is to minimize the number of function calls in private execution, in order to reduce total proving times. Batched calls are mostly intended for usage with note delivery precompiles, since these do not require synchronous execution, and allows for processing all notes to be encrypted and tagged with the same mechanism using the same call. Batched calls can also be used for other common functions that do not require to be executed synchronously and are likely to be invoked multiple times. \ No newline at end of file diff --git a/yellow-paper/docs/calls/delegate-calls.md b/yellow-paper/docs/calls/delegate-calls.md new file mode 100644 index 00000000000..103cc5ab48a --- /dev/null +++ b/yellow-paper/docs/calls/delegate-calls.md @@ -0,0 +1,11 @@ +--- +sidebar_position: 6 +--- + +# Delegate calls + +Delegate calls are function calls against a contract class identifier instead of an instance. Any call, synchronous or asynchronous, can be made as a delegate call. The behavior of a delegate call is to execute the function code in the specified class identifier but on the context of the current instance. This opens the door to script-like executions and upgradeable contracts. Delegate calls are based on [EIP7](https://eips.ethereum.org/EIPS/eip-7). + +At the protocol level, a delegate call is identified by a `is_delegate_call` flag in the `CircuitPublicInputs` of the `CallStackItem`. The `contract_address` field is reinterpreted as a contract class instead. When executing a delegate call, the kernel preserves the values of the `CallContext` `msgSender` and `storageContractAddress`. + +At the contract level, a caller can initiate a delegate call via a `delegateCallPrivateFunction` or `delegateCallPublicFunction` oracle call. The caller is responsible for asserting that the returned `CallStackItem` has the `is_delegate_call` flag correctly set. diff --git a/yellow-paper/docs/calls/index.md b/yellow-paper/docs/calls/index.md index fb6ad10355c..5122c700f29 100644 --- a/yellow-paper/docs/calls/index.md +++ b/yellow-paper/docs/calls/index.md @@ -4,7 +4,7 @@ title: Calls # Calls -Functions in the Aztec Network can call other functions. These calls are [synchronous](./sync-calls.md) when they they occur within private functions or within public functions, but are [enqueued](./enqueued-calls.md) when done from a private to a public function. The protocol also supports alternate call methods, such as static calls. +Functions in the Aztec Network can call other functions. These calls are [synchronous](./sync-calls.md) when they they occur within private functions or within public functions, but are [enqueued](./enqueued-calls.md) when done from a private to a public function, and optionally [batched](./batched-calls.md). The protocol also supports alternate call methods, such as [static](./static-calls.md), [delegate](./delegate-calls.md), and [unconstrained](./unconstrained-calls.md) calls. In addition to function calls, the protocol allows for communication via message-passing back-and-forth between L1 and L2, as well as from public to private functions. diff --git a/yellow-paper/docs/calls/public_private_messaging.md b/yellow-paper/docs/calls/public_private_messaging.md index 241c796df9f..9bd9dcca279 100644 --- a/yellow-paper/docs/calls/public_private_messaging.md +++ b/yellow-paper/docs/calls/public_private_messaging.md @@ -1,5 +1,5 @@ --- -sidebar_position: 5 +sidebar_position: 10 --- # Inter-Layer Calls diff --git a/yellow-paper/docs/calls/static-calls.md b/yellow-paper/docs/calls/static-calls.md index 6daff979909..b4a357fee62 100644 --- a/yellow-paper/docs/calls/static-calls.md +++ b/yellow-paper/docs/calls/static-calls.md @@ -1,5 +1,5 @@ --- -sidebar_position: 3 +sidebar_position: 5 --- # Static calls diff --git a/yellow-paper/docs/calls/unconstrained-calls.md b/yellow-paper/docs/calls/unconstrained-calls.md new file mode 100644 index 00000000000..0a569e68f9a --- /dev/null +++ b/yellow-paper/docs/calls/unconstrained-calls.md @@ -0,0 +1,15 @@ +--- +sidebar_position: 6 +--- + +# Unconstrained calls + + + +Private function calls can be executed as _unconstrained_. Unconstrained function calls execute the code at the target and return the result, but their execution is not constrained. It is responsibility of the caller to constrain the result, if needed. Unconstrained calls are a generalization of oracle function calls, where the call is not to a PXE function but to another contract. Side effects from unconstrained calls are ignored. Note that all calls executed from an unconstrained call frame will be unconstrained as well. + +Unconstrained calls are executed via a `unconstrainedCallPrivateFunction` oracle call, which accepts the same arguments as a regular `callPrivateFunction`, and return the result from the function call. Unconstrained calls are not pushed into the `private_call_stack` and do not incur in an additional kernel iteration. + +Rationale for unconstrained calls is to allows apps to consume results from functions that do not need to be provable. An example use case for unconstrained calls is unconstrained encryption and note tagging, which can be used when the sender is incentivized to ensure the recipient receives the data sent. + +Another motivation for unconstrained calls is for retrieving or computing data where the end result can be more efficiently constrained by the caller. \ No newline at end of file diff --git a/yellow-paper/docs/private-message-delivery/_category_.json b/yellow-paper/docs/private-message-delivery/_category_.json index e33e9f28dc2..35605080a4e 100644 --- a/yellow-paper/docs/private-message-delivery/_category_.json +++ b/yellow-paper/docs/private-message-delivery/_category_.json @@ -3,6 +3,6 @@ "position": 2, "link": { "type": "generated-index", - "description": "Delivering messages privately on the Aztec network..." + "description": "Private message delivery encompasses the encryption, tagging, and broadcasting of private messages on the Aztec Network." } } diff --git a/yellow-paper/docs/private-message-delivery/encryption-and-decryption.md b/yellow-paper/docs/private-message-delivery/encryption-and-decryption.md index bea51abc145..fb2ac2363bd 100644 --- a/yellow-paper/docs/private-message-delivery/encryption-and-decryption.md +++ b/yellow-paper/docs/private-message-delivery/encryption-and-decryption.md @@ -1,6 +1,25 @@ --- -sidebar_position: 2 +sidebar_position: 3 --- # Encryption and Decryption +Applications should be able to provably encrypt data for a target user, as part of private message delivery. As stated on the Keys section, we define three types of encrypted data, based on the sender and the recipient, from the perspective of a user: + +- Incoming data: data created by someone else, encrypted for and sent to the user. +- Outgoing data: data created by the user to be sent to someone else, encrypted for the user. +- Internal incoming data: data created by the user, encrypted for and sent to the user. + +Encryption mechanisms support these three types of encryption, which may rely on different keys advertised by the user. + +## Precompiles and Note Discovery + +Even though encryption is a well-solved problem, unlike note discovery, the protocol bundles both operations together for simplicity and efficiency. Most use cases call for encryption and note tagging to be executed together, so note tagging precompile contracts are expected to handle encryption as well. This allows users to choose their preferred encryption method, trading between encryption cost and bits of security. + +## Key Abstraction + +To support different kinds of encryption mechanisms, the protocol does not make any assumptions on the type of public keys advertised by each user. Validation of their public keys is handled by the precompile contract selected by the user. + +## Provable Decryption + +While provable encryption is required to guarantee correct private message delivery, provable decryption is required for disclosing activity within an application. This allows auditability and compliance use cases, as well as being able to prove that a user did not execute certain actions. To support this, encryption precompiles also allow for provable decryption. \ No newline at end of file diff --git a/yellow-paper/docs/private-message-delivery/note-discovery.md b/yellow-paper/docs/private-message-delivery/note-discovery.md index 9894f6b13c2..84dee4abe41 100644 --- a/yellow-paper/docs/private-message-delivery/note-discovery.md +++ b/yellow-paper/docs/private-message-delivery/note-discovery.md @@ -1,15 +1,23 @@ --- -sidebar_position: 3 +sidebar_position: 2 --- # Note Discovery -## Requirements - When users interact with contracts they will generate and publish encrypted notes for other network participants. In order for a user to consume notes that belong to them, they need to identify, retrieve and decrypt them. A simple, privacy-preserving approach to this would be to download all of the notes and attempt decryption. However, the total number of encrypted notes published by the network will be substantial, making it infeasible for some users to do this. Those users will want to utilize a note discovery protocol to privately identify their notes. -A number of techniques currently exist to help with this and it is a field into which a lot of research is being conducted. Therefore, our approach is not to dictate or enshrine a specific note discovery mechanism but to put in place the necessary abstractions such that users can freely choose. Additionally, through this approach we allow for integration of new or improved protocols in the future. +## Precompiles + +A number of techniques currently exist to help with note discovery and it is a field into which a lot of research is being conducted, where each technique has its tradeoffs. Therefore, our approach is not to dictate a specific note discovery mechanism, but to implement multiple options via precompiles that users can choose from. These precompiles define both encryption and tagging mechanisms, and allow constraining their correct execution. Additionally, through this approach we allow for integration of new or improved protocols in the future. + +> Note: Constraining tag generation is not solely about ensuring that the generated tag is of the correct format. It is also necessary to constrain that tags are generated in the correct sequence. A tag sequence with duplicate or missing tags makes it much more difficult for the recipient to retrieve their notes. This will likely require tags to be nullified once used. ## Tag Abstraction -When applications produce notes they will need to call a protocol defined function within the account contract of the recipient and request that a tag be generated. From the protocol's perspective, this tag will simply be a stream of bytes relevant only to the recipient's note discovery protocol. It will be up to the account contract to constrain that the correct tag has been generated and from there the protocol circuits along with the rollup contract will ensure that the tag is correctly published along with the note. \ No newline at end of file +When applications produce notes they will need to call a protocol defined function within the account contract of the recipient and request that a tag be generated. From the protocol's perspective, this tag will simply be a stream of bytes relevant only to the recipient's note discovery protocol. It will be up to the account contract to constrain that the correct tag has been generated and from there the protocol circuits along with the rollup contract will ensure that the tag is correctly published along with the note. + +## User Handshaking + +Even if Alice correctly encrypts the note she creates for Bob and generates the correct tag to go with it, how does Bob know that Alice has sent him a note? Bob's note discovery protocol may require him to speculatively 'look' for notes with the tags that Alice (and his other counterparties) have generated. If Alice and Bob know each other then they can communicate out-of-protocol. But if they have no way of interacting then the network needs to provide a mechanism by which Bob can be alerted to the need to start searching for a specific sequence of tags. + +To facilitate this we will deploy a canonical 'handshake' contract that can be used to create a private note for a recipient containing the sender's information (e.g. public key). It should only be necessary for a single handshake to take place between two users. The notes generated by this contract will be easy to identify enabling users to retrieve these notes, decrypt them and use the contents in any deterministic tag generation used by their chosen note discovery protocol. diff --git a/yellow-paper/docs/private-message-delivery/private-message-delivery.md b/yellow-paper/docs/private-message-delivery/private-message-delivery.md index 9c905c7b7d3..830edd73f69 100644 --- a/yellow-paper/docs/private-message-delivery/private-message-delivery.md +++ b/yellow-paper/docs/private-message-delivery/private-message-delivery.md @@ -4,39 +4,35 @@ sidebar_position: 1 # Private Message Delivery -## Requirements - Maintaining the core tenet of privacy within the Aztec Network imposes a number of requirements related to the transfer of notes from one user to another. If Alice executes a function that generates a note for Bob: -1. Alice will need to encrypt that note such that Bob, and only Bob is able to decrypt it. -2. Alice will need to broadcast the encrypted note so as to make it available for Bob to retrieve. -3. Alice will need to broadcast a 'tag' alongside the encrypted note. This tag must be identifiable by Bob's chosen [note discovery protocol](./note-discovery.md) but not identifiable by any third party. +1. Alice will need to **encrypt** that note such that Bob, and only Bob is able to decrypt it. +2. Alice will need to **broadcast** the encrypted note so as to make it available for Bob to retrieve. +3. Alice will need to **broadcast a 'tag'** alongside the encrypted note. This tag must be identifiable by Bob's chosen [note discovery protocol](./note-discovery.md) but not identifiable by any third party. + +## Requirements -Fulfilling these requirements will enable users to privately identify, retrieve, decrypt and consume their application notes. +- **Users must be able to choose their note tagging mechanism**. We expect improved note discovery schemes to be designed over time. The protocol should be flexible enough to accommodate them and for users to opt in to using them as they become available. This flexibility should be extensible to encryption mechanisms as well as a soft requirement. +- **Users must be able to receive notes before interacting with the network**. A user should be able to receive a note just by generating an address. It should not be necessary for them to deploy their account contract in order to receive a note. +- **Applications must be able to safely send notes to any address**. Sending a note to an account could potentially transfer control of the call to that account, allowing the account to control whether they want to accept the note or not, and potentially bricking an application, since there is no catching exceptions in private function execution. +- **Addresses must be as small as possible**. Addresses will be stored and broadcasted constantly in applications. Larger addresses means more data usage, which is the main driver for cost. Addresses must fit in at most 256 bits, or ideally a single field element. +- **Total number of function calls should be minimized**. Every function call requires an additional iteration of the private kernel circuit, which adds several seconds of proving time. +- **Encryption keys should be rotatable**. Users should be able to rotate their encryption keys in the event their private keys are compromised, so that any further interactions with apps can be private again, without having to migrate to a new account. ## Constraining Message Delivery -The network will constrain: +The protocol will allow constraining: 1. The encryption of a user's note. 2. The generation of the tag for that note. 3. The publication of that note to the correct data availability layer. -Constraining [note encryption](./encryption-and-decryption.md) and tagging will be done through protocol defined functions within a user's account contract. The advantages of this approach are: +Each app will define whether to constrain each step in private message delivery. Encryption and tagging will be done through a set of precompiled contracts, each contract offering a different mechanism, and users will advertise their preferred mechanisms in a canonical registry. + +The advantages of this approach are: -1. It enables a user to select their preferred [note discovery protocol](./note-discovery.md) and/or encryption scheme. +1. It enables a user to select their preferred [note discovery protocol](./note-discovery.md) and [encryption scheme](./encryption-and-decryption.md). 2. It ensures that notes are correctly encrypted with a user's public encryption key. 3. It ensures that notes are correctly tagged for a user's chosen [note discovery protocol](./note-discovery.md). 4. It provides scope for upgrading these functions or introducing new schemes as the field progresses. -5. It protects applications from malicious account contracts providing unprovable functions. - -> Note: Constraining tag generation is not solely about ensuring that the generated tag is of the correct format. It is also necessary to constrain that tags are generated in the correct sequence. A tag sequence with duplicate or missing tags makes it much more difficult for the recipient to retrieve their notes. This will likely require tags to be nullified once used. - -Constraining publication to the correct data availability layer will be performed via a combination of the protocol circuits and the rollup contract on L1. - -## User Handshaking - -Even if Alice correctly encrypts the note she creates for Bob and generates the correct tag to go with it, how does Bob know that Alice has sent him a note? Bob's [note discovery protocol](./note-discovery.md) may require him to speculatively 'look' for notes with the tags that Alice (and his other counterparties) have generated. If Alice and Bob know each other then they can communicate out-of-protocol. But if they have no way of interacting then the network needs to provide a mechanism by which Bob can be alerted to the need to start searching for a specific sequence of tags. - -To facilitate this we will deploy a 'handshake' contract that can be used to create a private note for a recipient containing the sender's information (e.g. public key). It should only be necessary for a single handshake to take place between two users. The notes generated by this contract will be easy to identify enabling users to retrieve these notes, decrypt them and use the contents in any deterministic tag generation used by their chosen note discovery protocol. - +5. It protects applications from malicious unprovable functions. diff --git a/yellow-paper/docs/private-message-delivery/registry.md b/yellow-paper/docs/private-message-delivery/registry.md new file mode 100644 index 00000000000..2c273e5241b --- /dev/null +++ b/yellow-paper/docs/private-message-delivery/registry.md @@ -0,0 +1,80 @@ +--- +sidebar_position: 4 +--- + +# Registry + +The protocol should allow users to express their preferences in terms of encryption and note tagging mechanisms, and also provably advertise their encryption public keys. A canonical registry contract provides an application-level solution to both problems. + +## Overview and Usage + +At the application level, a canonical singleton contract allows accounts register their public keys and their preference for encryption and tagging methods. This data is kept in public storage for anyone to check when they need to send a note to an account. + +An account can directly call the registry via a public function to set or update their public keys and encryption method. New accounts should register themselves on deployment. Alternatively, anyone can create an entry for a new account (but not update) if they show the public key and encryption method can be hashed to the address. This allows third party services to register addresses to improve usability. + +An app contract can provably read the registry during private execution via a merkle membership proof against the latest public state root. Rationale for not making a call to the registry to read is to reduce the number of function calls. When reading public state from private-land, apps must set a max-block-number for the current transaction to ensure the public state root is not more than N blocks old. This means that, if a user rotates their public key, for at most N blocks afterwards they may still receive notes encrypted using their old public key, which we consider to be acceptable. + +An app contract can also prove that an address is not registered in the registry via a non-inclusion proof, since the public state tree is implemented as an indexed merkle tree. To prevent an app from proving that an address is not registered when in fact it was registered less than N blocks ago, we implement this check as a public function. This means that the transaction may leak that an undisclosed application attempted to interact with a non-registered address but failed. + +Note that, if an account is not registered in the registry, a sender could choose to supply the public key along with the preimage of an address on-the-fly, if this preimage was shared with them off-chain. This allows a user to send notes to a recipient before the recipient has deployed their account contract. + +## Pseudocode + +The registry contract exposes functions for setting public keys and encryption methods, plus a public function for proving non-membership. Reads are meant to be done directly via storage proofs and not via calls to save on proving times. Encryption and tagging preferences are expressed via their associated precompile address. + +``` +contract Registry + + public mapping(address => { keys, precompile_address }) registry + + public fn set(keys, precompile_address) + this.do_set(msg_sender, keys, precompile_address) + + public fn set_from_preimage(address, keys, precompile_address, ...address_preimage) + assert address not in registry + assert hash(keys, precompile_address, ...address_preimage) == address + this.set(msg_sender, keys, precompile_address) + + public fn assert_non_membership(address) + assert address not in registry + + internal public fn do_set(address, keys, precompile_address) + assert precompile_address in ENCRYPTION_PRECOMPILE_ADDRESS_RANGE + assert precompile_address.validate_keys(keys) + assert keys.length < MAX_KEYS_LENGTH + registry[msg_sender] = { keys, precompile_address } +``` + +## Storage Optimizations + +The registry stores a struct for each user, which means that each entry requires multiple storage slots. Reading multiple storage slots requires multiple merkle membership proofs, which increase the total proving cost of any execution that needs access to the registry. + +To reduce the number of merkle membership proofs, the registry keeps in storage only the hash of the data stored, and emits the preimage as an unencrypted event. Nodes are expected to store these preimages, so they can be returned when clients query for the public keys for an address. Clients then prove that the preimage hashes to the commitment stored in the public data tree via a single merkle membership proof. + +Note that this optimization may also be included natively into the protocol, [pending this discussion](https://forum.aztec.network/t/storing-data-of-arbitrary-length-in-the-public-data-tree/2669). + +## Multiple Recipients per Address + +While account contracts that belong to individual users have a clear set of public keys to announce, some private contracts may be shared by a group of users, like in a multisig or an escrow contract. In these scenarios, we want all messages intended for the shared contract to actually be delivered to all participants, using the encryption method selected by each. + +This can be achieved by having the registry support multiple sets of keys and precompiles for each entry. Applications can then query the registry and obtain a list of recipients, rather than a single one. + +The registry limits multi-recipient registrations to no more than `MAX_ENTRIES_PER_ADDRESS` to prevent abuse, since this puts an additional burden on the sender, who needs to emit the same note multiple times, increasing the cost of their transaction. + +Contracts that intend to register multiple recipients should account for those recipients eventually rotating their keys. To support this, contracts should include a method to refresh the registered addresses: + +``` +contract Sample + + private address[] owners + + private fn register() + let to_register = owners.map(owner => read_registry(owner)) + registry.set(this, to_register) +``` + + + +## Discussion + +See [_Addresses, keys, and sending notes (Dec 2023 edition)_](https://forum.aztec.network/t/addresses-keys-and-sending-notes-dec-2023-edition/2633) for relevant discussions on this topic. \ No newline at end of file diff --git a/yellow-paper/docs/private-message-delivery/send-note-guidelines.md b/yellow-paper/docs/private-message-delivery/send-note-guidelines.md new file mode 100644 index 00000000000..2b48c7955e2 --- /dev/null +++ b/yellow-paper/docs/private-message-delivery/send-note-guidelines.md @@ -0,0 +1,67 @@ +--- +sidebar_position: 5 +--- + +# Guidelines + +Application contracts are in control of creating, encrypting, tagging, and broadcasting private notes to users. As such, each application is free to follow whatever scheme it prefers, choosing to override user preferences or use custom encryption and note tagging mechanisms. However, this may hinder composability, or not be compatible with existing wallet software. + +In order to satisfy the requirements established for private message delivery, we suggest the following guidelines when building applications, which leverage the canonical [registry](./registry.md) contract. + +## Provably Sending a Note + +To provably encrypt, tag, and send a note to a recipient, applications should first check the registry. This ensures that the latest preferences for the recipient are honored, in case they rotated their keys. The registry should be queried via a direct storage read and not a function call, in order to save an additional recursion which incurs in extra proving time. + +If the recipient is not in the registry, then the app should allow the sender to provide the recipient's public key from the recipient's address preimage. This allows users who have never interacted with the chain to receive encrypted notes, though it requires a collaborative sender. + +If the user is not in the registry and the sender cannot provide the address preimage, then the application must prove that the user was not in the registry, or a malicious sender could simply not submit a correct merkle membership proof for the read and grief the recipient. In this scenario, it is strongly recommended that the application skips the note for the recipient as opposed to failing. This prevents an unregistered address from accidentally or maliciously bricking an application, if there is a note delivery to them in a critical code path in the application. + +Execution of the precompile that implements the recipient's choice for encryption and tagging should be done using a batched delegated call, to amortize the cost of sending multiple notes using the same method, and to ensure the notes are broadcasted from the application contract's address. + +## Pseudocode + +The following pseudocode covers how to provably send a note to a recipient, given an `encryption_type` (incoming, outgoing, or internal incoming). Should the registry support [multiple entries for a given recipient](./registry.md#multiple-recipients-per-address), this method must execute a batched call per each entry recovered from the registry. + +``` +fn provably_send_note(recipient, note, encryption_type) + + let block_number = context.latest_block_number + let public_state_root = context.roots[block_number].public_state + let storage_slot = calculate_slot(registry_address, registry_base_slot, recipient) + + let public_keys, precompile_address + if storage_slot in public_state_root + context.update_tx_max_valid_block_number(block_number + N) + public_keys, precompile_address = indexed_merkle_read(public_state_root, storage_slot) + else if recipient in pxe_oracle + address_preimage = pxe_oracle.get_preimage(recipient) + assert hash(address_preimage) == recipient + public_keys, precompile_address = address_preimage + else + registry_address.assert_non_membership(recipient) + return + + batch_private_delegate_call(precompile_address.encrypt_and_broadcast, { public_keys, encryption_type, recipient, note }) +``` + +## Unconstrained Message Delivery + +Applications may choose not to constrain proper message delivery, based on their requirements. In this case, the guidelines are the same as above, but without constraining correct execution, and without the need to assert non-membership when the recipient is not in the registry. Apps can achieve this by issuing a synchronous [unconstrained call](../calls//unconstrained-calls.md) to the encryption precompile `encrypt_and_tag` function, and emitting the resulting encrypted note. + +This flexibility is useful in scenarios where the sender can be trusted to make its best effort so the recipient receives their private messages, since it reduces total proving time. An example is a standalone direct value transfer, where the sender wants the recipient to access the funds sent to them. + +## Delivering Messages for Self + +Applications may encrypt, tag, and broadcast messages for the same user who's initiating a transaction, using the outgoing or the incoming internal encryption key. This allows a user to have an on-chain backup of their private transaction history, which they can use to recover state in case they lose their private database. In this scenario, unconstrained message delivery is recommended, since the sender is incentivized to correctly encrypt message for themselves. + +Applications may also choose to query the user wallet software via an oracle call, so the wallet can decide whether to broadcast the note to self on chain based on user preferences. This allows users to save on gas costs by avoiding unnecessary note broadcasts if they rely on other backup strategies. + +Last, applications with strong compliance and auditability requirements may choose to enforce provable encryption, tagging, and delivery to the sender user. This ensures that all user activity within the application is stored on-chain, so the user can later provably disclose their activity or repudiate actions they did not take. + +## Delivering Messages to Multiple Recipients via Shared Secrets + +As an alternative to registering [multiple recipients for a given address](./registry.md#multiple-recipients-per-address), multisig participants may deploy a contract using a shared secret derived among them. This makes it cheaper to broadcast messages to the group, since every note does not need to be individually encrypted for each of them. However, it forces all recipients in the group to use the same encryption and tagging method, and adds an extra address they need to monitor for note discovery. + +## Discussions + +See [_Addresses, keys, and sending notes (Dec 2023 edition)_](https://forum.aztec.network/t/addresses-keys-and-sending-notes-dec-2023-edition/2633) and [_Broadcasting notes in token contracts_](https://forum.aztec.network/t/broadcasting-notes-in-token-contracts/2658) for relevant discussions on this topic. \ No newline at end of file