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