Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub(crate) fn constrain_secret_and_emit_nullifier(
sender: AztecAddress,
recipient: AztecAddress,
secret: Field,
sender_secret: Field,
bootstrapped: bool,
index: u32,
) {
Expand All @@ -37,10 +38,11 @@ pub(crate) fn constrain_secret_and_emit_nullifier(
sender,
recipient,
secret,
sender_secret,
bootstrapped,
index,
);
context.push_nullifier_unsafe(compute_constrained_msg_nullifier(sender, recipient, secret, index));
context.push_nullifier_unsafe(compute_constrained_msg_nullifier(sender, recipient, secret, sender_secret, index));
}

/// Anchors an untrusted `(secret, index)` to the registry before its constrained tag is emitted.
Expand All @@ -55,6 +57,7 @@ fn constrain_secret(
sender: AztecAddress,
recipient: AztecAddress,
secret: Field,
sender_secret: Field,
bootstrapped: bool,
index: u32,
) {
Expand All @@ -69,7 +72,7 @@ fn constrain_secret(
[sender.to_field(), recipient.to_field(), secret],
);
} else {
let prev_nullifier = compute_constrained_msg_nullifier(sender, recipient, secret, index - 1);
let prev_nullifier = compute_constrained_msg_nullifier(sender, recipient, secret, sender_secret, index - 1);
context.assert_nullifier_exists(compute_nullifier_existence_request(prev_nullifier, caller));
}
}
Expand All @@ -82,10 +85,11 @@ pub(crate) fn compute_constrained_msg_nullifier(
sender: AztecAddress,
recipient: AztecAddress,
secret: Field,
sender_secret: Field,
index: u32,
) -> Field {
poseidon2_hash_with_separator(
[sender.to_field(), recipient.to_field(), secret, index as Field],
[sender.to_field(), recipient.to_field(), secret, sender_secret, index as Field],
DOM_SEP__CONSTRAINED_MSG_NULLIFIER,
)
}
Expand All @@ -103,12 +107,13 @@ mod test {
sender: AztecAddress,
recipient: AztecAddress,
secret: Field,
sender_secret: Field,
index: u32,
) {
assert_eq(context.nullifiers.len(), 1);
assert_eq(
context.nullifiers.get(0).inner.value,
compute_constrained_msg_nullifier(sender, recipient, secret, index),
compute_constrained_msg_nullifier(sender, recipient, secret, sender_secret, index),
);
}

Expand All @@ -119,12 +124,22 @@ mod test {
let sender = AztecAddress::from_field(2);
let recipient = AztecAddress::from_field(4);
let secret: Field = 1234;
let sender_secret: Field = 5678;
let index: u32 = 0;

env.private_context(|context| {
constrain_secret_and_emit_nullifier(context, registry, sender, recipient, secret, true, index);
constrain_secret_and_emit_nullifier(
context,
registry,
sender,
recipient,
secret,
sender_secret,
true,
index,
);

assert_current_nullifier_emitted(context, sender, recipient, secret, index);
assert_current_nullifier_emitted(context, sender, recipient, secret, sender_secret, index);
assert_eq(context.private_call_requests.len(), 0);
assert_eq(context.nullifier_read_requests.len(), 0);
});
Expand All @@ -138,7 +153,7 @@ mod test {
let recipient = AztecAddress::from_field(4);

env.private_context(|context| {
constrain_secret_and_emit_nullifier(context, registry, sender, recipient, 1234, true, 1);
constrain_secret_and_emit_nullifier(context, registry, sender, recipient, 1234, 5678, true, 1);
});
}

Expand All @@ -149,6 +164,7 @@ mod test {
let sender = AztecAddress::from_field(2);
let recipient = AztecAddress::from_field(4);
let secret: Field = 1234;
let sender_secret: Field = 5678;
let index: u32 = 0;

env.private_context(|context| {
Expand All @@ -160,9 +176,18 @@ mod test {
.returns([child_call_end_counter, empty_returns_hash])
.times(1);

constrain_secret_and_emit_nullifier(context, registry, sender, recipient, secret, false, index);
constrain_secret_and_emit_nullifier(
context,
registry,
sender,
recipient,
secret,
sender_secret,
false,
index,
);

assert_current_nullifier_emitted(context, sender, recipient, secret, index);
assert_current_nullifier_emitted(context, sender, recipient, secret, sender_secret, index);
assert_eq(context.nullifier_read_requests.len(), 0);
assert_eq(context.private_call_requests.len(), 1);

Expand All @@ -182,14 +207,24 @@ mod test {
let sender = AztecAddress::from_field(2);
let recipient = AztecAddress::from_field(4);
let secret: Field = 1234;
let sender_secret: Field = 5678;
let index: u32 = 3;

env.private_context(|context| {
let _ = OracleMock::mock("aztec_prv_isNullifierPending").returns(false).times(1);

constrain_secret_and_emit_nullifier(context, registry, sender, recipient, secret, false, index);
constrain_secret_and_emit_nullifier(
context,
registry,
sender,
recipient,
secret,
sender_secret,
false,
index,
);

assert_current_nullifier_emitted(context, sender, recipient, secret, index);
assert_current_nullifier_emitted(context, sender, recipient, secret, sender_secret, index);
assert_eq(context.private_call_requests.len(), 0);
assert_eq(context.nullifier_read_requests.len(), 1);

Expand All @@ -199,7 +234,7 @@ mod test {
read_request.inner.inner,
compute_siloed_nullifier(
context.this_address(),
compute_constrained_msg_nullifier(sender, recipient, secret, index - 1),
compute_constrained_msg_nullifier(sender, recipient, secret, sender_secret, index - 1),
),
);
});
Expand Down
29 changes: 20 additions & 9 deletions noir-projects/aztec-nr/aztec/src/messages/delivery/handshake.nr
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ pub struct HandshakePage {
pub total_count: u32,
}

/// The app-siloed handshake secrets the registry returns for a `(sender, recipient)` pair: the shared secret (used
/// for the discovery tag) and the sender-only secret (folded into the constrained-delivery sequence nullifier so only
/// the sender can advance the sequence).
#[derive(Deserialize, Eq, Serialize)]
pub struct AppSiloedHandshakeSecrets {
pub shared: Field,
pub sender_only: Field,
}

pub(crate) global PROVIDED_SECRETS_ARRAY_BASE_SLOT: Field =
sha256_to_field("AZTEC_NR::PROVIDED_SECRETS_ARRAY_BASE_SLOT".as_bytes());

Expand All @@ -42,13 +51,15 @@ pub global NON_INTERACTIVE_HANDSHAKE_SELECTOR: FunctionSelector =
comptime { FunctionSelector::from_signature("non_interactive_handshake((Field),(Field))") };
pub global GET_HANDSHAKES_SELECTOR: FunctionSelector =
comptime { FunctionSelector::from_signature("get_handshakes((Field),u32)") };
pub global GET_APP_SILOED_HANDSHAKE_SECRETS_SELECTOR: FunctionSelector =
comptime { FunctionSelector::from_signature("get_app_siloed_handshake_secrets((Field),(Field))") };

/// Resolves the app-siloed handshake secret, bootstrapping when none exists.
///
/// Resolves the secret for `(sender, recipient)`, creating it via the registry's `non_interactive_handshake`
/// when no handshake exists yet.
///
/// Returns `(secret, bootstrapped)`, where `bootstrapped` is true when this call created the handshake.
/// Returns `(secrets, bootstrapped)`, where `bootstrapped` is true when this call created the handshake.
///
/// ## Batching
///
Expand All @@ -61,35 +72,35 @@ pub(crate) fn get_or_create_app_siloed_handshake_secret(
registry: AztecAddress,
sender: AztecAddress,
recipient: AztecAddress,
) -> (Field, bool) {
) -> (AppSiloedHandshakeSecrets, bool) {
// Safety: the response only selects which path runs. On `None` we bootstrap via `non_interactive_handshake`,
// whose constrained return value is the secret, so a forged empty response cannot fabricate one; it can only
// trigger an unnecessary re-handshake that replaces the registry note. The caller must constrain the returned
// `(secret, bootstrapped)` pair against the selected tagging index before emitting a handshake-derived tag.
let maybe_secret: Option<Field> = unsafe {
// `(secrets, bootstrapped)` pair against the selected tagging index before emitting a handshake-derived tag.
let maybe_secrets: Option<AppSiloedHandshakeSecrets> = unsafe {
let returns = call_utility_function(
registry,
GET_APP_SILOED_SECRET_SELECTOR,
GET_APP_SILOED_HANDSHAKE_SECRETS_SELECTOR,
[sender.to_field(), recipient.to_field()],
);
Deserialize::deserialize(returns)
};

maybe_secret.map(|secret| (secret, false)).unwrap_or_else(|| {
maybe_secrets.map(|secrets| (secrets, false)).unwrap_or_else(|| {
// Bootstrap: no handshake exists yet. The registry inserts a fresh note and returns the app-siloed
// secret to the caller. The constrained return is the source of truth for the secret, so no separate
// secrets to the caller. The constrained return is the source of truth, so no separate
// `validate_handshake` is needed by constrained delivery.
// TODO(F-660): dispatch to `perform_handshake(sender, recipient, handshake_type)` once interactive
// handshakes are supported.
let secret: Field = context
let secrets: AppSiloedHandshakeSecrets = context
.call_private_function(
registry,
NON_INTERACTIVE_HANDSHAKE_SELECTOR,
[sender.to_field(), recipient.to_field()],
)
.get_preimage();

(secret, true)
(secrets, true)
})
}

Expand Down
10 changes: 6 additions & 4 deletions noir-projects/aztec-nr/aztec/src/messages/delivery/tag.nr
Original file line number Diff line number Diff line change
Expand Up @@ -54,26 +54,28 @@ fn derive_handshake_log_tag(
recipient: AztecAddress,
mode: OnchainDeliveryMode,
) -> Field {
let (secret, bootstrapped) = get_or_create_app_siloed_handshake_secret(
let (secrets, bootstrapped) = get_or_create_app_siloed_handshake_secret(
context,
STANDARD_HANDSHAKE_REGISTRY_ADDRESS,
sender,
recipient,
);

// Safety: the returned index is untrusted and is constrained before the tag is emitted.
let index = unsafe { get_next_tagging_index(secret, mode) };
let index = unsafe { get_next_tagging_index(secrets.shared, mode) };
constrain_secret_and_emit_nullifier(
context,
STANDARD_HANDSHAKE_REGISTRY_ADDRESS,
sender,
recipient,
secret,
secrets.shared,
secrets.sender_only,
bootstrapped,
index,
);

tag_from_secret_and_index(secret, index, mode)
// The discovery tag stays derived from the shared secret only, so the recipient still finds every message.
tag_from_secret_and_index(secrets.shared, index, mode)
}

fn tag_domain_separator(mode: OnchainDeliveryMode) -> u32 {
Expand Down
2 changes: 1 addition & 1 deletion noir-projects/aztec-nr/aztec/src/standard_addresses.nr
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ pub global STANDARD_PUBLIC_CHECKS_ADDRESS: AztecAddress = AztecAddress::from_fie
);

pub global STANDARD_HANDSHAKE_REGISTRY_ADDRESS: AztecAddress = AztecAddress::from_field(
0x215f91f8907b8d6406a9b209b88b3d8d01c764c81d3704af257a2de6f0cd908d,
0x08112cdbb9286ad71c084784249cd7050b66a170774d1fdd815a085485b83ca1,
);
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,5 @@ pub global STANDARD_PUBLIC_CHECKS_ADDRESS: AztecAddress = AztecAddress::from_fie
);

pub global STANDARD_HANDSHAKE_REGISTRY_ADDRESS: AztecAddress = AztecAddress::from_field(
0x215f91f8907b8d6406a9b209b88b3d8d01c764c81d3704af257a2de6f0cd908d,
0x08112cdbb9286ad71c084784249cd7050b66a170774d1fdd815a085485b83ca1,
);
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
use aztec::{
keys::ecdh_shared_secret::compute_app_siloed_shared_secret,
macros::notes::note,
protocol::{address::AztecAddress, point::EmbeddedCurvePoint, traits::{Deserialize, Packable, Serialize}},
protocol::{
address::AztecAddress,
constants::DOM_SEP__CONSTRAINED_MSG_SENDER_SECRET,
hash::poseidon2_hash_with_separator,
point::EmbeddedCurvePoint,
scalar::Scalar,
traits::{Deserialize, Packable, Serialize, ToField},
},
};

/// A record of a handshake established by the note's owner (the sender).
Expand All @@ -21,16 +28,38 @@ pub struct HandshakeNote {
/// The raw ECDH shared-secret point `S = eph_sk * recipient_address_point`. Only this module can read it for
/// siloing, and it is never returned by an external function.
secret: EmbeddedCurvePoint,
/// The sender's ephemeral secret key, stored as its two scalar limbs (`EmbeddedCurveScalar` is not `Packable`).
/// Retained so the sender (the note owner) can derive a sender-only secret to fold into constrained-delivery
/// nullifiers; the recipient only ever sees the public `eph_pk`. It leaves the registry only via
/// [`HandshakeNote::nullifier_secret_for`], never unsiloed.
eph_sk_lo: Field,
eph_sk_hi: Field,
}

impl HandshakeNote {
pub(crate) fn new(shared_secret: EmbeddedCurvePoint, handshake_type: u8, recipient: AztecAddress) -> Self {
Self { handshake_type, recipient, secret: shared_secret }
pub(crate) fn new(
shared_secret: EmbeddedCurvePoint,
handshake_type: u8,
recipient: AztecAddress,
eph_sk: Scalar,
) -> Self {
Self { handshake_type, recipient, secret: shared_secret, eph_sk_lo: eph_sk.lo, eph_sk_hi: eph_sk.hi }
}

/// Returns the app-siloed shared secret for `caller`, computed via aztec-nr's canonical
/// [`compute_app_siloed_shared_secret`].
pub(crate) fn siloed_for(self, caller: AztecAddress) -> Field {
compute_app_siloed_shared_secret(self.secret, caller)
}

/// Returns the app-siloed sender-only secret for `caller`, folded into constrained-delivery nullifiers so only
/// the sender (who holds `eph_sk`) can advance a sequence. The recipient cannot derive it: it comes from the
/// secret `eph_sk`, not the public `eph_pk`. Siloing by `caller` keeps it app-specific, mirroring
/// [`HandshakeNote::siloed_for`].
pub(crate) fn nullifier_secret_for(self, caller: AztecAddress) -> Field {
poseidon2_hash_with_separator(
[self.eph_sk_lo, self.eph_sk_hi, caller.to_field()],
DOM_SEP__CONSTRAINED_MSG_SENDER_SECRET,
)
}
}
Loading
Loading