iota_light_client/proof.rs
1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use anyhow::anyhow;
6use iota_types::{
7 base_types::ObjectRef,
8 committee::Committee,
9 effects::{TransactionEffects, TransactionEffectsAPI, TransactionEvents},
10 event::{Event, EventID},
11 messages_checkpoint::{CertifiedCheckpointSummary, CheckpointContents, EndOfEpochData},
12 object::Object,
13 transaction::Transaction,
14};
15use serde::{Deserialize, Serialize};
16
17/// Define aspects of IOTA state that need to be certified in a proof
18#[derive(Default, Debug, Serialize, Deserialize)]
19pub struct ProofTarget {
20 /// Objects that need to be certified.
21 pub objects: Vec<(ObjectRef, Object)>,
22
23 /// Events that need to be certified.
24 pub events: Vec<(EventID, Event)>,
25
26 /// The next committee being certified.
27 pub committee: Option<Committee>,
28}
29
30impl ProofTarget {
31 /// Create a new empty proof target. An empty proof target still ensures
32 /// that the checkpoint summary is correct.
33 pub fn new() -> Self {
34 Self::default()
35 }
36
37 /// Add an object to be certified by object reference and content. A
38 /// verified proof will ensure that both the reference and content are
39 /// correct. Note that some content is metadata such as the transaction
40 /// that created this object.
41 pub fn add_object(mut self, object_ref: ObjectRef, object: Object) -> Self {
42 self.objects.push((object_ref, object));
43 self
44 }
45
46 /// Add an event to be certified by event ID and content. A verified proof
47 /// will ensure that both the ID and content are correct.
48 pub fn add_event(mut self, event_id: EventID, event: Event) -> Self {
49 self.events.push((event_id, event));
50 self
51 }
52
53 /// Add the next committee to be certified. A verified proof will ensure
54 /// that the next committee is correct.
55 pub fn set_committee(mut self, committee: Committee) -> Self {
56 self.committee = Some(committee);
57 self
58 }
59}
60
61/// Part of a proof that provides evidence relating to a specific transaction to
62/// certify objects and events.
63#[derive(Debug, Serialize, Deserialize)]
64pub struct TransactionProof {
65 /// Checkpoint contents including this transaction.
66 pub checkpoint_contents: CheckpointContents,
67
68 /// The transaction being certified.
69 pub transaction: Transaction,
70
71 /// The effects of the transaction being certified.
72 pub effects: TransactionEffects,
73
74 /// The events of the transaction being certified.
75 pub events: Option<TransactionEvents>,
76}
77
78/// A proof for specific targets. It certifies a checkpoint summary and
79/// optionally includes transaction evidence to certify objects and events.
80#[derive(Debug, Serialize, Deserialize)]
81pub struct Proof {
82 /// Targets of the proof are a committee, objects, or events that need to be
83 /// certified.
84 pub targets: ProofTarget,
85
86 /// A summary of the checkpoint being certified.
87 pub checkpoint_summary: CertifiedCheckpointSummary,
88
89 /// Optional transaction proof to certify objects and events.
90 pub contents_proof: Option<TransactionProof>,
91}
92
93/// Verify a proof against a committee. A proof is valid if it certifies the
94/// checkpoint summary and optionally includes transaction evidence to certify
95/// objects and events.
96///
97/// If the result is `Ok(())` then the proof is valid. If Err is returned then
98/// the proof is invalid and the error message will describe the reason. Once a
99/// proof is verified it can be trusted, and information in `targets` as well as
100/// `checkpoint_summary` or `contents_proof` can be trusted as being authentic.
101///
102/// The authoritative committee is required to verify the proof. The sequence of
103/// committees can be verified through a Committee proof target on the last
104/// checkpoint of each epoch, sequentially since the first epoch.
105pub fn verify_proof(committee: &Committee, proof: &Proof) -> anyhow::Result<()> {
106 // Get checkpoint summary and optional contents
107 let summary = &proof.checkpoint_summary;
108 let contents_ref = proof
109 .contents_proof
110 .as_ref()
111 .map(|x| &x.checkpoint_contents);
112
113 // Verify the checkpoint summary using the committee
114 summary.verify_with_contents(committee, contents_ref)?;
115
116 // MILESTONE 1 : summary and contents is correct
117 // Note: this is unconditional on the proof targets, and always checked.
118
119 // If the proof target is the next committee check it
120 if let Some(committee) = &proof.targets.committee {
121 match &summary.end_of_epoch_data {
122 Some(EndOfEpochData {
123 next_epoch_committee,
124 ..
125 }) => {
126 // Extract the end of epoch committee
127 let next_committee_data = next_epoch_committee.iter().cloned().collect();
128 let new_committee =
129 Committee::new(summary.epoch().checked_add(1).unwrap(), next_committee_data);
130
131 if new_committee != *committee {
132 return Err(anyhow!(
133 "Given committee does not match the end of epoch committee"
134 ));
135 }
136 }
137 None => {
138 return Err(anyhow!(
139 "No end of epoch committee in the checkpoint summary"
140 ));
141 }
142 }
143 }
144
145 // MILESTONE 2: committee if requested is correct
146
147 // Non empty object or event targets require the optional contents proof
148 // If it is not present return an error
149
150 if (!proof.targets.objects.is_empty() || !proof.targets.events.is_empty())
151 && proof.contents_proof.is_none()
152 {
153 return Err(anyhow!("Contents proof is missing"));
154 }
155
156 // MILESTONE 3: contents proof is present if required
157
158 if let Some(contents_proof) = &proof.contents_proof {
159 // Extract Transaction Digests and check they are in contents
160 let digests = contents_proof.effects.execution_digests();
161 if contents_proof.transaction.digest() != &digests.transaction {
162 return Err(anyhow!(
163 "Transaction digest does not match the execution digest"
164 ));
165 }
166
167 // Ensure the digests are in the checkpoint contents
168 if !contents_proof
169 .checkpoint_contents
170 .enumerate_transactions(summary)
171 .any(|x| x.1 == &digests)
172 {
173 // Could not find the digest in the checkpoint contents
174 return Err(anyhow!(
175 "Transaction digest not found in the checkpoint contents"
176 ));
177 }
178
179 // MILESTONE 4: Transaction & Effect correct and in contents
180
181 if contents_proof.effects.events_digest()
182 != contents_proof.events.as_ref().map(|e| e.digest()).as_ref()
183 {
184 return Err(anyhow!("Events digest does not match the execution digest"));
185 }
186
187 // If the target includes any events ensure the events digest is not None
188 if !proof.targets.events.is_empty() && contents_proof.events.is_none() {
189 return Err(anyhow!("Events digest is missing"));
190 }
191
192 // MILESTONE 5: Events digest & Events are correct and present if required
193
194 // Now we verify the content of any target events
195
196 for (event_id, event) in &proof.targets.events {
197 // Check the event corresponds to the transaction
198 if event_id.tx_digest != digests.transaction {
199 return Err(anyhow!("Event does not belong to the transaction"));
200 }
201
202 // The sequence number must be a valid index
203 // Note: safe to unwrap as we have checked that its not None above
204 if event_id.event_seq as usize >= contents_proof.events.as_ref().unwrap().data.len() {
205 return Err(anyhow!("Event sequence number out of bounds"));
206 }
207
208 // Now check that the contents of the event are the same
209 if &contents_proof.events.as_ref().unwrap().data[event_id.event_seq as usize] != event {
210 return Err(anyhow!("Event contents do not match"));
211 }
212 }
213
214 // MILESTONE 6: Event contents are correct
215
216 // Now check all object references are correct and in the effects
217 let changed_objects = contents_proof.effects.all_changed_objects();
218
219 for (object_ref, object) in &proof.targets.objects {
220 // Is the given reference correct?
221 if object_ref != &object.compute_object_reference() {
222 return Err(anyhow!("Object reference does not match the object"));
223 }
224
225 // Has this object been created in these effects?
226 changed_objects
227 .iter()
228 .find(|effects_object_ref| &effects_object_ref.0 == object_ref)
229 .ok_or(anyhow!("Object not found"))?;
230 }
231
232 // MILESTONE 7: Object references are correct and in the effects
233 }
234
235 Ok(())
236}