@@ -9,7 +9,7 @@ pub(crate) global NON_INTERACTIVE_HANDSHAKE: u8 = 1;
99/// Registry for the constrained-delivery shared-secret handshake protocol.
1010///
1111/// The registry establishes a master shared-secret point `S` between a sender and a recipient and stores one current
12- /// note for each `(recipient, sender)` pair . The raw `S` never leaves the registry: app contracts call
12+ /// note for each `(recipient, sender, mode )` tuple . The raw `S` never leaves the registry: app contracts call
1313/// [`HandshakeRegistry::non_interactive_handshake`] to receive the secret already siloed to the caller, call
1414/// [`HandshakeRegistry::get_app_siloed_secret`] offchain for an existing handshake, and use
1515/// [`HandshakeRegistry::validate_handshake`] to check an app-siloed secret against the current stored handshake. The
@@ -25,7 +25,7 @@ pub contract HandshakeRegistry {
2525 macros ::{functions:: external , storage:: storage },
2626 messages ::{
2727 encryption ::{aes128::AES128 , message_encryption::MessageEncryption },
28- message_delivery::MessageDelivery ,
28+ message_delivery ::{ MessageDelivery , ONCHAIN_CONSTRAINED , ONCHAIN_UNCONSTRAINED } ,
2929 },
3030 protocol ::{
3131 address::AztecAddress , constants::DOM_SEP__NON_INTERACTIVE_HANDSHAKE_LOG_TAG , hash:: compute_log_tag ,
@@ -36,18 +36,21 @@ pub contract HandshakeRegistry {
3636
3737 #[storage]
3838 struct Storage <Context > {
39- /// One current [`HandshakeNote`] per `(recipient, sender)` pair . Re-handshaking for the same pair replaces the
40- /// prior sender-owned note, so only the latest handshake remains valid.
41- handshakes : Map <AztecAddress , Owned <PrivateMutable <HandshakeNote , Context >, Context >, Context >,
39+ /// One current [`HandshakeNote`] per `(recipient, sender, mode )` tuple . Re-handshaking for the same tuple
40+ /// replaces the prior sender-owned note, so only the latest handshake remains valid for that mode .
41+ handshakes : Map <AztecAddress , Map < u8 , Owned <PrivateMutable <HandshakeNote , Context > , Context >, Context >, Context >,
4242 }
4343
4444 /// Performs a non-interactive handshake from `sender` to `recipient` and returns the app-siloed shared secret
4545 /// for the calling contract.
4646 ///
47+ /// `mode` sets the delivery mode for messages tagged with this handshake ([`ONCHAIN_UNCONSTRAINED`] or
48+ /// [`ONCHAIN_CONSTRAINED`]); the handshake note itself is always stored onchain.
49+ ///
4750 /// Generates a fresh ephemeral key pair `(eph_sk, eph_pk)`, computes the raw ECDH shared secret point
4851 /// `S = eph_sk * recipient_address_point`, and produces three effects:
4952 ///
50- /// 1. Inserts or replaces a [`HandshakeNote`] owned by `sender`, holding the raw point `S`.
53+ /// 1. Inserts or replaces a [`HandshakeNote`] owned by `sender` for `mode` , holding the raw point `S`.
5154 /// 2. Emits an encrypted private log under a recipient-keyed tag with payload `[eph_pk.x]`. The recipient
5255 /// discovers handshakes addressed to them by scanning their tag and recovers `S` from `eph_pk` via their own
5356 /// ECDH (`recipient_isk * eph_pk`). `eph_pk.y` is fixed positive by the
@@ -56,11 +59,15 @@ pub contract HandshakeRegistry {
5659 /// tag" into one call without a second hop into the registry.
5760 ///
5861 /// # Panics
62+ /// If `mode` is not a recognized delivery mode.
63+ ///
5964 /// If `recipient` is not a valid curve point. There are no upstream side effects in this call frame to
6065 /// protect, and a fallback would insert a permanent note recording a handshake with an invalid recipient,
6166 /// polluting registry state.
6267 #[external("private")]
63- fn non_interactive_handshake (sender : AztecAddress , recipient : AztecAddress ) -> Field {
68+ fn non_interactive_handshake (sender : AztecAddress , recipient : AztecAddress , mode : u8 ) -> Field {
69+ assert ((mode == ONCHAIN_UNCONSTRAINED ) | (mode == ONCHAIN_CONSTRAINED ), "unrecognized delivery mode" );
70+
6471 let recipient_point = recipient .to_address_point ().expect (f"recipient address is not on the curve" );
6572
6673 let (eph_sk , eph_pk ) = generate_positive_ephemeral_key_pair ();
@@ -72,7 +79,7 @@ pub contract HandshakeRegistry {
7279 // discover the handshake via the encrypted log emitted below, not via the sender's note.
7380 // We use onchain unconstrained delivery rather than `OFFCHAIN` so the note is
7481 // discoverable via normal PXE sync.
75- self .storage .handshakes .at (recipient ).at (sender ).initialize_or_replace (|_ | note ).deliver (
82+ self .storage .handshakes .at (recipient ).at (mode ). at ( sender ).initialize_or_replace (|_ | note ).deliver (
7683 MessageDelivery ::onchain_unconstrained ().with_sender (sender ),
7784 );
7885
@@ -87,19 +94,23 @@ pub contract HandshakeRegistry {
8794 note .siloed_for (self .msg_sender ())
8895 }
8996
90- /// Asserts that `app_siloed_secret` is the silo of a stored handshake from `sender` to `recipient`, for the
91- /// caller (`msg_sender()`).
97+ /// Asserts that `app_siloed_secret` is the silo of a stored handshake from `sender` to `recipient` in `mode` , for
98+ /// the caller (`msg_sender()`).
9299 ///
93100 /// Apps that receive an `app_siloed_secret` from an untrusted source call this once to validate that secret
94101 /// against the registry's stored handshake.
95102 ///
96103 /// # Panics
97- /// If no stored handshake for `(sender, recipient)` silos to `app_siloed_secret` under the calling contract's
98- /// address.
104+ /// If `mode` is not a recognized delivery mode.
105+ ///
106+ /// If no stored handshake for `(sender, recipient, mode)` silos to `app_siloed_secret` under the calling
107+ /// contract's address.
99108 #[external("private")]
100- fn validate_handshake (sender : AztecAddress , recipient : AztecAddress , app_siloed_secret : Field ) {
109+ fn validate_handshake (sender : AztecAddress , recipient : AztecAddress , mode : u8 , app_siloed_secret : Field ) {
110+ assert ((mode == ONCHAIN_UNCONSTRAINED ) | (mode == ONCHAIN_CONSTRAINED ), "unrecognized delivery mode" );
111+
101112 let caller = self .msg_sender ();
102- let replacement_note_message = self .storage .handshakes .at (recipient ).at (sender ).get_note ();
113+ let replacement_note_message = self .storage .handshakes .at (recipient ).at (mode ). at ( sender ).get_note ();
103114 let note = replacement_note_message .get_note ();
104115
105116 assert (note .siloed_for (caller ) == app_siloed_secret , "no matching handshake" );
@@ -109,23 +120,28 @@ pub contract HandshakeRegistry {
109120 replacement_note_message .deliver (MessageDelivery ::onchain_unconstrained ());
110121 }
111122
112- /// Returns the app-siloed shared secret for an existing handshake.
123+ /// Returns the app-siloed shared secret for an existing handshake in `mode`.
124+ ///
125+ /// This is the existing-handshake retrieval surface. It returns `silo(S, caller)`, never raw `S`; a different
126+ /// caller receives a different app-siloed value for the same registry note.
127+ /// Contracts should still call [HandshakeRegistry::validate_handshake] when they need a constrained proof that a
128+ /// supplied app-siloed secret matches the current handshake.
113129 ///
114- /// This is the existing-handshake retrieval surface for constrained delivery. It returns only `silo(S, caller)`,
115- /// never raw `S`; a different caller receives a different app-siloed value for the same registry note. This is
116- /// a utility function because it is a local read helper. Contracts should still call
117- /// [HandshakeRegistry::validate_handshake] when they need a constrained proof that a supplied app-siloed secret
118- /// matches the current handshake.
130+ /// # Panics
131+ /// If `mode` is not a recognized delivery mode.
119132 #[external("utility")]
120133 unconstrained fn get_app_siloed_secret (
121134 sender : AztecAddress ,
122135 recipient : AztecAddress ,
136+ mode : u8 ,
123137 // TODO(F-671): replace with `self.msg_sender()` once utility context exposes it. The
124- // explicit param forces hooks to also gate on `params[2]` ; otherwise any contract that
138+ // explicit param forces hooks to also gate on the caller ; otherwise any contract that
125139 // can authorize (target, selector) can read another caller's siloed secret.
126140 caller : AztecAddress ,
127141 ) -> Option <Field > {
128- let handshake = self .storage .handshakes .at (recipient ).at (sender );
142+ assert ((mode == ONCHAIN_UNCONSTRAINED ) | (mode == ONCHAIN_CONSTRAINED ), "unrecognized delivery mode" );
143+
144+ let handshake = self .storage .handshakes .at (recipient ).at (mode ).at (sender );
129145
130146 if handshake .is_initialized () {
131147 Option ::some (handshake .view_note ().siloed_for (caller ))
0 commit comments