Skip to content
Closed
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
4 changes: 2 additions & 2 deletions boxes/boxes/react/src/contracts/src/main.nr
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ contract BoxReact {
let new_number = FieldNote { value: number };

self.storage.numbers.at(owner).initialize(new_number).deliver(
MessageDelivery::onchain_constrained(),
MessageDelivery::onchain_unconstrained(),
);
}

#[external("private")]
fn setNumber(number: Field, owner: AztecAddress) {
self.storage.numbers.at(owner).replace(|_old| FieldNote { value: number }).deliver(
MessageDelivery::onchain_constrained(),
MessageDelivery::onchain_unconstrained(),
);
}

Expand Down
4 changes: 2 additions & 2 deletions boxes/boxes/vite/src/contracts/src/main.nr
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ contract BoxReact {
let new_number = FieldNote { value: number };

self.storage.numbers.at(owner).initialize(new_number).deliver(
MessageDelivery::onchain_constrained(),
MessageDelivery::onchain_unconstrained(),
);
}

#[external("private")]
fn setNumber(number: Field, owner: AztecAddress) {
self.storage.numbers.at(owner).replace(|_old| FieldNote { value: number }).deliver(
MessageDelivery::onchain_constrained(),
MessageDelivery::onchain_unconstrained(),
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ You can deliver the same event to multiple recipients with different delivery mo
```rust
let message = self.emit(Transfer { from, to, amount });
message.deliver_to(from, MessageDelivery::offchain());
message.deliver_to(to, MessageDelivery::onchain_constrained());
message.deliver_to(to, MessageDelivery::onchain_constrained().with_sender(from));
```

The `MessageDelivery` options are:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ pub contract PrivateToken {
fn mint(amount: u128, recipient: AztecAddress) {
// Adding to the balance returns a MaybeNoteMessage
self.storage.balances.at(recipient).add(amount)
.deliver(MessageDelivery::onchain_constrained());
.deliver(MessageDelivery::onchain_unconstrained());
}
}
```
Expand Down Expand Up @@ -148,9 +148,9 @@ self.storage.balances.at(admin).add(amount)
- **Privacy:** High - encrypted log reveals minimal information

```rust
// Minting to an arbitrary recipient - must guarantee delivery
// Minting to an arbitrary recipient - must guarantee delivery. Constrained delivery requires an explicit sender.
self.storage.balances.at(recipient).add(amount)
.deliver(MessageDelivery::onchain_constrained());
.deliver(MessageDelivery::onchain_constrained().with_sender(self.msg_sender()));
```

## Choosing a Delivery Mode
Expand Down Expand Up @@ -199,7 +199,7 @@ You can deliver a note to an address other than the note's owner using `.deliver
```rust
// Create a note owned by `owner` but deliver it to `auditor`
self.storage.balances.at(owner).add(amount)
.deliver_to(auditor, MessageDelivery::onchain_constrained());
.deliver_to(auditor, MessageDelivery::onchain_constrained().with_sender(owner));
```

**Important:** The recipient (e.g. an `auditor`) can see the note was created but **cannot use it** - only the owner can spend the note (this is authorized by the contract logic). The recipient also cannot see when/if the note is nullified.
Expand All @@ -224,7 +224,7 @@ fn transfer(amount: u128, sender: AztecAddress, recipient: AztecAddress) {
// Add to recipient - constrained delivery for untrusted sender
self.storage.balances.at(recipient)
.add(amount)
.deliver(MessageDelivery::onchain_constrained());
.deliver(MessageDelivery::onchain_constrained().with_sender(sender));
}
```

Expand All @@ -238,6 +238,6 @@ fn constructor(admin: AztecAddress) {
// Use unconstrained delivery since we don't know if deployer is incentivized
self.storage.admin
.initialize(AddressNote { address: admin }, admin)
.deliver(MessageDelivery::onchain_constrained());
.deliver(MessageDelivery::onchain_unconstrained());
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,7 @@ fn perform_admin_action() {
// value of the counter and can update it again in the future.
self.storage.admin_call_count
.replace(|current| UintNote{ value: current.value + 1 }) // wouldn't it be great if we didn't have to deal with this wrapping and unwrapping?
.deliver(MessageDelivery::onchain_constrained());
.deliver(MessageDelivery::onchain_constrained().with_sender(admin));

// ...
}
Expand Down Expand Up @@ -381,7 +381,9 @@ This function allows us to get the note of a `PrivateMutable`, essentially readi
#[external("private")]
fn read_settings() {
let owner = self.msg_sender();
self.storage.user_settings.at(owner).get_note().deliver(MessageDelivery::onchain_constrained());
self.storage.user_settings.at(owner).get_note().deliver(
MessageDelivery::onchain_constrained().with_sender(owner),
);
}
```

Expand Down Expand Up @@ -484,7 +486,9 @@ When initializing, you still pass an owner address, but this specifies who can d

```rust
// owner_address determines who can see the note, not where it's stored
self.storage.admin.initialize(note, owner_address).deliver(MessageDelivery::onchain_constrained());
self.storage.admin.initialize(note, owner_address).deliver(
MessageDelivery::onchain_constrained().with_sender(owner_address),
);
```

:::warning
Expand Down
2 changes: 1 addition & 1 deletion docs/docs-developers/docs/aztec-nr/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ A good example of this is writing to private state variables. These functions re

```rust
storage.votes.insert(new_vote); // compiler error - unused NoteMessage return value
storage.votes.insert(new_vote).deliver(MessageDelivery::onchain_constrained()); // deliver the note message onchain
storage.votes.insert(new_vote).deliver(MessageDelivery::onchain_unconstrained()); // deliver the note message onchain
```

## Contract Development
Expand Down
2 changes: 1 addition & 1 deletion docs/docs-developers/docs/aztec-nr/standards/escrow.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ pub fn _share_escrow(
) {
let event_struct = EscrowDetailsLogContent { escrow, master_secret_keys };
emit_event_in_private(context, event_struct).deliver_to(
account, MessageDelivery::onchain_constrained(),
account, MessageDelivery::onchain_constrained().with_sender(account),
);
}
```
Expand Down
13 changes: 12 additions & 1 deletion docs/docs-developers/docs/resources/migration_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,17 @@ Aztec is in active development. Each version may introduce breaking changes that

## TBD

### [Aztec.nr] Constrained delivery requires an explicit sender

`MessageDelivery::onchain_constrained()` now derives the discovery tag from a handshake-registry secret, and the sender is bound into the handshake lookup and the per-send nullifier chain. The sender can therefore no longer fall back to the wallet-supplied default and must be set explicitly with `with_sender`. This is validated at compile time, so a missing sender fails compilation with `constrained delivery requires a sender (use .with_sender())`:

```diff
- note.deliver(MessageDelivery::onchain_constrained());
+ note.deliver(MessageDelivery::onchain_constrained().with_sender(sender));
```

Constrained delivery uses the standard handshake registry. Contracts whose senders are not set up for registry handshakes should use `MessageDelivery::onchain_unconstrained()` instead, which keeps the wallet-driven tagging behavior.

### [Aztec.nr] `messages::message_delivery` module moved to `messages::delivery`

The `message_delivery` module has been renamed to `delivery`. Update imports accordingly:
Expand All @@ -35,7 +46,7 @@ The `set_sender_for_tags` oracle has been removed. Contracts that used it to ove
+ note.deliver(MessageDelivery::onchain_constrained().with_sender(some_address));
```

When `with_sender` is not called, `MessageDelivery` uses the wallet-supplied default sender.
For offchain and unconstrained delivery, `MessageDelivery` uses the wallet-supplied default sender when `with_sender` is not called. Constrained delivery requires an explicit sender (see the entry above).

### [Aztec.nr] `MessageDelivery` API syntax change

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ Let’s create a constructor method to run on deployment that assigns an initial

#include_code constructor /docs/examples/contracts/counter_contract/src/main.nr rust

This function accesses the counters from storage. It adds the `headstart` value to the `owner`'s counter using `at().add()`, then calls `.deliver(MessageDelivery::onchain_constrained())` to ensure the note is delivered onchain.
This function accesses the counters from storage. It adds the `headstart` value to the `owner`'s counter using `at().add()`, then calls `.deliver(MessageDelivery::onchain_unconstrained())` to ensure the note is delivered onchain.

We have annotated this and other functions with `#[external("private")]` which are ABI macros so the compiler understands it will handle private inputs.

Expand Down
10 changes: 5 additions & 5 deletions docs/examples/contracts/bob_token_contract/src/main.nr
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ pub contract BobToken {
self.enqueue_self._deduct_public_balance(sender, amount);
// Add to private balance
self.storage.private_balances.at(sender).add(amount as u128).deliver(
MessageDelivery::onchain_constrained(),
MessageDelivery::onchain_unconstrained(),
);
}
// docs:end:public_to_private
Expand All @@ -107,11 +107,11 @@ pub contract BobToken {
let sender = self.msg_sender();
// Spend sender's notes (consumes existing notes)
self.storage.private_balances.at(sender).sub(amount as u128).deliver(
MessageDelivery::onchain_constrained(),
MessageDelivery::onchain_unconstrained(),
);
// Create new notes for recipient
self.storage.private_balances.at(to).add(amount as u128).deliver(
MessageDelivery::onchain_constrained(),
MessageDelivery::onchain_unconstrained(),
);
}
// docs:end:transfer_private
Expand Down Expand Up @@ -144,7 +144,7 @@ pub contract BobToken {

// If check passes, mint tokens privately
self.storage.private_balances.at(employee).add(amount as u128).deliver(
MessageDelivery::onchain_constrained(),
MessageDelivery::onchain_unconstrained(),
);
}
// docs:end:mint_private
Expand All @@ -155,7 +155,7 @@ pub contract BobToken {
let sender = self.msg_sender();
// Remove from private balance
self.storage.private_balances.at(sender).sub(amount as u128).deliver(
MessageDelivery::onchain_constrained(),
MessageDelivery::onchain_unconstrained(),
);
// Enqueue public credit
self.enqueue_self._credit_public_balance(sender, amount);
Expand Down
4 changes: 2 additions & 2 deletions docs/examples/contracts/counter_contract/src/main.nr
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ pub contract Counter {
// We can name our initializer anything we want as long as it's marked as aztec(initializer)
fn initialize(headstart: u128, owner: AztecAddress) {
self.storage.counters.at(owner).add(headstart).deliver(
MessageDelivery::onchain_constrained(),
MessageDelivery::onchain_unconstrained(),
);
}
// docs:end:constructor
Expand All @@ -37,7 +37,7 @@ pub contract Counter {
#[external("private")]
fn increment(owner: AztecAddress) {
debug_log_format("Incrementing counter for owner {0}", [owner.to_field()]);
self.storage.counters.at(owner).add(1).deliver(MessageDelivery::onchain_constrained());
self.storage.counters.at(owner).add(1).deliver(MessageDelivery::onchain_unconstrained());
}
// docs:end:increment

Expand Down
2 changes: 1 addition & 1 deletion docs/examples/contracts/nft/src/main.nr
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ pub contract NFTPunk {

// we create an NFT note and insert it to the PrivateSet - a collection of notes meant to be read in private
let new_nft = NFTNote { token_id };
self.storage.owners.at(to).insert(new_nft).deliver(MessageDelivery::onchain_constrained());
self.storage.owners.at(to).insert(new_nft).deliver(MessageDelivery::onchain_unconstrained());

// calling the internal public function above to indicate that the NFT is taken
self.enqueue_self._mark_nft_exists(token_id, true);
Expand Down
2 changes: 1 addition & 1 deletion docs/examples/webapp-tutorial/contracts/src/main.nr
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ pub contract PodRacing {
.at(game_id)
.at(player)
.insert(GameRoundNote::new(track1, track2, track3, track4, track5, round, player))
.deliver(MessageDelivery::onchain_constrained());
.deliver(MessageDelivery::onchain_unconstrained());

self.enqueue(PodRacing::at(self.context.this_address()).validate_and_play_round(
player,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ impl<Storage, CallSelf, EnqueueSelf, CallSelfStatic, EnqueueSelfStatic, CallInte
///
/// let message: EventMessage = self.emit(Transfer { from, to, amount });
/// message.deliver_to(from, MessageDelivery::offchain());
/// message.deliver_to(to, MessageDelivery::onchain_constrained());
/// message.deliver_to(to, MessageDelivery::onchain_constrained().with_sender(from));
/// }
/// ```
///
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ use crate::oracle::{call_utility_function::call_utility_function, notes::get_nex
use crate::protocol::{
abis::function_selector::FunctionSelector,
address::AztecAddress,
constants::DOM_SEP__CONSTRAINED_MSG_NULLIFIER,
hash::poseidon2_hash_with_separator,
constants::{DOM_SEP__CONSTRAINED_MSG_LOG_TAG, DOM_SEP__CONSTRAINED_MSG_NULLIFIER},
hash::{compute_log_tag, poseidon2_hash, poseidon2_hash_with_separator},
traits::{Deserialize, ToField},
};

Expand Down Expand Up @@ -118,3 +118,36 @@ pub fn compute_constrained_msg_nullifier(
DOM_SEP__CONSTRAINED_MSG_NULLIFIER,
)
}

/// Computes the constrained-delivery discovery log tag for an `(app_siloed_secret, index)` pair.
///
/// Collapses the secret and index into a single raw tag and domain-separates it with
/// `DOM_SEP__CONSTRAINED_MSG_LOG_TAG`. This mirrors the recipient-side `Tag.compute` / `SiloedTag.compute` derivation
/// in the PXE: the recipient recomputes the same tag to discover the message, and the protocol silos the emitted log's
/// first field by the emitting contract address, completing the match.
pub fn compute_constrained_log_tag(app_siloed_secret: Field, index: u32) -> Field {
compute_log_tag(
poseidon2_hash([app_siloed_secret, index as Field]),
DOM_SEP__CONSTRAINED_MSG_LOG_TAG,
)
}

/// Emits the per-send chain nullifier for a constrained message and returns its discovery log tag.
///
/// Used by [`crate::messages::delivery::do_private_message_delivery`] on the constrained-delivery path. It wraps
/// [`resolve_secret_and_index`] to resolve the `(app_siloed_secret, index)` pair, then:
/// 1. Emits the chain nullifier. A subsequent send at `index + 1` proves this nullifier exists, transitively
/// constraining the chain back to the `index == 0` handshake validation in [`resolve_secret_and_index`].
/// 2. Returns the log tag. The recipient recomputes the same tag from the handshake secret to discover the message.
pub(crate) fn emit_nullifier_and_compute_constrained_tag(
context: &mut PrivateContext,
registry: AztecAddress,
sender: AztecAddress,
recipient: AztecAddress,
) -> Field {
let (secret, index) = resolve_secret_and_index(context, registry, sender, recipient);

context.push_nullifier_unsafe(compute_constrained_msg_nullifier(sender, recipient, secret, index));

compute_constrained_log_tag(secret, index)
}
30 changes: 25 additions & 5 deletions noir-projects/aztec-nr/aztec/src/messages/delivery/mod.nr
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use crate::{
logs::utils::compute_discovery_tag,
offchain_messages::deliver_offchain_message,
},
standard_addresses::STANDARD_HANDSHAKE_REGISTRY_ADDRESS,
utils::remove_constraints::remove_constraints_if,
};
use crate::protocol::{address::AztecAddress, constants::DOM_SEP__UNCONSTRAINED_MSG_LOG_TAG, hash::compute_log_tag};
Expand Down Expand Up @@ -63,6 +64,7 @@ where

let tag_secret_derivation = delivery.tag_secret_derivation();
tag_secret_derivation.assert_is_constant();
let sender_override = delivery.sender_override();

if !deliver_as_offchain_message {
let resolved_tag_secret_derivation = resolve_tag_secret_derivation(mode, tag_secret_derivation);
Expand All @@ -74,6 +76,12 @@ where
resolved_tag_secret_derivation == TagSecretDerivation::non_interactive_handshake(),
"constrained delivery requires non-interactive handshake tag derivation",
);
// The sender is bound into the handshake lookup and the per-send nullifier chain, so it cannot fall back
// to the wallet-supplied tagging hint the unconstrained path uses.
std::static_assert(
sender_override.is_some(),
"constrained delivery requires a sender (use .with_sender())",
);
} else {
// Unconstrained handshake-origin delivery is not yet implemented; see F-698.
std::static_assert(
Expand All @@ -93,11 +101,23 @@ where
if deliver_as_offchain_message {
deliver_offchain_message(ciphertext, recipient);
} else {
// 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);
let log_tag = if is_constrained {
// Constrained + non-interactive handshake: resolve the standard-registry secret, emit the per-send chain
// nullifier, and derive the constrained discovery tag. The static assert above guarantees the sender is
// populated, and the unchecked unwrap emits no constrain that could fold to false in other instantiations.
let sender = sender_override.unwrap_unchecked();
constrained_delivery::emit_nullifier_and_compute_constrained_tag(
context,
STANDARD_HANDSHAKE_REGISTRY_ADDRESS,
sender,
recipient,
)
} else {
// Unconstrained + address-pair origin: the discovery tag comes from the wallet-driven directional
// tagging secret.
let discovery_tag = compute_discovery_tag(recipient, sender_override);
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
// the context's arrays having unknown compile time write indices and hence dramatically increasing constraints
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(
0x2491f3a3cdfb66c89b1ea0d1d96fde4ea3e64cd48f122b81efeebf7ec24ce24d,
0x0097ae6f1e99026d558a3e8b372e3dcbf92f34b719583b1ab269ae717f6a12cf,
);
Loading
Loading