Skip to content

Commit 11552a0

Browse files
authored
chore(port): forward-port v5-next aztec-nr/contracts backlog to next (#24931)
Forward-ports the **noir / aztec-nr / contracts** slice of the v5-next → next backlog (work merged to `v5-next` after the ~2026-07-08 cut that reshaped `next`). ## Applied (clean cherry-picks, chronological) - fix: prevent reception of messages too far into the future (#24645) - fix(aztec-nr): reject infinity ephemeral key in message encryption (#24665) - fix(aztec-nr): tolerate malformed partial-note completion logs (#24668) - docs(aztec-nr): document partial note completion trust model (#24816) - refactor(aztec-nr): shared no-op sync handler for stateless contracts (#24844) - fix: dont panic on note msgs on contracts with no notes (#24852) - docs(noir-contracts): document standard-contract re-pin consequences (#24890) ## ⚠️ Needs owner conflict-resolution (conflict against reshaped `next`; not included here) Cherry-pick onto this branch and resolve: - [ ] `git cherry-pick -x 9f1167e` — feat!: make inbox secrets be multiple fields (#24599) - [ ] `git cherry-pick -x 4490597` — fix(aztec-nr): prevent recipient forging a colliding handshake (#24403) - [ ] `git cherry-pick -x 8b1903c` — feat!: forbid external note validation checks (#24644) - [ ] `git cherry-pick -x 10e339a` — fix(aztec-nr)!: compute note property selectors from the packed layout (#24689) - [ ] `git cherry-pick -x 15c7a1e` — chore: clarify scope of packable impl detection (#24820) - [ ] `git cherry-pick -x f66808c` — fix: change init and single claim nullif to incl owner address, add testing utilities (#24892) Part of the manual v5-next → next backlog sweep. Draft until conflicts are resolved and CI is green.
2 parents ab7be1f + 49d0830 commit 11552a0

95 files changed

Lines changed: 1801 additions & 381 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CLAUDE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ When staging files, prefer `git add -u` or name specific files rather than `git
6363
Never bulk-update lockfiles (`Cargo.lock`, `yarn.lock`). Use targeted updates only: `cargo update --precise <version> --package <name>` for Rust, and `yarn up <package>@<version>` in the relevant workspace for TypeScript. Bulk updates drag in unrelated transitive changes that make review impossible and frequently break reproducibility.
6464
</lockfile_discipline>
6565

66+
<standard_contract_repin>
67+
Never run `noir-projects/noir-contracts/bootstrap.sh pin-standard-build` on your own initiative. The pin exists so ordinary source or bytecode changes do NOT move the standard contracts' canonical addresses, and CI does not fail when the bytecode drifts. A re-pin is a deliberate redeploy decision for a human to make: if a change seems to need one, leave the pin, rebuild against it, and ask. See the comment on `pin-standard-build` for why re-pinning is breaking.
68+
</standard_contract_repin>
69+
6670
</git_workflow>
6771

6872
<code_formatting>

docs/docs-developers/docs/aztec-nr/framework-description/advanced/how_to_prove_history.md

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -56,16 +56,18 @@ Prove a note exists in the note hash tree:
5656
To prove a note was valid (existed AND wasn't nullified) at a historical block:
5757

5858
```rust
59-
use aztec::history::note::assert_note_was_valid_by;
59+
use aztec::history::note::assert_local_note_was_valid_by;
6060

6161
let header = self.context.get_anchor_block_header();
62-
assert_note_was_valid_by(header, hinted_note, &mut self.context);
62+
assert_local_note_was_valid_by(header, hinted_note, &mut self.context);
6363
```
6464

6565
This verifies both:
6666
1. The note was included in the note hash tree
6767
2. The note's nullifier was not in the nullifier tree
6868

69+
The `assert_local_note_was_*` helpers only work for the executing contract's own notes: this is because it is not possible for a contract to retrieve the app-siloed nullifier hiding secret key that corresponds to another contract. To prove facts about another contract's notes, use `assert_note_existed_by`, which supports notes of any contract (see [Prove note inclusion](#prove-note-inclusion)).
70+
6971
## Prove at a specific historical block
7072

7173
To prove against state at a specific past block (not just the anchor block):
@@ -83,13 +85,13 @@ Using `get_block_header_at` adds ~3k constraints to prove Archive tree membershi
8385

8486
## Prove a note was nullified
8587

86-
To prove a note has been spent/nullified:
88+
To prove a note has been nullified:
8789

8890
```rust
89-
use aztec::history::note::assert_note_was_nullified_by;
91+
use aztec::history::note::assert_local_note_was_nullified_by;
9092

9193
let header = self.context.get_anchor_block_header();
92-
assert_note_was_nullified_by(header, confirmed_note, &mut self.context);
94+
assert_local_note_was_nullified_by(header, confirmed_note, &mut self.context);
9395
```
9496

9597
## Prove contract bytecode was published
@@ -137,10 +139,10 @@ The `aztec::history` module provides these functions:
137139

138140
| Function | Module | Purpose |
139141
|----------|--------|---------|
140-
| `assert_note_existed_by` | `history::note` | Prove note exists in note hash tree |
141-
| `assert_note_was_valid_by` | `history::note` | Prove note exists and is not nullified |
142-
| `assert_note_was_nullified_by` | `history::note` | Prove note's nullifier is in nullifier tree |
143-
| `assert_note_was_not_nullified_by` | `history::note` | Prove note's nullifier is not in nullifier tree |
142+
| `assert_note_existed_by` | `history::note` | Prove note exists in note hash tree (any contract) |
143+
| `assert_local_note_was_valid_by` | `history::note` | Prove own note exists and is not nullified |
144+
| `assert_local_note_was_nullified_by` | `history::note` | Prove own note's nullifier is in nullifier tree |
145+
| `assert_local_note_was_not_nullified_by` | `history::note` | Prove own note's nullifier is not in nullifier tree |
144146
| `assert_nullifier_existed_by` | `history::nullifier` | Prove a siloed nullifier exists |
145147
| `assert_nullifier_did_not_exist_by` | `history::nullifier` | Prove a siloed nullifier does not exist |
146148
| `assert_contract_bytecode_was_published_by` | `history::deployment` | Prove a contract's bytecode was published |

docs/docs-developers/docs/resources/migration_notes.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,22 @@ Aztec is in active development. Each version may introduce breaking changes that
99

1010
## TBD
1111

12+
### [Aztec.nr] Standard contracts re-pinned at new addresses
13+
14+
The canonical `HandshakeRegistry` now protects handshake shared secrets from recipient forgery and includes the owner's address in its `PrivateMutable` initialization nullifiers, keeping the handshake state of accounts that share keys independent. All standard contracts have been re-pinned and move to new addresses. Handshakes established with a previous registry instance are not visible to the new one and must be re-established.
15+
16+
### [Aztec.nr] Note property selectors are typed and use packed-layout indices
17+
18+
The selectors in the generated `properties()` used the field's position in the note struct declaration, which pointed at the wrong packed field for any note with an earlier field packing to more than one `Field` (a `Point`, an array, a nested struct). Selector indices are now the field's offset in the note's packed representation, so `select`/`sort` criteria constrain the field they name.
19+
20+
Breaking changes:
21+
22+
- `PropertySelector<T>` carries the selected property's type. Hand-constructed literals need a type annotation, e.g. `let selector: PropertySelector<Field> = PropertySelector { index: 0, offset: 0, length: 32 };`.
23+
- `select`/`sort` reject properties that pack to more than one `Field` at compile time.
24+
- `select` takes its value typed as the property's type. Cast the value if a mixed-type comparison was intentional.
25+
- `properties()` cannot be used with a custom `Packable` layout. Define property selectors manually for such notes.
26+
- Every note field type must implement `Packable`, even when the note's own `Packable` is hand-written.
27+
1228
## 5.0.1
1329

1430
### [Aztec.nr] History note nullification helpers renamed and restricted to own-contract notes

docs/examples/contracts/aave_bridge/src/main.nr

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ pub contract AaveBridge {
5050
// Consume message and emit nullifier
5151
self.context.consume_l1_to_l2_message(
5252
content_hash,
53-
secret,
53+
[secret],
5454
config.portal,
5555
message_leaf_index,
5656
);
@@ -76,7 +76,7 @@ pub contract AaveBridge {
7676
let content_hash = get_mint_to_private_content_hash(amount);
7777
self.context.consume_l1_to_l2_message(
7878
content_hash,
79-
secret_for_L1_to_L2_message_consumption,
79+
[secret_for_L1_to_L2_message_consumption],
8080
config.portal,
8181
message_leaf_index,
8282
);

docs/examples/contracts/nft_bridge/src/main.nr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ pub contract NFTBridge {
3838
// Consume the L1 -> L2 message
3939
self.context.consume_l1_to_l2_message(
4040
content_hash,
41-
secret,
41+
[secret],
4242
self.storage.portal.read(),
4343
message_leaf_index,
4444
);

docs/netlify.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -980,3 +980,8 @@
980980
# Aztec-nr: contract errors
981981
from = "/errors/14"
982982
to = "/developers/docs/aztec-nr/debugging#contract-errors"
983+
984+
[[redirects]]
985+
# Aztec-nr: auto-generated note properties require the derived Packable layout
986+
from = "/errors/15"
987+
to = "/aztec-nr-api/mainnet/noir_aztec/note/note_interface/trait.NoteProperties.html"

noir-projects/aztec-nr/CLAUDE.md

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,36 @@ Follow the Rust stdlib style roughly. See `PublicImmutable` in `state_vars/publi
3434
- **Show practical patterns in examples.** Don't just show the API call — show it in context (e.g. inside a `#[external("public")]` function with realistic variable names).
3535
- **Document cost for methods.** When relevant, note which AVM opcodes are invoked and how many times (e.g. "`SLOAD` is invoked a number of times equal to `T`'s packed length").
3636

37+
### Brevity and attitude
38+
39+
Write for a caller using the API, not for an implementor or an auditor. State what the item does and any restriction a caller must respect, plus the alternative to reach for — then stop. Leave out the internal mechanism and the security rationale behind a restriction: those belong in code comments messages or design docs, not the rustdoc. A reader who hits a restriction needs to know it exists and where to go next, not the argument for why it holds.
40+
41+
- **State the restriction, not the reason it's sound.** Say "this can only be used for X", not the multi-sentence explanation of what goes wrong otherwise.
42+
- **Don't narrate the mechanism.** Key siloing, tree layouts, hash preimages, and similar internals are implementation detail; mention them only when a caller cannot use the API correctly without knowing (and then put them under `## Implementation Details`).
43+
- **Point to the alternative instead of enumerating trade-offs.** A single "use `[other_fn]` for the other case" beats a paragraph weighing options.
44+
45+
Example — documenting a helper that only works on the executing contract's own notes:
46+
47+
```rust
48+
// Too much: explains the key-siloing mechanism and the failure the guard prevents.
49+
/// The note's nullifier is recomputed using the executing contract's app-siloed nullifier key
50+
/// and then siloed with `confirmed_note.contract_address`. These only agree for the executing
51+
/// contract's own notes; otherwise the non-inclusion proof passes unconditionally and wrongly
52+
/// reports a nullified note as not nullified, so this asserts that
53+
/// `confirmed_note.contract_address == context.this_address()`.
54+
55+
// Right: states the restriction and the alternative, nothing more.
56+
/// ## Local notes only
57+
///
58+
/// This can only be used for notes of the executing contract. Use [`assert_note_existed_by`] to
59+
/// prove existence of another contract's notes.
60+
```
61+
62+
## Testing
63+
64+
- **No messages on assertions inside tests.** Write `assert_eq(actual, expected)` and `assert(cond)`, not `assert_eq(actual, expected, "values should match")`. The test's name and body already make clear what is being checked, so an assertion message is redundant noise. (Library code is the opposite: an `assert`/`panic` there must carry a message, since it explains a runtime failure to a contract developer.)
65+
- `#[test(should_fail_with = "...")]` is not an assertion message and is encouraged. The string matches a substring of the expected failure, pinning down *which* error the failing case must produce so the test can't pass for the wrong reason.
66+
3767
## Logging
3868

3969
- **Always use the prefixed logging functions** from `crate::logging` (e.g. `logging::aztecnr_debug_log!`, `logging::aztecnr_debug_log_format!`). These automatically prepend `[aztec-nr] ` to all messages at compile time.

noir-projects/aztec-nr/aztec/src/context/private_context.nr

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -869,14 +869,20 @@ impl PrivateContext {
869869
///
870870
/// # Arguments
871871
/// * `content` - The message content that was sent from L1
872-
/// * `secret` - Secret value used for message privacy (if needed)
872+
/// * `secret` - Secret fields used for message privacy (if needed)
873873
/// * `sender` - Ethereum address that sent the message
874874
/// * `leaf_index` - Index of the message in the L1-to-L2 message tree
875875
///
876876
/// # Advanced
877877
/// Validates message existence in the L1-to-L2 message tree and nullifies the message to prevent
878878
/// double-consumption.
879-
pub fn consume_l1_to_l2_message(&mut self, content: Field, secret: Field, sender: EthAddress, leaf_index: Field) {
879+
pub fn consume_l1_to_l2_message<let N: u32>(
880+
&mut self,
881+
content: Field,
882+
secret: [Field; N],
883+
sender: EthAddress,
884+
leaf_index: Field,
885+
) {
880886
let nullifier = process_l1_to_l2_message(
881887
self.anchor_block_header.state.l1_to_l2_message_tree.root,
882888
self.this_address(),

noir-projects/aztec-nr/aztec/src/context/public_context.nr

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ impl PublicContext {
226226
///
227227
/// # Arguments
228228
/// * `content` - The message content that was sent from L1
229-
/// * `secret` - Secret value used for message privacy (if needed)
229+
/// * `secret` - Secret fields used for message privacy (if needed)
230230
/// * `sender` - Ethereum address that sent the message
231231
/// * `leaf_index` - Index of the message in the L1-to-L2 message tree
232232
///
@@ -236,7 +236,13 @@ impl PublicContext {
236236
/// * Message hash is computed from all parameters + chain context
237237
/// * Will revert if message doesn't exist or was already consumed
238238
///
239-
pub fn consume_l1_to_l2_message(self: Self, content: Field, secret: Field, sender: EthAddress, leaf_index: Field) {
239+
pub fn consume_l1_to_l2_message<let N: u32>(
240+
self: Self,
241+
content: Field,
242+
secret: [Field; N],
243+
sender: EthAddress,
244+
leaf_index: Field,
245+
) {
240246
let secret_hash = compute_secret_hash(secret);
241247
let message_hash = compute_l1_to_l2_message_hash(
242248
sender,

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

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ use crate::protocol::{
1212

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

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

1919
pub fn compute_l1_to_l2_message_hash(
@@ -47,9 +47,9 @@ pub fn compute_l1_to_l2_message_hash(
4747
sha256_to_field(hash_bytes)
4848
}
4949

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

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

101101
// The following value was generated by `yarn-project/stdlib/src/hash/hash.test.ts`
102102
let secret_hash_from_ts = 0x1848b066724ab0ffb50ecb0ee3398eb839f162823d262bad959721a9c13d1e96;

0 commit comments

Comments
 (0)