Skip to main content

poi_rs/
builder.rs

1// Copyright 2020-2026 IOTA Stiftung
2// SPDX-License-Identifier: Apache-2.0
3
4#[cfg(feature = "native-grpc")]
5use iota_grpc_client::Client as GrpcClient;
6use iota_sdk_types::{ObjectId, ObjectReference, TransactionDigest};
7use iota_types::effects::TransactionEffectsExt;
8use iota_types::event::EventID;
9use iota_types::object::Object;
10
11use crate::{Proof, ProofTargets, ProofV1, Source, SourceError, TransactionProof};
12
13/// Error returned when a proof cannot be constructed by [`ProofBuilder`].
14#[derive(Debug, thiserror::Error)]
15#[non_exhaustive]
16pub enum ProofBuilderError {
17    /// No proof target was selected before building.
18    #[error("proof builder requires a target")]
19    MissingTarget,
20    /// The configured source failed while reading proof evidence.
21    #[error("source failed while reading proof evidence")]
22    Source {
23        /// Underlying source failure.
24        #[source]
25        source: SourceError,
26    },
27    /// The source did not return a requested transaction.
28    #[error("transaction {transaction_digest} was not found")]
29    TransactionNotFound {
30        /// Transaction digest that was not returned.
31        transaction_digest: TransactionDigest,
32    },
33    /// The source did not return the checkpoint containing the transaction.
34    #[error("checkpoint {sequence_number} was not found")]
35    CheckpointNotFound {
36        /// Checkpoint sequence number that was not returned.
37        sequence_number: u64,
38    },
39    /// The source did not return a requested object.
40    #[error("object {object_id} was not found")]
41    ObjectNotFound {
42        /// Object ID that was not returned.
43        object_id: ObjectId,
44    },
45    /// The requested event was not present in its transaction.
46    #[error("event {event_id:?} was not found")]
47    EventNotFound {
48        /// Event ID that was not present.
49        event_id: EventID,
50    },
51    /// The returned object does not match the requested ID or transaction effects.
52    #[error("object {object_id} reference does not match the requested object")]
53    ObjectReferenceMismatch {
54        /// Requested object ID.
55        object_id: ObjectId,
56    },
57    /// The selected transaction did not write a provable value for the requested object.
58    #[error(
59        "transaction {transaction_digest} did not write a provable value for object {object_id}; deleted and wrapped objects are unsupported"
60    )]
61    ObjectNotChangedByTransaction {
62        /// Requested object ID.
63        object_id: ObjectId,
64        /// Transaction selected by the other proof targets.
65        transaction_digest: TransactionDigest,
66    },
67    /// The targets belong to different transactions.
68    #[error("proof targets belong to different transactions: {actual}, expected {expected}")]
69    TransactionMismatch {
70        /// Transaction selected by the first target.
71        expected: TransactionDigest,
72        /// Transaction selected by a conflicting target.
73        actual: TransactionDigest,
74    },
75}
76
77/// Constructs Proof of Inclusion evidence from a caller-provided [`Source`].
78///
79/// The builder keeps proof construction independent of a specific transport.
80/// With the `native-grpc` feature enabled, SDK gRPC clients can be adapted
81/// through `ProofBuilder::from_grpc_client`.
82#[derive(Debug)]
83pub struct ProofBuilder<S> {
84    source: S,
85    transaction_digests: Vec<TransactionDigest>,
86    object_ids: Vec<ObjectId>,
87    event_ids: Vec<EventID>,
88}
89
90#[cfg(feature = "native-grpc")]
91impl ProofBuilder<GrpcClient> {
92    /// Creates a proof builder connected to the public IOTA mainnet gRPC endpoint.
93    ///
94    /// Selecting an endpoint does not establish verification trust. Verify the
95    /// constructed proof with a committee trusted for mainnet.
96    pub fn mainnet() -> iota_grpc_client::Result<Self> {
97        GrpcClient::new_mainnet().map(Self::from_grpc_client)
98    }
99
100    /// Creates a proof builder connected to the public IOTA testnet gRPC endpoint.
101    ///
102    /// Selecting an endpoint does not establish verification trust. Verify the
103    /// constructed proof with a committee trusted for testnet.
104    pub fn testnet() -> iota_grpc_client::Result<Self> {
105        GrpcClient::new_testnet().map(Self::from_grpc_client)
106    }
107
108    /// Creates a proof builder connected to the public IOTA devnet gRPC endpoint.
109    ///
110    /// Selecting an endpoint does not establish verification trust. Verify the
111    /// constructed proof with a committee trusted for devnet.
112    pub fn devnet() -> iota_grpc_client::Result<Self> {
113        GrpcClient::new_devnet().map(Self::from_grpc_client)
114    }
115
116    /// Creates a proof builder backed by an existing SDK gRPC client.
117    pub fn from_grpc_client(client: GrpcClient) -> Self {
118        Self::new(client)
119    }
120}
121
122impl<S: Source> ProofBuilder<S> {
123    /// Creates a proof builder backed by `source`.
124    pub fn new(source: S) -> Self {
125        Self {
126            source,
127            transaction_digests: Vec::new(),
128            object_ids: Vec::new(),
129            event_ids: Vec::new(),
130        }
131    }
132
133    /// Adds a transaction proof target.
134    pub fn transaction(mut self, transaction_digest: TransactionDigest) -> Self {
135        Self::push_unique(&mut self.transaction_digests, transaction_digest);
136        self
137    }
138
139    /// Adds an object proof target by object ID.
140    ///
141    /// Without a transaction or event target, the source resolves the object's
142    /// latest version at proof construction time.
143    pub fn object(mut self, object_id: ObjectId) -> Self {
144        Self::push_unique(&mut self.object_ids, object_id);
145        self
146    }
147
148    /// Adds multiple object proof targets by object ID.
149    ///
150    /// Object resolution follows the build-time semantics of [`Self::object`].
151    pub fn objects(mut self, object_ids: impl IntoIterator<Item = ObjectId>) -> Self {
152        for object_id in object_ids {
153            Self::push_unique(&mut self.object_ids, object_id);
154        }
155        self
156    }
157
158    /// Adds an event proof target.
159    pub fn event(mut self, event_id: EventID) -> Self {
160        Self::push_unique(&mut self.event_ids, event_id);
161        self
162    }
163
164    /// Adds multiple event proof targets.
165    pub fn events(mut self, event_ids: impl IntoIterator<Item = EventID>) -> Self {
166        for event_id in event_ids {
167            Self::push_unique(&mut self.event_ids, event_id);
168        }
169        self
170    }
171
172    /// Builds the requested proof from the configured source.
173    pub async fn build(self) -> Result<Proof, ProofBuilderError> {
174        if self.transaction_digests.is_empty() && self.object_ids.is_empty() && self.event_ids.is_empty() {
175            return Err(ProofBuilderError::MissingTarget);
176        }
177
178        self.build_proof().await
179    }
180
181    async fn build_proof(&self) -> Result<Proof, ProofBuilderError> {
182        let mut selected_transaction = None;
183
184        for transaction_digest in self.transaction_digests.iter().copied() {
185            Self::ensure_same_transaction(&mut selected_transaction, transaction_digest)?;
186        }
187        for event_id in &self.event_ids {
188            Self::ensure_same_transaction(&mut selected_transaction, event_id.tx_digest)?;
189        }
190
191        let (transaction, objects) = if let Some(transaction_digest) = selected_transaction {
192            let transaction = self.fetch_transaction(transaction_digest).await?;
193            let changed_objects = transaction.effects.all_changed_objects();
194            let mut objects = Vec::with_capacity(self.object_ids.len());
195
196            for object_id in self.object_ids.iter().copied() {
197                let object_ref = changed_objects
198                    .iter()
199                    .find_map(|(object_ref, _, _)| (object_ref.object_id == object_id).then_some(*object_ref))
200                    .ok_or(ProofBuilderError::ObjectNotChangedByTransaction {
201                        object_id,
202                        transaction_digest,
203                    })?;
204                objects.push(self.fetch_object(object_id, Some(object_ref)).await?);
205            }
206
207            (transaction, objects)
208        } else {
209            let mut objects = Vec::with_capacity(self.object_ids.len());
210
211            for object_id in self.object_ids.iter().copied() {
212                let object = self.fetch_object(object_id, None).await?;
213                Self::ensure_same_transaction(&mut selected_transaction, object.previous_transaction)?;
214                objects.push(object);
215            }
216
217            let transaction_digest =
218                selected_transaction.expect("ProofBuilder only builds a proof for non-empty targets");
219            let transaction = self.fetch_transaction(transaction_digest).await?;
220
221            (transaction, objects)
222        };
223
224        let chain_identifier = self
225            .source
226            .chain_identifier()
227            .await
228            .map_err(|source| ProofBuilderError::Source { source })?;
229        let checkpoint_sequence_number = transaction.checkpoint_sequence_number;
230        let checkpoint = self
231            .source
232            .checkpoint(checkpoint_sequence_number)
233            .await
234            .map_err(|source| ProofBuilderError::Source { source })?
235            .ok_or(ProofBuilderError::CheckpointNotFound {
236                sequence_number: checkpoint_sequence_number,
237            })?;
238        if !self.event_ids.is_empty() {
239            let events = transaction
240                .events
241                .as_ref()
242                .ok_or_else(|| ProofBuilderError::EventNotFound {
243                    event_id: self.event_ids[0],
244                })?;
245
246            for event_id in &self.event_ids {
247                let event_exists = usize::try_from(event_id.event_seq)
248                    .ok()
249                    .is_some_and(|index| events.get(index).is_some());
250                if !event_exists {
251                    return Err(ProofBuilderError::EventNotFound { event_id: *event_id });
252                }
253            }
254        }
255        let transaction_proof = TransactionProof::new(transaction.transaction, transaction.effects, transaction.events);
256        let mut targets = ProofTargets::new();
257        if let Some(transaction_digest) = self.transaction_digests.first().copied() {
258            targets = targets.set_transaction(transaction_digest);
259        }
260        for object in objects {
261            targets = targets.add_object(object);
262        }
263        for event_id in self.event_ids.iter().copied() {
264            targets = targets.add_event(event_id);
265        }
266
267        Ok(ProofV1::new(
268            chain_identifier,
269            targets,
270            checkpoint.summary,
271            checkpoint.contents,
272            transaction_proof,
273        )
274        .into())
275    }
276
277    async fn fetch_transaction(
278        &self,
279        transaction_digest: TransactionDigest,
280    ) -> Result<crate::SourceTransaction, ProofBuilderError> {
281        self.source
282            .transaction(transaction_digest)
283            .await
284            .map_err(|source| ProofBuilderError::Source { source })?
285            .ok_or(ProofBuilderError::TransactionNotFound { transaction_digest })
286    }
287
288    async fn fetch_object(
289        &self,
290        object_id: ObjectId,
291        expected_ref: Option<ObjectReference>,
292    ) -> Result<Object, ProofBuilderError> {
293        let object = self
294            .source
295            .object(object_id, expected_ref.map(|object_ref| object_ref.version))
296            .await
297            .map_err(|source| ProofBuilderError::Source { source })?
298            .ok_or(ProofBuilderError::ObjectNotFound { object_id })?;
299        let object_ref = object.as_inner().object_ref();
300
301        if object_ref.object_id != object_id || expected_ref.is_some_and(|expected| expected != object_ref) {
302            return Err(ProofBuilderError::ObjectReferenceMismatch { object_id });
303        }
304
305        Ok(object)
306    }
307
308    fn ensure_same_transaction(
309        selected: &mut Option<TransactionDigest>,
310        transaction_digest: TransactionDigest,
311    ) -> Result<(), ProofBuilderError> {
312        if let Some(expected) = selected {
313            if *expected != transaction_digest {
314                return Err(ProofBuilderError::TransactionMismatch {
315                    expected: *expected,
316                    actual: transaction_digest,
317                });
318            }
319        } else {
320            *selected = Some(transaction_digest);
321        }
322
323        Ok(())
324    }
325
326    fn push_unique<T: PartialEq>(values: &mut Vec<T>, value: T) {
327        if !values.contains(&value) {
328            values.push(value);
329        }
330    }
331}