Skip to content

Commit 78efa9e

Browse files
author
AztecBot
committed
chore: sync public-v5-next with upstream v5-next
2 parents fd097eb + 663bb9e commit 78efa9e

23 files changed

Lines changed: 914 additions & 206 deletions

File tree

.github/workflows/merge-train-create-pr.yml

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ jobs:
2424
branch="${{ github.ref_name }}"
2525
2626
# Determine base branch. Most trains target next; trains suffixed
27-
# with -v5 (e.g. spartan-v5, fairies-v5) target the v5 release line
28-
# (v5-next) instead.
27+
# with -v<N> (e.g. spartan-v5, fairies-v5, spartan-v6) target the
28+
# matching release line (v5-next, v6-next) instead.
2929
base_branch="next"
30-
if [[ "$branch" == *-v5 ]]; then
31-
base_branch="v5-next"
30+
if [[ "$branch" =~ -v([0-9]+)$ ]]; then
31+
base_branch="v${BASH_REMATCH[1]}-next"
3232
fi
3333
3434
# Skip if this is a merge commit (check for multiple parents)
@@ -52,7 +52,7 @@ jobs:
5252
5353
# Create PR with ci-no-squash label
5454
labels="ci-no-squash"
55-
if [[ "$branch" == "merge-train/spartan" || "$branch" == "merge-train/spartan-v5" || "$branch" == "merge-train/ci" ]]; then
55+
if [[ "$branch" == "merge-train/spartan" || "$branch" == "merge-train/spartan-v5" || "$branch" == "merge-train/spartan-v6" || "$branch" == "merge-train/ci" ]]; then
5656
labels="$labels,ci-full-no-test-cache"
5757
fi
5858
# Trains targeting the v5 release line are ports into v5-next.

.github/workflows/merge-train-next-to-branches.yml

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ on:
55
branches:
66
- next
77
- v5-next
8+
- v6-next
89

910
jobs:
1011
merge-to-trains:
@@ -27,13 +28,17 @@ jobs:
2728
GH_TOKEN: ${{ secrets.AZTEC_BOT_GITHUB_TOKEN }}
2829
COMMIT_SHA: ${{ github.sha }}
2930
run: |
30-
# The -v5 trains track the v5 release line (v5-next); every other
31-
# train tracks next. Sync only the trains fed by the branch that was
32-
# just pushed.
31+
# The -v<N> trains track the matching release line (v5-next, v6-next);
32+
# every other train tracks next. Sync only the trains fed by the
33+
# branch that was just pushed.
3334
if [[ "${{ github.ref_name }}" == "v5-next" ]]; then
3435
for branch in merge-train/spartan-v5 merge-train/fairies-v5; do
3536
./scripts/merge-train/merge-next.sh "$branch" v5-next || true
3637
done
38+
elif [[ "${{ github.ref_name }}" == "v6-next" ]]; then
39+
for branch in merge-train/spartan-v6; do
40+
./scripts/merge-train/merge-next.sh "$branch" v6-next || true
41+
done
3742
else
3843
for branch in merge-train/avm merge-train/barretenberg merge-train/ci merge-train/docs merge-train/fairies merge-train/spartan; do
3944
./scripts/merge-train/merge-next.sh "$branch" || true

.github/workflows/merge-train-stale-check.yml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,21 @@ jobs:
3636
BASE_BRANCH: v5-next
3737
run: ./ci3/merge_train_stale_check merge-train/spartan-v5 '#team-alpha'
3838

39+
spartan-v6:
40+
name: Check merge-train/spartan-v6
41+
runs-on: ubuntu-latest
42+
permissions:
43+
contents: read
44+
pull-requests: read
45+
steps:
46+
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5.0.1
47+
- name: Run stale check
48+
env:
49+
GH_TOKEN: ${{ secrets.AZTEC_BOT_GITHUB_TOKEN }}
50+
SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }}
51+
BASE_BRANCH: v6-next
52+
run: ./ci3/merge_train_stale_check merge-train/spartan-v6 '#team-alpha'
53+
3954
fairies-v5:
4055
name: Check merge-train/fairies-v5
4156
runs-on: ubuntu-latest

docs/docs-operate/operators/keystore/creating-keystores.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -394,6 +394,28 @@ This creates keystores at:
394394
- **Directory**: `~/.aztec/keystore/`
395395
- **Filename**: `key1.json`, `key2.json`, etc. (auto-increments)
396396

397+
### Encrypted Keystore Passwords
398+
399+
Use `--password`, `--password-file`, or `AZTEC_KEYSTORE_PASSWORD` to write encrypted ETH JSON V3 and BLS
400+
EIP-2335 keystore files with a shared password. To use different passwords, provide role-specific sources such as
401+
`--eth-password` and `--bls-password`.
402+
403+
```bash
404+
export AZTEC_KEYSTORE_PASSWORD='your-secure-password'
405+
aztec validator-keys new \
406+
--fee-recipient 0x0000000000000000000000000000000000000000000000000000000000000000 \
407+
--count 5 \
408+
--publishers 0x7988a4a779f058a0 \
409+
--encrypted-keystore-dir ~/.aztec/keystore/encrypted
410+
```
411+
412+
For each key type, role-specific sources take precedence over shared sources: `--eth-password`,
413+
`--eth-password-file`, and `AZTEC_ETH_KEYSTORE_PASSWORD` for ETH keystores; `--bls-password`,
414+
`--bls-password-file`, and `AZTEC_BLS_KEYSTORE_PASSWORD` for BLS keystores. Shared sources use `--password`,
415+
`--password-file`, then `AZTEC_KEYSTORE_PASSWORD`. Passwords cannot be empty. When the command generates a mnemonic
416+
while writing encrypted keystores, it does not print the mnemonic to stdout; provide `--mnemonic` if you need deterministic
417+
recovery.
418+
397419
## Verifying Your Keystore
398420

399421
Verify the keystore is valid JSON:

docs/docs-operate/operators/keystore/troubleshooting.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,13 @@ aztec validator-keys new [options]
370370
| `--gse-address <address>` | GSE contract address (required with --staker-output) | None |
371371
| `--l1-rpc-urls <urls>` | L1 RPC endpoints (required with --staker-output) | None |
372372
| `--legacy` | Use 2.1.4 BLS derivation path (only for regenerating old keys) | `false` |
373+
| `--password <str>` | Shared password for encrypted ETH and BLS keystore files | `AZTEC_KEYSTORE_PASSWORD` if set |
374+
| `--password-file <path>` | File containing the shared encrypted-keystore password | None |
375+
| `--eth-password <str>` | Password for encrypted ETH keystore files | `AZTEC_ETH_KEYSTORE_PASSWORD`, then shared password |
376+
| `--eth-password-file <path>` | File containing the ETH encrypted-keystore password | None |
377+
| `--bls-password <str>` | Password for encrypted BLS keystore files | `AZTEC_BLS_KEYSTORE_PASSWORD`, then shared password |
378+
| `--bls-password-file <path>` | File containing the BLS encrypted-keystore password | None |
379+
| `--encrypted-keystore-dir <path>` | Directory for encrypted ETH and BLS keystore files | Keystore output directory |
373380
| `--data-dir <path>` | Output directory | `~/.aztec/keystore` |
374381
| `--file <name>` | Keystore filename | `key1.json` |
375382

noir-projects/aztec-nr/aztec/src/messages/delivery/handshake.nr

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,26 @@ mod test {
233233
});
234234
}
235235

236+
#[test]
237+
unconstrained fn get_handshake_secrets_returns_empty_when_scope_keys_not_held() {
238+
let env = TestEnvironment::new();
239+
let scope = AztecAddress::from_field(1);
240+
let contract_address = AztecAddress { inner: 0xdeadbeef };
241+
242+
env.utility_context_at(contract_address, |_| {
243+
let mut items = BoundedVec::new();
244+
items.push(DiscoveredHandshake { eph_pk: point_from_x_coord(1).unwrap() });
245+
let page = HandshakePage { items, total_count: 1 };
246+
let _ = OracleMock::mock("aztec_utl_callUtilityFunction").returns(page.serialize());
247+
248+
let empty: [Field; 0] = [];
249+
mock_get_shared_secrets(empty);
250+
251+
let secrets = get_handshake_secrets(contract_address, scope);
252+
assert_eq(secrets.len(), 0);
253+
});
254+
}
255+
236256
unconstrained fn mock_get_shared_secrets<let N: u32>(response_values: [Field; N]) {
237257
let response_slot: Field = 99;
238258
let response_array: EphemeralArray<Field> = EphemeralArray::at(response_slot);

noir-projects/aztec-nr/aztec/src/messages/encryption/aes128.nr

Lines changed: 47 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -376,51 +376,53 @@ impl MessageEncryption for AES128 {
376376
.and_then(|(eph_pk_x, masked_fields)| {
377377
// With the x-coordinate of the ephemeral public key we can reconstruct the point as we know that the
378378
// y-coordinate must be positive. This may fail however, as not all x-coordinates are on the curve. In
379-
// that case, we simply return `Option::none`.
380-
point_from_x_coord_and_sign(eph_pk_x, true).and_then(|eph_pk| {
381-
let s_app = get_shared_secret(recipient, eph_pk, contract_address);
382-
383-
let unmasked_fields = masked_fields.mapi(|i, field| {
384-
let unmasked = unmask_field(s_app, i, field);
385-
// If we failed to unmask the field, we are dealing with the random padding. We'll ignore it
386-
// later, so we can simply set it to 0
387-
unmasked.unwrap_or(0)
388-
});
389-
let ciphertext_without_eph_pk_x = decode_bytes_from_fields(unmasked_fields);
390-
391-
// Derive symmetric keys:
392-
let pairs = derive_aes_symmetric_key_and_iv_from_shared_secret::<2>(s_app);
393-
let (body_sym_key, body_iv) = pairs[0];
394-
let (header_sym_key, header_iv) = pairs[1];
395-
396-
// Extract the header ciphertext
397-
let header_start = 0;
398-
let header_ciphertext: [u8; HEADER_CIPHERTEXT_SIZE_IN_BYTES] =
399-
array::subarray(ciphertext_without_eph_pk_x.storage(), header_start);
400-
// We need to convert the array to a BoundedVec because the oracle expects a BoundedVec as it's
401-
// designed to work with messages with unknown length at compile time. This would not be necessary
402-
// here as the header ciphertext length is fixed. But we do it anyway to not have to have duplicate
403-
// oracles.
404-
let header_ciphertext_bvec =
405-
BoundedVec::<u8, HEADER_CIPHERTEXT_SIZE_IN_BYTES>::from_array(header_ciphertext);
406-
407-
try_aes128_decrypt(header_ciphertext_bvec, header_iv, header_sym_key)
408-
// Extract ciphertext length from header (2 bytes, big-endian)
409-
.and_then(|header_plaintext| extract_ciphertext_length(header_plaintext))
410-
.filter(|ciphertext_length| ciphertext_length <= MESSAGE_PLAINTEXT_SIZE_IN_BYTES)
411-
.map(|ciphertext_length| {
412-
// Extract and decrypt main ciphertext
413-
let ciphertext_start = header_start + HEADER_CIPHERTEXT_SIZE_IN_BYTES;
414-
let ciphertext_with_padding: [u8; MESSAGE_PLAINTEXT_SIZE_IN_BYTES] =
415-
array::subarray(ciphertext_without_eph_pk_x.storage(), ciphertext_start);
416-
BoundedVec::from_parts(ciphertext_with_padding, ciphertext_length)
417-
})
418-
// Decrypt main ciphertext and return it
419-
.and_then(|ciphertext| try_aes128_decrypt(ciphertext, body_iv, body_sym_key))
420-
// Convert bytes back to fields (32 bytes per field). Returns None if the actual bytes are
421-
// not valid.
422-
.and_then(|plaintext_bytes| try_decode_fields_from_bytes(plaintext_bytes))
423-
})
379+
// that case, we simply return `Option::none`. Secret derivation may also fail when the PXE does not
380+
// hold the recipient's keys (e.g. when syncing the scope of an address we don't control), in which
381+
// case the message is undecryptable and equally skipped.
382+
point_from_x_coord_and_sign(eph_pk_x, true)
383+
.and_then(|eph_pk| get_shared_secret(recipient, eph_pk, contract_address))
384+
.and_then(|s_app| {
385+
let unmasked_fields = masked_fields.mapi(|i, field| {
386+
let unmasked = unmask_field(s_app, i, field);
387+
// If we failed to unmask the field, we are dealing with the random padding. We'll ignore it
388+
// later, so we can simply set it to 0
389+
unmasked.unwrap_or(0)
390+
});
391+
let ciphertext_without_eph_pk_x = decode_bytes_from_fields(unmasked_fields);
392+
393+
// Derive symmetric keys:
394+
let pairs = derive_aes_symmetric_key_and_iv_from_shared_secret::<2>(s_app);
395+
let (body_sym_key, body_iv) = pairs[0];
396+
let (header_sym_key, header_iv) = pairs[1];
397+
398+
// Extract the header ciphertext
399+
let header_start = 0;
400+
let header_ciphertext: [u8; HEADER_CIPHERTEXT_SIZE_IN_BYTES] =
401+
array::subarray(ciphertext_without_eph_pk_x.storage(), header_start);
402+
// We need to convert the array to a BoundedVec because the oracle expects a BoundedVec as it's
403+
// designed to work with messages with unknown length at compile time. This would not be
404+
// necessary here as the header ciphertext length is fixed. But we do it anyway to not have to
405+
// have duplicate oracles.
406+
let header_ciphertext_bvec =
407+
BoundedVec::<u8, HEADER_CIPHERTEXT_SIZE_IN_BYTES>::from_array(header_ciphertext);
408+
409+
try_aes128_decrypt(header_ciphertext_bvec, header_iv, header_sym_key)
410+
// Extract ciphertext length from header (2 bytes, big-endian)
411+
.and_then(|header_plaintext| extract_ciphertext_length(header_plaintext))
412+
.filter(|ciphertext_length| ciphertext_length <= MESSAGE_PLAINTEXT_SIZE_IN_BYTES)
413+
.map(|ciphertext_length| {
414+
// Extract and decrypt main ciphertext
415+
let ciphertext_start = header_start + HEADER_CIPHERTEXT_SIZE_IN_BYTES;
416+
let ciphertext_with_padding: [u8; MESSAGE_PLAINTEXT_SIZE_IN_BYTES] =
417+
array::subarray(ciphertext_without_eph_pk_x.storage(), ciphertext_start);
418+
BoundedVec::from_parts(ciphertext_with_padding, ciphertext_length)
419+
})
420+
// Decrypt main ciphertext and return it
421+
.and_then(|ciphertext| try_aes128_decrypt(ciphertext, body_iv, body_sym_key))
422+
// Convert bytes back to fields (32 bytes per field). Returns None if the actual bytes are
423+
// not valid.
424+
.and_then(|plaintext_bytes| try_decode_fields_from_bytes(plaintext_bytes))
425+
})
424426
})
425427
}
426428
}

noir-projects/aztec-nr/aztec/src/oracle/shared_secret.nr

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,21 @@ unconstrained fn get_shared_secrets_oracle(
1111
) -> EphemeralArray<Field> {}
1212

1313
/// Convenience wrapper around [`get_shared_secrets`] for a single ephemeral public key.
14+
///
15+
/// Returns `none` when the PXE does not hold `address`'s keys and therefore cannot derive the secret.
1416
pub unconstrained fn get_shared_secret(
1517
address: AztecAddress,
1618
eph_pk: EmbeddedCurvePoint,
1719
contract_address: AztecAddress,
18-
) -> Field {
20+
) -> Option<Field> {
1921
let eph_pks: EphemeralArray<EmbeddedCurvePoint> = EphemeralArray::empty_at(GET_SHARED_SECRET_SINGLE_SLOT);
2022
eph_pks.push(eph_pk);
2123
let secrets = get_shared_secrets(address, eph_pks, contract_address);
22-
secrets.get(0)
24+
if secrets.len() == 1 {
25+
Option::some(secrets.get(0))
26+
} else {
27+
Option::none()
28+
}
2329
}
2430

2531
/// Returns app-siloed shared secrets between `address` and someone who knows the secret keys behind the given
@@ -41,19 +47,26 @@ pub unconstrained fn get_shared_secret(
4147
///
4248
/// Callers derive indexed subkeys from `s_app` via
4349
/// [`derive_shared_secret_subkey`](crate::keys::ecdh_shared_secret::derive_shared_secret_subkey).
50+
///
51+
/// Returns an empty array when the PXE does not hold `address`'s keys and therefore cannot derive any secrets. This
52+
/// happens when syncing the scope of an address the PXE does not control, and is an expected outcome rather than an
53+
/// error. Any other response length mismatch is a malformed oracle response.
4454
pub unconstrained fn get_shared_secrets(
4555
address: AztecAddress,
4656
eph_pks: EphemeralArray<EmbeddedCurvePoint>,
4757
contract_address: AztecAddress,
4858
) -> EphemeralArray<Field> {
4959
let response = get_shared_secrets_oracle(address, eph_pks, contract_address);
50-
assert(response.len() == eph_pks.len(), "get_shared_secrets: response length does not match request length");
60+
assert(
61+
(response.len() == eph_pks.len()) | (response.len() == 0),
62+
"get_shared_secrets: response length does not match request length",
63+
);
5164
response
5265
}
5366

5467
mod test {
5568
use crate::ephemeral::EphemeralArray;
56-
use crate::oracle::shared_secret::get_shared_secrets;
69+
use crate::oracle::shared_secret::{get_shared_secret, get_shared_secrets};
5770
use crate::protocol::{address::AztecAddress, traits::FromField};
5871
use crate::test::helpers::test_environment::TestEnvironment;
5972
use crate::utils::point::point_from_x_coord;
@@ -110,6 +123,54 @@ mod test {
110123
});
111124
}
112125

126+
#[test]
127+
unconstrained fn returns_empty_when_pxe_does_not_hold_keys() {
128+
let env = TestEnvironment::new();
129+
env.utility_context(|_| {
130+
let empty: [Field; 0] = [];
131+
let _ = mock_get_shared_secrets(empty);
132+
133+
let pk = point_from_x_coord(1).unwrap();
134+
let eph_pks: EphemeralArray<_> = EphemeralArray::empty_at(200);
135+
eph_pks.push(pk);
136+
137+
let results: EphemeralArray<Field> = get_shared_secrets(
138+
AztecAddress::from_field(1),
139+
eph_pks,
140+
AztecAddress::from_field(2),
141+
);
142+
143+
assert_eq(results.len(), 0);
144+
});
145+
}
146+
147+
#[test]
148+
unconstrained fn single_secret_returns_the_secret() {
149+
let env = TestEnvironment::new();
150+
env.utility_context(|_| {
151+
let _ = mock_get_shared_secrets([111]);
152+
153+
let pk = point_from_x_coord(1).unwrap();
154+
let secret = get_shared_secret(AztecAddress::from_field(1), pk, AztecAddress::from_field(2));
155+
156+
assert_eq(secret.unwrap(), 111);
157+
});
158+
}
159+
160+
#[test]
161+
unconstrained fn single_secret_returns_none_when_pxe_does_not_hold_keys() {
162+
let env = TestEnvironment::new();
163+
env.utility_context(|_| {
164+
let empty: [Field; 0] = [];
165+
let _ = mock_get_shared_secrets(empty);
166+
167+
let pk = point_from_x_coord(1).unwrap();
168+
let secret = get_shared_secret(AztecAddress::from_field(1), pk, AztecAddress::from_field(2));
169+
170+
assert(secret.is_none());
171+
});
172+
}
173+
113174
#[test(should_fail_with = "response length does not match request length")]
114175
unconstrained fn cannot_return_mismatched_length() {
115176
let env = TestEnvironment::new();

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,17 @@
22
use protocol_types::{address::AztecAddress, traits::FromField};
33

44
pub global STANDARD_AUTH_REGISTRY_ADDRESS: AztecAddress = AztecAddress::from_field(
5-
0x0e7d3c56a185c40e4ce459eee03075b7dee2e9dc8f860157063afb3fde5ce097,
5+
0x0f53c9d679c6aa851b46bfc07e14bd070d5948b6ad1a4f7c54909f61dceb146a,
66
);
77

88
pub global STANDARD_MULTI_CALL_ENTRYPOINT_ADDRESS: AztecAddress = AztecAddress::from_field(
9-
0x03264f902d92f27dd0719cd6f5cc9c9d03ec6f5a48482e2158ebc2886e997210,
9+
0x251d9cb1aa645672268b9b9082dedba22986ba9068d5ad8ecfae53f24e16c109,
1010
);
1111

1212
pub global STANDARD_PUBLIC_CHECKS_ADDRESS: AztecAddress = AztecAddress::from_field(
13-
0x05b4a7bf960bac46cc0c22aa6ee5b663a928787c9e7410fcf99e78182f634c0b,
13+
0x2ebeb911fc3963e7d1869af2dc7fee318d1edbc957edb46a0e99b46867ba4492,
1414
);
1515

1616
pub global STANDARD_HANDSHAKE_REGISTRY_ADDRESS: AztecAddress = AztecAddress::from_field(
17-
0x26f11691c7efea98db7eb7b4460cbc3f75db19772db2c2dbb8978979bcc5e388,
17+
0x1523a141a33e5aa8d72624823b8592b87799c9ee0c086dd9f3c80661905366ab,
1818
);

noir-projects/noir-contracts/contracts/standard/handshake_registry_contract/src/test.nr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,7 @@ unconstrained fn non_interactive_handshake_is_discovered_by_recipient() {
466466
// The recipient derives the same app-siloed secret from the discovered eph_pk via ECDH.
467467
let handshake = discovered.items.get(0);
468468
let recipient_secret = env.utility_context_at(sender, |_| get_shared_secret(recipient, handshake.eph_pk, sender));
469-
assert_eq(recipient_secret, returned_secrets.shared);
469+
assert_eq(recipient_secret.unwrap(), returned_secrets.shared);
470470

471471
// The custom sync hook falls through to `do_sync_state`, so default note discovery still works.
472472
let utility_secrets = env

0 commit comments

Comments
 (0)