Skip to main content

sui_rosetta/
state.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use async_trait::async_trait;
5use chrono::DateTime;
6use futures::stream::{self, StreamExt, TryStreamExt};
7use prost_types::FieldMask;
8use std::str::FromStr;
9use std::sync::Arc;
10use sui_rpc::client::Client as GrpcClient;
11use sui_rpc::field::FieldMaskUtil;
12use sui_rpc::proto::sui::rpc::v2::{
13    Checkpoint, GetCheckpointRequest, GetServiceInfoRequest, get_checkpoint_request,
14};
15use sui_types::base_types::TransactionDigest;
16use sui_types::digests::CheckpointDigest;
17use sui_types::messages_checkpoint::CheckpointSequenceNumber;
18
19use sui_types::digests::ChainIdentifier;
20
21use crate::operations::Operations;
22use crate::types::{
23    Block, BlockHash, BlockIdentifier, BlockResponse, Transaction, TransactionIdentifier,
24};
25use crate::{CoinMetadataCache, Error};
26
27#[derive(Clone)]
28pub struct OnlineServerContext {
29    pub client: GrpcClient,
30    pub coin_metadata_cache: CoinMetadataCache,
31    pub chain_id: ChainIdentifier,
32    block_provider: Arc<dyn BlockProvider + Send + Sync>,
33}
34
35impl OnlineServerContext {
36    pub fn new(
37        client: GrpcClient,
38        block_provider: Arc<dyn BlockProvider + Send + Sync>,
39        coin_metadata_cache: CoinMetadataCache,
40        chain_id: ChainIdentifier,
41    ) -> Self {
42        Self {
43            client,
44            block_provider,
45            coin_metadata_cache,
46            chain_id,
47        }
48    }
49
50    pub fn blocks(&self) -> &(dyn BlockProvider + Sync + Send) {
51        &*self.block_provider
52    }
53}
54
55#[async_trait]
56pub trait BlockProvider {
57    async fn get_block_by_index(&self, index: u64) -> Result<BlockResponse, Error>;
58    async fn get_block_by_hash(&self, hash: BlockHash) -> Result<BlockResponse, Error>;
59    async fn current_block(&self) -> Result<BlockResponse, Error>;
60    async fn genesis_block_identifier(&self) -> Result<BlockIdentifier, Error>;
61    async fn oldest_block_identifier(&self) -> Result<BlockIdentifier, Error>;
62    async fn current_block_identifier(&self) -> Result<BlockIdentifier, Error>;
63}
64
65#[derive(Clone)]
66pub struct CheckpointBlockProvider {
67    client: GrpcClient,
68    coin_metadata_cache: CoinMetadataCache,
69}
70
71#[async_trait]
72impl BlockProvider for CheckpointBlockProvider {
73    async fn get_block_by_index(&self, index: u64) -> Result<BlockResponse, Error> {
74        let request = GetCheckpointRequest::by_sequence_number(index).with_read_mask(
75            FieldMask::from_paths([
76                "sequence_number",
77                "digest",
78                "summary.sequence_number",
79                "summary.previous_digest",
80                "summary.timestamp",
81                "transactions.digest",
82                "transactions.transaction.sender",
83                "transactions.transaction.gas_payment",
84                "transactions.transaction.kind",
85                "transactions.effects.gas_object",
86                "transactions.effects.gas_used",
87                "transactions.effects.status",
88                "transactions.balance_changes",
89                "transactions.events.events.event_type",
90                "transactions.events.events.json",
91            ]),
92        );
93
94        let mut client = self.client.clone();
95        let response = client
96            .ledger_client()
97            .get_checkpoint(request)
98            .await
99            .map_err(|e| Error::from(anyhow::anyhow!("Failed to get checkpoint: {}", e)))?
100            .into_inner();
101
102        let checkpoint = response
103            .checkpoint
104            .ok_or_else(|| Error::DataError("Checkpoint not found".to_string()))?;
105
106        self.create_block_response(checkpoint).await
107    }
108
109    async fn get_block_by_hash(&self, hash: BlockHash) -> Result<BlockResponse, Error> {
110        let mut request = GetCheckpointRequest::default().with_read_mask(FieldMask::from_paths([
111            "sequence_number",
112            "digest",
113            "summary.sequence_number",
114            "summary.previous_digest",
115            "summary.timestamp",
116            "transactions.digest",
117            "transactions.transaction.sender",
118            "transactions.transaction.gas_payment",
119            "transactions.transaction.kind",
120            "transactions.effects.gas_object",
121            "transactions.effects.gas_used",
122            "transactions.effects.status",
123            "transactions.balance_changes",
124            "transactions.events.events.event_type",
125            "transactions.events.events.json",
126        ]));
127        request.checkpoint_id = Some(get_checkpoint_request::CheckpointId::Digest(
128            hash.to_string(),
129        ));
130
131        let mut client = self.client.clone();
132        let response = client
133            .ledger_client()
134            .get_checkpoint(request)
135            .await?
136            .into_inner();
137        let checkpoint = response
138            .checkpoint
139            .ok_or_else(|| Error::DataError("Checkpoint not found".to_string()))?;
140
141        self.create_block_response(checkpoint).await
142    }
143
144    async fn current_block(&self) -> Result<BlockResponse, Error> {
145        let request = GetCheckpointRequest::latest().with_read_mask(FieldMask::from_paths([
146            "sequence_number",
147            "digest",
148            "summary.sequence_number",
149            "summary.previous_digest",
150            "summary.timestamp",
151            "transactions.digest",
152            "transactions.transaction.sender",
153            "transactions.transaction.gas_payment",
154            "transactions.transaction.kind",
155            "transactions.effects.gas_object",
156            "transactions.effects.gas_used",
157            "transactions.effects.status",
158            "transactions.balance_changes",
159            "transactions.events.events.event_type",
160            "transactions.events.events.json",
161        ]));
162
163        let mut client = self.client.clone();
164        let response = client
165            .ledger_client()
166            .get_checkpoint(request)
167            .await?
168            .into_inner();
169
170        let checkpoint = response
171            .checkpoint
172            .ok_or_else(|| Error::DataError("Checkpoint not found".to_string()))?;
173
174        self.create_block_response(checkpoint).await
175    }
176
177    async fn genesis_block_identifier(&self) -> Result<BlockIdentifier, Error> {
178        let response = self
179            .client
180            .clone()
181            .ledger_client()
182            .get_service_info(GetServiceInfoRequest::default())
183            .await?
184            .into_inner();
185        let chain_id = response
186            .chain_id
187            .ok_or_else(|| Error::DataError("Missing chain_id".to_string()))?;
188        let hash = CheckpointDigest::from_str(&chain_id)?;
189        Ok(BlockIdentifier { index: 0, hash })
190    }
191
192    async fn oldest_block_identifier(&self) -> Result<BlockIdentifier, Error> {
193        let response = self
194            .client
195            .clone()
196            .ledger_client()
197            .get_service_info(GetServiceInfoRequest::default())
198            .await?
199            .into_inner();
200        let lowest = response
201            .lowest_available_checkpoint
202            .ok_or_else(|| Error::DataError("Missing lowest_available_checkpoint".to_string()))?;
203        self.create_block_identifier(lowest).await
204    }
205
206    async fn current_block_identifier(&self) -> Result<BlockIdentifier, Error> {
207        let request = GetCheckpointRequest::latest()
208            .with_read_mask(FieldMask::from_paths(["sequence_number", "digest"]));
209
210        let response = self
211            .client
212            .clone()
213            .ledger_client()
214            .get_checkpoint(request)
215            .await?
216            .into_inner();
217
218        let checkpoint = response
219            .checkpoint
220            .ok_or_else(|| Error::DataError("Missing checkpoint".to_string()))?;
221
222        Ok(BlockIdentifier {
223            index: checkpoint.sequence_number(),
224            hash: CheckpointDigest::from_str(checkpoint.digest())?,
225        })
226    }
227}
228
229impl CheckpointBlockProvider {
230    pub fn new(client: GrpcClient, coin_metadata_cache: CoinMetadataCache) -> Self {
231        Self {
232            client,
233            coin_metadata_cache,
234        }
235    }
236
237    async fn create_block_response(&self, checkpoint: Checkpoint) -> Result<BlockResponse, Error> {
238        let summary = checkpoint.summary();
239        let index = summary.sequence_number();
240        let hash = CheckpointDigest::from_str(checkpoint.digest())?;
241        // Genesis checkpoint (index 0) has no previous digest
242        let previous_hash = if index == 0 {
243            hash
244        } else {
245            CheckpointDigest::from_str(summary.previous_digest())?
246        };
247        let timestamp_ms = summary
248            .timestamp
249            .ok_or_else(|| Error::DataError("Checkpoint timestamp is missing".to_string()))
250            .and_then(|ts| {
251                DateTime::from_timestamp(ts.seconds, ts.nanos as u32)
252                    .ok_or_else(|| Error::DataError(format!("Invalid timestamp: {}", ts)))
253            })?
254            .timestamp_millis() as u64;
255
256        let transactions: Vec<Transaction> = stream::iter(checkpoint.transactions)
257            .map(|executed_tx| async move {
258                let digest = TransactionDigest::from_str(executed_tx.digest())?;
259                Ok::<_, Error>(Transaction {
260                    transaction_identifier: TransactionIdentifier { hash: digest },
261                    // This is async because it makes a GetCoinMetadata call if the coin metadata
262                    // isn't already cached.
263                    operations: Operations::try_from_executed_transaction(
264                        executed_tx,
265                        &self.coin_metadata_cache,
266                    )
267                    .await?,
268                    related_transactions: vec![],
269                    metadata: None,
270                })
271            })
272            // A checkpoint can have thousands of transactions so
273            // limit the amount of work a single rosetta request can generate
274            // concurrently to prevent resource starvation issues.
275            .buffer_unordered(10)
276            .try_collect()
277            .await?;
278
279        let parent_block_identifier = if index == 0 {
280            // Genesis block is its own parent
281            BlockIdentifier { index, hash }
282        } else {
283            BlockIdentifier {
284                index: index - 1,
285                hash: previous_hash,
286            }
287        };
288
289        Ok(BlockResponse {
290            block: Block {
291                block_identifier: BlockIdentifier { index, hash },
292                parent_block_identifier,
293                timestamp: timestamp_ms,
294                transactions,
295                metadata: None,
296            },
297            other_transactions: vec![],
298        })
299    }
300
301    async fn create_block_identifier(
302        &self,
303        seq_number: CheckpointSequenceNumber,
304    ) -> Result<BlockIdentifier, Error> {
305        let grpc_request = GetCheckpointRequest::by_sequence_number(seq_number)
306            .with_read_mask(FieldMask::from_paths(["sequence_number", "digest"]));
307        let mut client = self.client.clone();
308        let response = client
309            .ledger_client()
310            .get_checkpoint(grpc_request)
311            .await?
312            .into_inner();
313
314        let checkpoint = response.checkpoint();
315        let index = checkpoint.sequence_number();
316        let hash = checkpoint.digest();
317
318        Ok(BlockIdentifier {
319            index,
320            hash: CheckpointDigest::from_str(hash)?,
321        })
322    }
323}
324
325#[cfg(test)]
326mod checkpoint_race_tests {
327    use super::*;
328    use crate::types::{
329        AccountBalanceRequest, AccountIdentifier, Currencies, Currency, NetworkIdentifier, SuiEnv,
330    };
331    use axum::extract::State;
332    use axum::{Extension, Json};
333    use axum_extra::extract::WithRejection;
334    use std::marker::PhantomData;
335    use std::num::NonZeroUsize;
336    use std::sync::atomic::{AtomicUsize, Ordering};
337    use sui_rpc::proto::sui::rpc::v2::ledger_service_server::{LedgerService, LedgerServiceServer};
338    use sui_rpc::proto::sui::rpc::v2::state_service_server::{StateService, StateServiceServer};
339    use sui_rpc::proto::sui::rpc::v2::{
340        Balance, CheckpointSummary, GetBalanceRequest, GetBalanceResponse, GetCheckpointResponse,
341        get_checkpoint_request,
342    };
343    use sui_types::base_types::SuiAddress;
344    use tonic::{Request, Response, Status};
345
346    const AHEAD_CHECKPOINT: u64 = 42;
347
348    #[derive(Clone)]
349    struct SplitHeightLedger {
350        latest_requests: Arc<AtomicUsize>,
351        sequence_requests: Arc<AtomicUsize>,
352    }
353
354    fn ahead_checkpoint() -> Checkpoint {
355        let mut summary = CheckpointSummary::default();
356        summary.sequence_number = Some(AHEAD_CHECKPOINT);
357        summary.previous_digest = Some(CheckpointDigest::new([1; 32]).to_string());
358        summary.timestamp = Some(prost_types::Timestamp {
359            seconds: 1,
360            nanos: 0,
361        });
362
363        let mut checkpoint = Checkpoint::default();
364        checkpoint.sequence_number = Some(AHEAD_CHECKPOINT);
365        checkpoint.digest = Some(CheckpointDigest::new([2; 32]).to_string());
366        checkpoint.summary = Some(summary);
367        checkpoint
368    }
369
370    #[tonic::async_trait]
371    impl LedgerService for SplitHeightLedger {
372        async fn get_checkpoint(
373            &self,
374            request: Request<GetCheckpointRequest>,
375        ) -> Result<Response<GetCheckpointResponse>, Status> {
376            match request.into_inner().checkpoint_id {
377                None => {
378                    self.latest_requests.fetch_add(1, Ordering::SeqCst);
379                    let mut response = GetCheckpointResponse::default();
380                    response.checkpoint = Some(ahead_checkpoint());
381                    Ok(Response::new(response))
382                }
383                Some(get_checkpoint_request::CheckpointId::SequenceNumber(AHEAD_CHECKPOINT)) => {
384                    self.sequence_requests.fetch_add(1, Ordering::SeqCst);
385                    Err(Status::not_found(format!(
386                        "Checkpoint {AHEAD_CHECKPOINT} not found"
387                    )))
388                }
389                Some(checkpoint_id) => Err(Status::invalid_argument(format!(
390                    "unexpected checkpoint request: {checkpoint_id:?}"
391                ))),
392            }
393        }
394    }
395
396    #[tonic::async_trait]
397    impl StateService for SplitHeightLedger {
398        async fn get_balance(
399            &self,
400            _request: Request<GetBalanceRequest>,
401        ) -> Result<Response<GetBalanceResponse>, Status> {
402            let mut balance = Balance::default();
403            balance.balance = Some(100);
404            let mut response = GetBalanceResponse::default();
405            response.balance = Some(balance);
406            Ok(Response::new(response))
407        }
408    }
409
410    async fn start_split_height_ledger(
411        ledger: SplitHeightLedger,
412    ) -> (GrpcClient, tokio::task::JoinHandle<()>) {
413        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
414        let address = listener.local_addr().unwrap();
415        let incoming = stream::unfold(listener, |listener| async move {
416            let result = listener.accept().await.map(|(stream, _)| stream);
417            Some((result, listener))
418        });
419        let handle = tokio::spawn(async move {
420            tonic::transport::Server::builder()
421                .add_service(LedgerServiceServer::new(ledger.clone()))
422                .add_service(StateServiceServer::new(ledger))
423                .serve_with_incoming(incoming)
424                .await
425                .unwrap();
426        });
427        let client = GrpcClient::new(format!("http://{address}")).unwrap();
428        (client, handle)
429    }
430
431    #[tokio::test]
432    async fn current_block_uses_single_latest_request() {
433        let latest_requests = Arc::new(AtomicUsize::new(0));
434        let sequence_requests = Arc::new(AtomicUsize::new(0));
435        let ledger = SplitHeightLedger {
436            latest_requests: latest_requests.clone(),
437            sequence_requests: sequence_requests.clone(),
438        };
439        let (client, server) = start_split_height_ledger(ledger).await;
440        let coin_metadata_cache =
441            CoinMetadataCache::new(client.clone(), NonZeroUsize::new(1).unwrap());
442        let provider = CheckpointBlockProvider::new(client, coin_metadata_cache);
443
444        let response = provider.current_block().await.unwrap();
445
446        assert_eq!(response.block.block_identifier.index, AHEAD_CHECKPOINT);
447        assert_eq!(latest_requests.load(Ordering::SeqCst), 1);
448        assert_eq!(sequence_requests.load(Ordering::SeqCst), 0);
449
450        server.abort();
451    }
452
453    #[tokio::test]
454    async fn current_block_identifier_uses_single_latest_request() {
455        let latest_requests = Arc::new(AtomicUsize::new(0));
456        let sequence_requests = Arc::new(AtomicUsize::new(0));
457        let ledger = SplitHeightLedger {
458            latest_requests: latest_requests.clone(),
459            sequence_requests: sequence_requests.clone(),
460        };
461        let (client, server) = start_split_height_ledger(ledger).await;
462        let coin_metadata_cache =
463            CoinMetadataCache::new(client.clone(), NonZeroUsize::new(1).unwrap());
464        let provider = CheckpointBlockProvider::new(client, coin_metadata_cache);
465
466        let identifier = provider.current_block_identifier().await.unwrap();
467
468        assert_eq!(identifier.index, AHEAD_CHECKPOINT);
469        assert_eq!(latest_requests.load(Ordering::SeqCst), 1);
470        assert_eq!(sequence_requests.load(Ordering::SeqCst), 0);
471
472        server.abort();
473    }
474
475    #[tokio::test]
476    async fn account_balance_uses_single_latest_request() {
477        let latest_requests = Arc::new(AtomicUsize::new(0));
478        let sequence_requests = Arc::new(AtomicUsize::new(0));
479        let ledger = SplitHeightLedger {
480            latest_requests: latest_requests.clone(),
481            sequence_requests: sequence_requests.clone(),
482        };
483        let (client, server) = start_split_height_ledger(ledger).await;
484        let coin_metadata_cache =
485            CoinMetadataCache::new(client.clone(), NonZeroUsize::new(1).unwrap());
486        let block_provider = Arc::new(CheckpointBlockProvider::new(
487            client.clone(),
488            coin_metadata_cache.clone(),
489        ));
490        let context = OnlineServerContext::new(
491            client,
492            block_provider,
493            coin_metadata_cache,
494            ChainIdentifier::from(CheckpointDigest::new([3; 32])),
495        );
496        let request = AccountBalanceRequest {
497            network_identifier: NetworkIdentifier {
498                blockchain: "sui".to_string(),
499                network: SuiEnv::LocalNet,
500            },
501            account_identifier: AccountIdentifier {
502                address: SuiAddress::ZERO,
503                sub_account: None,
504            },
505            block_identifier: Default::default(),
506            currencies: Currencies(vec![Currency::default()]),
507        };
508
509        let response = crate::account::balance(
510            State(context),
511            Extension(SuiEnv::LocalNet),
512            WithRejection(Json(request), PhantomData),
513        )
514        .await
515        .unwrap();
516
517        assert_eq!(response.block_identifier.index, AHEAD_CHECKPOINT);
518        assert_eq!(response.balances[0].value, 100);
519        assert_eq!(latest_requests.load(Ordering::SeqCst), 1);
520        assert_eq!(sequence_requests.load(Ordering::SeqCst), 0);
521
522        server.abort();
523    }
524}