1use std::collections::BTreeMap;
2use std::sync::RwLock;
3
4use borsh::{BorshDeserialize, BorshSerialize};
5use serde::{Deserialize, Serialize};
6use sha3::{Digest, Sha3_256};
7use strum::IntoDiscriminant;
8use strum_macros::{EnumDiscriminants, IntoStaticStr};
9use utoipa::{
10 openapi::{ArrayBuilder, ObjectBuilder, RefOr, Schema},
11 PartialSchema, ToSchema,
12};
13
14use crate::{api::APIRegisterContract, *};
15
16#[derive(
17 Debug, Serialize, Deserialize, Default, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize,
18)]
19pub struct Transaction {
20 pub version: u32,
21 pub transaction_data: TransactionData,
22}
23
24impl Transaction {
25 pub fn metadata(&self, parent_data_proposal_hash: DataProposalHash) -> TransactionMetadata {
26 TransactionMetadata {
27 version: self.version,
28 transaction_kind: self.transaction_data.discriminant(),
29 id: TxId(parent_data_proposal_hash, self.hashed()),
30 }
31 }
32}
33
34impl DataSized for Transaction {
35 fn estimate_size(&self) -> usize {
36 match &self.transaction_data {
37 TransactionData::Blob(tx) => tx.estimate_size(),
38 TransactionData::Proof(tx) => tx.estimate_size(),
39 TransactionData::VerifiedProof(tx) => tx.proof_size,
40 }
41 }
42}
43
44#[derive(Debug, Default, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
45pub struct TransactionMetadata {
46 pub version: u32,
47 pub transaction_kind: TransactionKind,
48 pub id: TxId,
49}
50
51#[derive(
52 EnumDiscriminants,
53 Debug,
54 Serialize,
55 Deserialize,
56 Clone,
57 PartialEq,
58 Eq,
59 BorshSerialize,
60 BorshDeserialize,
61 IntoStaticStr,
62)]
63#[strum_discriminants(derive(Default, BorshSerialize, BorshDeserialize))]
64#[strum_discriminants(name(TransactionKind))]
65pub enum TransactionData {
66 #[strum_discriminants(default)]
67 Blob(BlobTransaction),
68 Proof(ProofTransaction),
69 VerifiedProof(VerifiedProofTransaction),
70}
71
72impl Default for TransactionData {
73 fn default() -> Self {
74 TransactionData::Blob(BlobTransaction::default())
75 }
76}
77
78#[derive(
79 Serialize,
80 Deserialize,
81 ToSchema,
82 Default,
83 PartialEq,
84 Eq,
85 Clone,
86 BorshSerialize,
87 BorshDeserialize,
88)]
89pub struct ProofTransaction {
90 pub contract_name: ContractName,
91 pub program_id: ProgramId,
92 pub verifier: Verifier,
93 pub proof: ProofData,
94}
95
96impl ProofTransaction {
97 pub fn estimate_size(&self) -> usize {
98 borsh::to_vec(self).unwrap_or_default().len()
99 }
100}
101
102#[derive(
103 Default, Serialize, Deserialize, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize,
104)]
105pub struct VerifiedProofTransaction {
106 pub contract_name: ContractName,
107 pub program_id: ProgramId,
108 pub verifier: Verifier,
109 pub proof: Option<ProofData>, pub proof_hash: ProofDataHash,
111 pub proof_size: usize,
112 pub proven_blobs: Vec<BlobProofOutput>,
113 pub is_recursive: bool,
114}
115
116impl std::fmt::Debug for VerifiedProofTransaction {
117 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118 f.debug_struct("VerifiedProofTransaction")
119 .field("contract_name", &self.contract_name)
120 .field("proof_hash", &self.proof_hash)
121 .field("proof_size", &self.proof_size)
122 .field("proof", &"[HIDDEN]")
123 .field(
124 "proof_len",
125 &match &self.proof {
126 Some(v) => v.0.len(),
127 None => 0,
128 },
129 )
130 .field("proven_blobs", &self.proven_blobs)
131 .finish()
132 }
133}
134
135impl std::fmt::Debug for ProofTransaction {
136 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
137 f.debug_struct("ProofTransaction")
138 .field("contract_name", &self.contract_name)
139 .field("proof", &"[HIDDEN]")
140 .field("proof_len", &self.proof.0.len())
141 .finish()
142 }
143}
144
145impl Transaction {
146 pub fn wrap(data: TransactionData) -> Self {
147 Transaction {
148 version: 1,
149 transaction_data: data,
150 }
151 }
152}
153
154impl From<TransactionData> for Transaction {
155 fn from(data: TransactionData) -> Self {
156 Transaction::wrap(data)
157 }
158}
159
160impl From<BlobTransaction> for Transaction {
161 fn from(tx: BlobTransaction) -> Self {
162 Transaction::wrap(TransactionData::Blob(tx))
163 }
164}
165
166impl From<ProofTransaction> for Transaction {
167 fn from(tx: ProofTransaction) -> Self {
168 Transaction::wrap(TransactionData::Proof(tx))
169 }
170}
171
172impl From<VerifiedProofTransaction> for Transaction {
173 fn from(tx: VerifiedProofTransaction) -> Self {
174 Transaction::wrap(TransactionData::VerifiedProof(tx))
175 }
176}
177
178impl Hashed<TxHash> for Transaction {
179 fn hashed(&self) -> TxHash {
180 match &self.transaction_data {
181 TransactionData::Blob(tx) => tx.hashed(),
182 TransactionData::Proof(tx) => tx.hashed(),
183 TransactionData::VerifiedProof(tx) => tx.hashed(),
184 }
185 }
186}
187
188impl Hashed<TxHash> for ProofTransaction {
189 fn hashed(&self) -> TxHash {
190 let mut hasher = Sha3_256::new();
191 hasher.update(self.contract_name.0.as_bytes());
192 hasher.update(self.program_id.0.clone());
193 hasher.update(self.verifier.0.as_bytes());
194 hasher.update(self.proof.hashed().0);
195 let hash_bytes = hasher.finalize();
196 TxHash(hash_bytes.to_vec())
197 }
198}
199impl Hashed<TxHash> for VerifiedProofTransaction {
200 fn hashed(&self) -> TxHash {
201 let mut hasher = Sha3_256::new();
202 hasher.update(self.contract_name.0.as_bytes());
203 hasher.update(self.program_id.0.clone());
204 hasher.update(self.verifier.0.as_bytes());
205 hasher.update(&self.proof_hash.0);
206 let hash_bytes = hasher.finalize();
207 TxHash(hash_bytes.to_vec())
208 }
209}
210
211#[derive(Serialize, Deserialize, Default, BorshSerialize, BorshDeserialize)]
212#[readonly::make]
213pub struct BlobTransaction {
214 pub identity: Identity,
215 pub blobs: Vec<Blob>,
216 #[borsh(skip)]
218 #[serde(skip_serializing, skip_deserializing)]
219 hash_cache: RwLock<Option<TxHash>>,
220 #[borsh(skip)]
221 #[serde(skip_serializing, skip_deserializing)]
222 blobshash_cache: RwLock<Option<BlobsHashes>>,
223}
224
225impl BlobTransaction {
226 pub fn new(identity: impl Into<Identity>, blobs: Vec<Blob>) -> Self {
227 BlobTransaction {
228 identity: identity.into(),
229 blobs,
230 hash_cache: RwLock::new(None),
231 blobshash_cache: RwLock::new(None),
232 }
233 }
234}
235
236impl std::fmt::Debug for BlobTransaction {
238 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
239 f.debug_struct("BlobTransaction")
240 .field("identity", &self.identity)
241 .field("blobs", &self.blobs)
242 .finish()
243 }
244}
245
246impl PartialSchema for BlobTransaction {
247 fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
248 RefOr::T(Schema::Object(
249 ObjectBuilder::new()
250 .property("identity", Identity::schema())
251 .property("blobs", ArrayBuilder::new().items(Blob::schema()).build())
252 .required("identity")
253 .required("blobs")
254 .build(),
255 ))
256 }
257}
258
259impl ToSchema for BlobTransaction {}
260
261impl Clone for BlobTransaction {
262 fn clone(&self) -> Self {
263 BlobTransaction {
264 identity: self.identity.clone(),
265 blobs: self.blobs.clone(),
266 hash_cache: RwLock::new(self.hash_cache.read().unwrap().clone()),
267 blobshash_cache: RwLock::new(self.blobshash_cache.read().unwrap().clone()),
268 }
269 }
270}
271
272impl PartialEq for BlobTransaction {
273 fn eq(&self, other: &Self) -> bool {
274 self.identity == other.identity && self.blobs == other.blobs
275 }
276}
277
278impl Eq for BlobTransaction {}
279
280impl BlobTransaction {
281 pub fn estimate_size(&self) -> usize {
282 borsh::to_vec(self).unwrap_or_default().len()
283 }
284}
285
286impl Hashed<TxHash> for BlobTransaction {
287 fn hashed(&self) -> TxHash {
288 if let Some(hash) = self.hash_cache.read().unwrap().clone() {
289 return hash;
290 }
291 let mut hasher = Sha3_256::new();
292 hasher.update(self.identity.0.as_bytes());
293 for blob in self.blobs.iter() {
294 hasher.update(blob.hashed().0);
295 }
296 let hash_bytes = hasher.finalize();
297 let tx_hash = TxHash(hash_bytes.to_vec());
298 *self.hash_cache.write().unwrap() = Some(tx_hash.clone());
299 tx_hash
300 }
301}
302
303impl BlobTransaction {
304 pub fn blobs_hash(&self) -> BlobsHashes {
305 if let Some(hash) = self.blobshash_cache.read().unwrap().clone() {
306 return hash;
307 }
308 let hash: BlobsHashes = (&self.blobs).into();
309 self.blobshash_cache.write().unwrap().replace(hash.clone());
310 hash
311 }
312
313 pub fn validate_identity(&self) -> Result<(), anyhow::Error> {
314 let Some((identity, identity_contract_name)) = self.identity.0.rsplit_once("@") else {
316 anyhow::bail!("Transaction identity {} is not correctly formed. It should be in the form <id>@<contract_id_name>", self.identity.0);
317 };
318
319 if identity.is_empty() || identity_contract_name.is_empty() {
320 anyhow::bail!(
321 "Transaction identity {}@{} must not have empty parts",
322 identity,
323 identity_contract_name
324 );
325 }
326
327 if !self
329 .blobs
330 .iter()
331 .any(|blob| blob.contract_name.0 == identity_contract_name)
332 {
333 anyhow::bail!(
334 "Can't find blob that proves the identity on contract '{}'",
335 identity_contract_name
336 );
337 }
338 Ok(())
339 }
340}
341
342impl From<APIRegisterContract> for BlobTransaction {
343 fn from(payload: APIRegisterContract) -> Self {
344 let mut blobs = vec![RegisterContractAction {
345 verifier: payload.verifier,
346 program_id: payload.program_id,
347 state_commitment: payload.state_commitment,
348 contract_name: payload.contract_name.clone(),
349 timeout_window: payload
350 .timeout_window
351 .map(|(a, b)| TimeoutWindow::timeout(BlockHeight(a), BlockHeight(b))),
352 constructor_metadata: payload.constructor_metadata.clone(),
353 }
354 .as_blob("hyli".into())];
355
356 if let Some(constructor_metadata) = &payload.constructor_metadata {
357 blobs.push(Blob {
358 contract_name: payload.contract_name,
359 data: BlobData(constructor_metadata.clone()),
360 });
361 }
362
363 BlobTransaction::new("hyli@hyli", blobs)
364 }
365}
366
367#[derive(
368 Debug,
369 Default,
370 Clone,
371 Serialize,
372 Deserialize,
373 ToSchema,
374 Eq,
375 PartialEq,
376 Hash,
377 BorshSerialize,
378 BorshDeserialize,
379)]
380pub struct BlobsHashes {
381 pub hashes: BTreeMap<BlobIndex, BlobHash>,
382}
383
384impl From<&Vec<Blob>> for BlobsHashes {
385 fn from(iter: &Vec<Blob>) -> Self {
386 BlobsHashes {
387 hashes: iter
388 .iter()
389 .enumerate()
390 .map(|(index, blob)| (BlobIndex(index), blob.hashed()))
391 .collect(),
392 }
393 }
394}
395
396impl From<&IndexedBlobs> for BlobsHashes {
397 fn from(iter: &IndexedBlobs) -> Self {
398 BlobsHashes {
399 hashes: iter
400 .iter()
401 .map(|(index, blob)| (*index, blob.hashed()))
402 .collect(),
403 }
404 }
405}
406
407impl BlobsHashes {
408 pub fn includes_all(&self, other: &BlobsHashes) -> bool {
409 for (index, hash) in other.hashes.iter() {
410 if !self
411 .hashes
412 .iter()
413 .any(|(other_index, other_hash)| index == other_index && hash == other_hash)
414 {
415 return false;
416 }
417 }
418 true
419 }
420}
421
422impl std::fmt::Display for BlobsHashes {
423 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
424 for (BlobIndex(index), hash) in self.hashes.iter() {
425 write!(f, "[{index}]: {hash}")?;
426 }
427 Ok(())
428 }
429}