Skip to main content

uts_sdk/
stamp.rs

1use crate::{Result, Sdk, error::Error};
2use digest::{Digest, FixedOutputReset, Output};
3use http::Method;
4use std::path::PathBuf;
5use tokio::fs;
6use tracing::{debug, instrument};
7use url::Url;
8use uts_bmt::MerkleTree;
9use uts_core::{
10    alloc,
11    alloc::{Allocator, Global},
12    codec::{
13        DecodeIn,
14        v1::{DetachedTimestamp, DigestHeader, Timestamp, TimestampBuilder, opcode::DigestOpExt},
15    },
16    utils::{HashAsyncFsExt, Hexed},
17};
18
19impl Sdk {
20    /// Creates a timestamp for the given files.
21    pub async fn stamp_files<D>(&self, files: &[PathBuf]) -> Result<Vec<DetachedTimestamp>>
22    where
23        D: Digest + FixedOutputReset + DigestOpExt + Send,
24        Output<D>: Copy,
25    {
26        Ok(Vec::from_iter(
27            self.stamp_files_in::<_, D>(files, Global).await?,
28        ))
29    }
30
31    /// Creates a timestamp for the given digests.
32    pub async fn stamp_digest<D>(&self, digests: &[Output<D>]) -> Result<Vec<DetachedTimestamp>>
33    where
34        D: Digest + FixedOutputReset + DigestOpExt + Send,
35        Output<D>: Copy,
36    {
37        Ok(Vec::from_iter(
38            self.stamp_digests_in::<_, D>(digests, Global).await?,
39        ))
40    }
41
42    /// Creates a timestamp for the given files in the provided allocator.
43    ///
44    /// # Note
45    ///
46    /// This uses the `allocator_api2` crate for allocator api.
47    pub async fn stamp_files_in<A, D>(
48        &self,
49        files: &[PathBuf],
50        allocator: A,
51    ) -> Result<alloc::vec::Vec<DetachedTimestamp<A>, A>>
52    where
53        A: Allocator + Clone,
54        D: Digest + FixedOutputReset + DigestOpExt + Send,
55        Output<D>: Copy,
56    {
57        if files.is_empty() {
58            return Err(Error::EmptyInput);
59        }
60
61        let digests = futures::future::join_all(files.iter().map(|f| hash_file::<D>(f.clone())))
62            .await
63            .into_iter()
64            .collect::<Result<Vec<_>, _>>()?;
65
66        self.stamp_digests_in::<_, D>(&digests, allocator).await
67    }
68
69    /// Creates a timestamp for the given digests in the provided allocator.
70    ///
71    /// # Note
72    ///
73    /// This uses the `allocator_api2` crate for allocator api.
74    pub async fn stamp_digests_in<A, D>(
75        &self,
76        digests: &[Output<D>],
77        allocator: A,
78    ) -> Result<alloc::vec::Vec<DetachedTimestamp<A>, A>>
79    where
80        A: Allocator + Clone,
81        D: Digest + FixedOutputReset + DigestOpExt + Send,
82        Output<D>: Copy,
83    {
84        if digests.is_empty() {
85            return Err(Error::EmptyInput);
86        }
87
88        let mut builders: alloc::vec::Vec<TimestampBuilder<A>, A> = alloc::vec![in allocator.clone(); Timestamp::builder_in(allocator.clone()); digests.len() ];
89
90        let mut nonced_digest = alloc::vec::Vec::with_capacity_in(digests.len(), allocator.clone());
91
92        for (builder, digest) in builders.iter_mut().zip(digests.iter()) {
93            if self.inner.nonce_size == 0 {
94                nonced_digest.push(*digest);
95                continue;
96            }
97
98            let mut hasher = D::new();
99            Digest::update(&mut hasher, digest);
100
101            let mut nonce =
102                alloc::vec::Vec::with_capacity_in(self.inner.nonce_size, allocator.clone());
103            nonce.resize(self.inner.nonce_size, 0);
104            rand::fill(&mut nonce[..]);
105
106            Digest::update(&mut hasher, &nonce);
107            builder.append(nonce).digest::<D>();
108
109            nonced_digest.push(hasher.finalize())
110        }
111
112        let root = if digests.len() > 1 {
113            let internal_tire = MerkleTree::<D>::new(&nonced_digest);
114            let root = internal_tire.root();
115            debug!(internal_tire_root = ?Hexed(root));
116
117            for (builder, leaf) in builders.iter_mut().zip(nonced_digest) {
118                let proof = internal_tire.get_proof_iter(&leaf).expect("infallible");
119                builder.merkle_proof(proof);
120            }
121            *root
122        } else {
123            nonced_digest[0]
124        };
125
126        let stamps_futures = futures::future::join_all(
127            self.inner
128                .calendars
129                .iter()
130                .map(|calendar| self.request_calendar(calendar.clone(), &root, allocator.clone())),
131        )
132        .await
133        .into_iter()
134        .filter_map(|res| res.ok());
135        let mut results =
136            alloc::vec::Vec::with_capacity_in(self.inner.calendars.len(), allocator.clone());
137        for stamp in stamps_futures {
138            results.push(stamp);
139        }
140
141        if results.len() < self.inner.quorum {
142            return Err(Error::QuorumNotReached {
143                required: self.inner.quorum,
144                received: results.len(),
145            });
146        }
147
148        let merged = if results.len() == 1 {
149            results.into_iter().next().unwrap()
150        } else {
151            Timestamp::<A>::merge_in(results, allocator.clone())
152        };
153
154        let mut stamps = alloc::vec::Vec::with_capacity_in(builders.len(), allocator.clone());
155        for (builder, digest) in builders.into_iter().zip(digests.iter()) {
156            let timestamp = builder.concat(merged.clone());
157            let header = DigestHeader::new::<D>(*digest);
158            let timestamp = DetachedTimestamp::from_parts(header, timestamp);
159            stamps.push(timestamp);
160        }
161
162        Ok(stamps)
163    }
164
165    #[instrument(skip(self, allocator), level = "debug", err)]
166    async fn request_calendar<A: Allocator + Clone>(
167        &self,
168        calendar: Url,
169        root: &[u8],
170        allocator: A,
171    ) -> Result<Timestamp<A>> {
172        let url = calendar.join("digest")?;
173
174        let root = root.to_vec();
175        let (_, body) = self
176            .http_request_with_retry(
177                Method::POST,
178                url,
179                10 * 1024, // 10 KB
180                move |req| {
181                    req.header("Accept", "application/vnd.opentimestamps.v1")
182                        .body(root.clone())
183                },
184            )
185            .await?;
186
187        Ok(Timestamp::<A>::decode_in(&mut &*body, allocator)?)
188    }
189}
190
191async fn hash_file<D: DigestOpExt + Send>(path: PathBuf) -> Result<Output<D>> {
192    let mut hasher = D::new();
193    let file = fs::File::open(path).await?;
194    HashAsyncFsExt::update(&mut hasher, file).await?;
195    Ok(hasher.finalize())
196}