# Constrained Tagging & Handshaking <style> .ui-content.comment-panel #doc, body.pretty-comment-panel #doc { max-width: 80%; } </style> :::info Author: Mike June 2025 ::: > Lots of people have contributed to this thinking over the years. Charlie, Mike, Nico, Joe, Jan, Esau, Grego, Khashayar, Lasse, Zac, Ariel, Defi Wonderland, Obsidion,... ## Intro This doc starts with lots of background on the subject. It ends with a concrete proposal. ## Types of Message Delivery We've narrowed it down to 3 kinds of delivery type in Aztec: | Delivery Type | Constrained or Unconstrained? | Tx Effects | Proving Speed | Cost | | ----------------------------------- | ----------------------------- | ---------- | ------------- | --- | | Out-of-band, unconstrained delivery | Unconstrained | Offchain | Fast | Cheap | | In-band, unconstrained delivery | Unconstrained | Onchain | Fast | More | | In-band, constrained delivery | Constrained | Onchain | Slower | More | > Note: there are a couple of "In-band" logging approaches: Blobs and Shadow Logs. We don't explore them in this doc. ## Unconstrained Delivery is Easy Sender & Recipient agree on a Handshake Shared Secret offchain. They then trust each other to derive tags in the correct sequence > Interestingly, the circuit _does_ need to constrain that `is_constrained == false` in the tag computation. More on that in the technical section on tag computation. ## Why Constrained Delivery? To "Constrain delivery" is to _guarantee_ that the Recipient will be capable of discovering and decrypting the message. Some example applications that must constrain delivery: Any app which gives privacy to a _closed group_ of users. Any changes to private configuration variables _must_ be delivered to all participants, so that all users can read the config. If a user is not told about the config change, then that user cannot read the latest config variables, and they'll therefore be bricked from executing any functions of the app. Similarly, any important events _must_ be broadcast to all users. Another very simple example is if Alice gives "transferFrom" capabilities to Bob, then when Bob enacts a transferFrom, we need to constrain that he delivers Alice's change note back to her. [TODO: More compelling examples, plz] ## Constrained Delivery To "Constrain delivery" is to _guarantee_ that the Recipient will be capable of discovering and decrypting the message. There are several things that need to be constrained: - Encryption - We need to constrain that encryption was done correctly, so that we know decryption will succeed. - Tag Computation - For a given handshake shared secret, we need to constrain that the tag was computed correctly from it: - Correct Handshake Shared Secret; - Correct next index in the sequence; - Correctly prepended to the log. - Handshake Shared Secret correctness. - We need to verify that the Recipient knows about the handshake shared secret. If we don't constrain this, then we cannot guarantee delivery of the message. ## Constrained Handshaking "Constrained Handshaking" is a term we've used a lot in the past. Let's reframe this. It's not so much that the computation of a Handshake Shared Secret needs to be constrained; it's more that the smart contract needs to be convinced that the Recipient is capable of receiving the message. So the property we actually want is: **Verifiable evidence of delivery of a shared secret to the Recipient.** Or more pithily: ## Verifiable Handshakes These can come in two flavours: 1. **Onchain Handshake Computation**: - Derive the Handshake Shared Secret onchain, in a private function, and emit a private log in such a way that we _know_ the Sender is capable of discovering it. 2. **Signed Handshake Receipt**: - Agree on a Handshake Shared Secret offchain, and have the Recipient _acknowledge_ the secret with a _signature_. - The signature serves as "verifiable evidence of delivery of a shared secret to the Recipient". ## Use Cases The best handshaking scheme depends on the use case. (S=Sender, R=Recipient) | Approach | Verifiability | R needs to host and convey a URL? | S stays hidden from R? | R stays hidden from world? | Secret derivation (See appendix for forward-secrecy implications) | Scaleable? | R can outsource handshake discovery? | R can outsource tag discovery? [2] | | ----------------------------------------------------------------------------------------- | -------------------------------------------------------------- | --------------------------------- | --------------------------------------------------------------------------------- | -------------------------- | --------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | ----------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | | Onchain Bulletin Board | Onchain Handshake Computation ✅ | No ✅ | Yes ✅</br>(S can choose) | Yes ✅ | Ephemeral-Static | No (brute force) ❌ | Only if R's tagging keypair is used, instead of their address keypair [1]. (See later). | See [2] | | Offchain Bulletin Board | Not verifiable.</br>Cannot guarantee Recipient received it. ❌ | No ✅ | Yes ✅</br>(S can choose) | Yes ✅ | Ephemeral-Static | No (brute force) ❌ | '' | '' | | Onchain Handshake which reveals R in public | Onchain Handshake Computation ✅ | No ✅ | Yes ✅</br>(S can choose) | No ❌ | - Ephemeral-Static</br>- Random Secret | Yes ✅ | Yes ✅ (the whole world knows!) | '' | | Ping R's URL | Signed Handshake Receipt ✅ | Yes ❌ | Yes ✅ (if using VPN/Tor/MixNet)</br>(S can choose) | Yes ✅ | If Revealing S to R:</br>- Ephemeral-Ephemeral preferred.</br>If not revealing S to R:</br>- Ephemeral-Static</br>- Random Secret | Yes ✅ | Yes ✅. R can outsource the URL to be a Wallet or App. | '' | | Connect in real life.</br>QR codes, or whatever. | Signed Handshake Receipt ✅ | No ✅ | Tricky. It's IRL.</br>I guess S could be antisocial and not give his identity. ✅ | Yes ✅ | If Revealing S to R:</br>- Ephemeral-Ephemeral preferred.</br>If not revealing S to R:</br>- Ephemeral-Static</br>- Random Secret | Yes ✅ | N/A - it's IRL. No need to oursource. | '' | | Yolo-send private logs with static-static tags, and hope R finds them by searching for S. | Not verifiable.</br>Cannot guarantee Recipient will try S. ❌ | No ✅ | No ❌ | Yes ✅ | Static-Static. Not advisable. ❌ | No (brute force, and some addresses are not publicly known) ❌ | Only if R's tagging keypair is used, instead of their address keypair [1]. (See later). | '' | [1]: If the shared secret is derived from R's _Address_ Public Key, then R _MUST NOT_ divulge their address secret key to some 3rd party hadnshake-discovery service, because their address secret key is used to _decrypt_ all of their private messages. A separate tagging keypair would be needed, but there are tradeoffs. [2]: Once R has discovered their Handshake Shared Secret, there's not much benefit to oursourcing the computation of tags; R might as well compute all possible tags themselves (at the request of their Wallet or App), so as to keep their tagging shared secret(s) private. R might need to make queries to a full node to receive the private logs that correspond to their tags. ## Most of those use cases can be supported Most of the _Verifiable_ Handshake use cases can be supported, without much effort. Namely: - **Signed Handshake Receipt**: - Ping R's URL - Connect in real life (QR codes, or whatever) - **Onchain Handshake Computation** - Onchain Bulletin Board > This author thinks it's much too leaky (privacy-wise) to build the approach of "Onchain Handshake which reveals R in public", so they're stubbornly not going to write about how to do that. > Thankfully, Obsidion agree, so that's comforting! For the "Signed Handshake Receipt" approaches, we need a constrained function which can validate a signature from R. For the "Onchain Handshake Computation" approach, we need a constrained function which can compute an ephemeral-static ECDH shared secret and store it in a nullifier for future reference. ### Blockchain-only communication In defense of keeping a path for blockchain-only communication, despite its known scaling flaws: Sometimes blockchain-only communication is needed. Sometimes, apps want to be able to function completely trustlessly, without requiring any front-ends or web2 services. Don't knock it. It's the bread and butter of blockchain. Sometimes parties only have each others' addresses, and so **do not know how to reach each other via any URLs**. [Please add more compelling reasons to this section, if you like]. ### Ping R's URL If blockchain-only communication _isn't_ needed, then handshaking via THE INTERNET is quite neat. - The Sender pings R's URL. - Either: The Sender provides a shared secret to the Recipient, - Or: the Recipient's URL computes a shared secret and provides it to the Sender. - (It doesn't matter; both are acceptable) - If a verifiable handshake is needed, we need a Signed Handshake Receipt: - The Recipient's URL would sign the handshake info. - Notice that this requires the Recipient's secret key (undecided which one). - The Recipient might prune unused handshakes after some time (see `signature_expiry_timestamp` in the pseudocode below). But... - **How does the Recipient _convey_ their URL?** - **How does the Sender find the Recipient's URL?** - **Who hosts the URL?** The Recipient? Or can their Wallet? - Would their Wallet require access to the Recipients secret keys, if they are to service handshake requests? Recall: a verifiable handshake requires a signature, which requires a secret key. In some circumstances, the Sender and Recipient will both be using the same app, so perhaps the app can facilitate the handshaking, without requiring the Recipient to serve a URL? But it is the Recipient's _Wallet_ that needs to service handshake requests and sign the handshakes. And it is the Sender's _Wallet_ that needs to participate in generating handshake secrets. So when the Sender is using the app, how does the app find R's Wallet URL, in order to commence an interactive handshaking process? Either: - R needs to have pre-registered some Wallet URL with the app, and delegated handshaking permissions to the wallet (see next section for the difficulties involved there); - Or: R needs to have set up some kind of notification system within the app to be pinged every time the app wants to establish a new handshake, so that R can manually connect their wallet and finish the handshaking interaction; - Or: R needs to convey a URL alongside their address. **Do we want to consider a new "augmented address" type, for web2 interactions, of the form `address + URL`?** ## Outsourcing Handshaking to the Wallet This has come up many times in the past. Obsidion brought it up again recently. What if a user wants to outsource handshake discovery and/or handshake signing to their wallet? Responsibilities might include: > The notation is established in the appendix - whoops. | Responsibility, on behalf of the user | Computation that requires a secret key | Current secret key (not safe to share) | Shareable secret key | Comment | | --------------------------------------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | ------- | | Discover new handshakes from the non-interactive bulletin board | $sk \cdot E_S$ | $sk = a_R$</br>(the recipient's address secret key) | Would need to update to $sk = t_R$</br>(the recipient's tagging secret key). | But maybe the PXE can provide restricted access to $a_R$ for this purpose (see below). | | Compute a new handshake shared secret, each time the URL is queried. | Generation of an ECDHE shared secret (see appendix). | $sk = a_R$</br>(the recipient's address secret key) | Would need to update to $sk = t_R$</br>(the recipient's tagging secret key) | But maybe the PXE can provide restricted access to $a_R$ for this purpose (see below). | | Sign handshake shared secrets, that are established via the internet. | sign(handshake shared secret stuff) | Unsure.</br>Maybe $sk = a_R$ (the recipient's address secret key)</br>Maybe the signature uses the recipient's Abstract Tx Authorisation keys (a la authwits), but this would require extra authwit function calls. | Would need to update to $sk = t_R$</br>(the recipient's tagging secret key). | But maybe the PXE can provide restricted access to $a_R$ for this purpose (see below). | Some absolutes: - The Wallet SHOULD NOT be given the address secret key $a_R$, because that enables the Wallet to decypt all of the user's messages (even if the user chooses to migrate to another wallet some time later). - The Wallet MUST NOT be given the user's abstract secret key for Tx Authorisation (account abstraction), because that would enable the wallet to spend the user's funds and/or impersonate the user. ### PXE: providing functions with restricted access to secret keys Maybe we could give a Wallet _restricted_ oracle access to the secret keys, via very specific functions on the PXE: - `compute_ephemeral_static_tagging_shared_secret(ephemeral_point: Point)` that computes a tagging-domain-separated Tagging Shared Secret. The domain separation limits the wallet's ability to access the secret key for non-tegging-related queries. We wouldn't want to enable it to carte-blanche ask for any scalar multiplication it wants. - It's still quite leaky. The Wallet would be able to identify all logs relating to this tagging shared secret, forevermore. - `compute_ephemeral_ephemeral_public_key(...)` - details tbc. See appendix. - `sign_handshake(...)` which signs, but with a domain separator (`"aztec handshake"`) prepended to the message, which ensures the signature can only be used for handshaking. ### Q: Do we need to use a dedicated tagging keypair, to enable outsourced handshake discovery, or outsourced tag discovery? There is an unused tagging keypair $(t, T)$ baked into the preimage of an address. But if a Sender is only given the Recipient's address $A_R.x$, then they do not have $T$ and so cannot establish shared secrets with $T$. The only thing to do would be to modify the definition of an address. ## Pseudocode Enough already. Pseudocode. See the Appendix for a "mathematical symbols" version of this. #### Two approaches is inefficient We have two valid approaches through which the Sender can demonstrate that the Recipient has received the Handshake Shared Secret. We don't want every Handshake Shared Secret lookup to be encumbered with _two_ branches of constraints: a signature verification _and_ a historic nullifier read. We can't coerce the "historic nullifier read" approach into a "signature verification" approach, because of the non-interactive use case of the Bulletin Board: the Recipient R mustn't be required in that case, and so we cannot get hold of a signature from the Recipient. But we _can_ coerce the "signature verification" approaches into a "historic nullifier read" approach very easily: The first time the Sender submits a Signed Handshake Receipt to the handshaking contract, a nullifier containing the Handshake Shared Secret can be stored. #### Registering a new Handshake Shared Secret ```rust! contract HandshakingContract { // A non-interactive, ephemeral-static ECDH handshake fn non_interactive_handshake( recipient_address: AztecAddress, sender_address: AztecAddress, reveal_sender_address: bool, handshake_expiry_timestamp: Timestamp, // 0 if no expiry ) { let esk = unsafe { random() }; let epk = esk * G; // scalar mul let recipient_pk = address_public_key(recipient_address); let hs_shared_secret = esk * recipient_pk; // scalar mul let master_encryption_shared_secret = h("enc", ...hs_shared_secret.to_fields()); let master_tagging_shared_secret = h("tag", ...hs_shared_secret.to_fields()); let master_encryption_shared_secret_public_key = master_encryption_shared_secret * G; // scalar mul let master_tagging_shared_secret_public_key = master_tagging_shared_secret * G; // scalar mul let mut nullifier = 0; let mut header = 0; let mut ciphertext = 0; if reveal_sender { // Interesting problem: we can't authenticate that this call // has been made on behalf of the Sender, // without making an authwit call to the Sender! // But an authwit call would be another kernel recursion! sender_address.call("is_valid"...); // I don't know authwit syntax. // Even though this contract is allowed to see the master secret, // we'll still app-silo it for consistency with all other apps: let app_siloed_encryption_shared_secret = h(master_encryption_shared_secret, context.this_address()); let is_constrained = true; let i = 0; let j = 0; let sym_key_0 = h(is_constrained as Field, app_siloed_encryption_shared_secret, recipient_address, i, j); let mac = recipient_address; // some kind of mac, so that the recipient knows the message is for them. Could also be the iv. let message = [sender_address, mac]; // Reveals the sender to the recipient ciphertext = aes128_encrypt(message, sym_key_0); let sym_key_1 = h(is_constrained, app_siloed_encryption_shared_secret, recipient_address, i, j + 1); let ciphertext_length = ciphertext.len(); header = aes128_encrypt([ciphertext_length], sym_key_1); // We include the sender and recipient in the preimage, // so that when we tag in future, we can validate that // this is correct. // Only the Sender needs to be able to nullify this nullifier. // Only the Sender _sees_ the preimage of this nullifier. // 8 fields is quite a lot. nullifier = h("hs", sender_address, recipient_address, master_tagging_shared_secret_public_key.to_fields(), master_encryption_shared_secret_public_key.to_fields(), handshake_expiry_timestamp); } else { ciphertext = unsafe{ rand() }; header = unsafe{ rand() }; nullifier = h("hs", 0, recipient_address, master_tagging_shared_secret_public_key.to_fields(), master_encryption_shared_secret_public_key.to_fields(), handshake_expiry_timestamp); } // Padding computation not shown. // The "tag" of this log is a publicly-recognisable plaintext "handshake". // It will be siloed by the kernel with this HandshakingContract's address, // to result in some universally-known field element. // This is just an illustrative encoding. There's better ways to pack this data. let log = ["handshake", epk.x, sign(epk), header, ciphertext, padding]; emit nullifier; emit log; } // Notice: The way in which the handshake shared secret was derived is not prescribed; only that it has been agreed-upon by the recipient. // Note: instead of the signatures, we could do authwit calls, but that's extra function calls! fn register_one_way_signed_handshake_secret( signature: Signature, // some kind of signature scheme. Let's say Schnorr. signer: AztecAddress, master_tagging_shared_secret_public_key: Point, master_encryption_shared_secret_public_key: Point, handshake_expiry_timestamp: Timestamp, // 0 if no expiry signature_expiry_timestamp: Timestamp, ) { { let max_timestamp = context.header.timestamp + 1 hour; context.set_max_timestamp(max_timestamp); if signature_expiry_timestamp <= max_timestamp - 1 hour { panic!("signature expired"); } } let message = ["aztec handshake", master_tagging_shared_secret_public_key.to_fields(), master_encryption_shared_secret_public_key.to_fields(), handshake_expiry_timestamp]; verify_signature(signature, signer, message); let recipient_address = signer; nullifier = h("hs", 0, recipient_address, master_tagging_shared_secret_public_key.to_fields(), master_encryption_shared_secret_public_key.to_fields(), handshake_expiry_timestamp); emit nullifier; // Also emit an event to the recipient, tagged with the first tag in this sequence, // to let them know that the Handshake Shared Secret that they signed has been registered. // (Some users will discard unregistered/unused secrets after some time). // Alternatively: we could defer this requirement to the first app to use the tag? } // Notice: The way in which the handshake shared secret was derived is not prescribed; only that it has been agreed-upon by both sender & recipient. // Note: instead of the signatures, we could do authwit calls, but that's extra function calls! // TODO: NOT SURE IF NEEDED fn register_two_way_signed_handshake_secret( signature1: Signature, signature2: Signature, signer1: AztecAddress, signer2: AztecAddress, master_tagging_shared_secret_public_key: Point, master_encryption_shared_secret_public_key: Point, handshake_expiry_timestamp: Timestamp, // 0 if no expiry ) { { let max_timestamp = context.header.timestamp + 1 hour; context.set_max_timestamp(max_timestamp); if signature_expiry_timestamp <= max_timestamp - 1 hour { panic!("signature expired"); } } let message1 = ["aztec handshake", master_tagging_shared_secret_public_key.to_fields(), master_encryption_shared_secret_public_key.to_fields(), handshake_expiry_timestamp]; verify_signature(signature1, signer1, message1); let message2 = ["aztec handshake", signature1, signer1, message1]; verify_signature(signature2, signer2, message2); nullifier1 = h("hs", signer1, signer2, master_tagging_shared_secret_public_key.to_fields(), master_encryption_shared_secret_public_key.to_fields(), handshake_expiry_timestamp); nullifier2 = h("hs", signer2, signer1, master_tagging_shared_secret_public_key.to_fields(), master_encryption_shared_secret_public_key.to_fields(), handshake_expiry_timestamp); emit nullifier1; emit nullifier2; // Also emit an event to signer1, and another to signer2, tagged with the first tag in their respective sequences, // to let them know that the Handshake Shared Secret that they signed has been registered. // (Some users will discard unregistered/unused secrets after some time). // Alternatively: we could defer this requirement to the first app to use the tag? } } ``` #### Reading a Handshake Shared Secret When constraining tagging, the Handshake Shared Secret must be validated by the app to be correct relative to the Recipient. The above `HandshakingContract` is designed so that apps would read the handshake nullifier against a historic nullifier tree root (~3k constraints). So the flow would be: The very first time a tag is computed: - App needs to constrain delivery. - Either: - App calls the `HandshakeContract` to register / compute a new handshake shared secret; - Or: a handshake shared secret has already been registered. - App reads the handshake nullifier (either via a "settled" nullifier read request or a "transient" nullifier read request, depending on circumstances - the codepaths are the same) and validates that it relates to the correct recipient (by reading the preimage of the handshake nullifier). - App extracts the `master_tagging_shared_secret_public_key`, and the user injects a claim for the `app_siloed_tagging_shared_secret`. - A Key Validation Request to the Reset Kernel validates the app-siloed shared secret's correctness. - The tag is then computed from the `app_siloed_tagging_shared_secret`, with tagging index `0` (see derivation later). - The app can now store its own nullifier, to commence the constrainable sequence of tagging indices: - `nullifier = h("hs", sender_nsk_app, recipient, app_siloed_tagging_shared_secret, app_siloed_encryption_shared_secret, handshake_expiry_timestamp, i = 0);` - Note: the `sender_nsk_app` hides the contents of the nullifier, whilst keeping it deterministic. For subsequent tags: - App reads the previous tag nullifier, to get the shared secret and the previous index: - `nullifier = h("hs", sender_nsk_app, recipient, app_siloed_tagging_shared_secret, app_siloed_encryption_shared_secret, handshake_expiry_timestamp, i);` - App extracts`app_siloed_tagging_shared_secret` and `i`. - No Key Validation Request is needed now, because the `app_siloed_tagging_shared_secret` has been stored. - The tag is then computed from the `app_siloed_tagging_shared_secret`, with tagging index `i + 1`. - The app can now store the next nullifier in the sequence of tagging indices: - `nullifier = h("hs", sender_nsk_app, recipient, app_siloed_tagging_shared_secret, app_siloed_encryption_shared_secret, handshake_expiry_timestamp, i + 1);` Nice. It's quite a lot of constraints, but for some apps, they _need_ to constrain this process. ##### App-specific vs Universal Handshake? The above Handshaking code snippet is intended for a universal constrained handshaking contract that all wallets and apps recognise and use. Apps _do_ still have the option of implementing their own app-specific constrained handshaking logic. (But if we want standardisation between all wallets and all apps, a universally-recognised, standardised contract is more likely to achieve that). An app that doesn't care about the "Onchain, non-interactive" use case, (but which still cares about constraining handshaking) might solely wish to use signature validation to validate that the Recipient has acknowledged a Handshake Shared Secret. In that case, it might make sense for the app to verify the Signed Handshake Receipt directly, so as to avoid: - A call to the Handshaking Contract to store that nullifier; - A nullifier membership proof to prove existence of the handshake nullifier. > TODO: look up the difference in constraints between a nullifier membership proof (probably ~3k gates) and a signature verification. Also, if an app developer anticipates that _most_ of their app's transactions will be between "strangers" (parties _who have not handshaked before_), it might make sense for the app developer to do app-specific handshaking (even if it exactly conforms to this standard), so as to avoid the extra function call to the "Universal" `HandshakingContract`. #### Signature Expiry If a Handshake Shared Secret is agreed via a signature from the Recipient, the Recipient might want to set an expiry time on the validity of the signature, so they can prune their DB if they're spammed with lots of handshake requests. That's why a `signature_expiry` is included in the `HandshakeContract` functions. #### Handshake Expiry It's good practice to rotate encryption keys and tagging keys. Indeed, ECDHE is built on the premise of short-lived session keys. As already shown, Wallets have some choice over how to derive shared secrets, and whether to rotate those shared secrets. The above Universal `HandshakingContract` has an `handshake_expiry_timestamp` field, which gets baked into the initial handshake nullifier. If apps so wish, they therefore have to tools to enforce handshake shared secret rotation. > If an app _ingores_ the `expiry_timestamp`, that could be problematic, because the users (who set this all up via their wallets) might have an expectation of expiry. #### Spamming the bulletin board It'll cost money to handshake via the bulletin board function `non_interactive_handshake`. Think of it as an experiment. If it's getting spammed, we can hard-fork to make it more expensive or something. If it doesn't get spammed: great, we have a bulletin board. --- ## Tag derivation This section is agnostic to how the shared secret is derived; as long as it's a Grumpkin point. We'll give the handshake shared secret an agnostic name: $S_{hs}^{S, R}$ | Thing | Symbol | Derivation | Type | Differences from current impl? | | ----------------------- | ---------------- | --------------------------------------------------- | --------------------- | ------------------------------ | | Handshake shared secret | $S_{hs}^{S, R}$ | See appendix | $\in \mathbb{G}_{gr}$ | | | Tagging shared secret | $s_{tag}^{S, R}$ | $h(\text{"tag"}, S_{hs}^{S, R}.x, S_{hs}^{S, R}.y)$ | $\in \mathbb{F}_r$ | We don't do this, currently. | We hash the Handshake Shared Secret $S_{hs}^{S, R}$ to get the tagging shared secret $s_{tag}^{S, R}$ for the following reason: We might re-use the Handshake Shared Secret for symmetric encryption keys (see the Quantum doc). Hashing like this enables $s_{tag}^{S, R}$ to be shared with a trusted 3rd party, without leaking $S_{hs}^{S, R}$ to that party. | Thing | Symbol | Derivation | Type | Explanation | Differences from current impl? | | -------------------------------- | -------------------------------------------- | ------------------------------------------------------------ | --- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | | App-siloed tagging shared secret | $s_{tag, app}^{S, R}$ | $h(s_{tag}^{S, R}, \text{app_address})$ | $\in \mathbb{F}_r$ | The app is not allowed to see the master tagging shared secret $s_{tag}$, so $s_{tag, app}^{S, R}$ must be fed into the app circuit, and validated via a Key Validation Request to the Kernel Reset Circuit. See Appendix. | We're not consistently siloing with the address as the last element. | | A tag | $\text{tag}_{i, j}^{S \rightarrow R}$ | $h(\text{is_constrained}, s_{tag, app}^{S, R}, A_R.x, i, j)$ | $\in \mathbb{F}_r$ | $\text{is_constrained}$ ensures the constrainable sequence of indices $i$ doesn't collide with unconstrained indices.</br>A_R.x (the Recipient's address) gives directionality to the tag derivation.</br>$j$ is in case: for a given message, it needs to be split up across multiple private logs. (To be debated). | `is_constrained` and `j`. | | A siloed tag | $\text{siloed_tag}_{i, j}^{S \rightarrow R}$ | $h(\text{tag}_{i, j}^{S \rightarrow R}, \text{app_address})$ | $\in \mathbb{F}_r$ | Computed by the kernel to prevent tag impersonation. | We're not consistently siloing with the address as the last element. | # Appendix ## Validating an App-Siloed Tag No app is allowed to see the underlying handshake shared secret $S_{hs}^{S, R}$, because a malicious app could use it to derive all of a user's tags for all contracts, and unpick a huge amount of that user's privacy. That's why we pass the App-Siloed Tagging Shared Secret $s_{tag, app}^{S, R}$ into the app circuit. In the context of _Unconstrained_ Delivery, we just trust that the Sender is feeding the correct value into the app circuit. But in the context of _Constrained_ Delivery, the app circuit needs to validate that $s_{tag, app}^{S, R}$ is correct. ### Leveraging Key Validation Requests We already have a mechanism to enable an app circuit to validate an app-siloed secret: Key Validation Requests. They were designed to validate the correctness of an app-siloed nullifier secret key. Here's how they work: The user wants to claim some app-siloed secret key $sk_{app}$ is correct, relative to some master public key $Pk_m$, but the app is not allowed to see $sk_m$. | Thing | Symbol | Derivation | Type | Explanation | | --------------------- | ---------- | ----------------------------- | --------------------- | ----------- | | Master Secret Key | $sk_m$ | Random | $\in \mathbb{F}_r$ | | | Master Public Key | $Pk_m$ | $\mathbb{F}_q(sk_m) \cdot G$ | $\in \mathbb{G}_{gr}$ | | | App-siloed Secret Key | $sk_{app}$ | $h(sk_m, \text{app_address})$ | $\in \mathbb{F}_r$ | | The app is allowed to see $[Pk_m, sk_{app}]$, but not $sk_m$. The app sends a Key Validation Request to the Reset Kernel Circuit, of the form $[Pk_m, sk_{app}, \text{app_address}]$ The Reset Kernel is allowed to access the master secret key $sk_m$, because it's considered a "trusted" protocol circuit. The Reset Kernel perfoms these assertions: - $Pk_m == \mathbb{F}_q(sk_m) \cdot G$ - $sk_{app} == h(sk_m, \text{app_address})$ If these assertions succeed, then the app's usage of $sk_{app}$ is valid, and the tx will proceed. Trying not to increase protocol complexity, we can re-use Key Validation Requests to validate the App-Siloed Tagging Shared Secret $s_{tag, app}^{S, R}$. But we're missing a "master public key". So let's define a Tagging Shared Secret's Public Key as: | Thing | Symbol | Derivation | Type | Explanation | | -------------------------------- | ---------------- | ------------------------ | --------------------- | ----------- | | Tagging Shared Secret's Public Key | $S_{tag}^{S, R}$ | $s_{tag}^{S, R} \cdot G$ | $\in \mathbb{G}_{gr}$ | | > Note: doing a scalar multiplication here instead of a hash is more constraints, but the benefit is that we're re-using an interface that already exists. We can then use the above-described Key Validation Requests, substituting the following values: - $sk_m \gets s_{tag}^{S, R}$ - $Pk_m \gets S_{tag}^{S, R}$ - $sk_{app} \gets s_{tag, app}^{S, R}$ We would need a way to validate the correctness of $Pk_m = S_{tag}^{S, R}$. Refer to the above Pseudocode section, which stores these new public keys in a nullifier, for future reference. In the case of Onchain Handshake Computation, we could store the $Pk_m = S_{tag}^{S, R}$ somewhere onchain, so that the app can fetch an already-validated value. In the case of a Signed Handshake Receipt, $Pk_m = S_{tag}^{S, R}$ would need to be part of the signed message. ## Maths: Handshake Shared Secret There are a few common ways to derive a handshake shared secret. We'll touch on some below. One thing of note: We currently derive static-static handshake shared secrets. This is not advisable. ### Keys | Thing | Symbol | Derivation | Type | Explanation | | ---------------------------- | ------ | ------------------------------------- | --------------------- | ----------- | | Generator Point | $G$ | Generator $\in \mathbb{G}_{grumpkin}$ | $\in \mathbb{G}_{gr}$ | | | Recipient Address Secret Key | $a_R$ | Random | $\in \mathbb{F}_r$ | | | Recipient Address Public Key | $A_R$ | $\mathbb{F}_q(a_R) \cdot G$ | $\in \mathbb{G}_{gr}$ | | | Sender Address Secret Key | $a_S$ | Random | $\in \mathbb{F}_r$ | | | Sender Address Public Key | $A_S$ | $\mathbb{F}_q(a_S) \cdot G$ | $\in \mathbb{G}_{gr}$ | | ### Static vs Ephemeral We have some choice over how to derive the handshake shared secret. | Name | Sender | Recipient | Use Case | Forward Secrecy | | ------------------------------------------------- | ------------- | ------------- | --------------------------------- | ---------------------------------------------------------------- | | Static-static ECDH | static key | static key | Long-term trust anchors | ❌ No (Leakage of either static key leaks all messages) | | Ephemeral-static | ephemeral key | static key | Non-interactive messaging (ECIES) | ❌ No (leakage of the Recipient's static key leaks all messages) | | Ephemeral-ephemeral (ECDHE) | ephemeral key | ephemeral key | Interactive sessions (TLS, HTTPS) | ✅ Yes | | Random Secret (if already connected via internet) | - | - | | ✅ Yes | Forward secrecy is: If the static keys are compromised, do old messages remain private? The idea of ephemeral-ephemeral (ECDHE) shared secrets is: the shared secret is only used for a "session". That session might be a single defi interaction, or a single game, or it could be a window of time like a month or a year. If no onchain communication is needed to establish the shared secret (i.e. it's established 100% offchain), then sessions can be very short; much like visiting a website today. Ephemeral-Ephemeral is the de-factor standard in such cases. If onchain communication is needed, then the cost of doing so means longer-lived shared secrets are more practical, and ephemeral-static could be a better fit. ### Static-Static Handshake Shared Secret: | Thing | Symbol | Derivation | Type | Explanation | | --------------------------------------- | ------------------- | ------------------------------- | --- | ----------- | | Handshake shared secret (static-static) | $S_{hs}^{A_S, A_R}$ | $\mathbb{F}_q(a_S) \cdot A_R = \mathbb{F}_q(a_R) \cdot A_S$ | $\in \mathbb{G}_{gr}$ | | ❌ Static-Static shared secrets cannot be rotated. If someone's secret key is leaked, then the entire history of messages is leaked. It is NOT recommended. ### Ephemeral-Static Handshake Shared Secret: | Thing | Symbol | Derivation | Type | Explanation | | ------------------------------------------ | ------------------- | ----------------------------------------------------------- | --------------------- | ----------- | | Ephemeral secret key (Sender's) | $e_S$ | Random | $\in \mathbb{F}_r$ | | | Ephemeral public key (Sender's) | $E_S$ | $\mathbb{F}_q(e_S) \cdot G$ | $\in \mathbb{G}_{gr}$ | | | Handshake shared secret (ephemeral-static) | $S_{hs}^{E_S, A_R}$ | $\mathbb{F}_q(e_S) \cdot A_R = \mathbb{F}_q(a_R) \cdot E_S$ | $\in \mathbb{G}_{gr}$ | | Ephemeral-Static shared secrets are the only option for truly non-interactive secret sharing. The _Sender_ can occasionally rotate the shared secret, by sharing a new Ephemeral Public Key $E_S$. ### Ephemeral-Ephemeral Handshake Shared Secret: | Thing | Symbol | Derivation | Type | Explanation | | --------------------------------------------- | ------------------- | ------------------------------- | --- | ----------- | | Ephemeral secret key (Recipient's) | $e_R$ | Random | $\in \mathbb{F}_r$ | | | Ephemeral public key (Recipient's) | $E_R$ | $\mathbb{F}_q(e_R) \cdot G$ | $\in \mathbb{G}_{gr}$ | | | Handshake shared secret (ephemeral-ephemeral) | $S_{hs}^{E_S, E_R}$ | $\mathbb{F}_q(e_S) \cdot E_R = \mathbb{F}_q(e_R) \cdot E_S$ | $\in \mathbb{G}_{gr}$ | | For two parties who can _interact_, this is by far the preferred and most-adopted approach in internet communications today. Recommendation: The PXE should generate secrets in this way, instead of the current static-static approach. --- ## Disclaimer _The information set out herein is for discussion purposes only and does not represent any binding indication or commitment by Aztec Labs and its employees to take any action whatsoever, including relating to the structure and/or any potential operation of the Aztec protocol or the protocol roadmap. In particular: (i) nothing in these posts is intended to create any contractual or other form of legal relationship with Aztec Labs or third parties who engage with such posts (including, without limitation, by submitting a proposal or responding to posts), (ii) by engaging with any post, the relevant persons are consenting to Aztec Labs’ use and publication of such engagement and related information on an open-source basis (and agree that Aztec Labs will not treat such engagement and related information as confidential), and (iii) Aztec Labs is not under any duty to consider any or all engagements, and that consideration of such engagements and any decision to award grants or other rewards for any such engagement is entirely at Aztec Labs’ sole discretion. Please do not rely on any information on this forum for any purpose - the development, release, and timing of any products, features or functionality remains subject to change and is currently entirely hypothetical. Nothing on this forum should be treated as an offer to sell any security or any other asset by Aztec Labs or its affiliates, and you should not rely on any forum posts or content for advice of any kind, including legal, investment, financial, tax or other professional advice._