iota_light_client/
verifier.rs1use std::sync::Arc;
6
7use anyhow::{Context, Result, anyhow};
8use iota_config::genesis::Genesis;
9use iota_json_rpc_types::{IotaObjectDataOptions, IotaTransactionBlockResponseOptions};
10use iota_sdk::IotaClientBuilder;
11use iota_types::{
12 base_types::{ObjectID, TransactionDigest},
13 committee::Committee,
14 effects::{TransactionEffects, TransactionEffectsAPI, TransactionEvents},
15 full_checkpoint_content::CheckpointData,
16 messages_checkpoint::CheckpointSequenceNumber,
17 object::Object,
18};
19use tracing::info;
20
21use crate::{
22 checkpoint::{CheckpointList, read_checkpoint_list, read_checkpoint_summary},
23 config::Config,
24 object_store::CheckpointStore,
25};
26
27pub fn extract_verified_effects_and_events(
28 checkpoint: &CheckpointData,
29 committee: &Committee,
30 transaction_digest: TransactionDigest,
31) -> Result<(TransactionEffects, Option<TransactionEvents>)> {
32 let summary = &checkpoint.checkpoint_summary;
33
34 summary.verify_with_contents(committee, Some(&checkpoint.checkpoint_contents))?;
36
37 let contents = &checkpoint.checkpoint_contents;
39 let (matching_tx, _) = checkpoint
40 .transactions
41 .iter()
42 .zip(contents.iter())
43 .find(|(tx, digest)| {
46 tx.effects.execution_digests() == **digest && digest.transaction == transaction_digest
47 })
48 .ok_or_else(|| anyhow!("Transaction not found in checkpoint contents"))?;
49
50 let events_digest = matching_tx.events.as_ref().map(|events| events.digest());
52 anyhow::ensure!(
53 events_digest.as_ref() == matching_tx.effects.events_digest(),
54 "Events digest does not match"
55 );
56
57 Ok((matching_tx.effects.clone(), matching_tx.events.clone()))
59}
60
61pub async fn get_verified_object(config: &Config, object_id: ObjectID) -> Result<Object> {
62 let iota_client = Arc::new(
63 IotaClientBuilder::default()
64 .build(config.rpc_url.as_str())
65 .await?,
66 );
67
68 info!("Getting object: {object_id}");
69
70 let read_api = iota_client.read_api();
71 let object_json = read_api
72 .get_object_with_options(object_id, IotaObjectDataOptions::bcs_lossless())
73 .await
74 .expect("Cannot get object");
75 let object = object_json
76 .into_object()
77 .expect("Cannot make into object data");
78 let object: Object = object.try_into().expect("Cannot reconstruct object");
79
80 let (effects, _) = get_verified_effects_and_events(config, object.previous_transaction)
82 .await
83 .expect("Cannot get effects and events");
84
85 let target_object_ref = object.compute_object_reference();
87 effects
88 .all_changed_objects()
89 .iter()
90 .find(|object_ref| object_ref.0 == target_object_ref)
91 .ok_or_else(|| anyhow!("Object not found"))?;
92
93 Ok(object)
94}
95
96pub async fn get_verified_effects_and_events(
97 config: &Config,
98 transaction_digest: TransactionDigest,
99) -> Result<(TransactionEffects, Option<TransactionEvents>)> {
100 let iota_client = IotaClientBuilder::default()
101 .build(config.rpc_url.as_str())
102 .await?;
103 let read_api = iota_client.read_api();
104
105 info!("Getting effects and events for transaction: {transaction_digest}");
106
107 let options = IotaTransactionBlockResponseOptions::new();
109 let seq = read_api
110 .get_transaction_with_options(transaction_digest, options)
111 .await
112 .context("Cannot get transaction")?
113 .checkpoint
114 .ok_or_else(|| anyhow!("Transaction not found"))?;
115
116 let checkpoint = if config.checkpoint_store_config.is_some() {
117 let checkpoint_store = CheckpointStore::new(config)?;
118
119 checkpoint_store
121 .fetch_full_checkpoint(seq)
122 .await
123 .context("Cannot get full checkpoint")?
124 } else {
125 let client = iota_rest_api::Client::new(&config.rpc_url);
127 client.get_full_checkpoint(seq).await?
128 };
129
130 let checkpoints_list: CheckpointList = read_checkpoint_list(config)?;
132
133 let prev_ckp_id = checkpoints_list
135 .checkpoints()
136 .iter()
137 .filter(|ckp_id| **ckp_id < seq)
138 .next_back();
139
140 let committee = if let Some(prev_ckp_id) = prev_ckp_id {
141 let prev_ckp = read_checkpoint_summary(config, *prev_ckp_id)?;
143
144 anyhow::ensure!(
146 prev_ckp.epoch().checked_add(1).unwrap() == checkpoint.checkpoint_summary.epoch(),
147 "Checkpoint sequence number does not match. Need to Sync."
148 );
149
150 let current_committee = prev_ckp
152 .end_of_epoch_data
153 .as_ref()
154 .ok_or_else(|| anyhow!("Expected all checkpoints to be end-of-epoch checkpoints"))?
155 .next_epoch_committee
156 .iter()
157 .cloned()
158 .collect();
159
160 Committee::new(prev_ckp.epoch().checked_add(1).unwrap(), current_committee)
162 } else {
163 Genesis::load(config.genesis_blob_file_path())?
165 .committee()
166 .context("Cannot load Genesis")?
167 };
168
169 info!("Extracting effects and events for transaction: {transaction_digest}");
170
171 extract_verified_effects_and_events(&checkpoint, &committee, transaction_digest)
172 .context("Cannot extract effects and events")
173}
174
175pub async fn get_verified_checkpoint(
182 config: &Config,
183 object_id: ObjectID,
184) -> Result<CheckpointSequenceNumber> {
185 let iota_client = IotaClientBuilder::default()
186 .build(config.rpc_url.as_str())
187 .await?;
188 let read_api = iota_client.read_api();
189 let object_json = read_api
190 .get_object_with_options(object_id, IotaObjectDataOptions::bcs_lossless())
191 .await
192 .expect("Cannot get object");
193 let object = object_json
194 .into_object()
195 .expect("Cannot make into object data");
196 let object: Object = object.try_into().expect("Cannot reconstruct object");
197
198 let options = IotaTransactionBlockResponseOptions::new();
200 let seq = read_api
201 .get_transaction_with_options(object.previous_transaction, options)
202 .await
203 .context("Cannot get transaction")?
204 .checkpoint
205 .ok_or_else(|| anyhow!("Transaction not found"))?;
206
207 let (effects, _) = get_verified_effects_and_events(config, object.previous_transaction)
209 .await
210 .expect("Cannot get effects and events");
211
212 let target_object_ref = object.compute_object_reference();
214 effects
215 .all_changed_objects()
216 .iter()
217 .find(|object_ref| object_ref.0 == target_object_ref)
218 .ok_or_else(|| anyhow!("Object not found"))?;
219
220 let object_store = CheckpointStore::new(config)?;
222
223 let full_check_point = object_store
225 .fetch_full_checkpoint(seq)
226 .await
227 .context("Cannot get full checkpoint")?;
228
229 let checkpoints_list: CheckpointList = read_checkpoint_list(config)?;
231
232 let prev_ckp_id = checkpoints_list
234 .checkpoints()
235 .iter()
236 .filter(|ckp_id| **ckp_id < seq)
237 .next_back();
238
239 let committee = if let Some(prev_ckp_id) = prev_ckp_id {
240 let prev_ckp = read_checkpoint_summary(config, *prev_ckp_id)?;
242
243 anyhow::ensure!(
245 prev_ckp.epoch().checked_add(1).unwrap() == full_check_point.checkpoint_summary.epoch(),
246 "Checkpoint sequence number does not match. Need to Sync."
247 );
248
249 let current_committee = prev_ckp
251 .end_of_epoch_data
252 .as_ref()
253 .ok_or_else(|| anyhow!("Expected all checkpoints to be end-of-epoch checkpoints"))?
254 .next_epoch_committee
255 .iter()
256 .cloned()
257 .collect();
258
259 Committee::new(prev_ckp.epoch().checked_add(1).unwrap(), current_committee)
261 } else {
262 Genesis::load(config.genesis_blob_file_path())?
264 .committee()
265 .context("Cannot load Genesis")?
266 };
267
268 full_check_point
271 .checkpoint_summary
272 .verify_with_contents(&committee, Some(&full_check_point.checkpoint_contents))?;
273
274 anyhow::ensure!(
275 full_check_point
276 .transactions
277 .iter()
278 .any(|t| *t.transaction.digest() == object.previous_transaction),
279 "Transaction not found in checkpoint"
280 );
281 Ok(seq)
282}
283
284#[cfg(test)]
285mod tests {
286 use std::{fs, io::Read, path::PathBuf, str::FromStr};
287
288 use iota_types::{
289 event::Event,
290 messages_checkpoint::{CertifiedCheckpointSummary, FullCheckpointContents},
291 };
292
293 use super::*;
294
295 const FIXTURES_DIR: &str = "tests/fixtures";
296 const FIXTURE_1: &str = "235.sum";
297 const FIXTURE_2: &str = "469.chk";
298
299 async fn read_checkpoint_summary(
300 checkpoint_path: &PathBuf,
301 ) -> anyhow::Result<CertifiedCheckpointSummary> {
302 let mut reader = fs::File::open(checkpoint_path.clone())?;
303 let metadata = fs::metadata(checkpoint_path)?;
304 let mut buffer = vec![0; metadata.len() as usize];
305 reader.read_exact(&mut buffer)?;
306 bcs::from_bytes(&buffer).context("failed to deserialize summary from bcs bytes")
307 }
308
309 async fn read_full_checkpoint(checkpoint_path: &PathBuf) -> anyhow::Result<CheckpointData> {
310 let mut reader = fs::File::open(checkpoint_path.clone())?;
311 let metadata = fs::metadata(checkpoint_path)?;
312 let mut buffer = vec![0; metadata.len() as usize];
313 reader.read_exact(&mut buffer)?;
314 bcs::from_bytes(&buffer).context("failed to deserialize full checkpoint from bcs bytes")
315 }
316
317 async fn read_data() -> (Committee, CheckpointData) {
318 let checkpoint_summary_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
319 .join(FIXTURES_DIR)
320 .join(FIXTURE_1);
321 let summary_checkpoint = read_checkpoint_summary(&checkpoint_summary_path)
322 .await
323 .unwrap();
324 let prev_committee = summary_checkpoint
325 .end_of_epoch_data
326 .as_ref()
327 .expect("Expected all checkpoints to be end-of-epoch checkpoints")
328 .next_epoch_committee
329 .iter()
330 .cloned()
331 .collect();
332 let committee = Committee::new(
333 summary_checkpoint.epoch().checked_add(1).unwrap(),
334 prev_committee,
335 );
336 let full_checkpoint_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
337 .join(FIXTURES_DIR)
338 .join(FIXTURE_2);
339 let full_checkpoint = read_full_checkpoint(&full_checkpoint_path).await.unwrap();
340
341 (committee, full_checkpoint)
342 }
343
344 #[tokio::test]
345 async fn test_checkpoint_all_good() {
346 let (committee, full_checkpoint) = read_data().await;
347 let tx_digest_0 = *full_checkpoint.transactions[0].transaction.digest();
348
349 extract_verified_effects_and_events(&full_checkpoint, &committee, tx_digest_0).unwrap();
350 }
351
352 #[tokio::test]
353 async fn test_checkpoint_bad_committee() {
354 let (mut committee, full_checkpoint) = read_data().await;
355 let tx_digest_0 = *full_checkpoint.transactions[0].transaction.digest();
356
357 committee.epoch += 10;
359
360 assert!(
361 extract_verified_effects_and_events(&full_checkpoint, &committee, tx_digest_0,)
362 .is_err()
363 );
364 }
365
366 #[tokio::test]
367 async fn test_checkpoint_no_transaction() {
368 let (committee, full_checkpoint) = read_data().await;
369
370 assert!(
371 extract_verified_effects_and_events(
372 &full_checkpoint,
373 &committee,
374 TransactionDigest::from_str("11111111111111111111111111111111").unwrap(),
376 )
377 .is_err()
378 );
379 }
380
381 #[tokio::test]
382 async fn test_checkpoint_bad_contents() {
383 let (committee, mut full_checkpoint) = read_data().await;
384 let tx_digest_0 = *full_checkpoint.transactions[0].transaction.digest();
385
386 let random_contents = FullCheckpointContents::random_for_testing();
388 full_checkpoint.checkpoint_contents = random_contents.checkpoint_contents();
389
390 assert!(
391 extract_verified_effects_and_events(&full_checkpoint, &committee, tx_digest_0,)
392 .is_err()
393 );
394 }
395
396 #[tokio::test]
397 async fn test_checkpoint_bad_events() {
398 let (committee, mut full_checkpoint) = read_data().await;
399 let tx0 = &mut full_checkpoint.transactions[0];
401 let tx_digest_0 = *tx0.transaction.digest();
402
403 if tx0.events.is_none() {
404 tx0.events = Some(TransactionEvents {
406 data: vec![Event::random_for_testing()],
407 });
408 } else {
409 tx0.events
410 .as_mut()
411 .unwrap()
412 .data
413 .push(Event::random_for_testing());
414 }
415
416 assert!(
417 extract_verified_effects_and_events(&full_checkpoint, &committee, tx_digest_0,)
418 .is_err()
419 );
420 }
421}