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 Valid(Timestamp),
33 Invalid,
35 Pending,
37 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 Valid(Timestamp),
51 PartiallyValid(Timestamp),
53 Invalid,
55 Pending,
57 Unknown,
59}
60
61impl Sdk {
62 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 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 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]; 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 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}