Skip to content

Commit 5833a9e

Browse files
authored
feat: store discovered handshakes as facts (#24483)
1 parent 663bb9e commit 5833a9e

17 files changed

Lines changed: 555 additions & 60 deletions

File tree

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,4 +19,10 @@ pub struct LogRetrievalResponse {
1919
pub unique_note_hashes_in_tx: BoundedVec<Field, MAX_NOTE_HASHES_PER_TX>,
2020
/// The first nullifier created by `tx_hash`
2121
pub first_nullifier_in_tx: Field,
22+
/// The number of the block in which `tx_hash` was included
23+
pub block_number: u32,
24+
/// The timestamp of the block in which `tx_hash` was included
25+
pub block_timestamp: u64,
26+
/// The hash of the block in which `tx_hash` was included
27+
pub block_hash: Field,
2228
}

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

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,7 @@ pub unconstrained fn get_logs_by_tag(
5454
get_logs_by_tag_oracle(requests)
5555
}
5656

57-
// TODO: PXE also exposes `getLogsByTagV2`, whose response additionally carries the origin block number and timestamp
58-
// of each log. Switch this oracle (and `LogRetrievalResponse`) over to it once a consumer needs that context.
59-
#[oracle(aztec_utl_getLogsByTag)]
57+
#[oracle(aztec_utl_getLogsByTagV2)]
6058
unconstrained fn get_logs_by_tag_oracle(
6159
requests: EphemeralArray<LogRetrievalRequest>,
6260
) -> EphemeralArray<EphemeralArray<LogRetrievalResponse>> {}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
/// immediately if AZTEC_NR_MINOR > PXE_MINOR because if a contract is updated to use a newer Aztec.nr dependency
1212
/// without actually using any of the new oracles then there is no reason to throw.
1313
pub global ORACLE_VERSION_MAJOR: Field = 30;
14-
pub global ORACLE_VERSION_MINOR: Field = 3;
14+
pub global ORACLE_VERSION_MINOR: Field = 4;
1515

1616
/// Asserts that the version of the oracle is compatible with the version expected by the contract.
1717
pub fn assert_compatible_oracle_version() {

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-
0x0f53c9d679c6aa851b46bfc07e14bd070d5948b6ad1a4f7c54909f61dceb146a,
5+
0x0564d5361fd6d7501baa1706a59bbebd2035d1c2565c9e5daf8f1567da547a23,
66
);
77

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

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

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

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ global INTERACTIVE_HANDSHAKE: u8 = 2;
3535
pub contract HandshakeRegistry {
3636
use crate::{
3737
handshake_note::HandshakeNote, INTERACTIVE_HANDSHAKE, NON_INTERACTIVE_HANDSHAKE,
38-
recipient_signature::RecipientSignature, sync::get_all_handshakes,
38+
recipient_signature::RecipientSignature, sync::discovered_handshakes::get_all_handshakes,
3939
};
4040
use aztec::{
4141
keys::{ecdh_shared_secret::derive_ecdh_shared_secret, ephemeral::generate_positive_ephemeral_key_pair},
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
use crate::sync::rewindable_register::RewindableRegister;
2+
use aztec::{
3+
facts::{delete_fact_collection, OriginBlock},
4+
protocol::{abis::block_header::BlockHeader, address::AztecAddress, hash::sha256_to_field, traits::Hash},
5+
};
6+
7+
/// Fact-collection type id of the per-recipient sync cursor: a [`RewindableRegister`] holding the anchor block of
8+
/// the last completed scan.
9+
global SYNC_CURSOR_TYPE_ID: Field = sha256_to_field("HANDSHAKE_REGISTRY::SYNC_CURSOR".as_bytes());
10+
11+
/// Collection id of the singleton cursor register.
12+
global SYNC_CURSOR_COLLECTION_ID: Field = 0;
13+
14+
/// Reorg-aware cursor tracking the last scanned anchor block of a recipient's handshake sync.
15+
pub(crate) struct SyncCursor {
16+
contract_address: AztecAddress,
17+
scope: AztecAddress,
18+
}
19+
20+
impl SyncCursor {
21+
/// Returns the cursor of `scope`'s handshake sync in the given contract.
22+
pub(crate) unconstrained fn at(contract_address: AztecAddress, scope: AztecAddress) -> Self {
23+
Self { contract_address, scope }
24+
}
25+
26+
/// Returns the first block the next scan must cover, or `None` to scan from genesis.
27+
///
28+
/// The next scan starts one past the last completed scan's anchor block. `None` means no scan ever completed, or
29+
/// a reorg rolled back every recorded one.
30+
pub(crate) unconstrained fn next_block_to_scan(self) -> Option<u32> {
31+
self.register().read().map(|anchor| anchor + 1)
32+
}
33+
34+
/// Records a completed scan up to the given anchor block.
35+
///
36+
/// The cursor originates at the scanned anchor block itself, so a reorg pruning that block rolls the cursor back
37+
/// to the last surviving scan and the pruned range gets covered again.
38+
pub(crate) unconstrained fn advance(self, anchor_block_header: BlockHeader) {
39+
self.register().write(
40+
anchor_block_header.block_number(),
41+
OriginBlock { block_number: anchor_block_header.block_number(), block_hash: anchor_block_header.hash() },
42+
);
43+
}
44+
45+
/// Forgets every recorded scan, as if no sync had ever completed. Only used by tests, to emulate a reorg
46+
/// retracting every recorded scan.
47+
pub(crate) unconstrained fn clear(self) {
48+
delete_fact_collection(
49+
self.contract_address,
50+
self.scope,
51+
SYNC_CURSOR_TYPE_ID,
52+
SYNC_CURSOR_COLLECTION_ID,
53+
);
54+
}
55+
56+
unconstrained fn register(self) -> RewindableRegister<u32> {
57+
RewindableRegister::at(
58+
self.contract_address,
59+
self.scope,
60+
SYNC_CURSOR_TYPE_ID,
61+
SYNC_CURSOR_COLLECTION_ID,
62+
)
63+
}
64+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
use crate::sync::rewindable_register::{deserialize_payload, serialize_payload};
2+
use aztec::{
3+
ephemeral::EphemeralArray,
4+
facts::{get_fact_collection, OriginBlock, record_retractable_fact},
5+
messages::delivery::handshake::DiscoveredHandshake,
6+
protocol::{address::AztecAddress, hash::sha256_to_field},
7+
};
8+
9+
/// Fact-collection type id of the per-recipient discovered handshakes populated by [`record_discovered_handshake`]
10+
/// and read by [`get_all_handshakes`]. Each recipient (the fact scope) has a single collection of this type holding
11+
/// one fact per discovered handshake, in discovery order.
12+
global DISCOVERED_HANDSHAKES_TYPE_ID: Field = sha256_to_field("HANDSHAKE_REGISTRY::DISCOVERED_HANDSHAKES".as_bytes());
13+
14+
/// Collection id of the singleton discovered-handshakes collection.
15+
global DISCOVERED_HANDSHAKES_COLLECTION_ID: Field = 0;
16+
17+
/// Fact type id of the fact storing a serialized [`DiscoveredHandshake`] inside the discovered-handshakes collection.
18+
global HANDSHAKE_DISCOVERED: Field = sha256_to_field("HANDSHAKE_REGISTRY::HANDSHAKE_DISCOVERED".as_bytes());
19+
20+
/// Records a discovered handshake for `recipient`.
21+
///
22+
/// The handshake originates at the block its announcement log landed in, so a reorg pruning that block makes the
23+
/// handshake invisible again. Fact identity covers the payload and origin block, so re-recording the same handshake
24+
/// at the same origin is a no-op.
25+
pub(crate) unconstrained fn record_discovered_handshake(
26+
contract_address: AztecAddress,
27+
recipient: AztecAddress,
28+
handshake: DiscoveredHandshake,
29+
origin_block: OriginBlock,
30+
) {
31+
record_retractable_fact(
32+
contract_address,
33+
recipient,
34+
DISCOVERED_HANDSHAKES_TYPE_ID,
35+
DISCOVERED_HANDSHAKES_COLLECTION_ID,
36+
HANDSHAKE_DISCOVERED,
37+
serialize_payload(handshake),
38+
origin_block,
39+
);
40+
}
41+
42+
/// Returns the discovered handshakes for `recipient`.
43+
///
44+
/// The order of facts within a collection is not specified, but it is stable, keeping
45+
/// [`HandshakeRegistry::get_handshakes`] pagination consistent across pages.
46+
pub(crate) unconstrained fn get_all_handshakes(
47+
contract_address: AztecAddress,
48+
recipient: AztecAddress,
49+
) -> EphemeralArray<DiscoveredHandshake> {
50+
get_fact_collection(
51+
contract_address,
52+
recipient,
53+
DISCOVERED_HANDSHAKES_TYPE_ID,
54+
DISCOVERED_HANDSHAKES_COLLECTION_ID,
55+
)
56+
.map_or(EphemeralArray::empty(), |collection| collection.facts.map(|fact| deserialize_payload(fact.payload)))
57+
}
58+
59+
/// Returns the origin block number of each of `recipient`'s discovered handshakes. Only used by tests, to assert
60+
/// that handshake facts originate at the log's block rather than the sync anchor.
61+
pub(crate) unconstrained fn origin_block_numbers(
62+
contract_address: AztecAddress,
63+
recipient: AztecAddress,
64+
) -> EphemeralArray<u32> {
65+
get_fact_collection(
66+
contract_address,
67+
recipient,
68+
DISCOVERED_HANDSHAKES_TYPE_ID,
69+
DISCOVERED_HANDSHAKES_COLLECTION_ID,
70+
)
71+
.map_or(EphemeralArray::empty(), |collection| {
72+
collection.facts.map(|fact| fact.origin_block.unwrap().block_number)
73+
})
74+
}

noir-projects/noir-contracts/contracts/standard/handshake_registry_contract/src/sync.nr renamed to noir-projects/noir-contracts/contracts/standard/handshake_registry_contract/src/sync/mod.nr

Lines changed: 25 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1+
pub(crate) mod cursor;
2+
pub(crate) mod discovered_handshakes;
3+
pub(crate) mod rewindable_register;
4+
5+
use crate::sync::{cursor::SyncCursor, discovered_handshakes::record_discovered_handshake};
16
use aztec::{
2-
capsules::CapsuleArray,
37
context::UtilityContext,
48
ephemeral::EphemeralArray,
9+
facts::OriginBlock,
510
messages::{
611
delivery::handshake::DiscoveredHandshake,
712
discovery::{
@@ -14,7 +19,7 @@ use aztec::{
1419
offchain::OffchainInboxSync,
1520
},
1621
},
17-
oracle::{capsules, message_processing},
22+
oracle::message_processing,
1823
protocol::{
1924
address::AztecAddress,
2025
constants::DOM_SEP__NON_INTERACTIVE_HANDSHAKE_LOG_TAG,
@@ -24,25 +29,18 @@ use aztec::{
2429
utils::point::point_from_x_coord_and_sign,
2530
};
2631

27-
/// Capsule base slot for the per-recipient list of discovered handshakes populated by
28-
/// [`handshake_registry_sync`] and read by [`get_all_handshakes`].
29-
global HANDSHAKES_CAPSULE_SLOT: Field = sha256_to_field("HANDSHAKE_REGISTRY::HANDSHAKES".as_bytes());
30-
31-
/// Capsule slot for the per-recipient sync cursor: the next block (inclusive) the discovery scan should start from.
32-
global SYNC_CURSOR_CAPSULE_SLOT: Field = sha256_to_field("HANDSHAKE_REGISTRY::SYNC_CURSOR".as_bytes());
33-
3432
/// Ephemeral-array slot used by [`get_handshake_logs_in_range`] to batch a single log-retrieval request to the oracle.
3533
global LOG_RETRIEVAL_REQUEST_ARRAY_SLOT: Field =
3634
sha256_to_field("HANDSHAKE_REGISTRY::LOG_RETRIEVAL_REQUEST_ARRAY".as_bytes());
3735

3836
/// Discovers non-interactive handshakes addressed to the syncing account.
3937
///
4038
/// On every sync, scans the recipient-keyed bootstrap tag over the blocks mined since the previous sync, decrypts
41-
/// each matching log to recover the handshake `eph_pk`, and appends it to the recipient's
42-
/// capsule list. It then falls through to [`do_sync_state`] so that normal note/event/partial-note discovery still
43-
/// happens.
39+
/// each matching log to recover the handshake `eph_pk`, and records it as a retractable fact whose origin is the block
40+
/// the log landed in, so a reorg pruning that block makes the handshake invisible again. It then falls through to
41+
/// [`do_sync_state`] so that normal note/event/partial-note discovery still happens.
4442
///
45-
/// `scope` is the account PXE is syncing for, which doubles as both the recipient whose tag we scan and the capsule
43+
/// `scope` is the account PXE is syncing for, which doubles as both the recipient whose tag we scan and the fact
4644
/// scope we persist under, so handshakes for one account never leak into another's list.
4745
pub(crate) unconstrained fn handshake_registry_sync(
4846
contract_address: AztecAddress,
@@ -52,37 +50,36 @@ pub(crate) unconstrained fn handshake_registry_sync(
5250
offchain_inbox_sync: Option<OffchainInboxSync>,
5351
scope: AztecAddress,
5452
) {
55-
let cursor = capsules::load::<u32>(contract_address, SYNC_CURSOR_CAPSULE_SLOT, scope);
56-
let anchor_block = UtilityContext::new().block_number();
53+
let cursor = SyncCursor::at(contract_address, scope);
54+
let from_block = cursor.next_block_to_scan();
55+
let anchor_block_header = UtilityContext::new().block_header();
56+
let anchor_block = anchor_block_header.block_number();
5757

5858
// `to_block` is exclusive, so we add 1 to include the anchor block in the scan
5959
let next_block = anchor_block + 1;
60-
let should_scan = cursor.map(|c| c != next_block).unwrap_or(true);
60+
let should_scan = from_block.map(|b| b != next_block).unwrap_or(true);
6161
if should_scan {
6262
// Find all handshakes for the given recipient
6363
let tag = compute_log_tag(scope.to_field(), DOM_SEP__NON_INTERACTIVE_HANDSHAKE_LOG_TAG);
64-
let logs = get_handshake_logs_in_range(contract_address, tag, cursor, Option::some(next_block));
64+
let logs = get_handshake_logs_in_range(contract_address, tag, from_block, Option::some(next_block));
6565

66-
// Store all valid handshakes
67-
let handshakes: CapsuleArray<DiscoveredHandshake> =
68-
CapsuleArray::at(contract_address, HANDSHAKES_CAPSULE_SLOT, scope);
66+
// Record all valid handshakes
6967
logs.for_each(|_i, log| {
7068
let ciphertext = aztec::utils::array::subbvec(log.log_payload, 0);
7169

7270
let _ = AES128::decrypt(ciphertext, scope, contract_address)
7371
.and_then(|pt| {
7472
point_from_x_coord_and_sign(pt.get(0), true).map(|pk| DiscoveredHandshake { eph_pk: pk })
7573
})
76-
.map(|h| handshakes.push(h));
74+
.map(|h| record_discovered_handshake(
75+
contract_address,
76+
scope,
77+
h,
78+
OriginBlock { block_number: log.block_number, block_hash: log.block_hash },
79+
));
7780
});
7881

79-
// Update the cursor
80-
capsules::store(
81-
contract_address,
82-
SYNC_CURSOR_CAPSULE_SLOT,
83-
next_block,
84-
scope,
85-
);
82+
cursor.advance(anchor_block_header);
8683
}
8784

8885
do_sync_state_without_handshake_discovery(
@@ -95,14 +92,6 @@ pub(crate) unconstrained fn handshake_registry_sync(
9592
);
9693
}
9794

98-
/// Returns the capsule array of discovered handshakes for `recipient`.
99-
pub(crate) unconstrained fn get_all_handshakes(
100-
contract_address: AztecAddress,
101-
recipient: AztecAddress,
102-
) -> CapsuleArray<DiscoveredHandshake> {
103-
CapsuleArray::at(contract_address, HANDSHAKES_CAPSULE_SLOT, recipient)
104-
}
105-
10695
/// Fetches all private logs emitted under `tag` in the block range `[from_block, to_block)`.
10796
///
10897
/// Thin wrapper around [`message_processing::get_logs_by_tag`] that batches a single request and unwraps the single

0 commit comments

Comments
 (0)