Skip to content
Merged
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
33 changes: 33 additions & 0 deletions docs/docs-developers/docs/resources/migration_notes.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,39 @@ Aztec is in active development. Each version may introduce breaking changes that

## TBD

### [Aztec.nr] L1-to-L2 message consumption takes the secret as an array

`PrivateContext::consume_l1_to_l2_message` and `PublicContext::consume_l1_to_l2_message` now take the message secret as an arbitrary-length array `[Field; N]` instead of a single `Field`, so a consumer can derive its secret hash from more than one field. The helpers `compute_secret_hash` and `compute_l1_to_l2_message_nullifier` are likewise now generic over the secret length. A single-field secret behaves exactly as before (the hashes are unchanged for `N = 1`) — just wrap it in an array.

**Migration:**

```diff
- context.consume_l1_to_l2_message(content, secret, sender, leaf_index);
+ context.consume_l1_to_l2_message(content, [secret], sender, leaf_index);
```

```diff
- let secret_hash = compute_secret_hash(secret);
+ let secret_hash = compute_secret_hash([secret]);
```

**Impact**: Contracts that consume L1-to-L2 messages, or that call `compute_secret_hash` / `compute_l1_to_l2_message_nullifier`, must wrap their single-field secret in an array. Already-deployed contracts are unaffected: their unchanged bytecode keeps working, as PXE serves the previous L1-to-L2 membership-witness oracle through a compatibility adaptor.

### [Aztec.js] `computeL1ToL2MessageNullifier` replaced by `computeFeeJuiceMessageNullifier`

The `@aztec/stdlib` helper `computeL1ToL2MessageNullifier(contract, messageHash, secret)`, which returned the siloed message nullifier, has been removed. Its replacement `computeFeeJuiceMessageNullifier(messageHash, secret)` returns the **unsiloed** nullifier — siloing now happens at the point where the nullifier is looked up. `getL1ToL2MessageWitness` accordingly takes an optional `{ contractAddress, nullifier }` (unsiloed) and silos internally. `computeSecretHash` is unchanged, and `getNonNullifiedL1ToL2MessageWitness` keeps the same signature.

**Migration:**

```diff
- const nullifier = await computeL1ToL2MessageNullifier(contract, messageHash, secret);
+ // computeFeeJuiceMessageNullifier returns the UNSILOED nullifier; silo it before looking it up, or hand the
+ // unsiloed value to getL1ToL2MessageWitness which silos for you.
+ const nullifier = await siloNullifier(contract, await computeFeeJuiceMessageNullifier(messageHash, secret));
```

**Impact**: Only affects code calling these low-level messaging helpers directly - most integrations use `getNonNullifiedL1ToL2MessageWitness`, which is unchanged.

### [Aztec.js] Account signing keys are no longer derived from the privacy secret

Schnorr account signing keys used to be derived from the account's privacy secret (via the now-removed `deriveSigningKey`), which meant the ownership key could be reconstructed from a value the PXE holds. The relationship is now reversed: the signing key is the root, and the privacy secret is derived from it with `deriveSecretKeyFromSigningKey` (exported from `@aztec/accounts/utils`).
Expand Down
4 changes: 2 additions & 2 deletions docs/examples/contracts/aave_bridge/src/main.nr
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ pub contract AaveBridge {
// Consume message and emit nullifier
self.context.consume_l1_to_l2_message(
content_hash,
secret,
[secret],
config.portal,
message_leaf_index,
);
Expand All @@ -76,7 +76,7 @@ pub contract AaveBridge {
let content_hash = get_mint_to_private_content_hash(amount);
self.context.consume_l1_to_l2_message(
content_hash,
secret_for_L1_to_L2_message_consumption,
[secret_for_L1_to_L2_message_consumption],
config.portal,
message_leaf_index,
);
Expand Down
2 changes: 1 addition & 1 deletion docs/examples/contracts/nft_bridge/src/main.nr
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ pub contract NFTBridge {
// Consume the L1 -> L2 message
self.context.consume_l1_to_l2_message(
content_hash,
secret,
[secret],
self.storage.portal.read(),
message_leaf_index,
);
Expand Down
10 changes: 8 additions & 2 deletions noir-projects/aztec-nr/aztec/src/context/private_context.nr
Original file line number Diff line number Diff line change
Expand Up @@ -868,14 +868,20 @@ impl PrivateContext {
///
/// # Arguments
/// * `content` - The message content that was sent from L1
/// * `secret` - Secret value used for message privacy (if needed)
/// * `secret` - Secret fields used for message privacy (if needed)
/// * `sender` - Ethereum address that sent the message
/// * `leaf_index` - Index of the message in the L1-to-L2 message tree
///
/// # Advanced
/// Validates message existence in the L1-to-L2 message tree and nullifies the message to prevent
/// double-consumption.
pub fn consume_l1_to_l2_message(&mut self, content: Field, secret: Field, sender: EthAddress, leaf_index: Field) {
pub fn consume_l1_to_l2_message<let N: u32>(
&mut self,
content: Field,
secret: [Field; N],
sender: EthAddress,
leaf_index: Field,
) {
let nullifier = process_l1_to_l2_message(
self.anchor_block_header.state.l1_to_l2_message_tree.root,
self.this_address(),
Expand Down
10 changes: 8 additions & 2 deletions noir-projects/aztec-nr/aztec/src/context/public_context.nr
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ impl PublicContext {
///
/// # Arguments
/// * `content` - The message content that was sent from L1
/// * `secret` - Secret value used for message privacy (if needed)
/// * `secret` - Secret fields used for message privacy (if needed)
/// * `sender` - Ethereum address that sent the message
/// * `leaf_index` - Index of the message in the L1-to-L2 message tree
///
Expand All @@ -236,7 +236,13 @@ impl PublicContext {
/// * Message hash is computed from all parameters + chain context
/// * Will revert if message doesn't exist or was already consumed
///
pub fn consume_l1_to_l2_message(self: Self, content: Field, secret: Field, sender: EthAddress, leaf_index: Field) {
pub fn consume_l1_to_l2_message<let N: u32>(
self: Self,
content: Field,
secret: [Field; N],
sender: EthAddress,
leaf_index: Field,
) {
let secret_hash = compute_secret_hash(secret);
let message_hash = compute_l1_to_l2_message_hash(
sender,
Expand Down
12 changes: 6 additions & 6 deletions noir-projects/aztec-nr/aztec/src/hash.nr
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ use crate::protocol::{

pub use crate::protocol::hash::compute_siloed_nullifier;

pub fn compute_secret_hash(secret: Field) -> Field {
poseidon2_hash_with_separator([secret], DOM_SEP__SECRET_HASH)
pub fn compute_secret_hash<let N: u32>(secret: [Field; N]) -> Field {
poseidon2_hash_with_separator(secret, DOM_SEP__SECRET_HASH)
}

pub fn compute_l1_to_l2_message_hash(
Expand Down Expand Up @@ -47,9 +47,9 @@ pub fn compute_l1_to_l2_message_hash(
sha256_to_field(hash_bytes)
}

// The nullifier of a l1 to l2 message is the hash of the message salted with the secret
pub fn compute_l1_to_l2_message_nullifier(message_hash: Field, secret: Field) -> Field {
poseidon2_hash_with_separator([message_hash, secret], DOM_SEP__MESSAGE_NULLIFIER)
// The nullifier of an l1 to l2 message is the hash of the message salted with the secret.
pub fn compute_l1_to_l2_message_nullifier<let N: u32>(message_hash: Field, secret: [Field; N]) -> Field {
poseidon2_hash_with_separator([message_hash].concat(secret), DOM_SEP__MESSAGE_NULLIFIER)
}

// Computes the hash of input arguments or return values for private functions, or for authwit creation.
Expand Down Expand Up @@ -96,7 +96,7 @@ pub fn compute_public_bytecode_commitment(
#[test]
unconstrained fn secret_hash_matches_typescript() {
let secret = 8;
let hash = compute_secret_hash(secret);
let hash = compute_secret_hash([secret]);

// The following value was generated by `yarn-project/stdlib/src/hash/hash.test.ts`
let secret_hash_from_ts = 0x1848b066724ab0ffb50ecb0ee3398eb839f162823d262bad959721a9c13d1e96;
Expand Down
18 changes: 12 additions & 6 deletions noir-projects/aztec-nr/aztec/src/messaging.nr
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
use crate::{
hash::{compute_l1_to_l2_message_hash, compute_l1_to_l2_message_nullifier, compute_secret_hash},
oracle::get_l1_to_l2_membership_witness::get_l1_to_l2_membership_witness,
oracle::get_l1_to_l2_membership_witness::{get_l1_to_l2_membership_witness, UnsiloedNullifier},
};

use crate::protocol::{address::{AztecAddress, EthAddress}, merkle_tree::root::root_from_sibling_path};

pub fn process_l1_to_l2_message(
pub fn process_l1_to_l2_message<let N: u32>(
l1_to_l2_root: Field,
contract_address: AztecAddress,
portal_contract_address: EthAddress,
chain_id: Field,
version: Field,
content: Field,
secret: Field,
secret: [Field; N],
leaf_index: Field,
) -> Field {
let secret_hash = compute_secret_hash(secret);
Expand All @@ -26,14 +26,20 @@ pub fn process_l1_to_l2_message(
leaf_index,
);

let nullifier = compute_l1_to_l2_message_nullifier(message_hash, secret);

// We prove that `message_hash` is in the tree by showing the derivation of the tree root, using a merkle path we
// get from an oracle.
// Safety: The witness is only used as a "magical value" that makes the merkle proof below pass. Hence it's safe.
let (_leaf_index, sibling_path) =
unsafe { get_l1_to_l2_membership_witness(contract_address, message_hash, secret) };
let (_leaf_index, sibling_path) = unsafe {
get_l1_to_l2_membership_witness(
message_hash,
Option::some(UnsiloedNullifier { contract_address, nullifier }),
)
};

let root = root_from_sibling_path(message_hash, leaf_index, sibling_path);
assert_eq(root, l1_to_l2_root, "Message not in state");

compute_l1_to_l2_message_nullifier(message_hash, secret)
nullifier
}
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
use crate::protocol::{address::AztecAddress, constants::L1_TO_L2_MSG_TREE_HEIGHT};

/// An unsiloed L1 to L2 message nullifier.
pub struct UnsiloedNullifier {
pub contract_address: AztecAddress,
pub nullifier: Field,
}

/// Returns the leaf index and sibling path of an entry in the L1 to L2 messaging tree, which can then be used to prove
/// its existence.
///
/// When `nullifier` is `Some`, the returned witness is guaranteed to be for a message that has not yet been consumed:
/// PXE checks it is not present in the nullifier tree. Pass `None` to retrieve the witness regardless of whether the
/// message has been consumed.
pub unconstrained fn get_l1_to_l2_membership_witness(
contract_address: AztecAddress,
message_hash: Field,
secret: Field,
nullifier: Option<UnsiloedNullifier>,
) -> (Field, [Field; L1_TO_L2_MSG_TREE_HEIGHT]) {
get_l1_to_l2_membership_witness_oracle(contract_address, message_hash, secret)
get_l1_to_l2_membership_witness_oracle(message_hash, nullifier)
}

// Obtains membership witness (index and sibling path) for a message in the L1 to L2 message tree.
#[oracle(aztec_utl_getL1ToL2MembershipWitness)]
#[oracle(aztec_utl_getL1ToL2MembershipWitnessV2)]
unconstrained fn get_l1_to_l2_membership_witness_oracle(
_contract_address: AztecAddress,
_message_hash: Field,
_secret: Field,
_nullifier: Option<UnsiloedNullifier>,
) -> (Field, [Field; L1_TO_L2_MSG_TREE_HEIGHT]) {}
2 changes: 1 addition & 1 deletion noir-projects/aztec-nr/aztec/src/oracle/version.nr
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
/// immediately if AZTEC_NR_MINOR > PXE_MINOR because if a contract is updated to use a newer Aztec.nr dependency
/// without actually using any of the new oracles then there is no reason to throw.
pub global ORACLE_VERSION_MAJOR: Field = 30;
pub global ORACLE_VERSION_MINOR: Field = 5;
pub global ORACLE_VERSION_MINOR: Field = 6;

/// Asserts that the version of the oracle is compatible with the version expected by the contract.
pub fn assert_compatible_oracle_version() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -887,7 +887,7 @@ impl TestEnvironment {
sender: EthAddress,
recipient: AztecAddress,
) -> Field {
self.send_l1_to_l2_message_from_secret_hash(content, compute_secret_hash(secret), sender, recipient)
self.send_l1_to_l2_message_from_secret_hash(content, compute_secret_hash([secret]), sender, recipient)
}

/// Variant of [`send_l1_to_l2_message`](TestEnvironment::send_l1_to_l2_message) that takes the secret hash
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ unconstrained fn consume_seeded_message_from_secret() {
let leaf_index = env.send_l1_to_l2_message(CONTENT, SECRET, sender, recipient);

env.private_context_at(recipient, |context| {
context.consume_l1_to_l2_message(CONTENT, SECRET, sender, leaf_index);
context.consume_l1_to_l2_message(CONTENT, [SECRET], sender, leaf_index);
});
}

Expand All @@ -26,10 +26,10 @@ unconstrained fn consume_seeded_message_from_secret_hash() {
let recipient = AztecAddress::from_field(17);

let leaf_index =
env.send_l1_to_l2_message_from_secret_hash(CONTENT, compute_secret_hash(SECRET), sender, recipient);
env.send_l1_to_l2_message_from_secret_hash(CONTENT, compute_secret_hash([SECRET]), sender, recipient);

env.private_context_at(recipient, |context| {
context.consume_l1_to_l2_message(CONTENT, SECRET, sender, leaf_index);
context.consume_l1_to_l2_message(CONTENT, [SECRET], sender, leaf_index);
});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,7 @@ pub contract TokenBlacklist {
let to_roles = self.storage.roles.at(to).get_current_value();
assert(!to_roles.is_blacklisted, "Blacklisted: Recipient");

let secret_hash = compute_secret_hash(secret);
let secret_hash = compute_secret_hash([secret]);

// Pop 1 note (set_limit(1)) which has an amount stored in a field with index 0 (select(0, amount)) and
// a secret_hash stored in a field with index 1 (select(1, secret_hash)).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ pub contract TokenBridge {
let config = self.storage.config.read();

// Consume message and emit nullifier
self.context.consume_l1_to_l2_message(content_hash, secret, config.portal, message_leaf_index);
self.context.consume_l1_to_l2_message(content_hash, [secret], config.portal, message_leaf_index);

// Mint tokens
self.call(Token::at(config.token).mint_to_public(to, amount));
Expand Down Expand Up @@ -101,7 +101,7 @@ pub contract TokenBridge {
let content_hash = get_mint_to_private_content_hash(amount);
self.context.consume_l1_to_l2_message(
content_hash,
secret_for_L1_to_L2_message_consumption,
[secret_for_L1_to_L2_message_consumption],
config.portal,
message_leaf_index,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ pub contract Test {
let content_hash = get_mint_to_private_content_hash(amount);
self.context.consume_l1_to_l2_message(
content_hash,
secret_for_L1_to_L2_message_consumption,
[secret_for_L1_to_L2_message_consumption],
portal_address,
message_leaf_index,
);
Expand All @@ -359,7 +359,7 @@ pub contract Test {
message_leaf_index: Field,
) {
// Consume message and emit nullifier
self.context.consume_l1_to_l2_message(content, secret, sender, message_leaf_index);
self.context.consume_l1_to_l2_message(content, [secret], sender, message_leaf_index);
}

#[external("private")]
Expand All @@ -370,7 +370,7 @@ pub contract Test {
message_leaf_index: Field,
) {
// Consume message and emit nullifier
self.context.consume_l1_to_l2_message(content, secret, sender, message_leaf_index);
self.context.consume_l1_to_l2_message(content, [secret], sender, message_leaf_index);
}

#[external("private")]
Expand Down
Loading
Loading