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 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 summary.verify_with_contents(committee, Some(&checkpoint.checkpoint_contents))?;
32
33 let contents = &checkpoint.checkpoint_contents;
35 let (matching_tx, _) = checkpoint
36 .transactions
37 .iter()
38 .zip(contents.iter())
39 .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 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 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 let (effects, _) = get_verified_effects_and_events(config, object.previous_transaction)
78 .await
79 .expect("Cannot get effects and events");
80
81 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 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 let object_store = SuiObjectStore::new(config)?;
115
116 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 let checkpoints_list: CheckpointsList = read_checkpoint_list(config)?;
124
125 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 let prev_ckp = read_checkpoint(config, *prev_ckp_id)?;
135
136 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 extract_new_committee_info(&prev_ckp)?
144 } else {
145 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
158pub 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 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 let (effects, _) = get_verified_effects_and_events(config, object.previous_transaction)
192 .await
193 .expect("Cannot get effects and events");
194
195 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 let object_store = SuiObjectStore::new(config)?;
206
207 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 let checkpoints_list: CheckpointsList = read_checkpoint_list(config)?;
215
216 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 let prev_ckp = read_checkpoint(config, *prev_ckp_id)?;
226
227 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 extract_new_committee_info(&prev_ckp)?
235 } else {
236 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 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#[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 #[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 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 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}