Skip to content

Commit d4f7768

Browse files
committed
chore: merge merge-train/fairies-v5 (interactive handshake #24473) and re-pin standard contracts
2 parents 3622967 + c86e84c commit d4f7768

11 files changed

Lines changed: 396 additions & 95 deletions

File tree

docs/docs-developers/docs/foundational-topics/pxe/execution_hooks.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,7 @@ A general-purpose hook for *custom*, caller-defined requests. A contract reaches
105105
Pass a `resolveCustomRequest` hook when [creating the PXE](#configuring-hooks). It receives a `CustomRequest` with the issuing contract's address and class ID, the request `kind`, and the opaque `payload`, and returns the response. Because any contract can issue a request, the hook should check both the `kind` and the issuing contract before answering, dispatching on `kind` to the matching resolver.
106106

107107
When the hook is absent, the request cannot be served and simulation fails.
108+
109+
### Example: interactive handshakes
110+
111+
The `HandshakeRegistry`'s `interactive_handshake` uses this hook to obtain the recipient's signed authorization. The payload carries what the signer needs to decide: who the recipient is, the handshake being authorized, and the chain context, but never the sender. The response carries what the registry needs to verify the recipient's signature in-circuit.

noir-projects/aztec-nr/aztec/src/standard_addresses.nr

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,17 @@
22
use protocol_types::{address::AztecAddress, traits::FromField};
33

44
pub global STANDARD_AUTH_REGISTRY_ADDRESS: AztecAddress = AztecAddress::from_field(
5-
0x06008ab66245e1956e1b4d60a03efc2d022b0e81dcc0ae4b93e201f17f923cb4,
5+
0x23eb6e1c966b3fe333bcbdbf25d6f4d4fdf3726cca3a9892e461c348bdb21666,
66
);
77

88
pub global STANDARD_MULTI_CALL_ENTRYPOINT_ADDRESS: AztecAddress = AztecAddress::from_field(
9-
0x045810aaf33e2af970270654d41fd95ca49bbb8457cdeb3ed86f8b7cd2476b0a,
9+
0x12d0aab3c4488b825f9b92c10865ac26c969fd3ddbcc0dbd6c39d5893cc59f4b,
1010
);
1111

1212
pub global STANDARD_PUBLIC_CHECKS_ADDRESS: AztecAddress = AztecAddress::from_field(
13-
0x0440c9b433845f59301468cbd5604e4304ea21592a69942b08f11b267d4e5e8f,
13+
0x02bd15136aa4ea1ad6113aa5bfb81add7e986c85e1cdcc312c00ac1bb653984d,
1414
);
1515

1616
pub global STANDARD_HANDSHAKE_REGISTRY_ADDRESS: AztecAddress = AztecAddress::from_field(
17-
0x0ba9aabb087fbed77e41bdd3667df8b34ca39d6b0682cd94a521be21c0cae83b,
17+
0x01daf0f4a46ca4bf67594741832c57dbea5dda829326a7a0f4278b58797eefad,
1818
);

noir-projects/noir-contracts/contracts/standard/handshake_registry_contract/Nargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@ type = "contract"
66

77
[dependencies]
88
aztec = { path = "../../../../aztec-nr/aztec" }
9+
schnorr = { tag = "v0.4.0", git = "https://github.com/noir-lang/schnorr" }

noir-projects/noir-contracts/contracts/standard/handshake_registry_contract/src/main.nr

Lines changed: 148 additions & 65 deletions
Large diffs are not rendered by default.
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
use aztec::{
2+
protocol::{
3+
address::{AztecAddress, PartialAddress},
4+
constants::DOM_SEP__INTERACTIVE_HANDSHAKE_SIGNATURE,
5+
hash::poseidon2_hash_with_separator,
6+
public_keys::{hash_public_key, PublicKeys},
7+
traits::{Deserialize, ToField},
8+
},
9+
utils::point::point_from_x_coord_and_sign,
10+
};
11+
use std::embedded_curve_ops::EmbeddedCurveScalar;
12+
13+
/// The recipient's signed authorization for an interactive handshake.
14+
///
15+
/// Returned by the interactive-handshake
16+
/// [`resolve_custom_request`](aztec::oracle::resolve_custom_request::resolve_custom_request) oracle call. It is how
17+
/// the sender asserts that the recipient received the ephemeral key correctly and will be able to receive messages
18+
/// properly, and it carries everything needed to verify that in-circuit.
19+
#[derive(Deserialize)]
20+
pub struct RecipientSignature {
21+
/// The recipient's master public keys. With `partial_address` they derive the recipient's address, which is how
22+
/// [`verify`][Self::verify] binds them to `recipient`.
23+
pub public_keys: PublicKeys,
24+
/// The recipient's partial address, combined with `public_keys` to reproduce the recipient's address.
25+
pub partial_address: PartialAddress,
26+
/// The x-coordinate of the recipient's master message-signing public key. `PublicKeys` exposes this key only as
27+
/// its `mspk_m_hash`, so the point travels separately and is reconstructed on-curve, then tied back to that hash.
28+
pub mspk_x: Field,
29+
/// Whether that key's y-coordinate is positive, used to pick the correct y when reconstructing the point from
30+
/// `mspk_x`.
31+
pub mspk_y_is_positive: bool,
32+
/// The schnorr signature over the handshake message: its response scalar `s` and challenge `e`.
33+
pub signature: (EmbeddedCurveScalar, EmbeddedCurveScalar),
34+
}
35+
36+
impl RecipientSignature {
37+
/// Verifies that `recipient` authorized a handshake using ephemeral key `eph_pk.x` on this chain.
38+
///
39+
/// # Panics
40+
/// If the returned public keys do not derive `recipient`'s address, `mspk_x` is not on the curve, the signing
41+
/// key does not match the address-committed `mspk_m_hash`, or the signature does not verify. Note that an
42+
/// off-curve `ivpk_m` fails inside the embedded-curve addition in [`AztecAddress::compute`] rather than with
43+
/// this method's error messages.
44+
pub fn verify(
45+
self,
46+
recipient: AztecAddress,
47+
chain_id: Field,
48+
version: Field,
49+
registry: AztecAddress,
50+
eph_pk_x: Field,
51+
) {
52+
// Bind the returned keys to the recipient's address.
53+
assert_eq(
54+
recipient,
55+
AztecAddress::compute(self.public_keys, self.partial_address),
56+
"handshake public keys do not match recipient",
57+
);
58+
59+
// Reconstruct the signing key on-curve and tie it to the authenticated hash.
60+
let mspk = point_from_x_coord_and_sign(self.mspk_x, self.mspk_y_is_positive).expect(
61+
f"mspk_x is not a valid curve x-coordinate",
62+
);
63+
assert(
64+
hash_public_key(mspk) == self.public_keys.mspk_m_hash,
65+
"handshake signature key does not match recipient",
66+
);
67+
68+
// At this point `mspk` is proven to be the recipient's message-signing key, so a valid signature over the
69+
// recomputed message can only come from the recipient. The message commits to the ephemeral key and chain
70+
// context, never to the sender, so the recipient authorizes without learning who initiated it.
71+
let message = poseidon2_hash_with_separator(
72+
[chain_id, version, registry.to_field(), eph_pk_x],
73+
DOM_SEP__INTERACTIVE_HANDSHAKE_SIGNATURE,
74+
);
75+
assert(schnorr::verify_signature(mspk, self.signature, message), "invalid recipient handshake signature");
76+
}
77+
}
78+
79+
mod test {
80+
use super::RecipientSignature;
81+
use aztec::protocol::{
82+
address::{AztecAddress, PartialAddress},
83+
point::EmbeddedCurvePoint,
84+
public_keys::{IvpkM, PublicKeys},
85+
traits::{FromField, ToField},
86+
};
87+
use std::embedded_curve_ops::EmbeddedCurveScalar;
88+
89+
#[test]
90+
unconstrained fn interactive_signature_verifies_valid_fixture() {
91+
let (response, recipient, chain_id, version, registry, eph_pk_x) = interactive_handshake_fixture();
92+
response.verify(recipient, chain_id, version, registry, eph_pk_x);
93+
}
94+
95+
#[test(should_fail_with = "handshake public keys do not match recipient")]
96+
unconstrained fn interactive_signature_rejects_wrong_recipient() {
97+
let (response, recipient, chain_id, version, registry, eph_pk_x) = interactive_handshake_fixture();
98+
let wrong_recipient = AztecAddress::from_field(recipient.to_field() + 1);
99+
response.verify(wrong_recipient, chain_id, version, registry, eph_pk_x);
100+
}
101+
102+
#[test(should_fail_with = "handshake signature key does not match recipient")]
103+
unconstrained fn interactive_signature_rejects_wrong_signing_key() {
104+
let (mut response, recipient, chain_id, version, registry, eph_pk_x) = interactive_handshake_fixture();
105+
// x = 1 is the generator's x-coordinate: a valid curve point, but not the recipient's signing key.
106+
response.mspk_x = 1;
107+
response.verify(recipient, chain_id, version, registry, eph_pk_x);
108+
}
109+
110+
#[test(should_fail_with = "mspk_x is not a valid curve x-coordinate")]
111+
unconstrained fn interactive_signature_rejects_off_curve_signing_key() {
112+
let (mut response, recipient, chain_id, version, registry, eph_pk_x) = interactive_handshake_fixture();
113+
// x = 3 is a non-residue for the curve, so no point has this x-coordinate.
114+
response.mspk_x = 3;
115+
response.verify(recipient, chain_id, version, registry, eph_pk_x);
116+
}
117+
118+
#[test(should_fail_with = "invalid recipient handshake signature")]
119+
unconstrained fn interactive_signature_rejects_tampered_signature() {
120+
let (mut response, recipient, chain_id, version, registry, eph_pk_x) = interactive_handshake_fixture();
121+
response.signature.0.lo += 1;
122+
response.verify(recipient, chain_id, version, registry, eph_pk_x);
123+
}
124+
125+
#[test(should_fail_with = "invalid recipient handshake signature")]
126+
unconstrained fn interactive_signature_rejects_wrong_eph_pk() {
127+
let (response, recipient, chain_id, version, registry, eph_pk_x) = interactive_handshake_fixture();
128+
response.verify(recipient, chain_id, version, registry, eph_pk_x + 1);
129+
}
130+
131+
#[test(should_fail_with = "invalid recipient handshake signature")]
132+
unconstrained fn interactive_signature_rejects_wrong_registry() {
133+
let (response, recipient, chain_id, version, registry, eph_pk_x) = interactive_handshake_fixture();
134+
let wrong_registry = AztecAddress::from_field(registry.to_field() + 1);
135+
response.verify(recipient, chain_id, version, wrong_registry, eph_pk_x);
136+
}
137+
138+
#[test(should_fail_with = "invalid recipient handshake signature")]
139+
unconstrained fn interactive_signature_rejects_wrong_chain_context() {
140+
let (response, recipient, chain_id, version, registry, eph_pk_x) = interactive_handshake_fixture();
141+
response.verify(recipient, chain_id + 1, version, registry, eph_pk_x);
142+
}
143+
144+
// A real interactive-handshake response fixture generated offline: recipient keys, partial address,
145+
// message-signing key, and a genuine schnorr signature over `[chain_id, version, registry, eph_pk_x]`.
146+
unconstrained fn interactive_handshake_fixture() -> (RecipientSignature, AztecAddress, Field, Field, AztecAddress, Field) {
147+
let public_keys = PublicKeys {
148+
npk_m_hash: 0x1b6146272d5dcb12676fa83f74475545fee7e36b4cb93e0e1ae4b2098f442de2,
149+
ivpk_m: IvpkM {
150+
inner: EmbeddedCurvePoint {
151+
x: 0x03e89026e063059e6069e82a81a3e16d28f94a900ed08dadbd8407bf472d16c4,
152+
y: 0x0c8a8b9ab39ea7563eb22a85905408a061e09bde12ff788a84e772b3e6af2130,
153+
},
154+
},
155+
ovpk_m_hash: 0x279e44ade5739fdbdb91ea20489a20183fd6f38ddefafff8748bcfd8f72ad195,
156+
tpk_m_hash: 0x124b66b8152df6debefebd7561f43ad0179f908b3b7b573963efb1685c80b0fc,
157+
mspk_m_hash: 0x0e5480b4d52b9630b06e8d0e2ca8932457099274d522bb8ea6a60c1d7da871bf,
158+
fbpk_m_hash: 0x26dbd06da054ead46dcf2232ac07be3ab5839d8287ac0f4ed69f62eb7ffaa2d0,
159+
};
160+
let response = RecipientSignature {
161+
public_keys,
162+
partial_address: PartialAddress::from_field(0x0fedcba987654321),
163+
mspk_x: 0x22d507026547e1bd19b507a234a689833ab1604c00ff093574c44aea7176cfd5,
164+
mspk_y_is_positive: true,
165+
signature: (
166+
EmbeddedCurveScalar { lo: 0xd88c1b23cafce3c85432b3393c8f1975, hi: 0x04f3972857d4b92ef44c2ea1d15e6abb },
167+
EmbeddedCurveScalar { lo: 0x01b6f5567970c74d567270c239f235ab, hi: 0x0db0cf5e2cfba9c07df49a357b6dcb2c },
168+
),
169+
};
170+
let recipient = AztecAddress::from_field(
171+
0x1b94108a8172e103629879215c11f224522aa1bd0e4d50505c041c500f82d15b,
172+
);
173+
let chain_id = 0x7a69;
174+
let version = 1;
175+
let registry = AztecAddress::from_field(0x0abcabcabcabcabc);
176+
let eph_pk_x = 0x2468ace2468ace;
177+
(response, recipient, chain_id, version, registry, eph_pk_x)
178+
}
179+
}

noir-projects/noir-contracts/contracts/standard/handshake_registry_contract/src/test.nr

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,8 @@ unconstrained fn non_interactive_handshake_stores_handshake_for_sender_and_recip
8484
}
8585

8686
// The DH-direct flow lifts `recipient` to a curve point and fails loud if the address has no
87-
// corresponding point. We deliberately do not replicate AES128's "king of the hill" fallback here; see
88-
// the doc comment on [`HandshakeRegistry::non_interactive_handshake`] for the rationale.
87+
// corresponding point. We deliberately do not replicate AES128's "king of the hill" fallback here: a fallback
88+
// would insert a permanent note recording a handshake with an invalid recipient, polluting registry state.
8989
#[test(should_fail_with = "recipient address is not on the curve")]
9090
unconstrained fn handshake_to_invalid_recipient_panics() {
9191
let (env, registry_address, sender, _, _) = setup();
@@ -98,6 +98,25 @@ unconstrained fn handshake_to_invalid_recipient_panics() {
9898
let _ = env.call_private(sender, registry.non_interactive_handshake(sender, invalid_recipient));
9999
}
100100

101+
#[test(should_fail_with = "recipient address is not on the curve")]
102+
unconstrained fn interactive_handshake_to_invalid_recipient_panics() {
103+
let (env, registry_address, sender, _, _) = setup();
104+
let registry = HandshakeRegistry::at(registry_address);
105+
106+
let invalid_recipient = AztecAddress::from_field(3);
107+
let _ = env.call_private(sender, registry.interactive_handshake(sender, invalid_recipient));
108+
}
109+
110+
// `interactive_handshake` must obtain the recipient's signature from a `resolveCustomRequest` resolver before
111+
// storing anything. The TXE test environment configures no such resolver, so the call fails at the resolver hop.
112+
#[test(should_fail_with = "no resolveCustomRequest hook is configured")]
113+
unconstrained fn interactive_handshake_requires_a_resolver() {
114+
let (env, registry_address, sender, _, recipient) = setup();
115+
let registry = HandshakeRegistry::at(registry_address);
116+
117+
let _ = env.call_private(sender, registry.interactive_handshake(sender, recipient));
118+
}
119+
101120
// The handshake tag depends only on the recipient, so multiple senders posting to
102121
// the same recipient land under the same log tag.
103122
#[test]
59.6 KB
Binary file not shown.

noir-projects/noir-protocol-circuits/crates/types/src/constants.nr

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -737,6 +737,9 @@ pub global DOM_SEP__CONSTRAINED_MSG_SENDER_SECRET: u32 = 1182889476;
737737
/// Domain separator for non-interactive handshake log tags emitted by the handshake registry contract. Used by
738738
/// [`crate::hash::compute_log_tag`].
739739
pub global DOM_SEP__NON_INTERACTIVE_HANDSHAKE_LOG_TAG: u32 = 4046403018;
740+
/// Domain separator for the message a recipient signs to authorize an interactive handshake, hashed via
741+
/// [`crate::hash::poseidon2_hash_with_separator`] and verified in-circuit by the handshake registry contract.
742+
pub global DOM_SEP__INTERACTIVE_HANDSHAKE_SIGNATURE: u32 = 3098455647;
740743
/// Domain separator for private log tags.
741744
///
742745
/// Used by [`crate::hash::compute_siloed_private_log_first_field`].

noir-projects/noir-protocol-circuits/crates/types/src/constants_tests.nr

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@ use crate::{
1414
DOM_SEP__CONTRACT_ADDRESS_V2, DOM_SEP__CONTRACT_CLASS_ID, DOM_SEP__ECDH_FIELD_MASK,
1515
DOM_SEP__ECDH_SUBKEY, DOM_SEP__EVENT_COMMITMENT, DOM_SEP__EVENT_LOG_TAG, DOM_SEP__FBSK_M,
1616
DOM_SEP__FUNCTION_ARGS, DOM_SEP__INITIALIZATION_NULLIFIER, DOM_SEP__INITIALIZER,
17-
DOM_SEP__IVSK_M, DOM_SEP__MERKLE_HASH, DOM_SEP__MESSAGE_NULLIFIER, DOM_SEP__MSSK_M,
18-
DOM_SEP__NHK_M, DOM_SEP__NON_INTERACTIVE_HANDSHAKE_LOG_TAG,
19-
DOM_SEP__NOTE_COMPLETION_LOG_TAG, DOM_SEP__NOTE_HASH, DOM_SEP__NOTE_HASH_NONCE,
20-
DOM_SEP__NOTE_NULLIFIER, DOM_SEP__NULLIFIER_MERKLE, DOM_SEP__OVSK_M,
21-
DOM_SEP__PARTIAL_ADDRESS, DOM_SEP__PARTIAL_NOTE_COMMITMENT,
22-
DOM_SEP__PARTIAL_NOTE_VALIDITY_COMMITMENT, DOM_SEP__PRIVATE_FUNCTION_LEAF,
23-
DOM_SEP__PRIVATE_INITIALIZATION_NULLIFIER, DOM_SEP__PRIVATE_LOG_FIRST_FIELD,
24-
DOM_SEP__PRIVATE_TX_HASH, DOM_SEP__PROTOCOL_CONTRACTS, DOM_SEP__PUBLIC_BYTECODE,
25-
DOM_SEP__PUBLIC_CALLDATA, DOM_SEP__PUBLIC_DATA_MERKLE,
17+
DOM_SEP__INTERACTIVE_HANDSHAKE_SIGNATURE, DOM_SEP__IVSK_M, DOM_SEP__MERKLE_HASH,
18+
DOM_SEP__MESSAGE_NULLIFIER, DOM_SEP__MSSK_M, DOM_SEP__NHK_M,
19+
DOM_SEP__NON_INTERACTIVE_HANDSHAKE_LOG_TAG, DOM_SEP__NOTE_COMPLETION_LOG_TAG,
20+
DOM_SEP__NOTE_HASH, DOM_SEP__NOTE_HASH_NONCE, DOM_SEP__NOTE_NULLIFIER,
21+
DOM_SEP__NULLIFIER_MERKLE, DOM_SEP__OVSK_M, DOM_SEP__PARTIAL_ADDRESS,
22+
DOM_SEP__PARTIAL_NOTE_COMMITMENT, DOM_SEP__PARTIAL_NOTE_VALIDITY_COMMITMENT,
23+
DOM_SEP__PRIVATE_FUNCTION_LEAF, DOM_SEP__PRIVATE_INITIALIZATION_NULLIFIER,
24+
DOM_SEP__PRIVATE_LOG_FIRST_FIELD, DOM_SEP__PRIVATE_TX_HASH, DOM_SEP__PROTOCOL_CONTRACTS,
25+
DOM_SEP__PUBLIC_BYTECODE, DOM_SEP__PUBLIC_CALLDATA, DOM_SEP__PUBLIC_DATA_MERKLE,
2626
DOM_SEP__PUBLIC_INITIALIZATION_NULLIFIER, DOM_SEP__PUBLIC_KEYS_HASH,
2727
DOM_SEP__PUBLIC_LEAF_SLOT, DOM_SEP__PUBLIC_STORAGE_MAP_SLOT, DOM_SEP__PUBLIC_TX_HASH,
2828
DOM_SEP__RETRIEVED_BYTECODES_MERKLE, DOM_SEP__SALTED_INITIALIZATION_HASH,
@@ -145,7 +145,7 @@ impl<let NUM_VALUES: u32, let NUM_U32_VALUES: u32> HashedValueTester<NUM_VALUES,
145145

146146
#[test]
147147
fn hashed_values_match_derived() {
148-
let mut tester = HashedValueTester::<76, 69>::new();
148+
let mut tester = HashedValueTester::<77, 70>::new();
149149

150150
// -----------------
151151
// Domain separators
@@ -194,6 +194,10 @@ fn hashed_values_match_derived() {
194194
DOM_SEP__NON_INTERACTIVE_HANDSHAKE_LOG_TAG,
195195
"non_interactive_handshake_log_tag",
196196
);
197+
tester.assert_dom_sep_matches_derived(
198+
DOM_SEP__INTERACTIVE_HANDSHAKE_SIGNATURE,
199+
"interactive_handshake_signature",
200+
);
197201
tester.assert_dom_sep_matches_derived(DOM_SEP__MESSAGE_NULLIFIER, "message_nullifier");
198202
tester.assert_dom_sep_matches_derived(DOM_SEP__PRIVATE_FUNCTION_LEAF, "private_function_leaf");
199203
tester.assert_dom_sep_matches_derived(DOM_SEP__PUBLIC_BYTECODE, "public_bytecode");

yarn-project/constants/src/constants.gen.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -526,7 +526,9 @@ export enum DomainSeparator {
526526
UNCONSTRAINED_MSG_LOG_TAG = 1485357192,
527527
CONSTRAINED_MSG_LOG_TAG = 3715244738,
528528
CONSTRAINED_MSG_NULLIFIER = 3723577546,
529+
CONSTRAINED_MSG_SENDER_SECRET = 1182889476,
529530
NON_INTERACTIVE_HANDSHAKE_LOG_TAG = 4046403018,
531+
INTERACTIVE_HANDSHAKE_SIGNATURE = 3098455647,
530532
PRIVATE_LOG_FIRST_FIELD = 2769976252,
531533
PUBLIC_LEAF_SLOT = 1247650290,
532534
PUBLIC_STORAGE_MAP_SLOT = 4015149901,

0 commit comments

Comments
 (0)