Skip to main content

poi_rs/
proof.rs

1// Copyright 2020-2026 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4//! Proof types and verification.
5//!
6//! A [`Proof`] contains a certified checkpoint and the data needed to prove that
7//! a transaction, and optionally its objects or events, belong to that
8//! checkpoint. [`ProofVerifier`] verifies this data against a caller-provided
9//! [`Committee`] without making network requests.
10//!
11//! [`CertifiedCheckpointSummary`]: iota_types::messages_checkpoint::CertifiedCheckpointSummary
12//! [`Committee`]: iota_types::committee::Committee
13
14use iota_sdk_types::{CheckpointContents, Transaction as TransactionData, TransactionDigest};
15use iota_types::committee::Committee;
16use iota_types::digests::ChainIdentifier;
17use iota_types::effects::{TransactionEffects, TransactionEffectsAPI, TransactionEffectsExt, TransactionEvents};
18use iota_types::event::EventID;
19use iota_types::messages_checkpoint::CertifiedCheckpointSummary;
20use iota_types::object::Object;
21use iota_types::transaction::Transaction;
22use serde::{Deserialize, Serialize};
23
24use crate::BoxError;
25
26/// An error serializing or deserializing a proof.
27#[derive(Debug, thiserror::Error)]
28#[non_exhaustive]
29#[error("failed to serialize or deserialize Proof of Inclusion proof")]
30pub struct SerializationError {
31    /// The underlying error.
32    #[source]
33    pub kind: SerializationErrorKind,
34}
35
36/// The cause of a [`SerializationError`].
37#[derive(Debug, thiserror::Error)]
38#[non_exhaustive]
39pub enum SerializationErrorKind {
40    /// JSON encoding or decoding failed.
41    #[error("json serialization or deserialization failed")]
42    Json {
43        /// Error reported by `serde_json`.
44        #[source]
45        source: serde_json::Error,
46    },
47}
48
49/// An error verifying a proof.
50#[derive(Debug, thiserror::Error)]
51#[non_exhaustive]
52#[error("failed to verify Proof of Inclusion proof")]
53pub struct VerifyError {
54    /// The reason verification failed.
55    #[source]
56    pub kind: VerifyErrorKind,
57}
58
59/// The cause of a [`VerifyError`].
60#[derive(Debug, thiserror::Error)]
61#[non_exhaustive]
62pub enum VerifyErrorKind {
63    /// The committee signature or checkpoint-contents commitment is invalid.
64    #[error("checkpoint summary verification failed")]
65    CheckpointSummary {
66        /// The checkpoint verification error.
67        #[source]
68        source: BoxError,
69    },
70    /// The proof does not declare a transaction, object, or event target.
71    #[error("proof does not contain a target")]
72    MissingTarget,
73    /// The selected transaction differs from the transaction packaged in the proof.
74    #[error("transaction target does not match the packaged transaction")]
75    TransactionTargetMismatch,
76    /// The packaged transaction does not match the transaction digest in its effects.
77    #[error("transaction digest does not match the execution digest")]
78    TransactionDigestMismatch,
79    /// The packaged transaction effects are absent from the authenticated checkpoint contents.
80    #[error("transaction digest not found in the checkpoint contents")]
81    TransactionNotInCheckpoint,
82    /// The packaged user signatures differ from those committed by the checkpoint.
83    #[error("transaction signatures do not match the checkpoint contents")]
84    TransactionSignaturesMismatch,
85    /// The presence or digest of packaged events does not match the transaction effects.
86    #[error("events digest does not match the execution digest")]
87    EventsDigestMismatch,
88    /// Event targets are present but the proof does not contain transaction events.
89    #[error("event targets require transaction event data")]
90    MissingEvents,
91    /// An event target identifies a transaction other than the one proven by the envelope.
92    #[error("event target does not belong to the transaction")]
93    EventTransactionMismatch,
94    /// An event target refers to an index outside the packaged transaction events.
95    #[error("event sequence number {sequence} is out of bounds")]
96    EventSequenceOutOfBounds {
97        /// Transaction-local event index selected by the target.
98        sequence: u64,
99    },
100    /// A claimed object reference is absent from the packaged transaction effects.
101    #[error("object target was not found in the transaction effects")]
102    ObjectNotFound,
103}
104
105/// Values the caller selected for a [`Proof`].
106///
107/// Objects and events must belong to the proven transaction.
108#[derive(Default, Debug, Serialize, Deserialize, Clone)]
109pub struct ProofTargets {
110    /// Transaction explicitly selected by the caller.
111    pub transaction: Option<TransactionDigest>,
112
113    /// Objects explicitly selected by the caller.
114    pub objects: Vec<Object>,
115
116    /// Events explicitly selected by the caller.
117    pub events: Vec<EventID>,
118}
119
120impl ProofTargets {
121    /// Creates an empty set of targets.
122    pub fn new() -> Self {
123        Self::default()
124    }
125
126    /// Sets the selected transaction.
127    pub fn set_transaction(mut self, transaction: TransactionDigest) -> Self {
128        self.transaction = Some(transaction);
129        self
130    }
131
132    /// Adds a selected object.
133    pub fn add_object(mut self, object: Object) -> Self {
134        self.objects.push(object);
135        self
136    }
137
138    /// Adds a selected event.
139    pub fn add_event(mut self, event_id: EventID) -> Self {
140        self.events.push(event_id);
141        self
142    }
143
144    /// Returns whether no target has been selected.
145    pub fn is_empty(&self) -> bool {
146        self.transaction.is_none() && self.objects.is_empty() && self.events.is_empty()
147    }
148}
149
150/// Transaction-specific evidence carried by a [`Proof`].
151///
152/// The effects identify the transaction in its checkpoint. Proof construction
153/// includes the transaction's complete event list, regardless of event targets.
154#[derive(Clone, Debug, Serialize, Deserialize)]
155pub struct TransactionProof {
156    /// The transaction being proven.
157    pub transaction: Transaction,
158    /// The transaction's execution effects.
159    pub effects: TransactionEffects,
160    /// Complete event list returned by the source, when present.
161    pub events: Option<TransactionEvents>,
162}
163
164impl TransactionProof {
165    /// Creates transaction proof data.
166    pub fn new(
167        transaction: Transaction,
168        effects: TransactionEffects,
169        events: impl Into<Option<TransactionEvents>>,
170    ) -> Self {
171        Self {
172            transaction,
173            effects,
174            events: events.into(),
175        }
176    }
177}
178
179/// Authenticated transaction evidence and targets borrowed from a successfully verified proof.
180///
181/// Values are exposed through this type only after all checkpoint, transaction,
182/// object, and event checks have succeeded. The original [`Proof`] remains
183/// available for serialization and inspection, but its contents must not be
184/// treated as authenticated without a corresponding `VerifiedProof`.
185#[derive(Debug)]
186#[must_use = "read authenticated targets from the returned VerifiedProof"]
187pub struct VerifiedProof<'proof> {
188    checkpoint_summary: &'proof CertifiedCheckpointSummary,
189    transaction: &'proof TransactionData,
190    transaction_digest: TransactionDigest,
191    targets: &'proof ProofTargets,
192    objects: &'proof [Object],
193    effects: &'proof TransactionEffects,
194    events: Option<&'proof TransactionEvents>,
195}
196
197impl<'proof> VerifiedProof<'proof> {
198    /// Returns the epoch of the authenticated checkpoint.
199    pub fn checkpoint_epoch(&self) -> u64 {
200        self.checkpoint_summary.epoch()
201    }
202
203    /// Returns the authenticated checkpoint sequence number.
204    pub fn checkpoint_sequence_number(&self) -> u64 {
205        self.checkpoint_summary.sequence_number
206    }
207
208    /// Returns the authenticated checkpoint timestamp in milliseconds since the Unix epoch.
209    pub fn checkpoint_timestamp_ms(&self) -> u64 {
210        self.checkpoint_summary.timestamp_ms
211    }
212
213    /// Returns the transaction data included in the authenticated checkpoint.
214    ///
215    /// Verification authenticates the packaged user signatures against the checkpoint
216    /// contents, but this accessor returns only the transaction data.
217    pub const fn transaction(&self) -> &'proof TransactionData {
218        self.transaction
219    }
220
221    /// Returns the digest of the transaction included in the authenticated checkpoint.
222    pub const fn transaction_digest(&self) -> TransactionDigest {
223        self.transaction_digest
224    }
225
226    /// Returns the explicit transaction target, when the proof declared one.
227    pub const fn transaction_target(&self) -> Option<&'proof TransactionDigest> {
228        self.targets.transaction.as_ref()
229    }
230
231    /// Returns the authenticated targets explicitly selected by the caller.
232    pub const fn targets(&self) -> &'proof ProofTargets {
233        self.targets
234    }
235
236    /// Returns the authenticated object targets.
237    pub const fn objects(&self) -> &'proof [Object] {
238        self.objects
239    }
240
241    /// Returns the authenticated execution effects, including execution status and object changes.
242    pub const fn effects(&self) -> &'proof TransactionEffects {
243        self.effects
244    }
245
246    /// Returns the complete authenticated event list in transaction order.
247    ///
248    /// Returns `None` only when the authenticated effects commit to no events.
249    /// Explicitly selected event targets remain available through [`Self::targets`].
250    pub const fn events(&self) -> Option<&'proof TransactionEvents> {
251        self.events
252    }
253}
254
255/// Versioned evidence that a transaction is included in a certified checkpoint.
256///
257/// The enum is non-exhaustive so future crate versions can add support for new
258/// proof formats without making matches in downstream crates source-breaking.
259/// Its serialized representation is externally tagged, with the variant name
260/// identifying the proof format, for example `{ "ProofV1": { ... } }`.
261///
262/// The JSON representation of each variant is a compatibility contract. A
263/// release that supports `ProofV1` must continue to deserialize its existing
264/// shape and serialize the same field structure. Incompatible changes require
265/// a new [`Proof`] variant.
266#[derive(Clone, Debug, Serialize, Deserialize)]
267#[non_exhaustive]
268pub enum Proof {
269    /// A version 1 proof.
270    ProofV1(ProofV1),
271}
272
273/// Version 1 evidence that a transaction is included in a certified checkpoint.
274///
275/// [`ProofTargets`] records the values selected by the caller. The checkpoint
276/// and transaction proof fields contain the evidence for those targets.
277///
278/// [`Proof::chain`] identifies the network reported by the proof source. It is
279/// informational and is not checked during verification.
280#[derive(Clone, Debug, Serialize, Deserialize)]
281pub struct ProofV1 {
282    /// The network reported by the proof source.
283    pub chain: ChainIdentifier,
284    /// The values selected for this proof.
285    pub targets: ProofTargets,
286    /// The certified summary of the checkpoint containing the transaction.
287    pub checkpoint_summary: CertifiedCheckpointSummary,
288    /// Contents committed to by the checkpoint summary.
289    pub checkpoint_contents: CheckpointContents,
290    /// The transaction and its execution data
291    pub transaction_proof: TransactionProof,
292}
293
294impl ProofV1 {
295    const VERSION: u16 = 1;
296
297    /// Creates a version 1 proof payload.
298    pub fn new(
299        chain: ChainIdentifier,
300        targets: ProofTargets,
301        checkpoint_summary: CertifiedCheckpointSummary,
302        checkpoint_contents: CheckpointContents,
303        transaction_proof: TransactionProof,
304    ) -> Self {
305        Self {
306            chain,
307            targets,
308            checkpoint_summary,
309            checkpoint_contents,
310            transaction_proof,
311        }
312    }
313}
314
315impl From<ProofV1> for Proof {
316    fn from(proof: ProofV1) -> Self {
317        Self::ProofV1(proof)
318    }
319}
320
321impl Proof {
322    /// Returns the proof format version.
323    pub const fn version(&self) -> u16 {
324        match self {
325            Self::ProofV1(_) => ProofV1::VERSION,
326        }
327    }
328
329    /// Returns the unverified network reported by the proof source.
330    ///
331    /// This value is informational and is not authenticated by proof verification.
332    pub const fn chain(&self) -> &ChainIdentifier {
333        match self {
334            Self::ProofV1(proof) => &proof.chain,
335        }
336    }
337
338    /// Returns the unverified values declared by this proof.
339    ///
340    /// After verification, read authenticated targets from the returned
341    /// [`VerifiedProof`] instead.
342    pub const fn targets(&self) -> &ProofTargets {
343        match self {
344            Self::ProofV1(proof) => &proof.targets,
345        }
346    }
347
348    /// Returns the checkpoint summary carried by this unverified proof.
349    pub const fn checkpoint_summary(&self) -> &CertifiedCheckpointSummary {
350        match self {
351            Self::ProofV1(proof) => &proof.checkpoint_summary,
352        }
353    }
354
355    /// Returns the checkpoint contents carried by this unverified proof.
356    pub const fn checkpoint_contents(&self) -> &CheckpointContents {
357        match self {
358            Self::ProofV1(proof) => &proof.checkpoint_contents,
359        }
360    }
361
362    /// Returns the transaction-specific evidence carried by this unverified proof.
363    pub const fn transaction_proof(&self) -> &TransactionProof {
364        match self {
365            Self::ProofV1(proof) => &proof.transaction_proof,
366        }
367    }
368
369    /// Serializes the proof as JSON.
370    ///
371    /// # Errors
372    ///
373    /// Returns an error if the proof cannot be serialized.
374    pub fn to_json_vec(&self) -> Result<Vec<u8>, SerializationError> {
375        serde_json::to_vec(self).map_err(|source| SerializationError {
376            kind: SerializationErrorKind::Json { source },
377        })
378    }
379
380    /// Deserializes a proof from JSON.
381    ///
382    /// # Errors
383    ///
384    /// Returns an error if `bytes` do not contain a valid JSON representation of
385    /// a [`Proof`].
386    pub fn from_json_slice(bytes: &[u8]) -> Result<Self, SerializationError> {
387        serde_json::from_slice(bytes).map_err(|source| SerializationError {
388            kind: SerializationErrorKind::Json { source },
389        })
390    }
391}
392
393/// Verifies proofs against a trusted committee.
394///
395/// Verification is offline. The verifier does not resolve committee history or
396/// fetch missing proof data. The caller is responsible for supplying the
397/// committee that certified the proof's checkpoint.
398///
399/// The value of [`Proof::chain`] is not used to select or validate the committee.
400#[derive(Clone, Copy, Debug)]
401pub struct ProofVerifier<'committee> {
402    committee: &'committee Committee,
403}
404
405impl<'committee> ProofVerifier<'committee> {
406    /// Creates a verifier using `committee` as its trust root.
407    pub const fn new(committee: &'committee Committee) -> Self {
408        Self { committee }
409    }
410
411    /// Returns the committee used to verify checkpoint signatures.
412    pub const fn committee(&self) -> &'committee Committee {
413        self.committee
414    }
415
416    /// Verifies a proof and all of its targets.
417    ///
418    /// Verification checks that:
419    ///
420    /// - the committee certifies the checkpoint summary;
421    /// - the checkpoint contents match the digest in that summary;
422    /// - the transaction, effects, and optional events are internally consistent;
423    /// - the transaction effects occur in the authenticated checkpoint contents;
424    /// - the packaged user signatures match those committed by the checkpoint;
425    /// - the event list is present exactly when the effects commit to events, and its digest matches;
426    /// - every selected target matches the authenticated proof data.
427    ///
428    /// On success, returns a [`VerifiedProof`] borrowing the authenticated targets
429    /// from `proof`.
430    ///
431    /// # Errors
432    ///
433    /// Returns an error if any check fails.
434    pub fn verify<'proof>(&self, proof: &'proof Proof) -> Result<VerifiedProof<'proof>, VerifyError> {
435        match proof {
436            Proof::ProofV1(proof) => self.verify_v1(proof),
437        }
438    }
439
440    /// Verifies a version 1 proof and all of its targets.
441    fn verify_v1<'proof>(&self, proof: &'proof ProofV1) -> Result<VerifiedProof<'proof>, VerifyError> {
442        if proof.targets.is_empty() {
443            return Err(VerifyError {
444                kind: VerifyErrorKind::MissingTarget,
445            });
446        }
447
448        let summary = &proof.checkpoint_summary;
449        let contents = Some(&proof.checkpoint_contents);
450
451        summary
452            .verify_with_contents(self.committee, contents)
453            .map_err(|source| VerifyError {
454                kind: VerifyErrorKind::CheckpointSummary {
455                    source: Box::new(source),
456                },
457            })?;
458
459        self.verify_transaction_proof(&proof.checkpoint_contents, &proof.transaction_proof)?;
460        self.verify_targets(&proof.targets, &proof.transaction_proof)?;
461
462        Ok(VerifiedProof {
463            checkpoint_summary: summary,
464            transaction: proof.transaction_proof.transaction.data().transaction(),
465            transaction_digest: *proof.transaction_proof.transaction.digest(),
466            targets: &proof.targets,
467            objects: &proof.targets.objects,
468            effects: &proof.transaction_proof.effects,
469            events: proof.transaction_proof.events.as_ref(),
470        })
471    }
472
473    /// Checks the transaction-to-effects, effects-to-checkpoint, and effects-to-events links.
474    fn verify_transaction_proof(
475        &self,
476        checkpoint_contents: &CheckpointContents,
477        transaction_proof: &TransactionProof,
478    ) -> Result<(), VerifyError> {
479        let execution_digests = transaction_proof.effects.execution_digests();
480
481        if transaction_proof.transaction.digest() != &execution_digests.transaction {
482            return Err(VerifyError {
483                kind: VerifyErrorKind::TransactionDigestMismatch,
484            });
485        }
486
487        let checkpoint_transaction = checkpoint_contents
488            .transactions()
489            .iter()
490            .find(|transaction| {
491                transaction.transaction == execution_digests.transaction
492                    && transaction.effects == execution_digests.effects
493            })
494            .ok_or(VerifyError {
495                kind: VerifyErrorKind::TransactionNotInCheckpoint,
496            })?;
497
498        if checkpoint_transaction.signatures.as_slice() != transaction_proof.transaction.data().signatures() {
499            return Err(VerifyError {
500                kind: VerifyErrorKind::TransactionSignaturesMismatch,
501            });
502        }
503
504        let events_digest = transaction_proof.events.as_ref().map(TransactionEvents::digest);
505        if transaction_proof.effects.events_digest() != events_digest.as_ref() {
506            return Err(VerifyError {
507                kind: VerifyErrorKind::EventsDigestMismatch,
508            });
509        }
510
511        Ok(())
512    }
513
514    /// Checks every declared target against the transaction proof.
515    fn verify_targets(&self, targets: &ProofTargets, transaction_proof: &TransactionProof) -> Result<(), VerifyError> {
516        let transaction_digest = transaction_proof.effects.execution_digests().transaction;
517
518        if targets.transaction.is_some_and(|target| target != transaction_digest) {
519            return Err(VerifyError {
520                kind: VerifyErrorKind::TransactionTargetMismatch,
521            });
522        }
523
524        self.verify_event_targets(targets, transaction_proof)?;
525        self.verify_object_targets(targets, transaction_proof)?;
526
527        Ok(())
528    }
529
530    /// Checks each event target against the proven transaction and its packaged events.
531    fn verify_event_targets(
532        &self,
533        targets: &ProofTargets,
534        transaction_proof: &TransactionProof,
535    ) -> Result<(), VerifyError> {
536        if targets.events.is_empty() {
537            return Ok(());
538        }
539
540        let Some(events) = &transaction_proof.events else {
541            return Err(VerifyError {
542                kind: VerifyErrorKind::MissingEvents,
543            });
544        };
545
546        let execution_digests = transaction_proof.effects.execution_digests();
547        for event_id in &targets.events {
548            if event_id.tx_digest != execution_digests.transaction {
549                return Err(VerifyError {
550                    kind: VerifyErrorKind::EventTransactionMismatch,
551                });
552            }
553
554            let event_exists = usize::try_from(event_id.event_seq)
555                .ok()
556                .is_some_and(|index| events.get(index).is_some());
557            if !event_exists {
558                return Err(VerifyError {
559                    kind: VerifyErrorKind::EventSequenceOutOfBounds {
560                        sequence: event_id.event_seq,
561                    },
562                });
563            }
564        }
565
566        Ok(())
567    }
568
569    /// Checks each object target against the transaction effects.
570    fn verify_object_targets(
571        &self,
572        targets: &ProofTargets,
573        transaction_proof: &TransactionProof,
574    ) -> Result<(), VerifyError> {
575        if targets.objects.is_empty() {
576            return Ok(());
577        }
578
579        let changed_objects = transaction_proof.effects.all_changed_objects();
580        for object in &targets.objects {
581            let object_ref = object.as_inner().object_ref();
582            changed_objects
583                .iter()
584                .find(|changed_object_ref| changed_object_ref.0 == object_ref)
585                .ok_or(VerifyError {
586                    kind: VerifyErrorKind::ObjectNotFound,
587                })?;
588        }
589
590        Ok(())
591    }
592}