Skip to main content

uts_core/codec/v1/
attestation.rs

1//! # Attestations
2//!
3//! An attestation is a claim that some data existed at some time. It
4//! comes from some server or from a blockchain.
5
6use crate::{
7    alloc::{Allocator, Global, vec::Vec},
8    codec::{Decode, DecodeIn, Decoder, Encode, Encoder, v1::MayHaveInput},
9    error::{DecodeError, EncodeError},
10    utils::Hexed,
11};
12use alloy_chains::Chain;
13use alloy_primitives::{B256, FixedBytes, fixed_bytes as tag};
14use core::fmt;
15use std::{borrow::Cow, sync::OnceLock};
16
17/// Size in bytes of the tag identifying the attestation type.
18const TAG_SIZE: usize = 8;
19
20/// Tag identifying the attestation kind.
21pub type AttestationTag = FixedBytes<TAG_SIZE>;
22
23/// Tag indicating a Bitcoin attestation.
24static BITCOIN_TAG: AttestationTag = tag!("0x0588960d73d71901");
25
26/// Tag indicating a pending attestation.
27static PENDING_TAG: AttestationTag = tag!("0x83dfe30d2ef90c8e");
28
29/// Tag indicating an EAS attestation.
30///
31/// The attestation emits an event with signature `Attested(address,address,bytes32,bytes32)`, and
32/// `selector = 0x8bf46bf4cfd674fa735a3d63ec1c9ad4153f033c290341f3a588b75685141b35`.
33///
34/// `TAG = selector[..8]`
35static EAS_ATTEST_TAG: AttestationTag = tag!("0x8bf46bf4cfd674fa");
36
37/// Tag indicating an EAS Timestamp Log attestation.
38///
39/// The attestation emits an event with signature `Timestamped(bytes32,uint64)`, and
40/// `selector = 0x5aafceeb1c7ad58e4a84898bdee37c02c0fc46e7d24e6b60e8209449f183459f`.
41///
42/// `TAG = selector[..8]`
43static EAS_TIMESTAMP_TAG: AttestationTag = tag!("0x5aafceeb1c7ad58e");
44
45/// Raw Proof that some data existed at a given time.
46#[derive(Clone)]
47pub struct RawAttestation<A: Allocator = Global> {
48    pub tag: AttestationTag,
49    pub data: Vec<u8, A>,
50    /// Cached value for verifying the attestation.
51    pub(crate) value: OnceLock<Vec<u8, A>>,
52}
53
54impl<A: Allocator> fmt::Debug for RawAttestation<A> {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        f.debug_struct("RawAttestation")
57            .field("tag", &Hexed(&self.tag))
58            .field("data", &Hexed(&self.data))
59            .finish()
60    }
61}
62
63impl<A: Allocator> DecodeIn<A> for RawAttestation<A> {
64    fn decode_in(decoder: &mut impl Decoder, alloc: A) -> Result<Self, DecodeError> {
65        let tag = decoder.decode()?;
66
67        let len = decoder.decode()?;
68        let mut data = Vec::with_capacity_in(len, alloc);
69        data.resize(len, 0);
70        decoder.read_exact(&mut data)?;
71
72        Ok(RawAttestation {
73            tag,
74            data,
75            value: OnceLock::new(),
76        })
77    }
78}
79
80impl<A: Allocator> Encode for RawAttestation<A> {
81    #[inline]
82    fn encode(&self, encoder: &mut impl Encoder) -> Result<(), EncodeError> {
83        encoder.write_all(self.tag)?;
84        encoder.encode_bytes(&self.data)
85    }
86}
87
88impl<A: Allocator> PartialEq for RawAttestation<A> {
89    fn eq(&self, other: &Self) -> bool {
90        self.tag == other.tag && self.data.as_slice() == other.data.as_slice()
91    }
92}
93
94impl<A: Allocator> Eq for RawAttestation<A> {}
95
96impl<A: Allocator> RawAttestation<A> {
97    /// Returns the allocator used by this raw attestation.
98    #[inline]
99    pub fn allocator(&self) -> &A {
100        self.data.allocator()
101    }
102
103    /// Returns the cached value for verifying the attestation, if it exists.
104    #[inline]
105    pub fn value(&self) -> Option<&[u8]> {
106        self.value.get().map(|v| v.as_slice())
107    }
108}
109
110pub trait Attestation<'a>: Sized {
111    const TAG: AttestationTag;
112
113    fn from_raw<A: Allocator>(raw: &'a RawAttestation<A>) -> Result<Self, DecodeError> {
114        if raw.tag != Self::TAG {
115            return Err(DecodeError::BadAttestationTag);
116        }
117
118        Self::from_raw_data(&raw.data)
119    }
120
121    fn to_raw(&self) -> Result<RawAttestation, EncodeError> {
122        self.to_raw_in(Global)
123    }
124
125    fn to_raw_in<A: Allocator>(&self, alloc: A) -> Result<RawAttestation<A>, EncodeError> {
126        Ok(RawAttestation {
127            tag: Self::TAG,
128            data: self.to_raw_data_in(alloc)?,
129            value: OnceLock::new(),
130        })
131    }
132
133    fn from_raw_data(data: &'a [u8]) -> Result<Self, DecodeError>;
134    fn to_raw_data_in<A: Allocator>(&self, alloc: A) -> Result<Vec<u8, A>, EncodeError>;
135}
136
137#[derive(Debug, Clone, PartialEq, Eq)]
138pub struct BitcoinAttestation {
139    pub height: u32,
140}
141
142impl fmt::Display for BitcoinAttestation {
143    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
144        write!(f, "Bitcoin at height {}", self.height)
145    }
146}
147
148impl Attestation<'_> for BitcoinAttestation {
149    const TAG: AttestationTag = BITCOIN_TAG;
150
151    fn from_raw_data(data: &[u8]) -> Result<Self, DecodeError> {
152        let height = u32::decode(&mut &*data)?;
153        Ok(BitcoinAttestation { height })
154    }
155
156    fn to_raw_data_in<A: Allocator>(&self, alloc: A) -> Result<Vec<u8, A>, EncodeError> {
157        let mut buffer = Vec::with_capacity_in(u32::BITS.div_ceil(7) as usize, alloc);
158        buffer.encode(self.height)?;
159        Ok(buffer)
160    }
161}
162
163/// Attestation by `EAS::attest` using schema `0x5c5b8b295ff43c8e442be11d569e94a4cd5476f5e23df0f71bdd408df6b9649c`.
164#[derive(Debug, Clone, PartialEq, Eq)]
165pub struct EASAttestation {
166    pub chain: Chain,
167    pub uid: B256,
168}
169
170impl fmt::Display for EASAttestation {
171    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
172        write!(f, "EAS attestation {} on {}", self.uid, self.chain)
173    }
174}
175
176impl<'a> Attestation<'a> for EASAttestation {
177    const TAG: AttestationTag = EAS_ATTEST_TAG;
178
179    fn from_raw_data(data: &'a [u8]) -> Result<Self, DecodeError> {
180        let data = &mut &data[..];
181        let chain_id = u64::decode(data)?;
182        let uid = B256::decode(data)?;
183        Ok(EASAttestation {
184            chain: Chain::from_id(chain_id),
185            uid,
186        })
187    }
188
189    fn to_raw_data_in<A: Allocator>(&self, alloc: A) -> Result<Vec<u8, A>, EncodeError> {
190        let mut buffer =
191            Vec::with_capacity_in(u64::BITS.div_ceil(7) as usize + size_of::<B256>(), alloc);
192        buffer.encode(self.chain.id())?;
193        buffer.encode(self.uid)?;
194        Ok(buffer)
195    }
196}
197
198/// Attestation by `EAS::timestamp`
199#[derive(Debug, Clone, PartialEq, Eq)]
200pub struct EASTimestamped {
201    pub chain: Chain,
202}
203
204impl fmt::Display for EASTimestamped {
205    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
206        write!(f, "EAS timestamped on {}", self.chain)
207    }
208}
209
210impl<'a> Attestation<'a> for EASTimestamped {
211    const TAG: AttestationTag = EAS_TIMESTAMP_TAG;
212
213    fn from_raw_data(data: &'a [u8]) -> Result<Self, DecodeError> {
214        let chain_id = u64::decode(&mut &data[..])?;
215        Ok(EASTimestamped {
216            chain: Chain::from_id(chain_id),
217        })
218    }
219
220    fn to_raw_data_in<A: Allocator>(&self, alloc: A) -> Result<Vec<u8, A>, EncodeError> {
221        let mut buffer = Vec::with_capacity_in(u64::BITS.div_ceil(7) as usize, alloc);
222        buffer.encode(self.chain.id())?;
223        Ok(buffer)
224    }
225}
226
227#[derive(Debug, Clone, PartialEq, Eq)]
228pub struct PendingAttestation<'a> {
229    pub uri: Cow<'a, str>,
230}
231
232impl PendingAttestation<'_> {
233    /// Maximum length of a URI in a "pending" attestation.
234    pub const MAX_URI_LEN: usize = 1000;
235
236    #[inline]
237    pub fn validate_uri(uri: &str) -> bool {
238        uri.chars()
239            .all(|ch| matches!(ch, 'a'..='z' | 'A'..='Z' | '0'..='9' | '.' | '-' | '_' | '/' | ':'))
240    }
241}
242
243impl fmt::Display for PendingAttestation<'_> {
244    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
245        write!(f, "Pending at {}", self.uri)
246    }
247}
248
249impl<'a> Attestation<'a> for PendingAttestation<'a> {
250    const TAG: AttestationTag = PENDING_TAG;
251
252    fn from_raw_data(data: &'a [u8]) -> Result<Self, DecodeError> {
253        let data = &mut &data[..];
254        let length = u32::decode(data)? as usize; // length prefix
255        if length > Self::MAX_URI_LEN {
256            return Err(DecodeError::UriTooLong);
257        }
258        if data.len() < length {
259            return Err(DecodeError::UnexpectedEof);
260        }
261        let uri = core::str::from_utf8(&data[..length]).map_err(|_| DecodeError::InvalidUriChar)?;
262        if !Self::validate_uri(uri) {
263            return Err(DecodeError::InvalidUriChar);
264        }
265        Ok(PendingAttestation {
266            uri: Cow::Borrowed(uri),
267        })
268    }
269
270    fn to_raw_data_in<A: Allocator>(&self, alloc: A) -> Result<Vec<u8, A>, EncodeError> {
271        if self.uri.len() > Self::MAX_URI_LEN {
272            return Err(EncodeError::UriTooLong);
273        }
274        if !Self::validate_uri(&self.uri) {
275            return Err(EncodeError::InvalidUriChar);
276        }
277        let mut buffer =
278            Vec::with_capacity_in(self.uri.len() + u32::BITS.div_ceil(7) as usize, alloc);
279        buffer.encode_bytes(self.uri.as_bytes())?;
280        Ok(buffer)
281    }
282}
283
284impl<A: Allocator> fmt::Display for RawAttestation<A> {
285    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
286        match &self.tag {
287            tag if *tag == *PENDING_TAG => {
288                let att = PendingAttestation::from_raw(self).expect("Valid Pending attestation");
289                write!(f, "{}", att)
290            }
291            tag if *tag == *BITCOIN_TAG => {
292                let att = BitcoinAttestation::from_raw(self).expect("Valid Bitcoin attestation");
293                write!(f, "{}", att)
294            }
295            tag if *tag == *EAS_ATTEST_TAG => {
296                let att = EASAttestation::from_raw(self).expect("Valid EAS attestation");
297                write!(f, "{}", att)
298            }
299            tag if *tag == *EAS_TIMESTAMP_TAG => {
300                let att = EASTimestamped::from_raw(self).expect("Valid EAS Timestamp attestation");
301                write!(f, "{}", att)
302            }
303            _ => write!(f, "Unknown Attestation with tag {}", Hexed(&self.tag)),
304        }
305    }
306}
307
308impl<A: Allocator> MayHaveInput for RawAttestation<A> {
309    #[inline]
310    fn input(&self) -> Option<&[u8]> {
311        self.value.get().map(|v| v.as_slice())
312    }
313}