sui_light_client/
verifier.rs1use 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 summary.verify_with_contents(committee, Some(&checkpoint.checkpoint_contents))?;
31
32 let contents = &checkpoint.checkpoint_contents;
34 let (matching_tx, _) = checkpoint
35 .transactions
36 .iter()
37 .zip_debug_eq(contents.iter())
38 .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 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 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 let (effects, _) = get_verified_effects_and_events(config, object.previous_transaction)
65 .await
66 .expect("Cannot get effects and events");
67
68 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 let seq = client
90 .get_transaction(&tid)
91 .await?
92 .checkpoint
93 .ok_or(anyhow!("Transaction not found"))?;
94
95 let object_store = SuiObjectStore::new(config)?;
97
98 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 let checkpoints_list: CheckpointsList = read_checkpoint_list(config)?;
106
107 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 let prev_ckp = read_checkpoint(config, *prev_ckp_id)?;
116
117 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 extract_new_committee_info(&prev_ckp)?
125 } else {
126 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
137pub 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 let seq = client
152 .get_transaction(&object.previous_transaction)
153 .await?
154 .checkpoint
155 .ok_or(anyhow!("Transaction not found"))?;
156
157 let (effects, _) = get_verified_effects_and_events(config, object.previous_transaction)
159 .await
160 .expect("Cannot get effects and events");
161
162 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 let object_store = SuiObjectStore::new(config)?;
173
174 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 let checkpoints_list: CheckpointsList = read_checkpoint_list(config)?;
182
183 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 let prev_ckp = read_checkpoint(config, *prev_ckp_id)?;
192
193 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 extract_new_committee_info(&prev_ckp)?
201 } else {
202 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 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#[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 #[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 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 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}