|
| 1 | +use aztec::macros::aztec; |
| 2 | +mod handshake_note; |
| 3 | +mod test; |
| 4 | + |
| 5 | +/// Handshake type identifier for non-interactive handshakes (sender-generated random shared secret, no recipient |
| 6 | +/// signature). See [`HandshakeRegistry::non_interactive_handshake`]. |
| 7 | +pub(crate) global NON_INTERACTIVE_HANDSHAKE: u8 = 1; |
| 8 | + |
| 9 | +/// Registry for the constrained-delivery shared-secret handshake protocol. |
| 10 | +/// |
| 11 | +/// The registry's job is to establish a master shared secret between a sender and a recipient, and to store a |
| 12 | +/// per-sender note recording that the handshake happened. A contract that later wants to use this |
| 13 | +/// handshake retrieves the note via the [`HandshakeRegistry::get_handshake`] utility, asserts it exists in the |
| 14 | +/// registry, |
| 15 | +/// and uses the kernel key-validation mechanism to derive its own app-siloed shared secret from the master hash stored |
| 16 | +/// in the note. |
| 17 | +/// |
| 18 | +/// Currently only implements the non-interactive flow (see [`HandshakeRegistry::non_interactive_handshake`]). |
| 19 | +#[aztec] |
| 20 | +pub contract HandshakeRegistry { |
| 21 | + use crate::handshake_note::HandshakeNote; |
| 22 | + use crate::NON_INTERACTIVE_HANDSHAKE; |
| 23 | + use aztec::{ |
| 24 | + keys::{ecdh_shared_secret::derive_ecdh_shared_secret, ephemeral::generate_positive_ephemeral_key_pair}, |
| 25 | + macros::{functions::external, storage::storage}, |
| 26 | + messages::message_delivery::MessageDelivery, |
| 27 | + note::note_viewer_options::NoteViewerOptions, |
| 28 | + oracle::notes::set_sender_for_tags, |
| 29 | + protocol::{ |
| 30 | + address::AztecAddress, constants::DOM_SEP__NON_INTERACTIVE_HANDSHAKE_LOG_TAG, hash::compute_log_tag, |
| 31 | + traits::ToField, |
| 32 | + }, |
| 33 | + state_vars::{Owned, PrivateSet}, |
| 34 | + }; |
| 35 | + |
| 36 | + #[storage] |
| 37 | + struct Storage<Context> { |
| 38 | + /// Per-sender set of [`HandshakeNote`]s. The sender is chosen by the caller and is unconstrained throughout the |
| 39 | + /// constrained-delivery protocol. |
| 40 | + handshakes: Owned<PrivateSet<HandshakeNote, Context>, Context>, |
| 41 | + } |
| 42 | + |
| 43 | + /// Performs a non-interactive handshake from `sender` to `recipient`. |
| 44 | + /// |
| 45 | + /// Generates a fresh ephemeral key pair `(eph_sk, eph_pk)`, computes the raw ECDH shared secret point |
| 46 | + /// `S = eph_sk * recipient_address_point`, and produces two effects: |
| 47 | + /// |
| 48 | + /// 1. Inserts a [`HandshakeNote`] owned by `sender`. |
| 49 | + /// 2. Emits a 1-field private log under a recipient-keyed tag with payload `[eph_pk.x]`. The recipient |
| 50 | + /// discovers handshakes addressed to them by scanning their tag and recovers the master shared secret |
| 51 | + /// from `eph_pk` via their own ECDH (`recipient_isk * eph_pk`). `eph_pk.y` is fixed positive by the |
| 52 | + /// [`generate_positive_ephemeral_key_pair`] convention, so only `eph_pk.x` is transmitted. |
| 53 | + /// |
| 54 | + /// # Panics |
| 55 | + /// If `recipient` is not a valid curve point. There are no upstream side effects in this call frame to |
| 56 | + /// protect, and a fallback would insert a permanent note recording a handshake with an invalid recipient, |
| 57 | + /// polluting registry state. |
| 58 | + #[external("private")] |
| 59 | + fn non_interactive_handshake(sender: AztecAddress, recipient: AztecAddress) { |
| 60 | + let recipient_point = recipient.to_address_point().expect(f"recipient address is not on the curve"); |
| 61 | + |
| 62 | + let (eph_sk, eph_pk) = generate_positive_ephemeral_key_pair(); |
| 63 | + let s_raw = derive_ecdh_shared_secret(eph_sk, recipient_point.inner); |
| 64 | + |
| 65 | + let note = HandshakeNote::new(s_raw, NON_INTERACTIVE_HANDSHAKE, recipient); |
| 66 | + |
| 67 | + // Safety: The sender for tags is only used to compute unconstrained shared secrets for emitting logs, so it |
| 68 | + // is safe to set from a constrained context. |
| 69 | + unsafe { set_sender_for_tags(sender) }; |
| 70 | + // Delivery is `ONCHAIN_UNCONSTRAINED`. The recipient is not involved in this note's delivery. They |
| 71 | + // discover the handshake via the recipient-keyed log emitted below, not via the sender's note. |
| 72 | + // We use onchain unconstrained delivery rather than `OFFCHAIN` so the note is |
| 73 | + // discoverable via normal PXE sync. |
| 74 | + self.storage.handshakes.at(sender).insert(note).deliver(MessageDelivery.ONCHAIN_UNCONSTRAINED); |
| 75 | + |
| 76 | + let log_tag = compute_log_tag( |
| 77 | + recipient.to_field(), |
| 78 | + DOM_SEP__NON_INTERACTIVE_HANDSHAKE_LOG_TAG, |
| 79 | + ); |
| 80 | + self.context.emit_private_log_vec_unsafe(log_tag, BoundedVec::from_array([eph_pk.x])); |
| 81 | + } |
| 82 | + |
| 83 | + /// Returns the most recently inserted [`HandshakeNote`] held for `sender` matching |
| 84 | + /// `(recipient, handshake_type)`, or `None` if no such note exists. |
| 85 | + /// |
| 86 | + /// Returning the latest (rather than the first) lets a sender re-initiate by issuing a fresh handshake; e.g. |
| 87 | + /// if the previous secret was leaked. |
| 88 | + /// |
| 89 | + /// This is the discovery surface a contract uses to obtain a note hint before asserting existence |
| 90 | + /// of the note at this registry's address. |
| 91 | + #[external("utility")] |
| 92 | + unconstrained fn get_handshake( |
| 93 | + sender: AztecAddress, |
| 94 | + recipient: AztecAddress, |
| 95 | + handshake_type: u8, |
| 96 | + ) -> Option<HandshakeNote> { |
| 97 | + // We pull all notes for `sender` and filter (recipient, handshake_type) below, walking from the end so the |
| 98 | + // first hit is the most recently inserted match. |
| 99 | + let notes = self.storage.handshakes.at(sender).view_notes(NoteViewerOptions::new()); |
| 100 | + let mut found = Option::none(); |
| 101 | + let len = notes.len(); |
| 102 | + for i in 0..len { |
| 103 | + let note = notes.get(len - 1 - i); |
| 104 | + if (note.recipient == recipient) & (note.handshake_type == handshake_type) { |
| 105 | + found = Option::some(note); |
| 106 | + break; |
| 107 | + } |
| 108 | + } |
| 109 | + found |
| 110 | + } |
| 111 | +} |
0 commit comments