diff --git a/noir-projects/aztec-nr/aztec/src/messages/delivery/builder.nr b/noir-projects/aztec-nr/aztec/src/messages/delivery/builder.nr new file mode 100644 index 000000000000..642f77ee508b --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/messages/delivery/builder.nr @@ -0,0 +1,414 @@ +use crate::protocol::address::AztecAddress; +use super::mode::{DeliveryMode, OnchainDeliveryMode}; +use super::tag_secret_derivation::TagSecretDerivation; + +/// Specifies how to deliver a message to a recipient. +/// +/// All messages are delivered encrypted to their recipient's public address key, so no other account will be able to +/// read their contents. This struct instead configures which **guarantees** exist regarding delivery. +/// +/// There are two aspects to delivery guarantees: +/// - the medium on which the message is sent (off-chain or on-chain) +/// - whether the contract constrains the message to be constructed correctly +/// +/// For scenarios where the sender is incentivized to deliver the message correctly, use [`MessageDelivery::offchain`] +/// (the cheapest delivery option, but requiring that sender and recipient can communicate off-chain) or +/// [`MessageDelivery::onchain_unconstrained`]. If the sender cannot be trusted to send the message to the recipient, +/// use [`MessageDelivery::onchain_constrained`]. +/// +/// ## Construction +/// +/// The fields are private and there is no public constructor: a `MessageDelivery` can only be produced by a +/// [`MessageDeliveryBuilder`] that enforces valid configurations, so invalid field combinations cannot be +/// represented to the consumer. +pub struct MessageDelivery { + mode: DeliveryMode, + tag_secret_derivation: TagSecretDerivation, + sender_override: Option, +} + +impl MessageDelivery { + pub(crate) fn mode(self) -> DeliveryMode { + self.mode + } + + pub(crate) fn tag_secret_derivation(self) -> TagSecretDerivation { + self.tag_secret_derivation + } + + pub(crate) fn sender_override(self) -> Option { + self.sender_override + } + + /// Delivers the message fully off-chain, with no guarantees whatsoever. + /// + /// ## Use Cases + /// + /// This delivery method is suitable when the sender is required to send the message to the recipient because of + /// some external reason, and where the sender is able to directly contact the recipient off-chain. In these + /// cases, it might be unnecessary to force the sender to spend proving time guaranteeing message correctness, or + /// to pay transaction fees in order to use the chain as a medium. + /// + /// For example, if performing a payment in exchange for some good or service, the recipient will only accept the + /// payment once they receive note and event messages, allowing them to observe the balance increase. The sender + /// has no reason not to deliver the message correctly to the recipient, and in all likelihood has a way to send + /// it to them. + /// + /// Similarly, in games and other applications that might rely on some server processing state, players might be + /// required to update the server with their current state. + /// + /// Finally, any messages for which the recipient is a local account (e.g.: the message for the change note in a + /// token transfer) work well with this delivery option, since the sender would only be harming themselves by not + /// delivering correctly. + /// + /// ## Guarantees + /// + /// The sender of the message is free to both not deliver the message to the recipient at all (since no delivery + /// occurs on-chain), and to alter the message contents (possibly resulting in an undecryptable message, or one + /// with incorrect content). + /// + /// An undecryptable or otherwise invalid note or event message will however simply be ignored by the recipient, + /// who can always validate the existence of the note or event on-chain. + /// + /// Because the message is not stored on-chain, it is the sender's (and eventually recipient's) responsibility to + /// back it up and make sure it is not lost. + /// + /// ## Costs + /// + /// Because no data is emitted on-chain, this delivery option is the cheapest one in terms of transaction fees: + /// these are zero. + /// + /// Additionally, no circuit gates are introduced when the message is encrypted, since its provenance cannot be + /// authenticated anyway. Therefore, off-chain messages do not affect proving time at all. + /// + /// ## Privacy + /// + /// No information is revealed on-chain about sender, recipient, or the message contents. The message itself + /// reveals no information about the sender or recipient, and requires knowledge of the recipient's private address + /// keys in order to obtain the plaintext. + pub fn offchain() -> OffchainDelivery { + OffchainDelivery {} + } + + /// Delivers the message on-chain, but with no guarantees on the content. + /// + /// ## Use Cases + /// + /// This delivery method is suitable when the sender is required to send the message to the recipient because of + /// some external reason, but might not have a way to contact them off-chain, or does not wish to bear the + /// responsability of keeping backups. In these cases, it might be unnecessary to force the sender to spend proving + /// time guaranteeing message correctness. + /// + /// For example, when depositing funds into an escrow or sale contract the sender may not have an off-chain channel + /// through which they could send the recipient a message. But since the recipient will not acknowledge receipt and + /// proceed with the exchange unless they obtain the message, the sender has no reason not to deliver the message + /// correctly. + /// + /// ## Guarantees + /// + /// The message will be stored on-chain in a private log, as part of the transaction's effects, and will be + /// retrievable in the future without requiring any backups. However, the sender is free to alter the message + /// contents (possibly resulting in an undecryptable message, or one with incorrect content), including making it + /// so that the recipient cannot find it. + /// + /// An undecryptable or otherwise invalid note or event message will however simply be ignored by the recipient, + /// who can always validate the existence of the note or event on-chain. + /// + /// These guarantees make this delivery mechanism be quite similar to [`MessageDelivery::offchain`], except the + /// sender does not need to establish an off-chain communication channel with the recipient, and neither party + /// needs to worry about backups. + /// + /// ## Costs + /// + /// Because the encrypted message is emitted on-chain as transaction private logs, this delivery option results in + /// transaction fees associated with DA gas. The length of the original message is irrelevant to this cost, since + /// all private logs are padded to the same length with random data to enhance privacy. + /// + /// However, no circuit gates are introduced when the message is encrypted. Therefore, on-chain unconstrained + /// messages do not affect proving time at all. + /// + /// ## Privacy + /// + /// No information is revealed on-chain about sender, recipient, or the message contents. The message itself + /// reveals no information about the sender or recipient, and requires knowledge of the recipient's private address + /// keys in order to obtain the plaintext. + /// + /// Delivering the message does produce on-chain information in the form of private logs, so transactions that + /// deliver many messages this way might be identifiable by the large number of logs. + /// + /// Identifying that a log corresponds to a message between a given sender and recipient requires, among other + /// things, knowledge of both of their addresses **and** either the sender's or recipient's private address key. + pub fn onchain_unconstrained() -> OnchainUnconstrainedDelivery { + OnchainUnconstrainedDelivery::new() + } + + /// Delivers the message on-chain, guaranteeing the recipient will receive the correct content. + /// + /// >**WARNING**: this delivery mode is [currently NOT fully + /// constrained](https://github.com/AztecProtocol/aztec-packages/issues/14565). The log's tag is unconstrained, + /// meaning a malicious sender could manipulate it to prevent the recipient from finding the message. + /// + /// ## Use Cases + /// + /// This delivery method is suitable for all use cases, since it always works as expected. It is however the most + /// costly method, and there are multiple scenarios where alternatives such as [`MessageDelivery::offchain`] or + /// [`MessageDelivery::onchain_unconstrained`] will suffice. + /// + /// If the sender cannot be relied on to correctly send the message to the recipient (e.g. because they have no + /// incentive to do so, such as when paying a fee to a protocol, creating the change note after spending a third + /// party's tokens, or updating the configuration of a shared system like a multisig) then this is the only + /// suitable delivery option. + /// + /// ## Guarantees + /// + /// The message will be stored on-chain in a private log, as part of the transaction's effects, and will be + /// retrievable in the future without requiring any backups. The ciphertext will be decryptable by the recipient + /// using their address private key and the ephemeral public key that accompanies the message. + /// + /// The log will be tagged in such a way that the recipient will be able to efficiently find it after querying for + /// handshakes. + /// + /// ## Costs + /// + /// Because the encrypted message is emitted on-chain as transaction private logs, this delivery option results in + /// transaction fees associated with DA gas. The length of the original message is irrelevant to this cost, since + /// all private logs are padded to the same length with random data to enhance privacy. + /// + /// Additionally, the constraining of the log's tag results in additional DA usage and hence transaction fees due + /// to the emission of nullifiers. + /// + /// Proving time is also increased as circuit gates are introduced to guarantee both the correct encryption of + /// the message, and selection of log tag. + /// + /// ## Privacy + /// + /// No information is revealed on-chain about sender, recipient, or the message contents. The message itself + /// reveals no information about the sender or recipient, and requires knowledge of the recipient's private address + /// keys in order to obtain the plaintext. + /// + /// Delivering the message does produce on-chain information in the form of private logs and nullifiers, so + /// transactions that deliver many messages this way might be identifiable by these markers. + /// + /// Identifying that a log corresponds to a message between a given sender and recipient requires, among other + /// things, knowledge of both of their addresses **and** either the sender's or recipient's private address key. + pub fn onchain_constrained() -> OnchainConstrainedDelivery { + OnchainConstrainedDelivery::new() + } +} + +/// Builds the [`MessageDelivery`] configuration consumed by the message delivery APIs (e.g. `NoteMessage::deliver` and +/// `EventMessage::deliver`). Implemented by the builder types returned from the [`MessageDelivery`] constructors. +pub trait MessageDeliveryBuilder { + fn build_message_delivery(self) -> MessageDelivery; +} + +/// Off-chain delivery. Returned by [`MessageDelivery::offchain`]. +pub struct OffchainDelivery {} + +impl MessageDeliveryBuilder for OffchainDelivery { + fn build_message_delivery(self) -> MessageDelivery { + MessageDelivery { + mode: DeliveryMode::offchain(), + tag_secret_derivation: TagSecretDerivation::wallet_default(), + sender_override: Option::none(), + } + } +} + +/// On-chain delivery without constrained encryption/tagging. Returned by [`MessageDelivery::onchain_unconstrained`]. +pub struct OnchainUnconstrainedDelivery { + tag_secret_derivation: TagSecretDerivation, + sender_override: Option, +} + +impl OnchainUnconstrainedDelivery { + fn new() -> Self { + Self { + tag_secret_derivation: TagSecretDerivation::wallet_default(), + sender_override: Option::none(), + } + } + + /// Overrides the sender address used for discovery tag derivation. + /// + /// On-chain messages are tagged so that the recipient can find them efficiently without scanning all logs. The tag + /// is derived from a shared secret between a "sender" and the recipient. By default, the sender is the + /// wallet-supplied address (typically the account that initiated the transaction), but some contracts need to + /// override it so that recipients can discover the notes correctly. This is the case for account contracts in their + /// constructor: the deployer is the one that initiated the transaction, but any notes generated during deployment + /// should be tagged by the account contract itself. + /// + /// ## Examples + /// + /// ```noir + /// MessageDelivery::onchain_unconstrained().with_sender(self.address) + /// ``` + pub fn with_sender(&mut self, sender: AztecAddress) -> Self { + self.sender_override = Option::some(sender); + *self + } + + /// Uses the PXE-derived address Diffie-Hellman secret for discovery tags. + pub fn via_address_derived_secret(&mut self) -> Self { + self.tag_secret_derivation = TagSecretDerivation::address_secret(); + *self + } + + /// Uses the standard handshake registry's non-interactive handshake secret for discovery tags. + pub fn via_non_interactive_handshake(&mut self) -> Self { + self.tag_secret_derivation = TagSecretDerivation::non_interactive_handshake(); + *self + } +} + +impl MessageDeliveryBuilder for OnchainUnconstrainedDelivery { + fn build_message_delivery(self) -> MessageDelivery { + MessageDelivery { + mode: DeliveryMode::onchain_unconstrained(), + tag_secret_derivation: self.tag_secret_derivation, + sender_override: self.sender_override, + } + } +} + +impl From for OnchainDeliveryMode { + fn from(_delivery: OnchainUnconstrainedDelivery) -> OnchainDeliveryMode { + OnchainDeliveryMode::onchain_unconstrained() + } +} + +/// On-chain delivery with constrained encryption/tagging. Returned by [`MessageDelivery::onchain_constrained`]. +pub struct OnchainConstrainedDelivery { + tag_secret_derivation: TagSecretDerivation, + sender_override: Option, +} + +impl OnchainConstrainedDelivery { + fn new() -> Self { + Self { + tag_secret_derivation: TagSecretDerivation::wallet_default(), + sender_override: Option::none(), + } + } + + /// Overrides the sender address used for discovery tag derivation. + /// + /// On-chain messages are tagged so that the recipient can find them efficiently without scanning all logs. The tag + /// is derived from a shared secret between a "sender" and the recipient. By default, the sender is the + /// wallet-supplied address (typically the account that initiated the transaction), but some contracts need to + /// override it so that recipients can discover the notes correctly. This is the case for account contracts in their + /// constructor: the deployer is the one that initiated the transaction, but any notes generated during deployment + /// should be tagged by the account contract itself. + /// + /// ## Examples + /// + /// ```noir + /// MessageDelivery::onchain_constrained().with_sender(self.address) + /// ``` + pub fn with_sender(&mut self, sender: AztecAddress) -> Self { + self.sender_override = Option::some(sender); + *self + } + + /// Uses the standard handshake registry's non-interactive handshake secret for discovery tags. + pub fn via_non_interactive_handshake(&mut self) -> Self { + self.tag_secret_derivation = TagSecretDerivation::non_interactive_handshake(); + *self + } +} + +impl MessageDeliveryBuilder for OnchainConstrainedDelivery { + fn build_message_delivery(self) -> MessageDelivery { + MessageDelivery { + mode: DeliveryMode::onchain_constrained(), + tag_secret_derivation: self.tag_secret_derivation, + sender_override: self.sender_override, + } + } +} + +impl From for OnchainDeliveryMode { + fn from(_delivery: OnchainConstrainedDelivery) -> OnchainDeliveryMode { + OnchainDeliveryMode::onchain_constrained() + } +} + +mod test { + use crate::protocol::address::AztecAddress; + use crate::protocol::traits::FromField; + use super::{DeliveryMode, MessageDelivery, MessageDeliveryBuilder, OnchainDeliveryMode, TagSecretDerivation}; + + #[test] + fn onchain_deliveries_default_to_wallet_default_and_no_sender() { + let unconstrained_delivery = MessageDelivery::onchain_unconstrained().build_message_delivery(); + let constrained_delivery = MessageDelivery::onchain_constrained().build_message_delivery(); + + assert(unconstrained_delivery.tag_secret_derivation() == TagSecretDerivation::wallet_default()); + assert(unconstrained_delivery.sender_override().is_none()); + assert(constrained_delivery.tag_secret_derivation() == TagSecretDerivation::wallet_default()); + assert(constrained_delivery.sender_override().is_none()); + } + + #[test] + fn offchain_delivery_defaults_to_wallet_default() { + let delivery = MessageDelivery::offchain().build_message_delivery(); + + assert(delivery.tag_secret_derivation() == TagSecretDerivation::wallet_default()); + } + + #[test] + fn onchain_unconstrained_can_select_address_derived_secret_derivation() { + let delivery = MessageDelivery::onchain_unconstrained().via_address_derived_secret().build_message_delivery(); + + assert(delivery.tag_secret_derivation() == TagSecretDerivation::address_secret()); + } + + #[test] + fn onchain_deliveries_can_select_non_interactive_handshake_derivation() { + let unconstrained_delivery = + MessageDelivery::onchain_unconstrained().via_non_interactive_handshake().build_message_delivery(); + let constrained_delivery = + MessageDelivery::onchain_constrained().via_non_interactive_handshake().build_message_delivery(); + + assert( + unconstrained_delivery.tag_secret_derivation() == TagSecretDerivation::non_interactive_handshake(), + ); + assert( + constrained_delivery.tag_secret_derivation() == TagSecretDerivation::non_interactive_handshake(), + ); + } + + #[test] + fn with_sender_populates_the_config() { + let sender = AztecAddress::from_field(7); + + let unconstrained_delivery = + MessageDelivery::onchain_unconstrained().with_sender(sender).build_message_delivery(); + let constrained_delivery = MessageDelivery::onchain_constrained().with_sender(sender).build_message_delivery(); + + assert(unconstrained_delivery.sender_override().unwrap() == sender); + assert(constrained_delivery.sender_override().unwrap() == sender); + } + + #[test] + fn builders_convert_to_expected_onchain_modes() { + let unconstrained_mode: OnchainDeliveryMode = MessageDelivery::onchain_unconstrained().into(); + let constrained_mode: OnchainDeliveryMode = MessageDelivery::onchain_constrained().into(); + + assert(unconstrained_mode == OnchainDeliveryMode::onchain_unconstrained()); + assert(constrained_mode == OnchainDeliveryMode::onchain_constrained()); + } + + #[test] + fn built_delivery_mode_matches_the_mode() { + assert(MessageDelivery::offchain().build_message_delivery().mode() == DeliveryMode::offchain()); + assert( + MessageDelivery::onchain_unconstrained().build_message_delivery().mode() + == DeliveryMode::onchain_unconstrained(), + ); + assert( + MessageDelivery::onchain_constrained().build_message_delivery().mode() + == DeliveryMode::onchain_constrained(), + ); + } +} diff --git a/noir-projects/aztec-nr/aztec/src/messages/delivery/mod.nr b/noir-projects/aztec-nr/aztec/src/messages/delivery/mod.nr index 53089a61eadf..d01fb36cfbea 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/delivery/mod.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/delivery/mod.nr @@ -8,187 +8,17 @@ use crate::{ utils::remove_constraints::remove_constraints_if, }; use crate::protocol::{address::AztecAddress, constants::DOM_SEP__UNCONSTRAINED_MSG_LOG_TAG, hash::compute_log_tag}; +use mode::DeliveryMode; +use tag_secret_derivation::TagSecretDerivation; -/// Specifies how to deliver a message to a recipient. -/// -/// All messages are delivered encrypted to their recipient's public address key, so no other account will be able to -/// read their contents. This struct instead configures which **guarantees** exist regarding delivery. -/// -/// There are two aspects to delivery guarantees: -/// - the medium on which the message is sent (off-chain or on-chain) -/// - whether the contract constrains the message to be constructed correctly -/// -/// For scenarios where the sender is incentivized to deliver the message correctly, use [`MessageDelivery::offchain`] -/// (the cheapest delivery option, but requiring that sender and recipient can communicate off-chain) or -/// [`MessageDelivery::onchain_unconstrained`]. If the sender cannot be trusted to send the message to the recipient, -/// use [`MessageDelivery::onchain_constrained`]. -pub struct MessageDelivery { - variant: u8, - sender_override: Option, -} - -global OFFCHAIN: u8 = 1; -/// Delivery variant identifier for on-chain message delivery without constrained encryption/tagging. -pub global ONCHAIN_UNCONSTRAINED: u8 = 2; -/// Delivery variant identifier for on-chain message delivery with constrained encryption/tagging. -pub global ONCHAIN_CONSTRAINED: u8 = 3; +mod builder; +mod mode; +mod tag_secret_derivation; -impl MessageDelivery { - /// Delivers the message fully off-chain, with no guarantees whatsoever. - /// - /// ## Use Cases - /// - /// This delivery method is suitable when the sender is required to send the message to the recipient because of - /// some external reason, and where the sender is able to directly contact the recipient off-chain. In these - /// cases, it might be unnecessary to force the sender to spend proving time guaranteeing message correctness, or - /// to pay transaction fees in order to use the chain as a medium. - /// - /// For example, if performing a payment in exchange for some good or service, the recipient will only accept the - /// payment once they receive note and event messages, allowing them to observe the balance increase. The sender - /// has no reason not to deliver the message correctly to the recipient, and in all likelihood has a way to send - /// it to them. - /// - /// Similarly, in games and other applications that might rely on some server processing state, players might be - /// required to update the server with their current state. - /// - /// Finally, any messages for which the recipient is a local account (e.g.: the message for the change note in a - /// token transfer) work well with this delivery option, since the sender would only be harming themselves by not - /// delivering correctly. - /// - /// ## Guarantees - /// - /// The sender of the message is free to both not deliver the message to the recipient at all (since no delivery - /// occurs on-chain), and to alter the message contents (possibly resulting in an undecryptable message, or one - /// with incorrect content). - /// - /// An undecryptable or otherwise invalid note or event message will however simply be ignored by the recipient, - /// who can always validate the existence of the note or event on-chain. - /// - /// Because the message is not stored on-chain, it is the sender's (and eventually recipient's) responsability to - /// back it up and make sure it is not lost. - /// - /// ## Costs - /// - /// Because no data is emitted on-chain, this delivery option is the cheapest one in terms of transaction fees: - /// these are zero. - /// - /// Additionally, no circuit gates are introduced when the message is encrypted, since its provenance cannot be - /// authenticated anyway. Therefore, off-chain messages do not affect proving time at all. - /// - /// ## Privacy - /// - /// No information is revelead on-chain about sender, recipient, or the message contents. The message itself - /// reveals no information about the sender or recipient, and requires knowledge of the recipient's private address - /// keys in order to obtain the plaintext. - pub fn offchain() -> OffchainDelivery { - OffchainDelivery {} - } - - /// Delivers the message on-chain, but with no guarantees on the content. - /// - /// ## Use Cases - /// - /// This delivery method is suitable when the sender is required to send the message to the recipient because of - /// some external reason, but might not have a way to contact them off-chain, or does not wish to bear the - /// responsability of keeping backups. In these cases, it might be unnecessary to force the sender to spend proving - /// time guaranteeing message correctness. - /// - /// For example, when depositing funds into an escrow or sale contract the sender may not have an off-chain channel - /// through which they could send the recipient a message. But since the recipient will not acknowledge receipt and - /// proceed with the exchange unless they obtain the message, the sender has no reason not to deliver the message - /// correctly. - /// - /// ## Guarantees - /// - /// The message will be stored on-chain in a private log, as part of the transaction's effects, and will be - /// retrievable in the future without requiring any backups. However, the sender is free to alter the message - /// contents (possibly resulting in an undecryptable message, or one with incorrect content), including making it - /// so that the recipient cannot find it. - /// - /// An undecryptable or otherwise invalid note or event message will however simply be ignored by the recipient, - /// who can always validate the existence of the note or event on-chain. - /// - /// These guarantees make this delivery mechanism be quite similar to [`MessageDelivery::offchain`], except the - /// sender does not need to establish an off-chain communication channel with the recipient, and neither party - /// needs to worry about backups. - /// - /// ## Costs - /// - /// Because the encrypted message is emitted on-chain as transaction private logs, this delivery option results in - /// transaction fees associated with DA gas. The length of the original message is irrelevant to this cost, since - /// all private logs are padded to the same length with random data to enhance privacy. - /// - /// However, no circuit gates are introduced when the message is encrypted. Therefore, on-chain unconstrained - /// messages do not affect proving time at all. - /// - /// ## Privacy - /// - /// No information is revealed on-chain about sender, recipient, or the message contents. The message itself - /// reveals no information about the sender or recipient, and requires knowledge of the recipient's private address - /// keys in order to obtain the plaintext. - /// - /// Delivering the message does produce on-chain information in the form of private logs, so transactions that - /// deliver many messages this way might be identifiable by the large number of logs. - /// - /// Identifying that a log corresponds to a message between a given sender and recipient requires, among other - /// things, knowledge of both of their addresses **and** either the sender's or recipient's private address key. - pub fn onchain_unconstrained() -> OnchainDelivery { - OnchainDelivery { variant: ONCHAIN_UNCONSTRAINED, sender_override: Option::none() } - } - - /// Delivers the message on-chain, guaranteeing the recipient will receive the correct content. - /// - /// >**WARNING**: this delivery mode is [currently NOT fully - /// constrained](https://github.com/AztecProtocol/aztec-packages/issues/14565). The log's tag is unconstrained, - /// meaning a malicious sender could manipulate it to prevent the recipient from finding the message. - /// - /// ## Use Cases - /// - /// This delivery method is suitable for all use cases, since it always works as expected. It is however the most - /// costly method, and there are multiple scenarios where alternatives such as [`MessageDelivery::offchain`] or - /// [`MessageDelivery::onchain_unconstrained`] will suffice. - /// - /// If the sender cannot be relied on to correctly send the message to the recipient (e.g. because they have no - /// incentive to do so, such as when paying a fee to a protocol, creating the change note after spending a third - /// party's tokens, or updating the configuration of a shared system like a multisig) then this is the only - /// suitable delivery option. - /// - /// ## Guarantees - /// - /// The message will be stored on-chain in a private log, as part of the transaction's effects, and will be - /// retrievable in the future without requiring any backups. The ciphertext will be decryptable by the recipient - /// using their address private key and the ephemeral public key that accompanies the message. - /// - /// The log will be tagged in such a way that the recipient will be able to efficiently find it after querying for - /// handshakes. - /// - /// ## Costs - /// - /// Because the encrypted message is emitted on-chain as transaction private logs, this delivery option results in - /// transaction fees associated with DA gas. The length of the original message is irrelevant to this cost, since - /// all private logs are padded to the same length with random data to enhance privacy. - /// - /// Additionally, the constraining of the log's tag results in additional DA usage and hence transaction fees due - /// to the emission of nullifiers. - /// - /// Proving time is also increased as circuit gates are introduced to guarantee both the correct encryption of - /// the message, and selection of log tag. - /// - /// ## Privacy - /// - /// No information is revelead on-chain about sender, recipient, or the message contents. The message itself - /// reveals no information about the sender or recipient, and requires knowledge of the recipient's private address - /// keys in order to obtain the plaintext. - /// - /// Delivering the message does produce on-chain information in the form of private logs and nullifiers, so - /// transactions that deliver many messages this way might be identifiable by these markers. - /// - /// Identifying that a log corresponds to a message between a given sender and recipient requires, among other - /// things, knowledge of both of their addresses **and** either the sender's or recipient's private address key. - pub fn onchain_constrained() -> OnchainDelivery { - OnchainDelivery { variant: ONCHAIN_CONSTRAINED, sender_override: Option::none() } - } -} +pub use builder::{ + MessageDelivery, MessageDeliveryBuilder, OffchainDelivery, OnchainConstrainedDelivery, OnchainUnconstrainedDelivery, +}; +pub use mode::OnchainDeliveryMode; /// Performs private delivery of a message to `recipient` according to `delivery_mode`. /// @@ -197,13 +27,10 @@ impl MessageDelivery { /// message in scenarios where the plaintext will be encrypted with unconstrained encryption. /// /// `maybe_note_hash_counter` is only relevant for on-chain delivery modes (i.e. via protocol logs): if a newly created -/// note hash's side effect counter is passed, then the log will be squashed alongside the note should its nullifier be -/// emitted in the current transaction. This is typically only used for note messages: since the note will not actually -/// be created, there is no point in delivering the message. -/// -/// `delivery_mode` must be a value that implements [`MessageDeliveryBuilder`], such as [`OffchainDelivery`] or -/// [`OnchainDelivery`] (constructed via [`MessageDelivery::offchain`], [`MessageDelivery::onchain_unconstrained`], or -/// [`MessageDelivery::onchain_constrained`]). +/// note hash's side effect counter is passed and constrained tagging is not in use, then the log will be squashed +/// alongside the note should its nullifier be emitted in the current transaction. Constrained-tagged logs are not tied +/// to note squashing, because recipient discovery scans those tags sequentially and a removed log would break the +/// per-secret index chain. /// /// ## Privacy /// @@ -221,31 +48,52 @@ where { let delivery = delivery_mode.build_message_delivery(); - // This function relies on `delivery.variant` being a constant in order to reduce circuit constraints when + let mode = delivery.mode(); + + // This function relies on `mode` being a constant in order to reduce circuit constraints when // unconstrained usage is requested. If it were a runtime value the compiler would be unable to perform // dead-code elimination. - assert_constant(delivery.variant); + mode.assert_is_constant(); + + let deliver_as_offchain_message = mode == DeliveryMode::offchain(); + let is_constrained = mode == DeliveryMode::onchain_constrained(); - // The following maps out the 3 dimensions across which we configure message delivery. - let constrained_encryption = delivery.variant == ONCHAIN_CONSTRAINED; - let deliver_as_offchain_message = delivery.variant == OFFCHAIN; - // TODO(#14565): Add constrained tagging - let _constrained_tagging = delivery.variant == ONCHAIN_CONSTRAINED; + let tag_secret_derivation = delivery.tag_secret_derivation(); + tag_secret_derivation.assert_is_constant(); + + if !deliver_as_offchain_message { + let resolved_tag_secret_derivation = resolve_tag_secret_derivation(mode, tag_secret_derivation); + resolved_tag_secret_derivation.assert_is_constant(); + + if is_constrained { + // Constrained tagging derives the tag from a handshake-registry secret and emits a chain nullifier. + std::static_assert( + resolved_tag_secret_derivation == TagSecretDerivation::non_interactive_handshake(), + "constrained delivery requires non-interactive handshake tag derivation", + ); + } else { + // Unconstrained handshake-origin delivery is not yet implemented; see F-698. + std::static_assert( + resolved_tag_secret_derivation == TagSecretDerivation::address_secret(), + "unconstrained handshake delivery not yet implemented (F-698)", + ); + } + } let contract_address = context.this_address(); let ciphertext = remove_constraints_if( - !constrained_encryption, + !is_constrained, || AES128::encrypt(encode_into_message_plaintext(), recipient, contract_address), ); if deliver_as_offchain_message { deliver_offchain_message(ciphertext, recipient); } else { - // TODO(#14565): constrained tagging is not yet implemented. Both modes currently use the unconstrained - // domain separator because the discovery tag always comes from an oracle. Once constrained tagging lands, - // this should branch on `constrained_tagging` to select the appropriate separator. - let discovery_tag = compute_discovery_tag(recipient, delivery.sender_override); + // TODO(#14565): constrained tagging is not yet wired up. The tag-secret derivation is validated, but the tag is + // mocked with the wallet-driven unconstrained derivation so the builder API can land before the constrained + // helpers. + let discovery_tag = compute_discovery_tag(recipient, delivery.sender_override()); let log_tag = compute_log_tag(discovery_tag, DOM_SEP__UNCONSTRAINED_MSG_LOG_TAG); // We forbid this value not being constant to avoid predicating the context calls below, which might result in @@ -254,8 +102,12 @@ where // we're emitting a note or non-note message. assert_constant(maybe_note_hash_counter.is_some()); + // Constrained-tagged logs must not be squashed alongside the note: the recipient discovers them by scanning + // the per-secret tag sequence, so removing a log would break the index chain. + let squashable_note_log = maybe_note_hash_counter.is_some() & !is_constrained; + let log = BoundedVec::from_array(ciphertext); - if maybe_note_hash_counter.is_some() { + if squashable_note_log { // We associate the log with the note's side effect counter, so that if the note ends up being squashed in // the current transaction, the log will be removed as well. context.emit_raw_note_log_unsafe(log_tag, log, maybe_note_hash_counter.unwrap()); @@ -265,70 +117,47 @@ where } } -/// Builds the [`MessageDelivery`] configuration consumed by the message delivery APIs (e.g. `NoteMessage::deliver` and -/// `EventMessage::deliver`). Implemented by the builder types returned from the [`MessageDelivery`] constructors, -/// namely [`OffchainDelivery`] and [`OnchainDelivery`]. -pub trait MessageDeliveryBuilder { - fn build_message_delivery(self) -> MessageDelivery; -} - -/// Off-chain delivery. Returned by [`MessageDelivery::offchain`]. -pub struct OffchainDelivery {} - -impl MessageDeliveryBuilder for OffchainDelivery { - fn build_message_delivery(self) -> MessageDelivery { - MessageDelivery { variant: OFFCHAIN, sender_override: Option::none() } - } -} - -/// On-chain delivery. Returned by [`MessageDelivery::onchain_unconstrained`] and -/// [`MessageDelivery::onchain_constrained`]. -/// -/// Delivery can be further configured via [`with_sender`](OnchainDelivery::with_sender), which overrides the default -/// sender address used during discovery tag derivation. -pub struct OnchainDelivery { - variant: u8, - sender_override: Option, -} - -impl OnchainDelivery { - /// Overrides the sender address used for discovery tag derivation. - /// - /// On-chain messages are tagged so that the recipient can find them efficiently without scanning all logs. The tag - /// is derived from a shared secret between a "sender" and the recipient. By default, the sender is the - /// wallet-supplied address (typically the account that initiated the transaction), but some contracts need to - /// override it so that recipients can discover the notes correctly. This is the case for account contracts in their - /// constructor: the deployer is the one that initiated the transaction, but any notes generated during deployment - /// should be tagged by the account contract itself. - /// - /// ## Examples - /// - /// ```noir - /// MessageDelivery::onchain_constrained().with_sender(self.address) - /// ``` - pub fn with_sender(&mut self, sender: AztecAddress) -> Self { - self.sender_override = Option::some(sender); - *self - } -} - -impl MessageDeliveryBuilder for OnchainDelivery { - fn build_message_delivery(self) -> MessageDelivery { - MessageDelivery { variant: self.variant, sender_override: self.sender_override } +fn resolve_tag_secret_derivation( + mode: DeliveryMode, + tag_secret_derivation: TagSecretDerivation, +) -> TagSecretDerivation { + if tag_secret_derivation == TagSecretDerivation::wallet_default() { + if mode == DeliveryMode::onchain_constrained() { + TagSecretDerivation::non_interactive_handshake() + } else { + TagSecretDerivation::address_secret() + } + } else { + tag_secret_derivation } } mod test { - use super::{MessageDelivery, MessageDeliveryBuilder, OFFCHAIN, ONCHAIN_CONSTRAINED, ONCHAIN_UNCONSTRAINED}; + use super::{resolve_tag_secret_derivation, DeliveryMode, TagSecretDerivation}; #[test] - fn constructors_produce_distinct_variants() { - let offchain = MessageDelivery::offchain().build_message_delivery(); - let onchain_unc = MessageDelivery::onchain_unconstrained().build_message_delivery(); - let onchain_con = MessageDelivery::onchain_constrained().build_message_delivery(); + fn wallet_default_resolves_for_delivery_mode() { + assert( + resolve_tag_secret_derivation(DeliveryMode::onchain_unconstrained(), TagSecretDerivation::wallet_default()) + == TagSecretDerivation::address_secret(), + ); + assert( + resolve_tag_secret_derivation(DeliveryMode::onchain_constrained(), TagSecretDerivation::wallet_default()) + == TagSecretDerivation::non_interactive_handshake(), + ); + } - assert(offchain.variant == OFFCHAIN); - assert(onchain_unc.variant == ONCHAIN_UNCONSTRAINED); - assert(onchain_con.variant == ONCHAIN_CONSTRAINED); + #[test] + fn explicit_tag_secret_derivation_is_preserved() { + assert( + resolve_tag_secret_derivation(DeliveryMode::onchain_unconstrained(), TagSecretDerivation::address_secret()) + == TagSecretDerivation::address_secret(), + ); + assert( + resolve_tag_secret_derivation( + DeliveryMode::onchain_constrained(), + TagSecretDerivation::non_interactive_handshake(), + ) == TagSecretDerivation::non_interactive_handshake(), + ); } } diff --git a/noir-projects/aztec-nr/aztec/src/messages/delivery/mode.nr b/noir-projects/aztec-nr/aztec/src/messages/delivery/mode.nr new file mode 100644 index 000000000000..8bccdc2454f9 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/messages/delivery/mode.nr @@ -0,0 +1,131 @@ +use crate::protocol::{traits::{Deserialize, Serialize, ToField}, utils::reader::Reader}; + +/// Noir does not support enums, so this wrapper models delivery variants while keeping raw discriminants private. +pub(crate) struct DeliveryMode { + inner: u8, +} + +impl DeliveryMode { + pub(crate) fn offchain() -> Self { + Self { inner: 1 } + } + + pub(crate) fn onchain_unconstrained() -> Self { + OnchainDeliveryMode::onchain_unconstrained().into() + } + + pub(crate) fn onchain_constrained() -> Self { + OnchainDeliveryMode::onchain_constrained().into() + } + + pub(crate) fn assert_is_constant(self) { + assert_constant(self.inner); + } +} + +impl Eq for DeliveryMode { + fn eq(self, other: Self) -> bool { + self.inner == other.inner + } +} + +/// The mode of an on-chain message delivery: unconstrained or constrained tagging. +/// +/// This is kept separate from the private [`DeliveryMode`] wrapper because it is used in external ABIs and therefore +/// owns the serialization and validation for the on-chain-only subset. +/// +/// Consumers derive modes through the delivery API, e.g. +/// `let mode: OnchainDeliveryMode = MessageDelivery::onchain_unconstrained().into()`. +#[derive(Serialize)] +pub struct OnchainDeliveryMode { + inner: u8, +} + +impl OnchainDeliveryMode { + /// On-chain delivery without constrained encryption/tagging. + /// + /// `unconstrained` alone is a reserved Noir keyword. + pub(crate) fn onchain_unconstrained() -> Self { + Self { inner: 2 } + } + + /// On-chain delivery with constrained encryption/tagging. + pub(crate) fn onchain_constrained() -> Self { + Self { inner: 3 } + } + + fn from_u8(inner: u8) -> Self { + let mode = Self { inner }; + assert(mode.is_valid(), "unrecognized delivery mode"); + mode + } + + /// Whether `self` is one of the valid on-chain delivery modes. + fn is_valid(self) -> bool { + (self == Self::onchain_unconstrained()) | (self == Self::onchain_constrained()) + } +} + +impl Deserialize for OnchainDeliveryMode { + let N: u32 = ::N; + + fn deserialize(fields: [Field; Self::N]) -> Self { + Self::from_u8(::deserialize(fields)) + } + + fn stream_deserialize(reader: &mut Reader) -> Self { + Self::from_u8(reader.read() as u8) + } +} + +impl Eq for OnchainDeliveryMode { + fn eq(self, other: Self) -> bool { + self.inner == other.inner + } +} + +impl ToField for OnchainDeliveryMode { + fn to_field(self) -> Field { + self.inner as Field + } +} + +impl From for DeliveryMode { + fn from(mode: OnchainDeliveryMode) -> DeliveryMode { + DeliveryMode { inner: mode.inner } + } +} + +mod test { + use crate::protocol::traits::{Deserialize, Serialize}; + use super::{DeliveryMode, OnchainDeliveryMode}; + + #[test] + fn onchain_delivery_modes_are_valid() { + assert(OnchainDeliveryMode::onchain_unconstrained().is_valid()); + assert(OnchainDeliveryMode::onchain_constrained().is_valid()); + } + + #[test] + fn mode_roundtrips_through_serialization() { + let unconstrained_mode = OnchainDeliveryMode::onchain_unconstrained(); + let constrained_mode = OnchainDeliveryMode::onchain_constrained(); + + assert(OnchainDeliveryMode::deserialize(unconstrained_mode.serialize()) == unconstrained_mode); + assert(OnchainDeliveryMode::deserialize(constrained_mode.serialize()) == constrained_mode); + } + + #[test(should_fail_with = "unrecognized delivery mode")] + fn deserializing_invalid_mode_fails() { + let _ = OnchainDeliveryMode::deserialize([99]); + } + + #[test] + fn delivery_mode_onchain_constructors_match_onchain_delivery_modes() { + let unconstrained_delivery_mode: DeliveryMode = OnchainDeliveryMode::onchain_unconstrained().into(); + let constrained_delivery_mode: DeliveryMode = OnchainDeliveryMode::onchain_constrained().into(); + + assert(DeliveryMode::onchain_unconstrained() == unconstrained_delivery_mode); + assert(DeliveryMode::onchain_constrained() == constrained_delivery_mode); + } +} diff --git a/noir-projects/aztec-nr/aztec/src/messages/delivery/tag_secret_derivation.nr b/noir-projects/aztec-nr/aztec/src/messages/delivery/tag_secret_derivation.nr new file mode 100644 index 000000000000..689dfca493e7 --- /dev/null +++ b/noir-projects/aztec-nr/aztec/src/messages/delivery/tag_secret_derivation.nr @@ -0,0 +1,28 @@ +/// Noir does not support enums, so this wrapper models tag-secret derivation variants. +pub(crate) struct TagSecretDerivation { + inner: u8, +} + +impl TagSecretDerivation { + pub(crate) fn wallet_default() -> Self { + Self { inner: 0 } + } + + pub(crate) fn address_secret() -> Self { + Self { inner: 1 } + } + + pub(crate) fn non_interactive_handshake() -> Self { + Self { inner: 2 } + } + + pub(crate) fn assert_is_constant(self) { + assert_constant(self.inner); + } +} + +impl Eq for TagSecretDerivation { + fn eq(self, other: Self) -> bool { + self.inner == other.inner + } +} diff --git a/noir-projects/aztec-nr/aztec/src/messages/logs/utils.nr b/noir-projects/aztec-nr/aztec/src/messages/logs/utils.nr index a548cae86d59..d5a74d37a219 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/logs/utils.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/logs/utils.nr @@ -1,4 +1,4 @@ -use crate::messages::delivery::ONCHAIN_UNCONSTRAINED; +use crate::messages::delivery::MessageDelivery; use crate::oracle::{notes::{get_app_tagging_secret, get_next_tagging_index, get_sender_for_tags}, random::random}; use crate::protocol::{address::AztecAddress, hash::poseidon2_hash}; @@ -26,7 +26,7 @@ pub(crate) fn compute_discovery_tag(recipient: AztecAddress, sender_override: Op random() }, |secret| { - let index = get_next_tagging_index(secret, ONCHAIN_UNCONSTRAINED); + let index = get_next_tagging_index(secret, MessageDelivery::onchain_unconstrained()); poseidon2_hash([secret, index as Field]) }, ) diff --git a/noir-projects/aztec-nr/aztec/src/messages/processing/provided_secret.nr b/noir-projects/aztec-nr/aztec/src/messages/processing/provided_secret.nr index fad18cba6e04..f2dd5796ba0c 100644 --- a/noir-projects/aztec-nr/aztec/src/messages/processing/provided_secret.nr +++ b/noir-projects/aztec-nr/aztec/src/messages/processing/provided_secret.nr @@ -3,9 +3,7 @@ use crate::protocol::traits::Serialize; /// A tagging secret the app supplies explicitly to `get_pending_tagged_logs`. /// /// These are searched alongside the secrets PXE derives or stores internally, and exist for secrets PXE cannot -/// enumerate on its own (e.g. handshake-derived ones). `mode` is a delivery mode constant: -/// [`ONCHAIN_UNCONSTRAINED`](crate::messages::delivery::ONCHAIN_UNCONSTRAINED) or -/// [`ONCHAIN_CONSTRAINED`](crate::messages::delivery::ONCHAIN_CONSTRAINED). +/// enumerate on its own (e.g. handshake-derived ones). `mode` is an on-chain delivery mode serialized as a field. #[derive(Serialize)] #[allow(dead_code)] pub(crate) struct ProvidedSecret { diff --git a/noir-projects/aztec-nr/aztec/src/oracle/notes.nr b/noir-projects/aztec-nr/aztec/src/oracle/notes.nr index 738c4469c90b..b51c484fba3b 100644 --- a/noir-projects/aztec-nr/aztec/src/oracle/notes.nr +++ b/noir-projects/aztec-nr/aztec/src/oracle/notes.nr @@ -1,3 +1,4 @@ +use crate::messages::delivery::OnchainDeliveryMode; use crate::note::{HintedNote, note_interface::NoteType}; use crate::protocol::{address::AztecAddress, traits::Packable}; @@ -192,19 +193,22 @@ unconstrained fn get_app_tagging_secret_oracle(_sender: AztecAddress, _recipient /// /// The caller already holds the tagging `secret`, whether obtained from [`get_app_tagging_secret`] or computed by the /// contract, and only needs the simulator to hand out a fresh per-secret, per-mode index; the caller computes the tag -/// itself. `mode` is one of [`ONCHAIN_UNCONSTRAINED`](crate::messages::delivery::ONCHAIN_UNCONSTRAINED) / -/// [`ONCHAIN_CONSTRAINED`](crate::messages::delivery::ONCHAIN_CONSTRAINED) and determines the per-mode index -/// counter. Offchain messages are not tagged, so only the onchain delivery variants should reach this oracle. +/// itself. `mode` is converted into the [`OnchainDeliveryMode`] selecting the per-mode index counter, so callers can +/// pass either a concrete on-chain delivery builder or an explicit mode. Offchain messages are not tagged, so only the +/// onchain delivery variants can be used by this oracle. /// /// The simulator persists the index increment only if the tagged log is found in an actual block; a reverting /// transaction can otherwise cause the sender to skip indices and later produce notes that are not found by the /// recipient. -pub unconstrained fn get_next_tagging_index(secret: Field, mode: u8) -> u32 { - get_next_tagging_index_oracle(secret, mode) +pub unconstrained fn get_next_tagging_index(secret: Field, mode: M) -> u32 +where + M: Into, +{ + get_next_tagging_index_oracle(secret, mode.into()) } #[oracle(aztec_prv_getNextTaggingIndex)] -unconstrained fn get_next_tagging_index_oracle(_secret: Field, _mode: u8) -> u32 {} +unconstrained fn get_next_tagging_index_oracle(_secret: Field, _mode: OnchainDeliveryMode) -> u32 {} /// Gets the sender for tags. /// diff --git a/noir-projects/aztec-nr/aztec/src/standard_addresses.nr b/noir-projects/aztec-nr/aztec/src/standard_addresses.nr index 23792a3e9f1c..a7dac2a68612 100644 --- a/noir-projects/aztec-nr/aztec/src/standard_addresses.nr +++ b/noir-projects/aztec-nr/aztec/src/standard_addresses.nr @@ -14,5 +14,5 @@ pub global STANDARD_PUBLIC_CHECKS_ADDRESS: AztecAddress = AztecAddress::from_fie ); pub global STANDARD_HANDSHAKE_REGISTRY_ADDRESS: AztecAddress = AztecAddress::from_field( - 0x30280ec85136f131f9c34a9ac5c1390363c9207930c47eb93ddfd83f3601b5a1, + 0x2491f3a3cdfb66c89b1ea0d1d96fde4ea3e64cd48f122b81efeebf7ec24ce24d, ); diff --git a/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/standard_addresses.nr b/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/standard_addresses.nr index 23792a3e9f1c..a7dac2a68612 100644 --- a/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/standard_addresses.nr +++ b/noir-projects/noir-contracts/contracts/protocol/aztec_sublib/src/standard_addresses.nr @@ -14,5 +14,5 @@ pub global STANDARD_PUBLIC_CHECKS_ADDRESS: AztecAddress = AztecAddress::from_fie ); pub global STANDARD_HANDSHAKE_REGISTRY_ADDRESS: AztecAddress = AztecAddress::from_field( - 0x30280ec85136f131f9c34a9ac5c1390363c9207930c47eb93ddfd83f3601b5a1, + 0x2491f3a3cdfb66c89b1ea0d1d96fde4ea3e64cd48f122b81efeebf7ec24ce24d, ); diff --git a/noir-projects/noir-contracts/contracts/standard/handshake_registry_contract/src/main.nr b/noir-projects/noir-contracts/contracts/standard/handshake_registry_contract/src/main.nr index 7eab9f984b79..8255c019b870 100644 --- a/noir-projects/noir-contracts/contracts/standard/handshake_registry_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/standard/handshake_registry_contract/src/main.nr @@ -30,7 +30,7 @@ pub contract HandshakeRegistry { keys::{ecdh_shared_secret::derive_ecdh_shared_secret, ephemeral::generate_positive_ephemeral_key_pair}, macros::{functions::external, storage::storage}, messages::{ - delivery::{MessageDelivery, ONCHAIN_CONSTRAINED, ONCHAIN_UNCONSTRAINED}, + delivery::{MessageDelivery, OnchainDeliveryMode}, encryption::{aes128::AES128, message_encryption::MessageEncryption}, }, protocol::{ @@ -38,16 +38,16 @@ pub contract HandshakeRegistry { constants::DOM_SEP__NON_INTERACTIVE_HANDSHAKE_LOG_TAG, hash::compute_log_tag, point::EmbeddedCurvePoint, - traits::{Deserialize, Packable, Serialize, ToField}, + traits::{Deserialize, Serialize, ToField}, }, state_vars::{Map, Owned, PrivateMutable}, }; /// A handshake discovered during sync: the sender's ephemeral public key and the delivery mode. - #[derive(Deserialize, Eq, Packable, Serialize)] + #[derive(Deserialize, Eq, Serialize)] pub struct DiscoveredHandshake { pub eph_pk: EmbeddedCurvePoint, - pub mode: u8, + pub mode: OnchainDeliveryMode, } #[derive(Deserialize, Serialize)] @@ -60,14 +60,15 @@ pub contract HandshakeRegistry { struct Storage { /// One current [`HandshakeNote`] per `(recipient, sender, mode)` tuple. Re-handshaking for the same tuple /// replaces the prior sender-owned note, so only the latest handshake remains valid for that mode. - handshakes: Map, Context>, Context>, Context>, + handshakes: Map, Context>, Context>, Context>, } /// Performs a non-interactive handshake from `sender` to `recipient` and returns the app-siloed shared secret /// for the calling contract. /// - /// `mode` sets the delivery mode for messages tagged with this handshake ([`ONCHAIN_UNCONSTRAINED`] or - /// [`ONCHAIN_CONSTRAINED`]); the handshake note itself is always stored onchain. + /// `mode` sets the delivery mode for messages tagged with this handshake + /// ([`MessageDelivery::onchain_unconstrained`] or [`MessageDelivery::onchain_constrained`]); the handshake note + /// itself is always stored onchain. /// /// Generates a fresh ephemeral key pair `(eph_sk, eph_pk)`, computes the raw ECDH shared secret point /// `S = eph_sk * recipient_address_point`, and produces three effects: @@ -88,9 +89,7 @@ pub contract HandshakeRegistry { /// protect, and a fallback would insert a permanent note recording a handshake with an invalid recipient, /// polluting registry state. #[external("private")] - fn non_interactive_handshake(sender: AztecAddress, recipient: AztecAddress, mode: u8) -> Field { - assert((mode == ONCHAIN_UNCONSTRAINED) | (mode == ONCHAIN_CONSTRAINED), "unrecognized delivery mode"); - + fn non_interactive_handshake(sender: AztecAddress, recipient: AztecAddress, mode: OnchainDeliveryMode) -> Field { let recipient_point = recipient.to_address_point().expect(f"recipient address is not on the curve"); let (eph_sk, eph_pk) = generate_positive_ephemeral_key_pair(); @@ -98,10 +97,9 @@ pub contract HandshakeRegistry { let note = HandshakeNote::new(s_raw, NON_INTERACTIVE_HANDSHAKE, recipient); - // Delivery is `ONCHAIN_UNCONSTRAINED`. The recipient is not involved in this note's delivery. They - // discover the handshake via the encrypted log emitted below, not via the sender's note. - // We use onchain unconstrained delivery rather than `OFFCHAIN` so the note is - // discoverable via normal PXE sync. + // The recipient is not involved in this note's delivery: they discover the handshake via the encrypted log + // emitted below, not via the sender's note. We deliver onchain unconstrained rather than offchain so the + // note is discoverable via normal PXE sync. self.storage.handshakes.at(recipient).at(mode).at(sender).initialize_or_replace(|_| note).deliver( MessageDelivery::onchain_unconstrained().with_sender(sender), ); @@ -112,7 +110,7 @@ pub contract HandshakeRegistry { ); let ciphertext = AES128::encrypt( - [eph_pk.x, mode as Field], + [eph_pk.x, mode.to_field()], recipient, self.context.this_address(), ); @@ -133,9 +131,12 @@ pub contract HandshakeRegistry { /// If no stored handshake for `(sender, recipient, mode)` silos to `app_siloed_secret` under the calling /// contract's address. #[external("private")] - fn validate_handshake(sender: AztecAddress, recipient: AztecAddress, mode: u8, app_siloed_secret: Field) { - assert((mode == ONCHAIN_UNCONSTRAINED) | (mode == ONCHAIN_CONSTRAINED), "unrecognized delivery mode"); - + fn validate_handshake( + sender: AztecAddress, + recipient: AztecAddress, + mode: OnchainDeliveryMode, + app_siloed_secret: Field, + ) { let caller = self.msg_sender(); let replacement_note_message = self.storage.handshakes.at(recipient).at(mode).at(sender).get_note(); let note = replacement_note_message.get_note(); @@ -160,14 +161,12 @@ pub contract HandshakeRegistry { unconstrained fn get_app_siloed_secret( sender: AztecAddress, recipient: AztecAddress, - mode: u8, + mode: OnchainDeliveryMode, // TODO(F-671): replace with `self.msg_sender()` once utility context exposes it. The // explicit param forces hooks to also gate on the caller; otherwise any contract that // can authorize (target, selector) can read another caller's siloed secret. caller: AztecAddress, ) -> Option { - assert((mode == ONCHAIN_UNCONSTRAINED) | (mode == ONCHAIN_CONSTRAINED), "unrecognized delivery mode"); - let handshake = self.storage.handshakes.at(recipient).at(mode).at(sender); if handshake.is_initialized() { diff --git a/noir-projects/noir-contracts/contracts/standard/handshake_registry_contract/src/sync.nr b/noir-projects/noir-contracts/contracts/standard/handshake_registry_contract/src/sync.nr index b7eea98c39f2..3edf27f41e9d 100644 --- a/noir-projects/noir-contracts/contracts/standard/handshake_registry_contract/src/sync.nr +++ b/noir-projects/noir-contracts/contracts/standard/handshake_registry_contract/src/sync.nr @@ -5,6 +5,7 @@ use aztec::{ context::UtilityContext, ephemeral::EphemeralArray, messages::{ + delivery::OnchainDeliveryMode, discovery::{ComputeNoteHash, ComputeNoteNullifier, CustomMessageHandler, do_sync_state}, encryption::{aes128::AES128, message_encryption::MessageEncryption}, processing::{ @@ -18,7 +19,7 @@ use aztec::{ address::AztecAddress, constants::DOM_SEP__NON_INTERACTIVE_HANDSHAKE_LOG_TAG, hash::{compute_log_tag, sha256_to_field}, - traits::ToField, + traits::{Deserialize, ToField}, }, utils::point::point_from_x_coord_and_sign, }; @@ -71,7 +72,10 @@ pub(crate) unconstrained fn handshake_registry_sync( let _ = AES128::decrypt(ciphertext, scope, contract_address) .and_then(|pt| { point_from_x_coord_and_sign(pt.get(0), true).map(|pk| { - DiscoveredHandshake { eph_pk: pk, mode: pt.get(1) as u8 } + DiscoveredHandshake { + eph_pk: pk, + mode: OnchainDeliveryMode::deserialize([pt.get(1)]), + } }) }) .map(|h| handshakes.push(h)); diff --git a/noir-projects/noir-contracts/contracts/standard/handshake_registry_contract/src/test.nr b/noir-projects/noir-contracts/contracts/standard/handshake_registry_contract/src/test.nr index 5bbf558864c5..1332355de1fb 100644 --- a/noir-projects/noir-contracts/contracts/standard/handshake_registry_contract/src/test.nr +++ b/noir-projects/noir-contracts/contracts/standard/handshake_registry_contract/src/test.nr @@ -1,7 +1,7 @@ use crate::{HandshakeRegistry, MAX_HANDSHAKES_PER_PAGE}; use aztec::{ - messages::delivery::{ONCHAIN_CONSTRAINED, ONCHAIN_UNCONSTRAINED}, + messages::delivery::{MessageDelivery, OnchainDeliveryMode}, oracle::shared_secret::get_shared_secret, protocol::{ address::AztecAddress, @@ -14,6 +14,9 @@ use aztec::{ use std::test::OracleMock; +global ONCHAIN_UNCONSTRAINED: OnchainDeliveryMode = MessageDelivery::onchain_unconstrained().into(); +global ONCHAIN_CONSTRAINED: OnchainDeliveryMode = MessageDelivery::onchain_constrained().into(); + unconstrained fn setup() -> (TestEnvironment, AztecAddress, AztecAddress, AztecAddress, AztecAddress) { let mut env = TestEnvironment::new(); @@ -439,14 +442,6 @@ unconstrained fn validate_handshake_rejects_constrained_secret_for_unconstrained env.call_private(sender, registry.validate_handshake(sender, recipient, ONCHAIN_UNCONSTRAINED, constrained_secret)); } -#[test(should_fail_with = "unrecognized delivery mode")] -unconstrained fn non_interactive_handshake_rejects_unknown_mode() { - let (env, registry_address, sender, _, recipient) = setup(); - let registry = HandshakeRegistry::at(registry_address); - - let _ = env.call_private(sender, registry.non_interactive_handshake(sender, recipient, 99)); -} - #[test] unconstrained fn non_interactive_handshake_is_discovered_by_recipient() { let (env, registry_address, sender, _, recipient) = setup(); diff --git a/noir-projects/noir-contracts/contracts/test/pending_note_hashes_contract/src/main.nr b/noir-projects/noir-contracts/contracts/test/pending_note_hashes_contract/src/main.nr index 17f8c85667fe..ac7b605514ec 100644 --- a/noir-projects/noir-contracts/contracts/test/pending_note_hashes_contract/src/main.nr +++ b/noir-projects/noir-contracts/contracts/test/pending_note_hashes_contract/src/main.nr @@ -45,7 +45,7 @@ pub contract PendingNoteHashes { // Insert note // docs:start:private_set_insert - owner_balance.insert(note).deliver(MessageDelivery::onchain_constrained()); + owner_balance.insert(note).deliver(MessageDelivery::onchain_unconstrained()); // docs:end:private_set_insert let options = NoteGetterOptions::with_filter(filter_notes_min_sum, amount); @@ -90,6 +90,19 @@ pub contract PendingNoteHashes { let note = FieldNote { value: amount }; + // Insert note + owner_balance.insert(note).deliver(MessageDelivery::onchain_unconstrained()); + } + + // Nested/inner function to create and insert a note using constrained delivery. Constrained note logs are not + // linked to the note for squashing (recipients discover them by scanning the per-secret tag sequence, so a + // removed log would break the index chain), meaning the log survives even when the note itself is squashed. + #[external("private")] + fn insert_note_constrained(amount: Field, owner: AztecAddress, sender: AztecAddress) { + let owner_balance = self.storage.balances.at(owner); + + let note = FieldNote { value: amount }; + // Insert note owner_balance.insert(note).deliver(MessageDelivery::onchain_constrained()); } @@ -106,7 +119,7 @@ pub contract PendingNoteHashes { let note = FieldNote::unpack([amount.to_field()]); // Insert note - owner_balance.insert(note).deliver(MessageDelivery::onchain_constrained()); + owner_balance.insert(note).deliver(MessageDelivery::onchain_unconstrained()); } // Nested/inner function to create and insert a note @@ -120,10 +133,10 @@ pub contract PendingNoteHashes { // Insert note let message = owner_balance.insert(note); - message.deliver(MessageDelivery::onchain_constrained()); + message.deliver(MessageDelivery::onchain_unconstrained()); // Deliver note message again - message.deliver(MessageDelivery::onchain_constrained()); + message.deliver(MessageDelivery::onchain_unconstrained()); } // Nested/inner function to get a note and confirm it matches the expected value @@ -315,7 +328,7 @@ pub contract PendingNoteHashes { let recipient_balance = self.storage.balances.at(recipient); let note = FieldNote { value: i as Field }; - recipient_balance.insert(note).deliver(MessageDelivery::onchain_constrained()); + recipient_balance.insert(note).deliver(MessageDelivery::onchain_unconstrained()); } } diff --git a/noir-projects/noir-contracts/pinned-standard-contracts.tar.gz b/noir-projects/noir-contracts/pinned-standard-contracts.tar.gz index 07fa58b223ee..a65a1fe544c3 100644 Binary files a/noir-projects/noir-contracts/pinned-standard-contracts.tar.gz and b/noir-projects/noir-contracts/pinned-standard-contracts.tar.gz differ diff --git a/yarn-project/end-to-end/src/e2e_pending_note_hashes_contract.test.ts b/yarn-project/end-to-end/src/e2e_pending_note_hashes_contract.test.ts index 7eebbe478baf..30d3e84d9c5b 100644 --- a/yarn-project/end-to-end/src/e2e_pending_note_hashes_contract.test.ts +++ b/yarn-project/end-to-end/src/e2e_pending_note_hashes_contract.test.ts @@ -125,6 +125,29 @@ describe('e2e_pending_note_hashes_contract', () => { await expectNoteLogsSquashedExcept(0); }); + it('Squash! Aztec.nr function can "create" and "nullify" note in the same TX but the constrained note log survives', async () => { + // Kernel will squash the noteHash and its nullifier, but NOT the note log: constrained-delivery logs are not + // linked to the note for squashing, because a removed log would break the index chain. + const mintAmount = 65n; + + const deployedContract = await deployContract(); + + const sender = owner; + await deployedContract.methods + .test_insert_then_get_then_nullify_all_in_nested_calls( + mintAmount, + owner, + sender, + await deployedContract.methods.insert_note_constrained.selector(), + await deployedContract.methods.get_then_nullify_note.selector(), + ) + .send({ from: owner }); + + await expectNoteHashesSquashedExcept(0); + await expectNullifiersSquashedExcept(0); + await expectNoteLogsSquashedExcept(1); + }); + it('Squash! Aztec.nr function can "create" and "nullify" note in the same TX with 2 note logs', async () => { // Kernel will squash the noteHash and its nullifier and both note logs // Realistic way to describe this test is "Mint note A, then burn note A in the same transaction" diff --git a/yarn-project/standard-contracts/src/standard_contract_data.ts b/yarn-project/standard-contracts/src/standard_contract_data.ts index 2e49fc46a5da..9df0df3305a1 100644 --- a/yarn-project/standard-contracts/src/standard_contract_data.ts +++ b/yarn-project/standard-contracts/src/standard_contract_data.ts @@ -23,14 +23,14 @@ export const StandardContractAddress: Record AuthRegistry: AztecAddress.fromString('0x023cb6fed0ebb1235f1c2a6656c3b2438b84d24705a517211dc186db7d1ba754'), MultiCallEntrypoint: AztecAddress.fromString('0x27d70a9a022dcd1195a8d2a4a3a8c89b5af1d7831a68891d052af0125a1f1341'), PublicChecks: AztecAddress.fromString('0x2f5e1e2b07b1fab93a0d8217643d988da90e12bd9c41a42265eabfe66c1824a8'), - HandshakeRegistry: AztecAddress.fromString('0x30280ec85136f131f9c34a9ac5c1390363c9207930c47eb93ddfd83f3601b5a1'), + HandshakeRegistry: AztecAddress.fromString('0x2491f3a3cdfb66c89b1ea0d1d96fde4ea3e64cd48f122b81efeebf7ec24ce24d'), }; export const StandardContractClassId: Record = { AuthRegistry: Fr.fromString('0x0a2e03b7a5b45285478faf0981baafed6a2a23ef72f52cc9c7f44d3e5056e2fa'), MultiCallEntrypoint: Fr.fromString('0x0e6d4ba224e83a0883923cd280ceca43832cddb97496c3a81ea73c6833d9d52a'), PublicChecks: Fr.fromString('0x0642156526e3d53f5c83f9554ed147f5102e7ac87dead1bf835e4256bbbdec13'), - HandshakeRegistry: Fr.fromString('0x17919296e72cb4fbd9a6c9d91b3cd4ffbd65b5893ca5f8e0d5147e37ceb97ce8'), + HandshakeRegistry: Fr.fromString('0x1de63eec38ce0ea399518ab624f0eaf9e0dc0ceb98a13acf3614b0682f126955'), }; export const StandardContractClassIdPreimage: Record< @@ -53,8 +53,8 @@ export const StandardContractClassIdPreimage: Record< publicBytecodeCommitment: Fr.fromString('0x013c4f854a5c87c9daf86c5f9bc07a42c2a061f1d924a5b3564ec7edc8e18cb7'), }, HandshakeRegistry: { - artifactHash: Fr.fromString('0x1662bedcee99614ec387e5184ad5b3027ee09fbe53227b52f400b145cdd0b6ae'), - privateFunctionsRoot: Fr.fromString('0x0cf78fb872901f2883a6f9e940156658f5b317f4c437d80a67eb2a47d3bfc0c3'), + artifactHash: Fr.fromString('0x2c24b71c2047a248a4906a1618503b73a77ab4a199e674515273bca67a2ae1d9'), + privateFunctionsRoot: Fr.fromString('0x1ac8dab0c4d3d0b97f608db9f6552f827d6ef1dd514aaa95727f5753336e3fbf'), publicBytecodeCommitment: Fr.fromString('0x0ce4c618c3ed7f3a20410e618c06bb701e150af7fe28a3e92f68e7733809f33e'), }, }; @@ -90,13 +90,13 @@ export const StandardContractPrivateFunctions: Record< HandshakeRegistry: [ { selector: FunctionSelector.fromField( - Fr.fromString('0x000000000000000000000000000000000000000000000000000000005d4db100'), + Fr.fromString('0x000000000000000000000000000000000000000000000000000000009968d9e2'), ), vkHash: Fr.fromString('0x035db3173b6dc6305d989fe910690cc0a556bf30261c6b4235144403e5378635'), }, { selector: FunctionSelector.fromField( - Fr.fromString('0x000000000000000000000000000000000000000000000000000000005fa93894'), + Fr.fromString('0x00000000000000000000000000000000000000000000000000000000f7b8f754'), ), vkHash: Fr.fromString('0x086f9209118872f060094869666a20edb9ad69272c3a1b12fc93dbe839d271c7'), }, diff --git a/yarn-project/stdlib/src/logs/app_tagging_secret_kind.ts b/yarn-project/stdlib/src/logs/app_tagging_secret_kind.ts index d2ac9d3719b9..3203b2636f8f 100644 --- a/yarn-project/stdlib/src/logs/app_tagging_secret_kind.ts +++ b/yarn-project/stdlib/src/logs/app_tagging_secret_kind.ts @@ -5,7 +5,7 @@ export const AppTaggingSecretKind = { export type AppTaggingSecretKind = (typeof AppTaggingSecretKind)[keyof typeof AppTaggingSecretKind]; -// Keep in sync with aztec::messages::delivery::{ONCHAIN_UNCONSTRAINED, ONCHAIN_CONSTRAINED}. +// Keep in sync with aztec::messages::delivery::OnchainDeliveryMode. const ONCHAIN_UNCONSTRAINED_DELIVERY_MODE = 2; const ONCHAIN_CONSTRAINED_DELIVERY_MODE = 3;