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}