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 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 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 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 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, 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}