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 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                            // An item never proves its own checkpoint
153                            // complete (list_transactions semantics).
154                            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
331/// Render a full `Checkpoint` message from live executed-checkpoint data,
332/// including the `transactions.balance_changes` special case that
333/// `merge_from` cannot fill (it needs the checkpoint's `ObjectSet`).
334fn 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
369/// Render one `ExecutedTransaction` message from live executed-checkpoint
370/// data. The nested-transaction `merge_from` does not set `checkpoint` /
371/// `timestamp` (the checkpoint-level merge does), so set them here, along
372/// with `balance_changes` which needs the checkpoint's `ObjectSet`.
373fn 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
418/// Fold a subscription progress tick into the stream's checkpoint coverage.
419/// A subscription's first tick names the checkpoint immediately before its
420/// entry checkpoint; later ticks name checkpoints the actor fully processed.
421/// Both establish a safe covered boundary.
422fn 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}