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