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, bail};
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                    bail!("Given committee does not match the end of epoch committee");
133                }
134            }
135            None => bail!("No end of epoch committee in the checkpoint summary"),
136        }
137    }
138
139    // MILESTONE 2: committee if requested is correct
140
141    // Non empty object or event targets require the optional contents proof
142    // If it is not present return an error
143
144    if (!proof.targets.objects.is_empty() || !proof.targets.events.is_empty())
145        && proof.contents_proof.is_none()
146    {
147        bail!("Contents proof is missing");
148    }
149
150    // MILESTONE 3: contents proof is present if required
151
152    if let Some(contents_proof) = &proof.contents_proof {
153        // Extract Transaction Digests and check they are in contents
154        let digests = contents_proof.effects.execution_digests();
155        if contents_proof.transaction.digest() != &digests.transaction {
156            bail!("Transaction digest does not match the execution digest");
157        }
158
159        // Ensure the digests are in the checkpoint contents
160        if !contents_proof
161            .checkpoint_contents
162            .enumerate_transactions(summary)
163            .any(|x| x.1 == &digests)
164        {
165            // Could not find the digest in the checkpoint contents
166            bail!("Transaction digest not found in the checkpoint contents");
167        }
168
169        // MILESTONE 4: Transaction & Effect correct and in contents
170
171        if contents_proof.effects.events_digest()
172            != contents_proof.events.as_ref().map(|e| e.digest()).as_ref()
173        {
174            bail!("Events digest does not match the execution digest");
175        }
176
177        // If the target includes any events ensure the events digest is not None
178        if !proof.targets.events.is_empty() && contents_proof.events.is_none() {
179            bail!("Events digest is missing");
180        }
181
182        // MILESTONE 5: Events digest & Events are correct and present if required
183
184        // Now we verify the content of any target events
185
186        for (event_id, event) in &proof.targets.events {
187            // Check the event corresponds to the transaction
188            if event_id.tx_digest != digests.transaction {
189                bail!("Event does not belong to the transaction");
190            }
191
192            // The sequence number must be a valid index
193            // Note: safe to unwrap as we have checked that its not None above
194            if event_id.event_seq as usize >= contents_proof.events.as_ref().unwrap().data.len() {
195                bail!("Event sequence number out of bounds");
196            }
197
198            // Now check that the contents of the event are the same
199            if &contents_proof.events.as_ref().unwrap().data[event_id.event_seq as usize] != event {
200                bail!("Event contents do not match");
201            }
202        }
203
204        // MILESTONE 6: Event contents are correct
205
206        // Now check all object references are correct and in the effects
207        let changed_objects = contents_proof.effects.all_changed_objects();
208
209        for (object_ref, object) in &proof.targets.objects {
210            // Is the given reference correct?
211            if object_ref != &object.compute_object_reference() {
212                bail!("Object reference does not match the object");
213            }
214
215            // Has this object been created in these effects?
216            changed_objects
217                .iter()
218                .find(|effects_object_ref| &effects_object_ref.0 == object_ref)
219                .ok_or_else(|| anyhow!("Object not found"))?;
220        }
221
222        // MILESTONE 7: Object references are correct and in the effects
223    }
224
225    Ok(())
226}