11use aztec:: {
2- capsules::CapsuleArray ,
32 context::UtilityContext ,
43 ephemeral::EphemeralArray ,
4+ facts ::{
5+ delete_fact_collection , Fact , FactCollection , get_fact_collection , get_fact_collections_by_type , OriginBlock ,
6+ record_retractable_fact ,
7+ },
58 messages ::{
69 delivery::handshake::DiscoveredHandshake ,
710 discovery ::{
@@ -14,22 +17,34 @@ use aztec::{
1417 offchain::OffchainInboxSync ,
1518 },
1619 },
17- oracle ::{ capsules , message_processing } ,
20+ oracle:: message_processing ,
1821 protocol ::{
1922 address::AztecAddress ,
2023 constants::DOM_SEP__NON_INTERACTIVE_HANDSHAKE_LOG_TAG ,
21- hash ::{compute_log_tag , sha256_to_field },
22- traits::ToField ,
24+ hash ::{compute_log_tag , poseidon2_hash , sha256_to_field },
25+ traits ::{ Deserialize , Serialize , ToField } ,
2326 },
2427 utils::point:: point_from_x_coord_and_sign ,
2528};
2629
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+ /// Fact-collection type id of the per-recipient discovered handshakes populated by [`handshake_registry_sync`] and
31+ /// read by [`get_all_handshakes`]. Each discovered handshake lives in its own collection (see
32+ /// [`handshake_collection_id`]).
33+ pub (crate ) global DISCOVERED_HANDSHAKES_TYPE_ID : Field =
34+ sha256_to_field ("HANDSHAKE_REGISTRY::DISCOVERED_HANDSHAKES" .as_bytes ());
35+
36+ /// Fact type id of the fact storing a serialized [`DiscoveredHandshake`] inside a discovered-handshake collection.
37+ pub (crate ) global HANDSHAKE_DISCOVERED : Field = sha256_to_field ("HANDSHAKE_REGISTRY::HANDSHAKE_DISCOVERED" .as_bytes ());
38+
39+ /// Fact-collection type id of the per-recipient sync cursor.
40+ pub (crate ) global SYNC_CURSOR_TYPE_ID : Field = sha256_to_field ("HANDSHAKE_REGISTRY::SYNC_CURSOR" .as_bytes ());
3041
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 ());
42+ /// Fact type id of the single fact in the cursor collection. The fact has an empty payload: the cursor value is its
43+ /// origin block (the anchor block of the last completed scan).
44+ pub (crate ) global SYNC_CURSOR : Field = sha256_to_field ("HANDSHAKE_REGISTRY::SYNC_CURSOR_FACT" .as_bytes ());
45+
46+ /// Collection id of the singleton cursor collection.
47+ pub (crate ) global SYNC_CURSOR_COLLECTION_ID : Field = 0 ;
3348
3449/// Ephemeral-array slot used by [`get_handshake_logs_in_range`] to batch a single log-retrieval request to the oracle.
3550global LOG_RETRIEVAL_REQUEST_ARRAY_SLOT : Field =
@@ -38,11 +53,11 @@ global LOG_RETRIEVAL_REQUEST_ARRAY_SLOT: Field =
3853/// Discovers non-interactive handshakes addressed to the syncing account.
3954///
4055/// 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.
56+ /// each matching log to recover the handshake `eph_pk`, and records it as a retractable fact whose origin is the block
57+ /// the log landed in, so a reorg pruning that block makes the handshake invisible again. It then falls through to
58+ /// [`do_sync_state`] so that normal note/event/partial-note discovery still happens.
4459///
45- /// `scope` is the account PXE is syncing for, which doubles as both the recipient whose tag we scan and the capsule
60+ /// `scope` is the account PXE is syncing for, which doubles as both the recipient whose tag we scan and the fact
4661/// scope we persist under, so handshakes for one account never leak into another's list.
4762pub (crate ) unconstrained fn handshake_registry_sync (
4863 contract_address : AztecAddress ,
@@ -52,7 +67,7 @@ pub(crate) unconstrained fn handshake_registry_sync(
5267 offchain_inbox_sync : Option <OffchainInboxSync >,
5368 scope : AztecAddress ,
5469) {
55- let cursor = capsules:: load ::< u32 > (contract_address , SYNC_CURSOR_CAPSULE_SLOT , scope );
70+ let cursor = load_sync_cursor (contract_address , scope );
5671 let anchor_block = UtilityContext ::new ().block_number ();
5772
5873 // `to_block` is exclusive, so we add 1 to include the anchor block in the scan
@@ -63,26 +78,18 @@ pub(crate) unconstrained fn handshake_registry_sync(
6378 let tag = compute_log_tag (scope .to_field (), DOM_SEP__NON_INTERACTIVE_HANDSHAKE_LOG_TAG );
6479 let logs = get_handshake_logs_in_range (contract_address , tag , cursor , Option ::some (next_block ));
6580
66- // Store all valid handshakes
67- let handshakes : CapsuleArray <DiscoveredHandshake > =
68- CapsuleArray ::at (contract_address , HANDSHAKES_CAPSULE_SLOT , scope );
81+ // Record all valid handshakes
6982 logs .for_each (|_i , log | {
7083 let ciphertext = aztec::utils::array:: subbvec (log .log_payload , 0 );
7184
7285 let _ = AES128 ::decrypt (ciphertext , scope , contract_address )
7386 .and_then (|pt | {
7487 point_from_x_coord_and_sign (pt .get (0 ), true ).map (|pk | DiscoveredHandshake { eph_pk : pk })
7588 })
76- .map (|h | handshakes . push ( h ));
89+ .map (|h | record_discovered_handshake ( contract_address , scope , h , log . block_number ));
7790 });
7891
79- // Update the cursor
80- capsules:: store (
81- contract_address ,
82- SYNC_CURSOR_CAPSULE_SLOT ,
83- next_block ,
84- scope ,
85- );
92+ store_sync_cursor (contract_address , scope , anchor_block );
8693 }
8794
8895 do_sync_state_without_handshake_discovery (
@@ -95,12 +102,99 @@ pub(crate) unconstrained fn handshake_registry_sync(
95102 );
96103}
97104
98- /// Returns the capsule array of discovered handshakes for `recipient`.
105+ /// Returns the discovered handshakes for `recipient`, one per canonical handshake fact collection.
106+ ///
107+ /// Ordering is unspecified.
99108pub (crate ) unconstrained fn get_all_handshakes (
100109 contract_address : AztecAddress ,
101110 recipient : AztecAddress ,
102- ) -> CapsuleArray <DiscoveredHandshake > {
103- CapsuleArray ::at (contract_address , HANDSHAKES_CAPSULE_SLOT , recipient )
111+ ) -> EphemeralArray <DiscoveredHandshake > {
112+ get_fact_collections_by_type (contract_address , recipient , DISCOVERED_HANDSHAKES_TYPE_ID )
113+ .map (|collection : FactCollection | {
114+ let fact = collection .facts .find (|f : Fact | f .fact_type_id == HANDSHAKE_DISCOVERED ).unwrap ();
115+ handshake_from_payload (fact .payload )
116+ })
117+ }
118+
119+ /// Records a discovered handshake as a retractable fact originated at the block its announcement log landed in.
120+ ///
121+ /// The collection id is a hash of the handshake, so re-recording the same handshake at the same origin (e.g. when
122+ /// rescanning after the cursor was retracted by a reorg) is a no-op, and the same `eph_pk` can never appear twice.
123+ unconstrained fn record_discovered_handshake (
124+ contract_address : AztecAddress ,
125+ scope : AztecAddress ,
126+ handshake : DiscoveredHandshake ,
127+ block_number : u32 ,
128+ ) {
129+ record_retractable_fact (
130+ contract_address ,
131+ scope ,
132+ DISCOVERED_HANDSHAKES_TYPE_ID ,
133+ handshake_collection_id (handshake ),
134+ HANDSHAKE_DISCOVERED ,
135+ handshake_to_payload (handshake ),
136+ // The log retrieval oracle intentionally exposes only the origin block number, and FactStore retraction and
137+ // origin-state classification key on block number alone, so no real hash is available or needed.
138+ OriginBlock { block_number , block_hash : 0 },
139+ );
140+ }
141+
142+ /// Computes the fact-collection id identifying `handshake` in the [fact store](aztec::facts).
143+ fn handshake_collection_id (handshake : DiscoveredHandshake ) -> Field {
144+ poseidon2_hash (handshake .serialize ())
145+ }
146+
147+ /// Serializes a [`DiscoveredHandshake`] into a fact payload.
148+ unconstrained fn handshake_to_payload (handshake : DiscoveredHandshake ) -> EphemeralArray <Field > {
149+ let fields = handshake .serialize ();
150+ let payload : EphemeralArray <Field > = EphemeralArray ::empty ();
151+ for i in 0 ..fields .len () {
152+ payload .push (fields [i ]);
153+ }
154+ payload
155+ }
156+
157+ /// Deserializes a [`DiscoveredHandshake`] from a fact payload.
158+ unconstrained fn handshake_from_payload (payload : EphemeralArray <Field >) -> DiscoveredHandshake {
159+ let n = <DiscoveredHandshake as Deserialize >::N ;
160+ let mut fields = [0 ; <DiscoveredHandshake as Deserialize >::N ];
161+ for i in 0 ..n {
162+ fields [i ] = payload .get (i );
163+ }
164+ Deserialize ::deserialize (fields )
165+ }
166+
167+ /// Returns the next block (inclusive) the discovery scan should start from, or `None` if no canonical cursor exists,
168+ /// either because this scope never synced or because the cursor fact's origin block was reorg'd away. In the latter
169+ /// case the caller rescans the full range and fact idempotency deduplicates re-discovered handshakes.
170+ unconstrained fn load_sync_cursor (contract_address : AztecAddress , scope : AztecAddress ) -> Option <u32 > {
171+ get_fact_collection (
172+ contract_address ,
173+ scope ,
174+ SYNC_CURSOR_TYPE_ID ,
175+ SYNC_CURSOR_COLLECTION_ID ,
176+ )
177+ .map (|collection : FactCollection | collection .facts .get (0 ).origin_block .unwrap ().block_number + 1 )
178+ }
179+
180+ /// Replaces the cursor fact with one originated at `anchor_block` (the block the completed scan ran against), so a
181+ /// reorg pruning any scanned block also retracts the cursor and the next sync rescans.
182+ unconstrained fn store_sync_cursor (contract_address : AztecAddress , scope : AztecAddress , anchor_block : u32 ) {
183+ delete_fact_collection (
184+ contract_address ,
185+ scope ,
186+ SYNC_CURSOR_TYPE_ID ,
187+ SYNC_CURSOR_COLLECTION_ID ,
188+ );
189+ record_retractable_fact (
190+ contract_address ,
191+ scope ,
192+ SYNC_CURSOR_TYPE_ID ,
193+ SYNC_CURSOR_COLLECTION_ID ,
194+ SYNC_CURSOR ,
195+ EphemeralArray ::empty (),
196+ OriginBlock { block_number : anchor_block , block_hash : 0 },
197+ );
104198}
105199
106200/// Fetches all private logs emitted under `tag` in the block range `[from_block, to_block)`.
0 commit comments