Skip to main content

sui_rpc_api/grpc/v2/
subscription_service.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4//! v2 `SubscriptionService`: filtered, real-time streams of checkpoints,
5//! transactions, and events.
6//!
7//! Each Subscribe API pairs with the LedgerService List API of the same name:
8//! requests take the same DNF filter message and responses use identical cursor
9//! semantics, so clients can replay subscription gaps with the paired List API.
10//!
11//! A subscription behaves like an unbounded ascending scan. Every transaction
12//! and event frame carries a `watermark`; its payload is optional. Every
13//! checkpoint frame carries a scalar `cursor`; its checkpoint payload is
14//! optional, and progress-only checkpoint frames occur only on filtered
15//! streams. At non-genesis entry, transaction and event streams and filtered
16//! checkpoint streams begin with a progress-only frame establishing the safe
17//! position immediately before entry. Unfiltered checkpoint streams begin with
18//! a checkpoint payload. Further progress-only frames are emitted when a stream
19//! advances a configured number of checkpoints without a matching item (see
20//! `RpcConfig::subscription_watermark_interval`). Streams have no successful
21//! end: when the subscription actor drops a subscriber (lag or backpressure),
22//! the stream simply closes and the client reconnects, backfilling via List.
23
24use 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                            // An item never proves its own checkpoint
157                            // complete (list_transactions semantics).
158                            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
335/// Render a full `Checkpoint` message from live executed-checkpoint data,
336/// filling what `merge_from` cannot (effects object types, clever errors,
337/// event JSON, and `transactions.balance_changes`) as `GetCheckpoint` does.
338fn 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        // One JSON budget for the whole checkpoint, as in `GetCheckpoint`.
347        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
392/// Render one `ExecutedTransaction` message from live executed-checkpoint
393/// data. The nested-transaction `merge_from` does not set `checkpoint` /
394/// `timestamp` (the checkpoint-level merge does), so set them here, along
395/// with `balance_changes` which needs the checkpoint's `ObjectSet`.
396fn 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    // Every event in the frame draws its JSON from one per-response budget,
405    // as `GetCheckpoint` does across a checkpoint. `GetTransaction` and
406    // `ListTransactions` instead give each event its own
407    // `max_json_move_value_size` budget, so the same transaction can differ:
408    // an event over that per-value cap has `json` here but not there, and once
409    // a transaction's events exhaust `max_json_move_value_response_size`, the
410    // remaining events have `json` there but not here.
411    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
456/// Fold a subscription progress tick into the stream's checkpoint coverage.
457/// A subscription's first tick names the checkpoint immediately before its
458/// entry checkpoint; later ticks name checkpoints the actor fully processed.
459/// Both establish a safe covered boundary.
460fn 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}