Skip to main content

uts_sdk/
verify.rs

1use crate::{Error, Result, Sdk};
2#[cfg(any(feature = "eas-verifier", feature = "bitcoin-verifier"))]
3use backon::RetryableWithContext;
4use digest::Digest;
5use jiff::Timestamp;
6use std::path::Path;
7use tokio::{
8    fs::File,
9    io::{AsyncReadExt, BufReader},
10};
11use uts_core::{
12    alloc,
13    alloc::Allocator,
14    codec::v1::{
15        Attestation, DetachedTimestamp, PendingAttestation, RawAttestation,
16        opcode::{KECCAK256, RIPEMD160, SHA1, SHA256},
17    },
18};
19#[cfg(feature = "eas-verifier")]
20use {
21    alloy_provider::DynProvider,
22    uts_contracts::eas::EAS_ADDRESSES,
23    uts_core::codec::v1::{EASAttestation, EASTimestamped},
24    uts_core::verifier::EASVerifier,
25};
26#[cfg(feature = "bitcoin-verifier")]
27use {uts_core::codec::v1::BitcoinAttestation, uts_core::verifier::BitcoinVerifier};
28
29#[derive(Debug, Copy, Clone, PartialEq, Eq)]
30pub enum AttestationStatusKind {
31    /// The attestation is valid.
32    Valid(Timestamp),
33    /// The attestation is invalid.
34    Invalid,
35    /// The attestation is pending and has not yet been verified.
36    Pending,
37    /// The attestation is unknown, either because it is of an unsupported type or because an error occurred during verification.
38    Unknown,
39}
40
41#[derive(Debug, Clone)]
42pub struct AttestationStatus<A: Allocator = alloc::Global> {
43    pub attestation: RawAttestation<A>,
44    pub status: AttestationStatusKind,
45}
46
47#[derive(Debug, Copy, Clone, PartialEq, Eq)]
48pub enum VerifyStatus {
49    /// The timestamp is valid and all attestations are valid.
50    Valid(Timestamp),
51    /// The timestamp is partially valid, at least one attestation is not valid.
52    PartiallyValid(Timestamp),
53    /// The timestamp is invalid, all attestations are invalid.
54    Invalid,
55    /// All attestations are pending.
56    Pending,
57    /// All attestations are unknown.
58    Unknown,
59}
60
61impl Sdk {
62    /// Verifies the given file against the given detached timestamp, returning a list of attestation statuses.
63    pub async fn verify(
64        &self,
65        file: impl AsRef<Path>,
66        timestamp: &DetachedTimestamp,
67    ) -> Result<Vec<AttestationStatus>> {
68        Ok(Vec::from_iter(
69            self.verify_in(file, timestamp, alloc::Global).await?,
70        ))
71    }
72
73    /// Verifies the given file against the given detached timestamp, returning a list of attestation statuses.
74    ///
75    /// This is the same as `verify`, but allows specifying a custom allocator for the attestation statuses.
76    ///
77    /// # Note
78    ///
79    /// This uses the `allocator_api2` crate for allocator api.
80    pub async fn verify_in<A: Allocator + Clone>(
81        &self,
82        file: impl AsRef<Path>,
83        timestamp: &DetachedTimestamp<A>,
84        allocator: A,
85    ) -> Result<alloc::vec::Vec<AttestationStatus<A>, A>> {
86        let digest_header = timestamp.header();
87        match digest_header.kind().tag() {
88            SHA1 => {
89                self.verify_digest::<sha1::Sha1>(file, digest_header.digest())
90                    .await
91            }
92            RIPEMD160 => {
93                self.verify_digest::<ripemd::Ripemd160>(file, digest_header.digest())
94                    .await
95            }
96            SHA256 => {
97                self.verify_digest::<sha2::Sha256>(file, digest_header.digest())
98                    .await
99            }
100            KECCAK256 => {
101                self.verify_digest::<sha3::Keccak256>(file, digest_header.digest())
102                    .await
103            }
104            _ => return Err(Error::Unsupported("unknown digest algorithm")),
105        }?;
106
107        timestamp.try_finalize()?;
108        let mut result =
109            alloc::vec::Vec::with_capacity_in(timestamp.attestations().count(), allocator);
110        for attestation in timestamp.attestations() {
111            let attestation = attestation.to_owned();
112
113            if attestation.tag == PendingAttestation::TAG {
114                result.push(AttestationStatus {
115                    attestation,
116                    status: AttestationStatusKind::Pending,
117                });
118                continue;
119            }
120
121            let status = self
122                .verify_attestation_inner(&attestation)
123                .await
124                .unwrap_or(AttestationStatusKind::Unknown);
125
126            result.push(AttestationStatus {
127                attestation,
128                status,
129            });
130        }
131
132        Ok(result)
133    }
134
135    /// Verifies the digest of the given file against the expected digest.
136    pub async fn verify_digest<D: Digest>(
137        &self,
138        file: impl AsRef<Path>,
139        expected: &[u8],
140    ) -> Result<()> {
141        let mut file = BufReader::new(File::open(file.as_ref()).await?);
142        let mut hasher = D::new();
143        let mut buffer = [0u8; 64 * 1024]; // 64KB buffer
144        loop {
145            let bytes_read = file.read(&mut buffer).await?;
146            if bytes_read == 0 {
147                break;
148            }
149            hasher.update(&buffer[..bytes_read]);
150        }
151        let actual = hasher.finalize();
152
153        if *actual != *expected {
154            return Err(Error::DigestMismatch {
155                expected: expected.to_vec().into_boxed_slice(),
156                actual: actual.to_vec().into_boxed_slice(),
157            });
158        }
159        Ok(())
160    }
161
162    /// Aggregate the individual attestation statuses into an overall verification status for the timestamp.
163    ///
164    /// The earliest valid attestation timestamp is used as the timestamp for the overall status, if there is at least one valid attestation.
165    ///
166    /// The logic is as follows:
167    /// - If there is at least one VALID attestation:
168    ///  - If there are also INVALID or UNKNOWN attestations, the overall status is PARTIAL_VALID
169    ///  - Otherwise, the overall status is VALID
170    /// - If there are no VALID attestations, but at least one PENDING attestation, the overall status is PENDING
171    /// - If there are no VALID attestations, but at least one UNKNOWN attestation, the overall status is UNKNOWN
172    /// - If there are no VALID or PENDING attestations, the overall status is INVALID
173    pub fn aggregate_verify_results(&self, results: &[AttestationStatus]) -> VerifyStatus {
174        let mut valid_ts = None;
175        let mut has_invalid = false;
176        let mut has_unknown = false;
177        let mut has_pending = false;
178
179        for status in results {
180            match status.status {
181                AttestationStatusKind::Valid(ts) => {
182                    if valid_ts.is_none() || ts < valid_ts.unwrap() {
183                        valid_ts = Some(ts);
184                    }
185                }
186                AttestationStatusKind::Invalid => has_invalid = true,
187                AttestationStatusKind::Unknown => has_unknown = true,
188                AttestationStatusKind::Pending => has_pending = true,
189            }
190        }
191
192        if let Some(ts) = valid_ts {
193            if has_invalid || has_unknown {
194                VerifyStatus::PartiallyValid(ts)
195            } else {
196                VerifyStatus::Valid(ts)
197            }
198        } else if has_pending {
199            VerifyStatus::Pending
200        } else if has_unknown {
201            VerifyStatus::Unknown
202        } else {
203            VerifyStatus::Invalid
204        }
205    }
206
207    async fn verify_attestation_inner<A: Allocator + Clone>(
208        &self,
209        attestation: &RawAttestation<A>,
210    ) -> Result<AttestationStatusKind, Error> {
211        let _expected = attestation
212            .value()
213            .expect("Attestation value should be finalized");
214
215        #[cfg(feature = "eas-verifier")]
216        if attestation.tag == EASAttestation::TAG {
217            let attestation = EASAttestation::from_raw(attestation)?;
218            return self.verify_eas_attestation(_expected, attestation).await;
219        } else if attestation.tag == EASTimestamped::TAG {
220            let attestation = EASTimestamped::from_raw(attestation)?;
221            return self.verify_eas_timestamped(_expected, attestation).await;
222        }
223
224        #[cfg(feature = "bitcoin-verifier")]
225        if attestation.tag == BitcoinAttestation::TAG {
226            let attestation = BitcoinAttestation::from_raw(attestation)?;
227            return self.verify_bitcoin(_expected, attestation).await;
228        }
229
230        Ok(AttestationStatusKind::Unknown)
231    }
232
233    #[cfg(feature = "eas-verifier")]
234    async fn verify_eas_attestation(
235        &self,
236        expected: &[u8],
237        attestation: EASAttestation,
238    ) -> Result<AttestationStatusKind> {
239        let chain = attestation.chain;
240        let provider = self
241            .inner
242            .eth_providers
243            .get(&chain.id())
244            .ok_or_else(|| Error::UnsupportedChain(chain.id()))?;
245        let eas_address = EAS_ADDRESSES
246            .get(&chain.id())
247            .ok_or_else(|| Error::UnsupportedChain(chain.id()))?;
248
249        let (_, result) = {
250            |verifier: EASVerifier<DynProvider>| async {
251                let res = verifier.verify_attestation(&attestation, expected).await;
252                (verifier, res)
253            }
254        }
255        .retry(self.inner.retry)
256        .when(|e| e.should_retry())
257        .context(EASVerifier::new(*eas_address, provider.clone()))
258        .await;
259
260        match result {
261            Ok(result) => {
262                let ts = Timestamp::from_second(result.time.try_into().expect("i64 overflow"))?;
263                Ok(AttestationStatusKind::Valid(ts))
264            }
265            Err(e) if e.is_fatal() => Ok(AttestationStatusKind::Invalid),
266            Err(_) => Ok(AttestationStatusKind::Unknown),
267        }
268    }
269
270    #[cfg(feature = "eas-verifier")]
271    async fn verify_eas_timestamped(
272        &self,
273        expected: &[u8],
274        attestation: EASTimestamped,
275    ) -> Result<AttestationStatusKind> {
276        let chain = attestation.chain;
277        let provider = self
278            .inner
279            .eth_providers
280            .get(&chain.id())
281            .ok_or_else(|| Error::UnsupportedChain(chain.id()))?;
282        let eas_address = EAS_ADDRESSES
283            .get(&chain.id())
284            .ok_or_else(|| Error::UnsupportedChain(chain.id()))?;
285
286        let (_, result) = {
287            |verifier: EASVerifier<DynProvider>| async {
288                let res = verifier.verify_timestamped(&attestation, expected).await;
289                (verifier, res)
290            }
291        }
292        .retry(self.inner.retry)
293        .when(|e| e.should_retry())
294        .context(EASVerifier::new(*eas_address, provider.clone()))
295        .await;
296
297        match result {
298            Ok(time) => {
299                let ts = Timestamp::from_second(time.try_into().expect("i64 overflow"))?;
300                Ok(AttestationStatusKind::Valid(ts))
301            }
302            Err(e) if e.is_fatal() => Ok(AttestationStatusKind::Invalid),
303            Err(_) => Ok(AttestationStatusKind::Unknown),
304        }
305    }
306
307    #[cfg(feature = "bitcoin-verifier")]
308    async fn verify_bitcoin(
309        &self,
310        expected: &[u8],
311        attestation: BitcoinAttestation,
312    ) -> Result<AttestationStatusKind> {
313        let (_, result) = {
314            |verifier: BitcoinVerifier| async {
315                let res = verifier.verify(&attestation, expected).await;
316                (verifier, res)
317            }
318        }
319        .retry(self.inner.retry)
320        .when(|e| e.should_retry())
321        .context(BitcoinVerifier::from_parts(
322            self.inner.http_client.clone(),
323            self.inner.bitcoin_rpc.clone(),
324            self.inner.retry,
325        ))
326        .await;
327
328        match result {
329            Ok(header) => {
330                let ts = Timestamp::from_second(header.time.try_into().expect("i64 overflow"))?;
331                Ok(AttestationStatusKind::Valid(ts))
332            }
333            Err(e) if e.is_fatal() => Ok(AttestationStatusKind::Invalid),
334            Err(_) => Ok(AttestationStatusKind::Unknown),
335        }
336    }
337}