sui_light_client/
verifier.rs

1// Copyright (c) Mysten Labs, Inc.
2// SPDX-License-Identifier: Apache-2.0
3
4use crate::checkpoint::{CheckpointsList, read_checkpoint, read_checkpoint_list};
5use crate::committee::extract_new_committee_info;
6use crate::config::Config;
7use crate::object_store::SuiObjectStore;
8use anyhow::{Result, anyhow};
9use std::sync::Arc;
10use sui_config::genesis::Genesis;
11use sui_json_rpc_types::{SuiObjectDataOptions, SuiTransactionBlockResponseOptions};
12use sui_sdk::SuiClientBuilder;
13use sui_types::base_types::{ObjectID, TransactionDigest};
14use sui_types::committee::Committee;
15use sui_types::effects::{TransactionEffects, TransactionEvents};
16use sui_types::full_checkpoint_content::CheckpointData;
17use sui_types::messages_checkpoint::CheckpointSequenceNumber;
18use sui_types::object::Object;
19use tracing::info;
20
21use sui_types::effects::TransactionEffectsAPI;
22
23pub fn extract_verified_effects_and_events(
24    checkpoint: &CheckpointData,
25    committee: &Committee,
26    tid: TransactionDigest,
27) -> Result<(TransactionEffects, Option<TransactionEvents>)> {
28    let summary = &checkpoint.checkpoint_summary;
29
30    // Verify the checkpoint summary using the committee
31    summary.verify_with_contents(committee, Some(&checkpoint.checkpoint_contents))?;
32
33    // Check the validity of the transaction
34    let contents = &checkpoint.checkpoint_contents;
35    let (matching_tx, _) = checkpoint
36        .transactions
37        .iter()
38        .zip(contents.iter())
39        // Note that we get the digest of the effects to ensure this is
40        // indeed the correct effects that are authenticated in the contents.
41        .find(|(tx, digest)| {
42            tx.effects.execution_digests() == **digest && digest.transaction == tid
43        })
44        .ok_or(anyhow!("Transaction not found in checkpoint contents"))?;
45
46    // Check the events are all correct.
47    let events_digest = matching_tx.events.as_ref().map(|events| events.digest());
48    anyhow::ensure!(
49        events_digest.as_ref() == matching_tx.effects.events_digest(),
50        "Events digest does not match"
51    );
52
53    // Since we do not check objects we do not return them
54    Ok((matching_tx.effects.clone(), matching_tx.events.clone()))
55}
56
57pub async fn get_verified_object(config: &Config, id: ObjectID) -> Result<Object> {
58    let sui_client: Arc<sui_sdk::SuiClient> = Arc::new(
59        SuiClientBuilder::default()
60            .build(config.full_node_url.as_str())
61            .await?,
62    );
63
64    info!("Getting object: {}", id);
65
66    let read_api = sui_client.read_api();
67    let object_json = read_api
68        .get_object_with_options(id, SuiObjectDataOptions::bcs_lossless())
69        .await
70        .expect("Cannot get object");
71    let object = object_json
72        .into_object()
73        .expect("Cannot make into object data");
74    let object: Object = object.try_into().expect("Cannot reconstruct object");
75
76    // Need to authenticate this object
77    let (effects, _) = get_verified_effects_and_events(config, object.previous_transaction)
78        .await
79        .expect("Cannot get effects and events");
80
81    // check that this object ID, version and hash is in the effects
82    let target_object_ref = object.compute_object_reference();
83    effects
84        .all_changed_objects()
85        .iter()
86        .find(|object_ref| object_ref.0 == target_object_ref)
87        .ok_or(anyhow!("Object not found"))
88        .expect("Object not found");
89
90    Ok(object)
91}
92
93pub async fn get_verified_effects_and_events(
94    config: &Config,
95    tid: TransactionDigest,
96) -> Result<(TransactionEffects, Option<TransactionEvents>)> {
97    let sui_mainnet: sui_sdk::SuiClient = SuiClientBuilder::default()
98        .build(config.full_node_url.as_str())
99        .await?;
100    let read_api = sui_mainnet.read_api();
101
102    info!("Getting effects and events for TID: {}", tid);
103
104    // Lookup the transaction id and get the checkpoint sequence number
105    let options = SuiTransactionBlockResponseOptions::new();
106    let seq = read_api
107        .get_transaction_with_options(tid, options)
108        .await
109        .map_err(|e| anyhow!(format!("Cannot get transaction: {e}")))?
110        .checkpoint
111        .ok_or(anyhow!("Transaction not found"))?;
112
113    // Create object store
114    let object_store = SuiObjectStore::new(config)?;
115
116    // Download the full checkpoint for this sequence number
117    let full_check_point = object_store
118        .get_full_checkpoint(seq)
119        .await
120        .map_err(|e| anyhow!(format!("Cannot get full checkpoint: {e}")))?;
121
122    // Load the list of stored checkpoints
123    let checkpoints_list: CheckpointsList = read_checkpoint_list(config)?;
124
125    // find the stored checkpoint before the seq checkpoint
126    let prev_ckp_id = checkpoints_list
127        .checkpoints
128        .iter()
129        .filter(|ckp_id| **ckp_id < seq)
130        .next_back();
131
132    let committee = if let Some(prev_ckp_id) = prev_ckp_id {
133        // Read it from the store
134        let prev_ckp = read_checkpoint(config, *prev_ckp_id)?;
135
136        // Check we have the right checkpoint
137        anyhow::ensure!(
138            prev_ckp.epoch().checked_add(1).unwrap() == full_check_point.checkpoint_summary.epoch(),
139            "Checkpoint sequence number does not match. Need to Sync."
140        );
141
142        // Get the committee from the previous checkpoint
143        extract_new_committee_info(&prev_ckp)?
144    } else {
145        // Since we did not find a small committee checkpoint we use the genesis
146        let mut genesis_path = config.checkpoint_summary_dir.clone();
147        genesis_path.push(&config.genesis_filename);
148        Genesis::load(&genesis_path)?
149            .committee()
150            .map_err(|e| anyhow!(format!("Cannot load Genesis: {e}")))?
151    };
152
153    info!("Extracting effects and events for TID: {}", tid);
154    extract_verified_effects_and_events(&full_check_point, &committee, tid)
155        .map_err(|e| anyhow!(format!("Cannot extract effects and events: {e}")))
156}
157
158/// Get the verified checkpoint sequence number for an object.
159/// This function will verify that the object is in the transaction's effects,
160/// and that the transaction is in the checkpoint
161/// and that the checkpoint is signed by the committee
162/// and the committee is read from the verified checkpoint summary
163/// which is signed by the previous committee.
164pub async fn get_verified_checkpoint(
165    id: ObjectID,
166    config: &Config,
167) -> Result<CheckpointSequenceNumber> {
168    let sui_client: sui_sdk::SuiClient = SuiClientBuilder::default()
169        .build(config.full_node_url.as_str())
170        .await?;
171    let read_api = sui_client.read_api();
172    let object_json = read_api
173        .get_object_with_options(id, SuiObjectDataOptions::bcs_lossless())
174        .await
175        .expect("Cannot get object");
176    let object = object_json
177        .into_object()
178        .expect("Cannot make into object data");
179    let object: Object = object.try_into().expect("Cannot reconstruct object");
180
181    // Lookup the transaction id and get the checkpoint sequence number
182    let options = SuiTransactionBlockResponseOptions::new();
183    let seq = read_api
184        .get_transaction_with_options(object.previous_transaction, options)
185        .await
186        .map_err(|e| anyhow!(format!("Cannot get transaction: {e}")))?
187        .checkpoint
188        .ok_or(anyhow!("Transaction not found"))?;
189
190    // Need to authenticate this object
191    let (effects, _) = get_verified_effects_and_events(config, object.previous_transaction)
192        .await
193        .expect("Cannot get effects and events");
194
195    // check that this object ID, version and hash is in the effects
196    let target_object_ref = object.compute_object_reference();
197    effects
198        .all_changed_objects()
199        .iter()
200        .find(|object_ref| object_ref.0 == target_object_ref)
201        .ok_or(anyhow!("Object not found"))
202        .expect("Object not found");
203
204    // Create object store
205    let object_store = SuiObjectStore::new(config)?;
206
207    // Download the full checkpoint for this sequence number
208    let full_check_point = object_store
209        .get_full_checkpoint(seq)
210        .await
211        .map_err(|e| anyhow!(format!("Cannot get full checkpoint: {e}")))?;
212
213    // Load the list of stored checkpoints
214    let checkpoints_list: CheckpointsList = read_checkpoint_list(config)?;
215
216    // find the stored checkpoint before the seq checkpoint
217    let prev_ckp_id = checkpoints_list
218        .checkpoints
219        .iter()
220        .filter(|ckp_id| **ckp_id < seq)
221        .next_back();
222
223    let committee = if let Some(prev_ckp_id) = prev_ckp_id {
224        // Read it from the store
225        let prev_ckp = read_checkpoint(config, *prev_ckp_id)?;
226
227        // Check we have the right checkpoint
228        anyhow::ensure!(
229            prev_ckp.epoch().checked_add(1).unwrap() == full_check_point.checkpoint_summary.epoch(),
230            "Checkpoint sequence number does not match. Need to Sync."
231        );
232
233        // Get the committee from the previous checkpoint
234        extract_new_committee_info(&prev_ckp)?
235    } else {
236        // Since we did not find a small committee checkpoint we use the genesis
237        let mut genesis_path = config.checkpoint_summary_dir.clone();
238        genesis_path.push(&config.genesis_filename);
239        Genesis::load(&genesis_path)?
240            .committee()
241            .map_err(|e| anyhow!(format!("Cannot load Genesis: {e}")))?
242    };
243
244    // Verify that committee signed this checkpoint and checkpoint contents with digest
245    full_check_point
246        .checkpoint_summary
247        .verify_with_contents(&committee, Some(&full_check_point.checkpoint_contents))?;
248
249    if full_check_point
250        .transactions
251        .iter()
252        .any(|t| *t.transaction.digest() == object.previous_transaction)
253    {
254        Ok(seq)
255    } else {
256        Err(anyhow!("Transaction not found in checkpoint"))
257    }
258}
259
260// Make a test namespace
261#[cfg(test)]
262mod tests {
263    use std::fs;
264    use std::io::{Read, Write};
265    use sui_types::messages_checkpoint::{CheckpointSummary, FullCheckpointContents};
266
267    use super::*;
268    use std::path::{Path, PathBuf};
269    use std::str::FromStr;
270    use sui_types::crypto::AuthorityQuorumSignInfo;
271    use sui_types::message_envelope::Envelope;
272
273    async fn read_full_checkpoint(checkpoint_path: &PathBuf) -> anyhow::Result<CheckpointData> {
274        let mut reader = fs::File::open(checkpoint_path.clone())?;
275        let metadata = fs::metadata(checkpoint_path)?;
276        let mut buffer = vec![0; metadata.len() as usize];
277        reader.read_exact(&mut buffer)?;
278        bcs::from_bytes(&buffer).map_err(|_| anyhow!("Unable to parse checkpoint file"))
279    }
280
281    // clippy ignore dead-code
282    #[allow(dead_code)]
283    async fn write_full_checkpoint(
284        checkpoint_path: &Path,
285        checkpoint: &CheckpointData,
286    ) -> anyhow::Result<()> {
287        let mut writer = fs::File::create(checkpoint_path)?;
288        let bytes = bcs::to_bytes(&checkpoint)
289            .map_err(|_| anyhow!("Unable to serialize checkpoint summary"))?;
290        writer.write_all(&bytes)?;
291        Ok(())
292    }
293
294    async fn read_data() -> (Committee, CheckpointData) {
295        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
296        d.push("test_files/20873329.yaml");
297
298        let mut reader = fs::File::open(d.clone()).unwrap();
299        let metadata = fs::metadata(&d).unwrap();
300        let mut buffer = vec![0; metadata.len() as usize];
301        reader.read_exact(&mut buffer).unwrap();
302        let checkpoint: Envelope<CheckpointSummary, AuthorityQuorumSignInfo<true>> =
303            bcs::from_bytes(&buffer)
304                .map_err(|_| anyhow!("Unable to parse checkpoint file"))
305                .unwrap();
306
307        let committee = extract_new_committee_info(&checkpoint).unwrap();
308
309        let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
310        d.push("test_files/20958462.bcs");
311
312        let full_checkpoint = read_full_checkpoint(&d).await.unwrap();
313
314        (committee, full_checkpoint)
315    }
316
317    #[tokio::test]
318    async fn test_checkpoint_all_good() {
319        let (committee, full_checkpoint) = read_data().await;
320
321        extract_verified_effects_and_events(
322            &full_checkpoint,
323            &committee,
324            TransactionDigest::from_str("8RiKBwuAbtu8zNCtz8SrcfHyEUzto6zi6cMVA9t4WhWk").unwrap(),
325        )
326        .unwrap();
327    }
328
329    #[tokio::test]
330    async fn test_checkpoint_bad_committee() {
331        let (mut committee, full_checkpoint) = read_data().await;
332
333        // Change committee
334        committee.epoch += 10;
335
336        assert!(
337            extract_verified_effects_and_events(
338                &full_checkpoint,
339                &committee,
340                TransactionDigest::from_str("8RiKBwuAbtu8zNCtz8SrcfHyEUzto6zi6cMVA9t4WhWk")
341                    .unwrap(),
342            )
343            .is_err()
344        );
345    }
346
347    #[tokio::test]
348    async fn test_checkpoint_no_transaction() {
349        let (committee, full_checkpoint) = read_data().await;
350
351        assert!(
352            extract_verified_effects_and_events(
353                &full_checkpoint,
354                &committee,
355                TransactionDigest::from_str("8RiKBwuAbtu8zNCtz8SrcfHyEUzto6zj6cMVA9t4WhWk")
356                    .unwrap(),
357            )
358            .is_err()
359        );
360    }
361
362    #[tokio::test]
363    async fn test_checkpoint_bad_contents() {
364        let (committee, mut full_checkpoint) = read_data().await;
365
366        // Change contents
367        let random_contents = FullCheckpointContents::random_for_testing();
368        full_checkpoint.checkpoint_contents = random_contents.checkpoint_contents();
369
370        assert!(
371            extract_verified_effects_and_events(
372                &full_checkpoint,
373                &committee,
374                TransactionDigest::from_str("8RiKBwuAbtu8zNCtz8SrcfHyEUzto6zj6cMVA9t4WhWk")
375                    .unwrap(),
376            )
377            .is_err()
378        );
379    }
380
381    #[tokio::test]
382    async fn test_checkpoint_bad_events() {
383        let (committee, mut full_checkpoint) = read_data().await;
384
385        let event = full_checkpoint.transactions[4]
386            .events
387            .as_ref()
388            .unwrap()
389            .data[0]
390            .clone();
391
392        for t in &mut full_checkpoint.transactions {
393            if let Some(events) = &mut t.events {
394                events.data.push(event.clone());
395            }
396        }
397
398        assert!(
399            extract_verified_effects_and_events(
400                &full_checkpoint,
401                &committee,
402                TransactionDigest::from_str("8RiKBwuAbtu8zNCtz8SrcfHyEUzto6zj6cMVA9t4WhWk")
403                    .unwrap(),
404            )
405            .is_err()
406        );
407    }
408}