1use std::time::Instant;
25
26use mysten_common::ZipDebugEqIteratorExt;
27use sui_inverted_index::BitmapQuery;
28use sui_rpc::field::FieldMaskTree;
29use sui_rpc::merge::Merge;
30use sui_rpc::proto::sui::rpc::v2::Checkpoint;
31use sui_rpc::proto::sui::rpc::v2::Event as ProtoEvent;
32use sui_rpc::proto::sui::rpc::v2::EventFilter;
33use sui_rpc::proto::sui::rpc::v2::ExecutedTransaction;
34use sui_rpc::proto::sui::rpc::v2::ObjectSet as ProtoObjectSet;
35use sui_rpc::proto::sui::rpc::v2::SubscribeCheckpointsRequest;
36use sui_rpc::proto::sui::rpc::v2::SubscribeCheckpointsResponse;
37use sui_rpc::proto::sui::rpc::v2::SubscribeEventsRequest;
38use sui_rpc::proto::sui::rpc::v2::SubscribeEventsResponse;
39use sui_rpc::proto::sui::rpc::v2::SubscribeTransactionsRequest;
40use sui_rpc::proto::sui::rpc::v2::SubscribeTransactionsResponse;
41use sui_rpc::proto::sui::rpc::v2::TransactionFilter;
42use sui_rpc::proto::sui::rpc::v2::subscription_service_server::SubscriptionService;
43use sui_rpc_cursor::Position;
44use sui_types::balance_change::derive_balance_changes_2;
45use sui_types::effects::TransactionEffectsAPI;
46use tokio::sync::mpsc;
47use tonic::codegen::BoxStream;
48
49use crate::RpcError;
50use crate::RpcService;
51use crate::ledger_history::filter::event_filter_to_query;
52use crate::ledger_history::filter::transaction_filter_to_query;
53use crate::ledger_history::query_options::QueryOptions;
54use crate::ledger_history::watermark::advance_covered_bound_before_checkpoint;
55use crate::ledger_history::watermark::boundary_watermark;
56use crate::ledger_history::watermark::item_watermark;
57use crate::ledger_history::watermark::merge_covered_checkpoint_bound;
58use crate::metrics::SubscriptionFrameKind;
59use crate::metrics::SubscriptionStreamMetrics;
60use crate::read_mask_defaults;
61use crate::subscription::SubscriptionKind;
62use crate::subscription::SubscriptionSpec;
63use crate::subscription::SubscriptionUpdate;
64
65#[tonic::async_trait]
66impl SubscriptionService for RpcService {
67 async fn subscribe_checkpoints(
68 &self,
69 request: tonic::Request<SubscribeCheckpointsRequest>,
70 ) -> Result<tonic::Response<BoxStream<SubscribeCheckpointsResponse>>, tonic::Status> {
71 let request = request.into_inner();
72 let read_mask = read_mask_defaults::validate_read_mask::<Checkpoint>(
73 request.read_mask,
74 read_mask_defaults::CHECKPOINT,
75 )?;
76 let query = compile_transaction_filter(self, request.filter.as_ref())?;
77 let (mut receiver, stream_metrics) = register(
78 self,
79 SubscriptionSpec {
80 kind: SubscriptionKind::Checkpoints,
81 query,
82 },
83 )
84 .await?;
85
86 let service = self.clone();
87 let response = Box::pin(async_stream::stream! {
88 while let Some(update) = receiver.recv().await {
89 let mut response = SubscribeCheckpointsResponse::default();
90 let frame_kind = match update {
91 SubscriptionUpdate::Matched(matched) => {
92 let cp = matched.checkpoint.summary.sequence_number;
93 response.cursor = Some(cp);
94 response.checkpoint = Some(render_checkpoint_message(
95 &service,
96 &matched.checkpoint,
97 &read_mask,
98 ));
99 SubscriptionFrameKind::Payload
100 }
101 SubscriptionUpdate::WatermarkTick { checkpoint: cp, .. } => {
102 response.cursor = Some(cp);
103 SubscriptionFrameKind::Watermark
104 }
105 };
106 stream_metrics.observe_frame(&response, frame_kind);
107 let yielded_at = Instant::now();
108 yield Ok(response);
109 stream_metrics.observe_yield_wait(yielded_at.elapsed());
110 }
111 });
112
113 Ok(tonic::Response::new(response))
114 }
115
116 async fn subscribe_transactions(
117 &self,
118 request: tonic::Request<SubscribeTransactionsRequest>,
119 ) -> Result<tonic::Response<BoxStream<SubscribeTransactionsResponse>>, tonic::Status> {
120 let request = request.into_inner();
121 let read_mask = read_mask_defaults::validate_read_mask::<ExecutedTransaction>(
122 request.read_mask,
123 read_mask_defaults::TRANSACTION,
124 )?;
125 let query = compile_transaction_filter(self, request.filter.as_ref())?;
126 let (mut receiver, stream_metrics) = register(
127 self,
128 SubscriptionSpec {
129 kind: SubscriptionKind::Transactions,
130 query,
131 },
132 )
133 .await?;
134
135 let service = self.clone();
136 let options = QueryOptions::subscription();
137 let response = Box::pin(async_stream::stream! {
138 let mut boundary: Option<u64> = None;
139 let mut entry_checkpoint = None;
140 while let Some(update) = receiver.recv().await {
141 match update {
142 SubscriptionUpdate::Matched(matched) => {
143 let checkpoint = &matched.checkpoint;
144 let Some(indices) = matched
145 .matches
146 .transaction_indices(checkpoint.transactions.len() as u32)
147 else {
148 continue;
149 };
150 let cp = checkpoint.summary.sequence_number;
151 let entry_checkpoint = *entry_checkpoint.get_or_insert(cp);
152 let tx_hi = checkpoint.summary.data().network_total_transactions;
153 let tx_lo = tx_hi - checkpoint.transactions.len() as u64;
154 for i in indices {
155 let tx_seq = tx_lo + i as u64;
156 boundary = advance_covered_bound_before_checkpoint(
159 boundary,
160 cp,
161 entry_checkpoint,
162 &options,
163 );
164 let mut response = SubscribeTransactionsResponse::default();
165 response.transaction = Some(render_transaction_message(
166 &service,
167 checkpoint,
168 i as usize,
169 &read_mask,
170 ));
171 response.watermark = Some(item_watermark(
172 Position::Transactions {
173 checkpoint: cp,
174 tx_seq,
175 },
176 boundary,
177 ));
178 stream_metrics.observe_frame(
179 &response,
180 SubscriptionFrameKind::Payload,
181 );
182 let yielded_at = Instant::now();
183 yield Ok(response);
184 stream_metrics.observe_yield_wait(yielded_at.elapsed());
185 }
186 }
187 SubscriptionUpdate::WatermarkTick { checkpoint: cp, tx_hi } => {
188 record_watermark_tick_coverage(
189 &mut boundary,
190 &mut entry_checkpoint,
191 cp,
192 &options,
193 );
194 let mut response = SubscribeTransactionsResponse::default();
195 response.watermark = Some(boundary_watermark(
196 Position::Transactions {
197 checkpoint: cp + 1,
198 tx_seq: tx_hi,
199 },
200 boundary,
201 ));
202 stream_metrics.observe_frame(
203 &response,
204 SubscriptionFrameKind::Watermark,
205 );
206 let yielded_at = Instant::now();
207 yield Ok(response);
208 stream_metrics.observe_yield_wait(yielded_at.elapsed());
209 }
210 }
211 }
212 });
213
214 Ok(tonic::Response::new(response))
215 }
216
217 async fn subscribe_events(
218 &self,
219 request: tonic::Request<SubscribeEventsRequest>,
220 ) -> Result<tonic::Response<BoxStream<SubscribeEventsResponse>>, tonic::Status> {
221 let request = request.into_inner();
222 let read_mask = read_mask_defaults::validate_read_mask::<ProtoEvent>(
223 request.read_mask,
224 read_mask_defaults::EVENT,
225 )?;
226 let query = compile_event_filter(self, request.filter.as_ref())?;
227 let (mut receiver, stream_metrics) = register(
228 self,
229 SubscriptionSpec {
230 kind: SubscriptionKind::Events,
231 query,
232 },
233 )
234 .await?;
235
236 let service = self.clone();
237 let options = QueryOptions::subscription();
238 let response = Box::pin(async_stream::stream! {
239 let mut boundary: Option<u64> = None;
240 let mut entry_checkpoint = None;
241 while let Some(update) = receiver.recv().await {
242 match update {
243 SubscriptionUpdate::Matched(matched) => {
244 let checkpoint = &matched.checkpoint;
245 let Some(pairs) = matched.matches.event_indices(checkpoint) else {
246 continue;
247 };
248 let cp = checkpoint.summary.sequence_number;
249 let entry_checkpoint = *entry_checkpoint.get_or_insert(cp);
250 let tx_hi = checkpoint.summary.data().network_total_transactions;
251 let tx_lo = tx_hi - checkpoint.transactions.len() as u64;
252 for (tx_idx, ev) in pairs {
253 let tx = &checkpoint.transactions[tx_idx as usize];
254 let tx_seq = tx_lo + tx_idx as u64;
255 boundary = advance_covered_bound_before_checkpoint(
256 boundary,
257 cp,
258 entry_checkpoint,
259 &options,
260 );
261 let event = &tx
262 .events
263 .as_ref()
264 .expect("matched event implies events")
265 .data[ev as usize];
266 let mut proto_event = service.render_event_to_proto(
267 event,
268 &read_mask,
269 &checkpoint.object_set,
270 );
271 if read_mask.contains(ProtoEvent::CHECKPOINT_FIELD.name) {
272 proto_event.checkpoint = Some(cp);
273 }
274 if read_mask.contains(ProtoEvent::TRANSACTION_DIGEST_FIELD.name) {
275 proto_event.transaction_digest =
276 Some(tx.effects.transaction_digest().base58_encode());
277 }
278 if read_mask.contains(ProtoEvent::TRANSACTION_INDEX_FIELD.name) {
279 proto_event.transaction_index = Some(tx_idx as u64);
280 }
281 if read_mask.contains(ProtoEvent::EVENT_INDEX_FIELD.name) {
282 proto_event.event_index = Some(ev);
283 }
284 let mut response = SubscribeEventsResponse::default();
285 response.event = Some(proto_event);
286 response.watermark = Some(item_watermark(
287 Position::Events {
288 checkpoint: cp,
289 tx_seq,
290 event_index: ev,
291 },
292 boundary,
293 ));
294 stream_metrics.observe_frame(
295 &response,
296 SubscriptionFrameKind::Payload,
297 );
298 let yielded_at = Instant::now();
299 yield Ok(response);
300 stream_metrics.observe_yield_wait(yielded_at.elapsed());
301 }
302 }
303 SubscriptionUpdate::WatermarkTick { checkpoint: cp, tx_hi } => {
304 record_watermark_tick_coverage(
305 &mut boundary,
306 &mut entry_checkpoint,
307 cp,
308 &options,
309 );
310 let mut response = SubscribeEventsResponse::default();
311 response.watermark = Some(boundary_watermark(
312 Position::Events {
313 checkpoint: cp + 1,
314 tx_seq: tx_hi,
315 event_index: 0,
316 },
317 boundary,
318 ));
319 stream_metrics.observe_frame(
320 &response,
321 SubscriptionFrameKind::Watermark,
322 );
323 let yielded_at = Instant::now();
324 yield Ok(response);
325 stream_metrics.observe_yield_wait(yielded_at.elapsed());
326 }
327 }
328 }
329 });
330
331 Ok(tonic::Response::new(response))
332 }
333}
334
335fn render_checkpoint_message(
339 service: &RpcService,
340 checkpoint: &sui_types::full_checkpoint_content::Checkpoint,
341 read_mask: &FieldMaskTree,
342) -> Checkpoint {
343 let mut checkpoint_message = Checkpoint::merge_from(checkpoint, read_mask);
344
345 if let Some(tx_mask) = read_mask.subtree(Checkpoint::TRANSACTIONS_FIELD) {
346 let mut json_budget = service.config.max_json_move_value_response_size();
348 for (message, tx) in checkpoint_message
349 .transactions_mut()
350 .iter_mut()
351 .zip_debug_eq(checkpoint.transactions.iter())
352 {
353 service.annotate_checkpoint_transaction(
354 message,
355 tx,
356 &checkpoint.object_set,
357 &tx_mask,
358 &mut json_budget,
359 );
360 }
361 }
362
363 if read_mask.contains("transactions.balance_changes") {
364 for (txn, effects) in checkpoint_message
365 .transactions_mut()
366 .iter_mut()
367 .zip_debug_eq(checkpoint.transactions.iter().map(|t| &t.effects))
368 {
369 *txn.balance_changes_mut() = derive_balance_changes_2(effects, &checkpoint.object_set)
370 .into_iter()
371 .map(Into::into)
372 .collect();
373 }
374 }
375
376 checkpoint_message
377}
378
379fn transaction_objects<'a>(
380 transaction: &'a sui_types::full_checkpoint_content::ExecutedTransaction,
381 checkpoint_objects: &'a sui_types::full_checkpoint_content::ObjectSet,
382) -> impl Iterator<Item = &'a sui_types::object::Object> + 'a {
383 sui_types::storage::get_transaction_object_set(
384 &transaction.transaction,
385 &transaction.effects,
386 &transaction.unchanged_loaded_runtime_objects,
387 )
388 .into_iter()
389 .filter_map(move |key| checkpoint_objects.get(&key))
390}
391
392fn render_transaction_message(
397 service: &RpcService,
398 checkpoint: &sui_types::full_checkpoint_content::Checkpoint,
399 index: usize,
400 read_mask: &FieldMaskTree,
401) -> ExecutedTransaction {
402 let tx = &checkpoint.transactions[index];
403 let mut message = ExecutedTransaction::merge_from(tx, read_mask);
404 let mut json_budget = service.config.max_json_move_value_response_size();
412 service.annotate_checkpoint_transaction(
413 &mut message,
414 tx,
415 &checkpoint.object_set,
416 read_mask,
417 &mut json_budget,
418 );
419
420 if let Some(object_mask) = read_mask
421 .subtree(ExecutedTransaction::OBJECTS_FIELD)
422 .and_then(|submask| submask.subtree(ProtoObjectSet::OBJECTS_FIELD))
423 {
424 message.objects = Some(
425 ProtoObjectSet::default().with_objects(
426 transaction_objects(tx, &checkpoint.object_set)
427 .map(|object| {
428 service.render_object_to_proto(object, &object_mask, &checkpoint.object_set)
429 })
430 .collect(),
431 ),
432 );
433 }
434
435 if read_mask.contains(ExecutedTransaction::CHECKPOINT_FIELD) {
436 message.checkpoint = Some(checkpoint.summary.sequence_number);
437 }
438 if read_mask.contains(ExecutedTransaction::TIMESTAMP_FIELD) {
439 message.timestamp = Some(sui_rpc::proto::timestamp_ms_to_proto(
440 checkpoint.summary.timestamp_ms,
441 ));
442 }
443 if read_mask.contains(ExecutedTransaction::BALANCE_CHANGES_FIELD) {
444 message.balance_changes = derive_balance_changes_2(&tx.effects, &checkpoint.object_set)
445 .into_iter()
446 .map(Into::into)
447 .collect();
448 }
449 if read_mask.contains(ExecutedTransaction::TRANSACTION_INDEX_FIELD) {
450 message.transaction_index = Some(index as u64);
451 }
452
453 message
454}
455
456fn record_watermark_tick_coverage(
461 covered_checkpoint_bound: &mut Option<u64>,
462 entry_checkpoint: &mut Option<u64>,
463 checkpoint: u64,
464 options: &QueryOptions,
465) {
466 if entry_checkpoint.is_none() {
467 *entry_checkpoint = Some(checkpoint.saturating_add(1));
468 }
469 *covered_checkpoint_bound =
470 merge_covered_checkpoint_bound(*covered_checkpoint_bound, checkpoint, options);
471}
472
473fn compile_transaction_filter(
474 service: &RpcService,
475 filter: Option<&TransactionFilter>,
476) -> Result<Option<BitmapQuery>, RpcError> {
477 let max_literals = service.config.ledger_history().max_bitmap_filter_literals();
478 filter
479 .map(|filter| transaction_filter_to_query(filter, max_literals))
480 .transpose()
481}
482
483fn compile_event_filter(
484 service: &RpcService,
485 filter: Option<&EventFilter>,
486) -> Result<Option<BitmapQuery>, RpcError> {
487 let max_literals = service.config.ledger_history().max_bitmap_filter_literals();
488 filter
489 .map(|filter| event_filter_to_query(filter, max_literals))
490 .transpose()
491}
492
493async fn register(
494 service: &RpcService,
495 spec: SubscriptionSpec,
496) -> Result<
497 (
498 mpsc::Receiver<SubscriptionUpdate>,
499 SubscriptionStreamMetrics,
500 ),
501 tonic::Status,
502> {
503 let handle = service
504 .subscription_service_handle
505 .as_ref()
506 .ok_or_else(|| tonic::Status::unimplemented("subscription service not enabled"))?;
507 let kind = spec.kind;
508 let receiver = handle
509 .register_subscription(spec)
510 .await
511 .ok_or_else(|| tonic::Status::unavailable("subscription service is unavailable"))?;
512 let stream_metrics = handle.stream_metrics(kind);
513 Ok((receiver, stream_metrics))
514}
515
516#[cfg(test)]
517mod tests {
518 use sui_types::test_checkpoint_data_builder::TestCheckpointBuilder;
519
520 use super::*;
521 #[test]
522 fn initial_watermark_tick_covers_checkpoint_before_entry() {
523 let options = QueryOptions::subscription();
524 let mut covered_checkpoint_bound = None;
525 let mut entry_checkpoint = None;
526
527 record_watermark_tick_coverage(
528 &mut covered_checkpoint_bound,
529 &mut entry_checkpoint,
530 41,
531 &options,
532 );
533 let watermark = boundary_watermark(
534 Position::Transactions {
535 checkpoint: 42,
536 tx_seq: 100,
537 },
538 covered_checkpoint_bound,
539 );
540
541 assert_eq!(entry_checkpoint, Some(42));
542 assert_eq!(watermark.checkpoint, Some(41));
543 }
544
545 #[test]
546 fn transaction_objects_exclude_checkpoint_siblings() {
547 let checkpoint = TestCheckpointBuilder::new(1)
548 .start_transaction(0)
549 .create_owned_object(10)
550 .finish_transaction()
551 .start_transaction(1)
552 .create_owned_object(20)
553 .finish_transaction()
554 .build_checkpoint();
555 let first_object = TestCheckpointBuilder::derive_object_id(10);
556 let second_object = TestCheckpointBuilder::derive_object_id(20);
557
558 let first_ids = transaction_objects(&checkpoint.transactions[0], &checkpoint.object_set)
559 .map(|object| object.id())
560 .collect::<Vec<_>>();
561 let second_ids = transaction_objects(&checkpoint.transactions[1], &checkpoint.object_set)
562 .map(|object| object.id())
563 .collect::<Vec<_>>();
564
565 assert!(first_ids.contains(&first_object));
566 assert!(!first_ids.contains(&second_object));
567 assert!(second_ids.contains(&second_object));
568 assert!(!second_ids.contains(&first_object));
569 }
570}