Skip to content

Commit ede2dec

Browse files
authored
feat(pxe)!: add source and block-range filtering to get_logs_by_tag (#23326)
1 parent e2a3ae3 commit ede2dec

10 files changed

Lines changed: 461 additions & 124 deletions

File tree

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

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

1010
## TBD
1111

12+
### [Aztec.nr] `LogRetrievalRequest` now includes `source`, `from_block`, and `to_block` fields
13+
14+
`LogRetrievalRequest` has been extended with three new fields to support filtering logs by source and block range. The `get_logs_by_tag` oracle now also returns all matching logs per tag instead of only the first match.
15+
16+
A `LogRetrievalRequest::new(contract_address, tag)` constructor is provided that defaults to querying both public and private logs with no block range filter:
17+
18+
```rust
19+
LogRetrievalRequest::new(contract_address, my_tag)
20+
```
21+
22+
If you need to customize source or block range, construct the struct manually with the new fields:
23+
24+
```diff
25+
LogRetrievalRequest {
26+
tag: my_tag,
27+
+ source: LogSource.PUBLIC_AND_PRIVATE,
28+
+ from_block: Option::none(),
29+
+ to_block: Option::none(),
30+
}
31+
```
32+
33+
`source` controls which RPCs are queried: `LogSource.PRIVATE`, `LogSource.PUBLIC`, or `LogSource.PUBLIC_AND_PRIVATE`. `from_block` and `to_block` define a half-open `[from, to)` block range filter. Both are `Option<Field>` and default to `Option::none()` (no filtering).
34+
1235
### [Aztec.nr] `emit_private_log_unsafe` / `emit_raw_note_log_unsafe` now take `BoundedVec`
1336

1437
The old array-based `emit_private_log_unsafe(tag, log: [Field; N], length)` and `emit_raw_note_log_unsafe(tag, log: [Field; N], length, note_hash_counter)` have been removed. The temporary `_vec_unsafe` variants introduced in a prior release have been renamed to take their place.

noir-projects/aztec-nr/aztec/src/messages/discovery/partial_notes.nr

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
use crate::{
22
capsules::CapsuleArray,
3+
ephemeral::EphemeralArray,
34
messages::{
45
discovery::{ComputeNoteHash, ComputeNoteNullifier, nonce_discovery::attempt_note_nonce_discovery},
56
encoding::MAX_MESSAGE_CONTENT_LEN,
@@ -88,17 +89,17 @@ pub(crate) unconstrained fn fetch_and_process_partial_note_completion_logs(
8889
// Each of the pending partial notes might get completed by a log containing its public values. For performance
8990
// reasons, we fetch all of these logs concurrently and then process them one by one, minimizing the amount of time
9091
// waiting for the node roundtrip.
91-
let maybe_completion_logs = get_pending_partial_notes_completion_logs(contract_address, pending_partial_notes);
92+
let completion_logs = get_pending_partial_notes_completion_logs(contract_address, pending_partial_notes);
9293

93-
// Each entry in the maybe completion logs array corresponds to the entry in the pending partial notes array at the
94-
// same index. This means we can use the same index as we iterate through the responses to get both the partial
95-
// note and the log that might complete it.
96-
assert_eq(maybe_completion_logs.len(), pending_partial_notes.len());
94+
// Each entry in the completion logs array corresponds to the entry in the pending partial notes array at the same
95+
// index. Each inner array contains all matching LogRetrievalResponses.
96+
assert_eq(completion_logs.len(), pending_partial_notes.len());
9797

98-
maybe_completion_logs.for_each(|i, maybe_log: Option<LogRetrievalResponse>| {
98+
completion_logs.for_each(|i, logs_for_tag: EphemeralArray<LogRetrievalResponse>| {
9999
let pending_partial_note = pending_partial_notes.get(i);
100+
let num_logs = logs_for_tag.len();
100101

101-
if maybe_log.is_none() {
102+
if num_logs == 0 {
102103
aztecnr_debug_log_format!("Found no completion logs for partial note with tag {}")(
103104
[pending_partial_note.note_completion_log_tag],
104105
);
@@ -107,10 +108,15 @@ pub(crate) unconstrained fn fetch_and_process_partial_note_completion_logs(
107108
// searching for this tagged log when performing message discovery in the future until we either find it or
108109
// the entry is somehow removed from the array.
109110
} else {
111+
assert(
112+
num_logs == 1,
113+
f"Expected at most 1 completion log per partial note, got {num_logs}",
114+
);
115+
110116
aztecnr_debug_log_format!("Completion log found for partial note with tag {}")([
111117
pending_partial_note.note_completion_log_tag,
112118
]);
113-
let log = maybe_log.unwrap();
119+
let log = logs_for_tag.get(0);
114120

115121
// The first field in the completion log payload is the storage slot, followed by the public note
116122
// content fields.

noir-projects/aztec-nr/aztec/src/messages/processing/log_retrieval_request.nr

Lines changed: 70 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,89 @@
11
use crate::protocol::{address::AztecAddress, traits::Serialize};
22

3-
/// A request for the `bulk_retrieve_logs` oracle to fetch either:
4-
/// - a public log emitted by `contract_address` with `unsiloed_tag`
5-
/// - a private log with tag equal to `compute_siloed_private_log_first_field(contract_address, unsiloed_tag)`.
3+
pub(crate) struct LogSourceEnum {
4+
pub PRIVATE: Field,
5+
pub PUBLIC: Field,
6+
pub PUBLIC_AND_PRIVATE: Field,
7+
}
8+
9+
pub(crate) global LogSource: LogSourceEnum =
10+
LogSourceEnum { PRIVATE: 0, PUBLIC: 1, PUBLIC_AND_PRIVATE: 2 };
11+
12+
/// A request for the `bulk_retrieve_logs` oracle to fetch all logs matching a tag.
613
#[derive(Serialize)]
714
pub(crate) struct LogRetrievalRequest {
815
pub contract_address: AztecAddress,
916
pub unsiloed_tag: Field,
10-
// TODO(#15052): choose source: public, private or either (current behavior)
17+
/// Which log source to query: public, private, or both (the default). See [`LogSource`].
18+
pub source: Field,
19+
/// Inclusive lower bound on block number. When unset, logs from the first block are included.
20+
pub from_block: Option<Field>,
21+
/// Exclusive upper bound on block number. When unset, logs up to the anchor block are included.
22+
pub to_block: Option<Field>,
23+
}
24+
25+
impl LogRetrievalRequest {
26+
/// Creates a request that queries both public and private logs with no block range filter.
27+
pub(crate) fn new(contract_address: AztecAddress, unsiloed_tag: Field) -> Self {
28+
LogRetrievalRequest {
29+
contract_address,
30+
unsiloed_tag,
31+
source: LogSource.PUBLIC_AND_PRIVATE,
32+
from_block: Option::none(),
33+
to_block: Option::none(),
34+
}
35+
}
1136
}
1237

1338
mod test {
1439
use crate::protocol::{address::AztecAddress, traits::{FromField, Serialize}};
15-
use super::LogRetrievalRequest;
40+
use super::{LogRetrievalRequest, LogSource};
41+
42+
#[test]
43+
fn serialization_of_defaults_matches_typescript() {
44+
let request = LogRetrievalRequest {
45+
contract_address: AztecAddress::from_field(1),
46+
unsiloed_tag: 2,
47+
source: LogSource.PUBLIC_AND_PRIVATE,
48+
from_block: Option::none(),
49+
to_block: Option::none(),
50+
};
51+
52+
// We define the serialization in Noir and the deserialization in TS. If the deserialization changes from
53+
// the snapshot value below, then log_retrieval_request.test.ts must be updated with the same value.
54+
// Ideally we'd autogenerate this, but for now we only have single-sided snapshot generation, from TS to
55+
// Noir, which is not what we need here.
56+
let expected_serialization = [
57+
0x0000000000000000000000000000000000000000000000000000000000000001,
58+
0x0000000000000000000000000000000000000000000000000000000000000002,
59+
0x0000000000000000000000000000000000000000000000000000000000000002,
60+
0x0000000000000000000000000000000000000000000000000000000000000000,
61+
0x0000000000000000000000000000000000000000000000000000000000000000,
62+
0x0000000000000000000000000000000000000000000000000000000000000000,
63+
0x0000000000000000000000000000000000000000000000000000000000000000,
64+
];
65+
66+
assert_eq(request.serialize(), expected_serialization);
67+
}
1668

1769
#[test]
18-
fn serialization_matches_typescript() {
19-
let request = LogRetrievalRequest { contract_address: AztecAddress::from_field(1), unsiloed_tag: 2 };
70+
fn serialization_with_values_matches_typescript() {
71+
let request = LogRetrievalRequest {
72+
contract_address: AztecAddress::from_field(1),
73+
unsiloed_tag: 2,
74+
source: LogSource.PUBLIC,
75+
from_block: Option::some(10),
76+
to_block: Option::some(20),
77+
};
2078

21-
// We define the serialization in Noir and the deserialization in TS. If the deserialization changes from the
22-
// snapshot value below, then log_retrieval_request.test.ts must be updated with the same value. Ideally we'd
23-
// autogenerate this, but for now we only have single-sided snapshot generation, from TS to Noir, which is not
24-
// what we need here.
2579
let expected_serialization = [
2680
0x0000000000000000000000000000000000000000000000000000000000000001,
2781
0x0000000000000000000000000000000000000000000000000000000000000002,
82+
0x0000000000000000000000000000000000000000000000000000000000000001,
83+
0x0000000000000000000000000000000000000000000000000000000000000001,
84+
0x000000000000000000000000000000000000000000000000000000000000000a,
85+
0x0000000000000000000000000000000000000000000000000000000000000001,
86+
0x0000000000000000000000000000000000000000000000000000000000000014,
2887
];
2988

3089
assert_eq(request.serialize(), expected_serialization);

noir-projects/aztec-nr/aztec/src/messages/processing/mod.nr

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ use crate::{
1919
discovery::partial_notes::DeliveredPendingPartialNote,
2020
encoding::MESSAGE_CIPHERTEXT_LEN,
2121
logs::{event::MAX_EVENT_SERIALIZED_LEN, note::MAX_NOTE_PACKED_LEN},
22-
processing::{log_retrieval_request::LogRetrievalRequest, log_retrieval_response::LogRetrievalResponse},
22+
processing::{
23+
log_retrieval_request::LogRetrievalRequest,
24+
log_retrieval_response::LogRetrievalResponse,
25+
},
2326
},
2427
oracle::message_processing,
2528
};
@@ -151,22 +154,19 @@ pub unconstrained fn validate_and_store_enqueued_notes_and_events(scope: AztecAd
151154
}
152155

153156
/// Efficiently queries the node for logs that result in the completion of all `DeliveredPendingPartialNote`s stored in
154-
/// a `CapsuleArray` by performing all node communication concurrently. Returns an `EphemeralArray` with Options
155-
/// for the responses that correspond to the pending partial notes at the same index.
156-
///
157-
/// For example, given an array with pending partial notes `[ p1, p2, p3 ]`, where `p1` and `p3` have corresponding
158-
/// completion logs but `p2` does not, the returned `EphemeralArray` will have contents `[some(p1_log), none(),
159-
/// some(p3_log)]`.
157+
/// a `CapsuleArray` by performing all node communication concurrently. Returns a nested `EphemeralArray`, one inner
158+
/// array per pending partial note, each containing all matching `LogRetrievalResponse`s (which may be empty if no
159+
/// logs were found).
160160
pub(crate) unconstrained fn get_pending_partial_notes_completion_logs(
161161
contract_address: AztecAddress,
162162
pending_partial_notes: CapsuleArray<DeliveredPendingPartialNote>,
163-
) -> EphemeralArray<Option<LogRetrievalResponse>> {
163+
) -> EphemeralArray<EphemeralArray<LogRetrievalResponse>> {
164164
let log_retrieval_requests = EphemeralArray::at(LOG_RETRIEVAL_REQUESTS_ARRAY_BASE_SLOT);
165165

166-
// We create a LogRetrievalRequest for each PendingPartialNote in the EphemeralArray. Because we need the indices in
167-
// the request array to match the indices in the partial note array, we can't use EphemeralArray::for_each, as that
168-
// function has arbitrary iteration order. Instead, we manually iterate the array from the beginning and push into
169-
// the requests array, which we expect to be empty.
166+
// We create a LogRetrievalRequest for each PendingPartialNote in the EphemeralArray. Because we need the indices
167+
// in the request array to match the indices in the partial note array, we can't use EphemeralArray::for_each, as
168+
// that function has arbitrary iteration order. Instead, we manually iterate the array from the beginning and push
169+
// into the requests array, which we expect to be empty.
170170
let mut i = 0;
171171
let pending_partial_notes_count = pending_partial_notes.len();
172172
while i < pending_partial_notes_count {
@@ -177,7 +177,7 @@ pub(crate) unconstrained fn get_pending_partial_notes_completion_logs(
177177
pending_partial_note.note_completion_log_tag,
178178
DOM_SEP__NOTE_COMPLETION_LOG_TAG,
179179
);
180-
log_retrieval_requests.push(LogRetrievalRequest { contract_address, unsiloed_tag: log_tag });
180+
log_retrieval_requests.push(LogRetrievalRequest::new(contract_address, log_tag));
181181
i += 1;
182182
}
183183

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

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,17 +42,20 @@ unconstrained fn validate_and_store_enqueued_notes_and_events_oracle(
4242
scope: AztecAddress,
4343
) {}
4444

45-
/// Fetches logs by tag from an ephemeral request array and returns a response ephemeral array.
45+
/// Fetches all logs matching each request's tag and returns a nested ephemeral array.
46+
///
47+
/// Each element in the outer array is an inner `EphemeralArray<LogRetrievalResponse>` containing all matching logs for
48+
/// the request at the same index (which may be empty if no logs were found).
4649
pub(crate) unconstrained fn get_logs_by_tag(
4750
requests: EphemeralArray<LogRetrievalRequest>,
48-
) -> EphemeralArray<Option<LogRetrievalResponse>> {
51+
) -> EphemeralArray<EphemeralArray<LogRetrievalResponse>> {
4952
get_logs_by_tag_oracle(requests)
5053
}
5154

5255
#[oracle(aztec_utl_getLogsByTag)]
5356
unconstrained fn get_logs_by_tag_oracle(
5457
requests: EphemeralArray<LogRetrievalRequest>,
55-
) -> EphemeralArray<Option<LogRetrievalResponse>> {}
58+
) -> EphemeralArray<EphemeralArray<LogRetrievalResponse>> {}
5659

5760
/// Resolves message contexts for tx hashes in an ephemeral request array and returns a response ephemeral array.
5861
pub(crate) unconstrained fn get_message_contexts_by_tx_hash(
Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,88 @@
1+
import { BlockNumber } from '@aztec/foundation/branded-types';
12
import { Fr } from '@aztec/foundation/curves/bn254';
23
import { AztecAddress } from '@aztec/stdlib/aztec-address';
34
import { Tag } from '@aztec/stdlib/logs';
45

5-
import { LogRetrievalRequest } from './log_retrieval_request.js';
6+
import { LogRetrievalRequest, LogSource } from './log_retrieval_request.js';
67

78
describe('LogRetrievalRequest', () => {
8-
it('output of Noir serialization deserializes as expected', () => {
9+
it('output of Noir serialization with defaults deserializes as expected', () => {
910
const serialized = [
1011
'0x0000000000000000000000000000000000000000000000000000000000000001',
1112
'0x0000000000000000000000000000000000000000000000000000000000000002',
13+
'0x0000000000000000000000000000000000000000000000000000000000000002',
14+
'0x0000000000000000000000000000000000000000000000000000000000000000',
15+
'0x0000000000000000000000000000000000000000000000000000000000000000',
16+
'0x0000000000000000000000000000000000000000000000000000000000000000',
17+
'0x0000000000000000000000000000000000000000000000000000000000000000',
18+
].map(Fr.fromHexString);
19+
20+
const request = LogRetrievalRequest.fromFields(serialized);
21+
22+
expect(request.contractAddress).toEqual(AztecAddress.fromBigInt(1n));
23+
expect(request.tag).toEqual(new Tag(new Fr(2)));
24+
expect(request.source).toEqual(LogSource.PUBLIC_AND_PRIVATE);
25+
expect(request.fromBlock).toBeUndefined();
26+
expect(request.toBlock).toBeUndefined();
27+
});
28+
29+
it('output of Noir serialization with values deserializes as expected', () => {
30+
const serialized = [
31+
'0x0000000000000000000000000000000000000000000000000000000000000001',
32+
'0x0000000000000000000000000000000000000000000000000000000000000002',
33+
'0x0000000000000000000000000000000000000000000000000000000000000001',
34+
'0x0000000000000000000000000000000000000000000000000000000000000001',
35+
'0x000000000000000000000000000000000000000000000000000000000000000a',
36+
'0x0000000000000000000000000000000000000000000000000000000000000001',
37+
'0x0000000000000000000000000000000000000000000000000000000000000014',
1238
].map(Fr.fromHexString);
1339

1440
const request = LogRetrievalRequest.fromFields(serialized);
1541

1642
expect(request.contractAddress).toEqual(AztecAddress.fromBigInt(1n));
1743
expect(request.tag).toEqual(new Tag(new Fr(2)));
44+
expect(request.source).toEqual(LogSource.PUBLIC);
45+
expect(request.fromBlock).toEqual(BlockNumber(10));
46+
expect(request.toBlock).toEqual(BlockNumber(20));
47+
});
48+
49+
it('rejects an invalid LogSource value', () => {
50+
const serialized = [
51+
'0x0000000000000000000000000000000000000000000000000000000000000001',
52+
'0x0000000000000000000000000000000000000000000000000000000000000002',
53+
'0x000000000000000000000000000000000000000000000000000000000000002a', // 42 — invalid
54+
'0x0000000000000000000000000000000000000000000000000000000000000000',
55+
'0x0000000000000000000000000000000000000000000000000000000000000000',
56+
'0x0000000000000000000000000000000000000000000000000000000000000000',
57+
'0x0000000000000000000000000000000000000000000000000000000000000000',
58+
].map(Fr.fromHexString);
59+
60+
expect(() => LogRetrievalRequest.fromFields(serialized)).toThrow(/Invalid LogSource value 42/);
61+
});
62+
63+
it('accepts all valid LogSource values', () => {
64+
for (const source of [LogSource.PRIVATE, LogSource.PUBLIC, LogSource.PUBLIC_AND_PRIVATE]) {
65+
const fields = new LogRetrievalRequest(AztecAddress.fromBigInt(1n), new Tag(new Fr(2)), source).toFields();
66+
const restored = LogRetrievalRequest.fromFields(fields);
67+
expect(restored.source).toEqual(source);
68+
}
69+
});
70+
71+
it('round-trips through toFields and fromFields', () => {
72+
const original = new LogRetrievalRequest(
73+
AztecAddress.fromBigInt(42n),
74+
new Tag(new Fr(99)),
75+
LogSource.PRIVATE,
76+
BlockNumber(5),
77+
BlockNumber(100),
78+
);
79+
80+
const restored = LogRetrievalRequest.fromFields(original.toFields());
81+
82+
expect(restored.contractAddress).toEqual(original.contractAddress);
83+
expect(restored.tag).toEqual(original.tag);
84+
expect(restored.source).toEqual(original.source);
85+
expect(restored.fromBlock).toEqual(original.fromBlock);
86+
expect(restored.toBlock).toEqual(original.toBlock);
1887
});
1988
});

0 commit comments

Comments
 (0)