Skip to main content

hyli_model/
transaction.rs

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>, // Kept only on the local lane for indexing purposes
110    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    // FIXME: add a nonce or something to prevent BlobTransaction to share the same hash
217    #[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
236// Custom implem to skip the cached fields
237impl 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        // Checks that there is a blob that proves the identity
315        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        // Check that there is at least one blob that has identity_contract_name as contract name
328        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}