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