Skip to main content

hyli_model/
contract.rs

1use alloc::{
2    format,
3    string::{String, ToString},
4    vec::Vec,
5};
6use core::{
7    fmt::Display,
8    ops::{Add, Deref, DerefMut, Sub},
9};
10
11use borsh::{BorshDeserialize, BorshSerialize};
12use serde::{Deserialize, Serialize};
13
14use crate::{utils::TimestampMs, LaneId};
15
16#[derive(
17    Serialize,
18    Deserialize,
19    Clone,
20    BorshSerialize,
21    BorshDeserialize,
22    PartialEq,
23    Eq,
24    Default,
25    Ord,
26    PartialOrd,
27)]
28#[cfg_attr(feature = "full", derive(utoipa::ToSchema))]
29pub struct ConsensusProposalHash(#[serde(with = "crate::utils::hex_bytes")] pub Vec<u8>);
30pub type BlockHash = ConsensusProposalHash;
31
32impl core::hash::Hash for ConsensusProposalHash {
33    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
34        state.write(&self.0);
35    }
36}
37
38impl core::fmt::Debug for ConsensusProposalHash {
39    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
40        write!(f, "ConsensusProposalHash({})", hex::encode(&self.0))
41    }
42}
43
44pub trait Hashed<T> {
45    fn hashed(&self) -> T;
46}
47
48pub trait DataSized {
49    fn estimate_size(&self) -> usize;
50}
51
52/// Blob of the transactions the contract uses to validate its transition
53#[derive(
54    Default,
55    Serialize,
56    Deserialize,
57    Debug,
58    Clone,
59    PartialEq,
60    Eq,
61    Hash,
62    BorshSerialize,
63    BorshDeserialize,
64)]
65#[cfg_attr(feature = "full", derive(utoipa::ToSchema))]
66pub struct IndexedBlobs(pub Vec<(BlobIndex, Blob)>);
67
68impl Deref for IndexedBlobs {
69    type Target = Vec<(BlobIndex, Blob)>;
70
71    fn deref(&self) -> &Self::Target {
72        &self.0
73    }
74}
75
76impl DerefMut for IndexedBlobs {
77    fn deref_mut(&mut self) -> &mut Self::Target {
78        &mut self.0
79    }
80}
81
82impl IndexedBlobs {
83    pub fn get(&self, index: &BlobIndex) -> Option<&Blob> {
84        self.0.iter().find(|(i, _)| i == index).map(|(_, b)| b)
85    }
86}
87
88impl<T> From<T> for IndexedBlobs
89where
90    T: IntoIterator<Item = Blob>,
91{
92    fn from(vec: T) -> Self {
93        let mut blobs = IndexedBlobs::default();
94        for (i, blob) in vec.into_iter().enumerate() {
95            blobs.push((BlobIndex(i), blob));
96        }
97        blobs
98    }
99}
100
101impl<'a> IntoIterator for &'a IndexedBlobs {
102    type Item = &'a (BlobIndex, Blob);
103    type IntoIter = core::slice::Iter<'a, (BlobIndex, Blob)>;
104
105    fn into_iter(self) -> Self::IntoIter {
106        self.0.iter()
107    }
108}
109
110/// This struct is passed from the application backend to the contract as an input.
111/// It contains the data that the contract will use to run the blob's action on its state.
112#[derive(Default, Serialize, Deserialize, BorshSerialize, BorshDeserialize, Debug, Clone)]
113pub struct Calldata {
114    /// TxHash of the BlobTransaction being proved
115    pub tx_hash: TxHash,
116    /// User's identity used for the BlobTransaction
117    pub identity: Identity,
118    /// Subset of [Blob]s of the BlobTransaction
119    pub blobs: IndexedBlobs,
120    /// Number of ALL blobs in the transaction. tx_blob_count >= blobs.len()
121    pub tx_blob_count: usize,
122    /// Index of the blob corresponding to the contract.
123    /// The [Blob] referenced by this index has to be parsed by the contract
124    pub index: BlobIndex,
125    /// Optional additional context of the BlobTransaction
126    pub tx_ctx: Option<TxContext>,
127    /// Additional input for the contract that is not written on-chain in the BlobTransaction
128    pub private_input: Vec<u8>,
129}
130
131impl Calldata {
132    pub fn get_blob(&self) -> Result<&Blob, String> {
133        self.blobs
134            .get(&self.index)
135            .ok_or_else(|| format!("Blob with index {} not found in calldata", self.index.0))
136    }
137}
138
139/// State commitment of the contract.
140#[derive(
141    Default, Serialize, Deserialize, Clone, PartialEq, Eq, Hash, BorshSerialize, BorshDeserialize,
142)]
143#[cfg_attr(feature = "full", derive(utoipa::ToSchema))]
144pub struct StateCommitment(pub Vec<u8>);
145
146impl core::fmt::Debug for StateCommitment {
147    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
148        write!(f, "StateCommitment({})", hex::encode(&self.0))
149    }
150}
151
152#[derive(
153    Default,
154    Serialize,
155    Deserialize,
156    Debug,
157    Clone,
158    PartialEq,
159    Eq,
160    Hash,
161    BorshSerialize,
162    BorshDeserialize,
163    Ord,
164    PartialOrd,
165)]
166#[cfg_attr(feature = "full", derive(utoipa::ToSchema))]
167/// An identity is a string that identifies the person that sent
168/// the BlobTransaction
169pub struct Identity(pub String);
170
171#[derive(
172    Default,
173    Serialize,
174    Deserialize,
175    Clone,
176    PartialEq,
177    Eq,
178    Hash,
179    BorshSerialize,
180    BorshDeserialize,
181    Ord,
182    PartialOrd,
183)]
184#[cfg_attr(feature = "full", derive(utoipa::ToSchema))]
185pub struct TxHash(#[serde(with = "crate::utils::hex_bytes")] pub Vec<u8>);
186
187#[derive(
188    Default,
189    Serialize,
190    Deserialize,
191    Debug,
192    Clone,
193    PartialEq,
194    Eq,
195    Hash,
196    BorshDeserialize,
197    BorshSerialize,
198    Copy,
199    PartialOrd,
200    Ord,
201)]
202#[cfg_attr(feature = "full", derive(utoipa::ToSchema))]
203pub struct BlobIndex(pub usize);
204
205impl Add<usize> for BlobIndex {
206    type Output = BlobIndex;
207    fn add(self, other: usize) -> BlobIndex {
208        BlobIndex(self.0 + other)
209    }
210}
211
212#[derive(
213    Default, Serialize, Deserialize, Clone, PartialEq, Eq, Hash, BorshSerialize, BorshDeserialize,
214)]
215#[cfg_attr(feature = "full", derive(utoipa::ToSchema))]
216pub struct BlobData(pub Vec<u8>);
217
218impl core::fmt::Debug for BlobData {
219    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
220        if self.0.len() > 20 {
221            write!(f, "BlobData({}...)", hex::encode(&self.0[..20]))
222        } else {
223            write!(f, "BlobData({})", hex::encode(&self.0))
224        }
225    }
226}
227
228/**
229This struct allows to define cross-contract calls (aka contract composition).
230A contract `A` can "call" an other contract `B` by being it's "caller":
231
232Blob for contract `A` has to be a `StructuredBlobData` with callees vec including the blob index of
233contract `B`.
234
235Blob for contract `B` has to be a `StructuredBlobData` with caller = the blob index of contract
236`A`.
237
238## When to use cross-contract calls ?
239
240When a contract needs to do an operation on an other one. Like transfering funds from
241contract's wallet to the user doing the transaction.
242
243### Example: Bob Swap 2 USDC to 2 USDT
244
245A swap contract can use transactions with 4 blobs:
246
247```text
248┌─ Blob 0
249│  Identity verification for user Bob
250└─────
251┌─ Blob 1 - Contract = "amm"
252│  Swap action
253│  callees = vec![2]
254└─────
255┌─ Blob 2 - Contract = "usdt"
256│  Transfer action of 2 USDT to "Bob"
257│  caller = 1
258└─────
259┌─ Blob 3 - Contract = "usdc"
260│  Transfer action of 2 USDC to "amm"
261└─────
262```
263
264Blob 2 will do various checks on the swap to ensure its validity (correct transfer amounts...)
265
266As Blob 2 has a "caller", the identity used by the contract will be "amm", thus the
267transfer of USDT will be done FROM "amm" TO Bob
268
269And as Blob 3 has no "caller", the identity used by the contract will be the same as the
270transaction identity, i.e: Bob.
271
272
273An alternative way that is more evm-like with an token approve would look like:
274```text
275┌─ Blob 0
276│  Identity verification for user Bob
277└─────
278┌─ Blob 1 - Contract = "usdc"
279│  Approve action of 2 USDC for "amm"
280└─────
281┌─ Blob 2 - Contract = "amm"
282│  Swap action
283│  callees = vec![3, 4]
284└─────
285┌─ Blob 3 - Contract = "usdt"
286│  Transfer action of 2 USDT to "Bob"
287│  caller = 2
288└─────
289┌─ Blob 4 - Contract = "usdc"
290│  TransferFrom action from "Bob" of 2 USDC to "amm"
291│  caller = 2
292└─────
293```
294
295As Blob 4 now has a "caller", the identity used by the contract will be "amm" and not "Bob".
296Note that here we are using a TransferFrom in blob 4, contract "amm" got the approval from Bob
297to initate a transfer on its behalf with blob 1.
298
299You can find an example of this implementation in our [amm contract](https://github.com/hyli-org/hyli/tree/main/crates/contracts/amm/src/lib.rs)
300*/
301#[derive(Debug, BorshSerialize)]
302pub struct StructuredBlobData<Action> {
303    pub caller: Option<BlobIndex>,
304    pub callees: Option<Vec<BlobIndex>>,
305    pub parameters: Action,
306}
307
308/// Struct used to be able to deserialize a StructuredBlobData
309/// without knowing the concrete type of `Action`
310/// warning: this will drop the end of the reader, thus, you can't
311/// deserialize a structure that contains a `StructuredBlobData<DropEndOfReader>`
312/// Unless this struct is at the end of your data structure.
313/// It's not meant to be used outside the sdk internal logic.
314pub struct DropEndOfReader;
315
316impl<Action: BorshDeserialize> BorshDeserialize for StructuredBlobData<Action> {
317    fn deserialize_reader<R: borsh::io::Read>(reader: &mut R) -> borsh::io::Result<Self> {
318        let caller = Option::<BlobIndex>::deserialize_reader(reader)?;
319        let callees = Option::<Vec<BlobIndex>>::deserialize_reader(reader)?;
320        let parameters = Action::deserialize_reader(reader)?;
321        Ok(StructuredBlobData {
322            caller,
323            callees,
324            parameters,
325        })
326    }
327}
328
329impl BorshDeserialize for StructuredBlobData<DropEndOfReader> {
330    fn deserialize_reader<R: borsh::io::Read>(reader: &mut R) -> borsh::io::Result<Self> {
331        let caller = Option::<BlobIndex>::deserialize_reader(reader)?;
332        let callees = Option::<Vec<BlobIndex>>::deserialize_reader(reader)?;
333        // read_to_end is not available in no_std; drain manually
334        let mut buf = [0u8; 64];
335        loop {
336            if reader.read(&mut buf)? == 0 {
337                break;
338            }
339        }
340        let parameters = DropEndOfReader;
341        Ok(StructuredBlobData {
342            caller,
343            callees,
344            parameters,
345        })
346    }
347}
348
349impl<Action: BorshSerialize> From<StructuredBlobData<Action>> for BlobData {
350    fn from(val: StructuredBlobData<Action>) -> Self {
351        BlobData(borsh::to_vec(&val).expect("failed to encode BlobData"))
352    }
353}
354impl<Action: BorshDeserialize> TryFrom<BlobData> for StructuredBlobData<Action> {
355    type Error = borsh::io::Error;
356
357    fn try_from(val: BlobData) -> Result<StructuredBlobData<Action>, Self::Error> {
358        borsh::from_slice(&val.0)
359    }
360}
361impl TryFrom<BlobData> for StructuredBlobData<DropEndOfReader> {
362    type Error = borsh::io::Error;
363
364    fn try_from(val: BlobData) -> Result<StructuredBlobData<DropEndOfReader>, Self::Error> {
365        borsh::from_slice(&val.0)
366    }
367}
368
369#[derive(
370    Debug,
371    Serialize,
372    Deserialize,
373    Default,
374    Clone,
375    PartialEq,
376    Eq,
377    BorshSerialize,
378    BorshDeserialize,
379    Hash,
380)]
381#[cfg_attr(feature = "full", derive(utoipa::ToSchema))]
382/// A Blob is a binary-serialized action that the contract has to parse
383/// An action is often written as an enum representing the call of a specific
384/// contract function.
385pub struct Blob {
386    pub contract_name: ContractName,
387    pub data: BlobData,
388}
389
390#[derive(
391    Default, Clone, Serialize, Deserialize, Eq, PartialEq, Hash, BorshSerialize, BorshDeserialize,
392)]
393#[cfg_attr(feature = "full", derive(utoipa::ToSchema))]
394pub struct BlobHash(#[serde(with = "crate::utils::hex_bytes")] pub Vec<u8>);
395
396#[cfg(feature = "full")]
397impl Hashed<BlobHash> for Blob {
398    fn hashed(&self) -> BlobHash {
399        use sha3::{Digest, Sha3_256};
400
401        let mut hasher = Sha3_256::new();
402        hasher.update(self.contract_name.0.clone());
403        hasher.update(self.data.0.clone());
404        let hash_bytes = hasher.finalize();
405        BlobHash(hash_bytes.to_vec())
406    }
407}
408
409impl core::fmt::Debug for BlobHash {
410    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
411        write!(f, "BlobHash({})", hex::encode(&self.0))
412    }
413}
414
415impl core::fmt::Display for BlobHash {
416    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
417        write!(f, "{}", hex::encode(&self.0))
418    }
419}
420
421#[derive(Debug, BorshSerialize, BorshDeserialize)]
422pub struct StructuredBlob<Action> {
423    pub contract_name: ContractName,
424    pub data: StructuredBlobData<Action>,
425}
426
427impl<Action: BorshSerialize> From<StructuredBlob<Action>> for Blob {
428    fn from(val: StructuredBlob<Action>) -> Self {
429        Blob {
430            contract_name: val.contract_name,
431            data: BlobData::from(val.data),
432        }
433    }
434}
435
436impl<Action: BorshDeserialize> TryFrom<Blob> for StructuredBlob<Action> {
437    type Error = borsh::io::Error;
438
439    fn try_from(val: Blob) -> Result<StructuredBlob<Action>, Self::Error> {
440        let data = borsh::from_slice(&val.data.0)?;
441        Ok(StructuredBlob {
442            contract_name: val.contract_name,
443            data,
444        })
445    }
446}
447
448pub trait ContractAction: Send {
449    fn as_blob(
450        &self,
451        contract_name: ContractName,
452        caller: Option<BlobIndex>,
453        callees: Option<Vec<BlobIndex>>,
454    ) -> Blob;
455}
456
457#[derive(
458    Default,
459    Debug,
460    Clone,
461    Serialize,
462    Deserialize,
463    Eq,
464    PartialEq,
465    Hash,
466    BorshSerialize,
467    BorshDeserialize,
468    Ord,
469    PartialOrd,
470)]
471#[cfg_attr(feature = "full", derive(utoipa::ToSchema))]
472pub struct ContractName(pub String);
473
474impl ContractName {
475    pub fn validate(&self) -> Result<(), String> {
476        if self.0.is_empty()
477            || !self.0.chars().all(|c| {
478                c.is_ascii_lowercase()
479                    || c.is_ascii_digit()
480                    || c == '-'
481                    || c == '_'
482                    || c == '/'
483                    || c == '.'
484            })
485        {
486            return Err("ContractName must be a non-empty string containing only lowercase letters, digits, hyphens, underscores, slashes or dots.".to_string());
487        }
488        Ok(())
489    }
490}
491
492#[derive(
493    Default,
494    Debug,
495    Clone,
496    Serialize,
497    Deserialize,
498    Eq,
499    PartialEq,
500    Hash,
501    BorshSerialize,
502    BorshDeserialize,
503)]
504#[cfg_attr(feature = "full", derive(utoipa::ToSchema))]
505pub struct Verifier(pub String);
506
507#[derive(
508    Default,
509    Debug,
510    Clone,
511    Serialize,
512    Deserialize,
513    Eq,
514    PartialEq,
515    Hash,
516    BorshSerialize,
517    BorshDeserialize,
518)]
519#[cfg_attr(feature = "full", derive(utoipa::ToSchema))]
520pub struct ProgramId(pub Vec<u8>);
521
522impl core::fmt::Display for ProgramId {
523    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
524        write!(f, "{}", hex::encode(&self.0))
525    }
526}
527
528#[derive(Debug, Default, PartialEq, Eq, Clone, BorshSerialize, BorshDeserialize)]
529#[cfg_attr(feature = "full", derive(Serialize, utoipa::ToSchema))]
530pub struct ProofData(#[cfg_attr(feature = "full", serde(with = "base64_field"))] pub Vec<u8>);
531
532#[derive(Debug, Default, PartialEq, Eq, Clone, BorshSerialize, BorshDeserialize)]
533#[cfg_attr(feature = "full", derive(Serialize, utoipa::ToSchema))]
534pub struct ProofMetadata {
535    pub cycles: Option<u64>,
536    pub prover: Option<String>,
537    /// SessionId, TxHash, ... of the proof request on a prover network
538    pub id: Option<String>,
539}
540
541#[derive(Debug, Default, PartialEq, Eq, Clone, BorshSerialize, BorshDeserialize)]
542#[cfg_attr(feature = "full", derive(Serialize, utoipa::ToSchema))]
543pub struct Proof {
544    pub data: ProofData,
545    pub metadata: ProofMetadata,
546}
547
548#[cfg(feature = "full")]
549impl<'de> Deserialize<'de> for ProofData {
550    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
551    where
552        D: serde::Deserializer<'de>,
553    {
554        struct ProofDataVisitor;
555
556        impl<'de> serde::de::Visitor<'de> for ProofDataVisitor {
557            type Value = ProofData;
558
559            fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
560                formatter.write_str("a Base64 string or a Vec<u8>")
561            }
562
563            fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
564            where
565                E: serde::de::Error,
566            {
567                use base64::prelude::*;
568                let decoded = BASE64_STANDARD
569                    .decode(value)
570                    .map_err(serde::de::Error::custom)?;
571                Ok(ProofData(decoded))
572            }
573
574            fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
575            where
576                A: serde::de::SeqAccess<'de>,
577            {
578                let vec_u8: Vec<u8> = serde::de::Deserialize::deserialize(
579                    serde::de::value::SeqAccessDeserializer::new(seq),
580                )?;
581                Ok(ProofData(vec_u8))
582            }
583        }
584
585        deserializer.deserialize_any(ProofDataVisitor)
586    }
587}
588
589#[derive(
590    Default, Serialize, Deserialize, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize,
591)]
592pub struct ProofDataHash(#[serde(with = "crate::utils::hex_bytes")] pub Vec<u8>);
593
594impl From<Vec<u8>> for ProofDataHash {
595    fn from(v: Vec<u8>) -> Self {
596        ProofDataHash(v)
597    }
598}
599impl From<&[u8]> for ProofDataHash {
600    fn from(v: &[u8]) -> Self {
601        ProofDataHash(v.to_vec())
602    }
603}
604impl<const N: usize> From<&[u8; N]> for ProofDataHash {
605    fn from(v: &[u8; N]) -> Self {
606        ProofDataHash(v.to_vec())
607    }
608}
609impl ProofDataHash {
610    pub fn from_hex(s: &str) -> Result<Self, hex::FromHexError> {
611        crate::utils::decode_hex_string_checked(s).map(ProofDataHash)
612    }
613}
614
615impl core::fmt::Debug for ProofDataHash {
616    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
617        write!(f, "ProofDataHash({})", hex::encode(&self.0))
618    }
619}
620
621impl Display for ProofDataHash {
622    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
623        write!(f, "{}", hex::encode(&self.0))
624    }
625}
626
627#[cfg(feature = "full")]
628impl Hashed<ProofDataHash> for ProofData {
629    fn hashed(&self) -> ProofDataHash {
630        use sha3::Digest;
631        let mut hasher = sha3::Sha3_256::new();
632        hasher.update(self.0.as_slice());
633        let hash_bytes = hasher.finalize();
634        ProofDataHash(hash_bytes.to_vec())
635    }
636}
637
638#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
639#[cfg_attr(feature = "full", derive(utoipa::ToSchema))]
640/// Enum for various side-effects blobs can have on the chain.
641/// This is implemented as an enum for easier forward compatibility.
642pub enum OnchainEffect {
643    /// RegisterContractWithConstructor means that we expect the next blob from the contract to be a placeholder containing the constructor_metadata
644    RegisterContractWithConstructor(RegisterContractEffect),
645    /// RegisterContract means that we expect the contract's next blob to be a real blob that will get proven
646    RegisterContract(RegisterContractEffect),
647    DeleteContract(ContractName),
648    UpdateContractProgramId(ContractName, ProgramId),
649    UpdateTimeoutWindow(ContractName, TimeoutWindow),
650}
651
652pub struct OnchainEffectHash(pub Vec<u8>);
653
654#[cfg(feature = "full")]
655impl Hashed<OnchainEffectHash> for OnchainEffect {
656    fn hashed(&self) -> OnchainEffectHash {
657        use sha3::{Digest, Sha3_256};
658        let mut hasher = Sha3_256::new();
659        match self {
660            OnchainEffect::RegisterContractWithConstructor(c) => hasher.update(&c.hashed().0),
661            OnchainEffect::RegisterContract(c) => hasher.update(&c.hashed().0),
662            OnchainEffect::DeleteContract(cn) => hasher.update(cn.0.as_bytes()),
663            OnchainEffect::UpdateContractProgramId(cn, pid) => {
664                hasher.update(cn.0.as_bytes());
665                hasher.update(pid.0.clone());
666            }
667            OnchainEffect::UpdateTimeoutWindow(cn, timeout_window) => {
668                hasher.update(cn.0.as_bytes());
669                match timeout_window {
670                    TimeoutWindow::NoTimeout => hasher.update(0u8.to_le_bytes()),
671                    TimeoutWindow::Timeout {
672                        hard_timeout,
673                        soft_timeout,
674                    } => {
675                        hasher.update(hard_timeout.0.to_le_bytes());
676                        hasher.update(soft_timeout.0.to_le_bytes());
677                    }
678                }
679            }
680        };
681        OnchainEffectHash(hasher.finalize().to_vec())
682    }
683}
684
685/// This struct has to be the zkvm committed output. It will be used by
686/// hyli node to verify & settle the blob transaction.
687#[derive(
688    Default, Serialize, Deserialize, Debug, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize,
689)]
690#[cfg_attr(feature = "full", derive(utoipa::ToSchema))]
691pub struct HyliOutput {
692    /// The version of the HyliOutput. This is unchecked for now.
693    pub version: u32,
694    /// The initial state of the contract. This is the state before the transaction is executed.
695    pub initial_state: StateCommitment,
696    /// The state of the contract after the transaction is executed.
697    pub next_state: StateCommitment,
698    /// The identity used to execute the transaction.
699    /// This must match the one used in the BlobTransaction.
700    pub identity: Identity,
701
702    /// The index of the blob being proven.
703    pub index: BlobIndex,
704    /// The blobs that were used by the contract. It has to be a subset of the transaction blobs.
705    /// It can be the complete list of blobs if the contract used all of them.
706    /// No further semantic checks are enforced by node state: contracts and verifiers are
707    /// responsible for validating that the provided blob subset is sufficient for their logic.
708    pub blobs: IndexedBlobs,
709    /// Number of blobs in the transaction.
710    /// This must match the originating BlobTransaction's total blob count exactly.
711    pub tx_blob_count: usize,
712
713    /// TxHash of the BlobTransaction.
714    pub tx_hash: TxHash, // Technically redundant with identity + blobs hash
715
716    /// Whether the execution was successful or not. If false, the BlobTransaction will be
717    /// settled as failed.
718    pub success: bool,
719
720    /// List of other contracts used by the proof. Each state commitment will be checked
721    /// against the contract's state when settling.
722    pub state_reads: Vec<(ContractName, StateCommitment)>,
723
724    /// Optional - if empty, these won't be checked, but also can't be used inside the program.
725    pub tx_ctx: Option<TxContext>,
726
727    pub onchain_effects: Vec<OnchainEffect>,
728
729    /// Arbitrary data that could be used by indexers or other tools. Some contracts write a utf-8
730    /// string here, but it can be anything.
731    /// Note: As this data is generated by the contract in the zkvm, this can be trusted.
732    pub program_outputs: Vec<u8>,
733}
734
735#[derive(
736    Default,
737    Serialize,
738    Deserialize,
739    Debug,
740    Clone,
741    PartialEq,
742    Eq,
743    Hash,
744    BorshSerialize,
745    BorshDeserialize,
746)]
747#[cfg_attr(feature = "full", derive(utoipa::ToSchema))]
748pub struct TxContext {
749    pub lane_id: LaneId,
750    pub block_hash: BlockHash,
751    pub block_height: BlockHeight,
752    pub timestamp: TimestampMs,
753    #[serde(
754        serialize_with = "serialize_chain_id",
755        deserialize_with = "deserialize_chain_id"
756    )]
757    pub chain_id: u128,
758}
759
760fn serialize_chain_id<S>(chain_id: &u128, serializer: S) -> Result<S::Ok, S::Error>
761where
762    S: serde::Serializer,
763{
764    serializer.serialize_str(&chain_id.to_string())
765}
766
767fn deserialize_chain_id<'de, D>(deserializer: D) -> Result<u128, D::Error>
768where
769    D: serde::Deserializer<'de>,
770{
771    let s: String = serde::Deserialize::deserialize(deserializer)?;
772    s.parse::<u128>().map_err(serde::de::Error::custom)
773}
774
775impl Identity {
776    pub fn new<S: Into<Self>>(s: S) -> Self {
777        s.into()
778    }
779}
780impl<S: Into<String>> From<S> for Identity {
781    fn from(s: S) -> Self {
782        Identity(s.into())
783    }
784}
785
786impl ConsensusProposalHash {
787    pub fn new<S: Into<Self>>(s: S) -> Self {
788        s.into()
789    }
790}
791impl From<Vec<u8>> for ConsensusProposalHash {
792    fn from(v: Vec<u8>) -> Self {
793        ConsensusProposalHash(v)
794    }
795}
796impl From<&[u8]> for ConsensusProposalHash {
797    fn from(v: &[u8]) -> Self {
798        ConsensusProposalHash(v.to_vec())
799    }
800}
801impl<const N: usize> From<&[u8; N]> for ConsensusProposalHash {
802    fn from(v: &[u8; N]) -> Self {
803        ConsensusProposalHash(v.to_vec())
804    }
805}
806impl ConsensusProposalHash {
807    pub fn from_hex(s: &str) -> Result<Self, hex::FromHexError> {
808        crate::utils::decode_hex_string_checked(s).map(ConsensusProposalHash)
809    }
810}
811
812impl TxHash {
813    pub fn new<S: Into<Self>>(s: S) -> Self {
814        s.into()
815    }
816}
817impl From<Vec<u8>> for TxHash {
818    fn from(v: Vec<u8>) -> Self {
819        TxHash(v)
820    }
821}
822impl From<&[u8]> for TxHash {
823    fn from(v: &[u8]) -> Self {
824        TxHash(v.to_vec())
825    }
826}
827impl<const N: usize> From<&[u8; N]> for TxHash {
828    fn from(v: &[u8; N]) -> Self {
829        TxHash(v.to_vec())
830    }
831}
832impl TxHash {
833    pub fn from_hex(s: &str) -> Result<Self, hex::FromHexError> {
834        crate::utils::decode_hex_string_checked(s).map(TxHash)
835    }
836}
837
838impl ContractName {
839    pub fn new<S: Into<Self>>(s: S) -> Self {
840        s.into()
841    }
842}
843impl<S: Into<String>> From<S> for ContractName {
844    fn from(s: S) -> Self {
845        ContractName(s.into())
846    }
847}
848
849impl Verifier {
850    pub fn new<S: Into<Self>>(s: S) -> Self {
851        s.into()
852    }
853}
854impl<S: Into<String>> From<S> for Verifier {
855    fn from(s: S) -> Self {
856        Verifier(s.into())
857    }
858}
859impl From<Vec<u8>> for ProgramId {
860    fn from(v: Vec<u8>) -> Self {
861        ProgramId(v.clone())
862    }
863}
864impl From<&Vec<u8>> for ProgramId {
865    fn from(v: &Vec<u8>) -> Self {
866        ProgramId(v.clone())
867    }
868}
869impl<const N: usize> From<&[u8; N]> for ProgramId {
870    fn from(v: &[u8; N]) -> Self {
871        ProgramId(v.to_vec())
872    }
873}
874impl From<&[u8]> for ProgramId {
875    fn from(v: &[u8]) -> Self {
876        ProgramId(v.to_vec().clone())
877    }
878}
879
880impl Display for TxHash {
881    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
882        write!(f, "{}", hex::encode(&self.0))
883    }
884}
885impl core::fmt::Debug for TxHash {
886    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
887        write!(f, "TxHash({})", hex::encode(&self.0))
888    }
889}
890impl Display for BlobIndex {
891    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
892        write!(f, "{}", &self.0)
893    }
894}
895impl Display for ContractName {
896    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
897        write!(f, "{}", &self.0)
898    }
899}
900impl Display for Identity {
901    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
902        write!(f, "{}", &self.0)
903    }
904}
905impl Display for Verifier {
906    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
907        write!(f, "{}", &self.0)
908    }
909}
910impl From<usize> for BlobIndex {
911    fn from(i: usize) -> Self {
912        BlobIndex(i)
913    }
914}
915
916#[cfg_attr(feature = "full", derive(derive_more::derive::Display))]
917#[derive(
918    Default,
919    Debug,
920    Clone,
921    Serialize,
922    Deserialize,
923    Eq,
924    PartialEq,
925    Hash,
926    Copy,
927    BorshSerialize,
928    BorshDeserialize,
929    PartialOrd,
930    Ord,
931)]
932#[cfg_attr(feature = "full", derive(utoipa::ToSchema))]
933pub struct BlockHeight(pub u64);
934
935impl Add<BlockHeight> for u64 {
936    type Output = BlockHeight;
937    fn add(self, other: BlockHeight) -> BlockHeight {
938        BlockHeight(self + other.0)
939    }
940}
941
942impl Add<u64> for BlockHeight {
943    type Output = BlockHeight;
944    fn add(self, other: u64) -> BlockHeight {
945        BlockHeight(self.0 + other)
946    }
947}
948
949impl Sub<u64> for BlockHeight {
950    type Output = BlockHeight;
951    fn sub(self, other: u64) -> BlockHeight {
952        BlockHeight(self.0 - other)
953    }
954}
955
956impl Add<BlockHeight> for BlockHeight {
957    type Output = BlockHeight;
958    fn add(self, other: BlockHeight) -> BlockHeight {
959        BlockHeight(self.0 + other.0)
960    }
961}
962
963#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize)]
964#[cfg_attr(feature = "full", derive(utoipa::ToSchema))]
965pub enum TimeoutWindow {
966    NoTimeout,
967    Timeout {
968        // The hard_timeout is the timeout value used to set a transaction's timeout when the blobs for the contract are NOT proved.
969        // It is expected that hard_timeout <= soft_timeout.
970        hard_timeout: BlockHeight,
971        // The soft_timeout is the timeout value used when the blobs for the contract ARE proved.
972        // In other words, if blobs are proved, soft_timeout is used; if not proved, hard_timeout is used.
973        soft_timeout: BlockHeight,
974    },
975}
976impl Display for TimeoutWindow {
977    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
978        match &self {
979            TimeoutWindow::NoTimeout => write!(f, "NoTimeout"),
980            TimeoutWindow::Timeout {
981                hard_timeout,
982                soft_timeout,
983            } => write!(f, "Timeout({},{})", hard_timeout.0, soft_timeout.0),
984        }
985    }
986}
987
988impl Default for TimeoutWindow {
989    fn default() -> Self {
990        TimeoutWindow::timeout(BlockHeight(100), BlockHeight(100))
991    }
992}
993
994/// Creates a new timeout window with hard and soft timeout block heights.
995///
996/// This function automatically ensures that `hard_timeout <= soft_timeout` by
997/// swapping the parameters if they are provided in the wrong order. If the
998/// `hard_timeout` is greater than `soft_timeout`, the values will be swapped
999/// internally to maintain the invariant that hard timeout should occur before
1000/// or at the same time as the soft timeout.
1001///
1002/// # Parameters
1003///
1004/// * `hard_timeout` - The block height for the hard timeout (will be the earlier timeout)
1005/// * `soft_timeout` - The block height for the soft timeout (will be the later timeout)
1006///
1007/// # Returns
1008///
1009/// A `TimeoutWindow::Timeout` variant with properly ordered timeout values.
1010impl TimeoutWindow {
1011    pub fn timeout(hard_timeout: BlockHeight, soft_timeout: BlockHeight) -> Self {
1012        let (hard_timeout, soft_timeout) = if hard_timeout <= soft_timeout {
1013            (hard_timeout, soft_timeout)
1014        } else {
1015            (soft_timeout, hard_timeout)
1016        };
1017        TimeoutWindow::Timeout {
1018            hard_timeout,
1019            soft_timeout,
1020        }
1021    }
1022}
1023
1024#[derive(
1025    Debug, Serialize, Deserialize, Default, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize,
1026)]
1027#[cfg_attr(feature = "full", derive(utoipa::ToSchema))]
1028/// Used as a blob action to register a contract.
1029pub struct RegisterContractAction {
1030    /// Verifier to use for transactions sent to this new contract.
1031    pub verifier: Verifier,
1032    /// Other verifier data, such as a Risc0 program ID or noir public key.
1033    /// Transactions sent to this contract will have to match this program ID.
1034    pub program_id: ProgramId,
1035    /// Initial state commitment of the contract to register.
1036    pub state_commitment: StateCommitment,
1037    /// Name of the contract to register.
1038    pub contract_name: ContractName,
1039    /// Optionally set the timeout window for the contract.
1040    /// If the contract exists, the timeout window will be unchanged, otherwise, the default value will be used.
1041    pub timeout_window: Option<TimeoutWindow>,
1042    /// Optional data for indexers to construct the initial state of the contract.
1043    /// Importantly, this is *never* checked by the node. You should verify manually it matches the state commitment.
1044    pub constructor_metadata: Option<Vec<u8>>,
1045}
1046
1047#[cfg(feature = "full")]
1048impl Hashed<TxHash> for RegisterContractAction {
1049    fn hashed(&self) -> TxHash {
1050        use sha3::{Digest, Sha3_256};
1051
1052        let mut hasher = Sha3_256::new();
1053        hasher.update(self.verifier.0.clone());
1054        hasher.update(self.program_id.0.clone());
1055        hasher.update(self.state_commitment.0.clone());
1056        hasher.update(self.contract_name.0.clone());
1057        if let Some(timeout_window) = &self.timeout_window {
1058            match timeout_window {
1059                TimeoutWindow::NoTimeout => hasher.update(0u8.to_le_bytes()),
1060                TimeoutWindow::Timeout {
1061                    hard_timeout,
1062                    soft_timeout,
1063                } => {
1064                    hasher.update(hard_timeout.0.to_le_bytes());
1065                    hasher.update(soft_timeout.0.to_le_bytes());
1066                }
1067            }
1068        }
1069        // We don't hash the constructor metadata.
1070        let hash_bytes = hasher.finalize();
1071        TxHash(hash_bytes.to_vec())
1072    }
1073}
1074
1075impl RegisterContractAction {
1076    pub fn as_blob(&self, contract_name: ContractName) -> Blob {
1077        <Self as ContractAction>::as_blob(self, contract_name, None, None)
1078    }
1079}
1080
1081impl ContractAction for RegisterContractAction {
1082    fn as_blob(
1083        &self,
1084        contract_name: ContractName,
1085        _caller: Option<BlobIndex>,
1086        _callees: Option<Vec<BlobIndex>>,
1087    ) -> Blob {
1088        Blob {
1089            contract_name,
1090            data: BlobData(borsh::to_vec(self).expect("failed to encode RegisterContractAction")),
1091        }
1092    }
1093}
1094
1095#[derive(
1096    Debug, Serialize, Deserialize, Default, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize,
1097)]
1098pub struct UpdateContractProgramIdAction {
1099    pub contract_name: ContractName,
1100    pub program_id: ProgramId,
1101}
1102
1103impl UpdateContractProgramIdAction {
1104    pub fn as_blob(&self, contract_name: ContractName) -> Blob {
1105        <Self as ContractAction>::as_blob(self, contract_name, None, None)
1106    }
1107}
1108
1109impl ContractAction for UpdateContractProgramIdAction {
1110    fn as_blob(
1111        &self,
1112        contract_name: ContractName,
1113        _caller: Option<BlobIndex>,
1114        _callees: Option<Vec<BlobIndex>>,
1115    ) -> Blob {
1116        Blob {
1117            contract_name,
1118            data: BlobData(
1119                borsh::to_vec(self).expect("failed to encode UpdateContractProgramIdAction"),
1120            ),
1121        }
1122    }
1123}
1124
1125#[derive(
1126    Debug, Serialize, Deserialize, Default, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize,
1127)]
1128pub struct UpdateContractTimeoutWindowAction {
1129    pub contract_name: ContractName,
1130    pub timeout_window: TimeoutWindow,
1131}
1132
1133impl UpdateContractTimeoutWindowAction {
1134    pub fn as_blob(&self, contract_name: ContractName) -> Blob {
1135        <Self as ContractAction>::as_blob(self, contract_name, None, None)
1136    }
1137}
1138
1139impl ContractAction for UpdateContractTimeoutWindowAction {
1140    fn as_blob(
1141        &self,
1142        contract_name: ContractName,
1143        _caller: Option<BlobIndex>,
1144        _callees: Option<Vec<BlobIndex>>,
1145    ) -> Blob {
1146        Blob {
1147            contract_name,
1148            data: BlobData(
1149                borsh::to_vec(self).expect("failed to encode UpdateContractTimeoutWindowAction"),
1150            ),
1151        }
1152    }
1153}
1154
1155#[derive(
1156    Debug, Serialize, Deserialize, Default, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize,
1157)]
1158/// Used as a blob action to delete a contract.
1159pub struct DeleteContractAction {
1160    pub contract_name: ContractName,
1161}
1162
1163impl DeleteContractAction {
1164    pub fn as_blob(&self, contract_name: ContractName) -> Blob {
1165        <Self as ContractAction>::as_blob(self, contract_name, None, None)
1166    }
1167}
1168
1169impl ContractAction for DeleteContractAction {
1170    fn as_blob(
1171        &self,
1172        contract_name: ContractName,
1173        _caller: Option<BlobIndex>,
1174        _callees: Option<Vec<BlobIndex>>,
1175    ) -> Blob {
1176        Blob {
1177            contract_name,
1178            data: BlobData(borsh::to_vec(self).expect("failed to encode DeleteContractAction")),
1179        }
1180    }
1181}
1182
1183/// Used by the Hyli node to recognize contract registration.
1184/// Simply output this struct in your HyliOutput registered_contracts.
1185/// See uuid-tld for examples.
1186#[derive(
1187    Debug, Serialize, Deserialize, Default, Clone, PartialEq, Eq, BorshSerialize, BorshDeserialize,
1188)]
1189#[cfg_attr(feature = "full", derive(utoipa::ToSchema))]
1190pub struct RegisterContractEffect {
1191    /// Verifier to use for transactions sent to this new contract.
1192    pub verifier: Verifier,
1193    /// Other verifier data, such as a Risc0 program ID or noir public key.
1194    /// Transactions sent to this contract will have to match this program ID.
1195    pub program_id: ProgramId,
1196    /// Initial state commitment of the contract to register.
1197    pub state_commitment: StateCommitment,
1198    /// Name of the contract to register.
1199    pub contract_name: ContractName,
1200    /// Optionally set the timeout window for the contract.
1201    /// If the contract exists, the timeout window will be unchanged, otherwise, the default value will be used.
1202    pub timeout_window: Option<TimeoutWindow>,
1203}
1204
1205impl From<RegisterContractAction> for RegisterContractEffect {
1206    fn from(action: RegisterContractAction) -> Self {
1207        RegisterContractEffect {
1208            verifier: action.verifier,
1209            program_id: action.program_id,
1210            state_commitment: action.state_commitment,
1211            contract_name: action.contract_name,
1212            timeout_window: action.timeout_window,
1213        }
1214    }
1215}
1216
1217#[cfg(feature = "full")]
1218impl Hashed<TxHash> for RegisterContractEffect {
1219    fn hashed(&self) -> TxHash {
1220        use sha3::{Digest, Sha3_256};
1221
1222        let mut hasher = Sha3_256::new();
1223        hasher.update(self.verifier.0.clone());
1224        hasher.update(self.program_id.0.clone());
1225        hasher.update(self.state_commitment.0.clone());
1226        hasher.update(self.contract_name.0.clone());
1227        if let Some(timeout_window) = &self.timeout_window {
1228            match timeout_window {
1229                TimeoutWindow::NoTimeout => hasher.update(0u8.to_le_bytes()),
1230                TimeoutWindow::Timeout {
1231                    hard_timeout,
1232                    soft_timeout,
1233                } => {
1234                    hasher.update(hard_timeout.0.to_le_bytes());
1235                    hasher.update(soft_timeout.0.to_le_bytes());
1236                }
1237            }
1238        }
1239        let hash_bytes = hasher.finalize();
1240        TxHash(hash_bytes.to_vec())
1241    }
1242}
1243
1244#[cfg(feature = "full")]
1245pub mod base64_field {
1246    use base64::prelude::*;
1247    use serde::{Deserialize, Deserializer, Serializer};
1248
1249    pub fn serialize<S>(bytes: &Vec<u8>, serializer: S) -> Result<S::Ok, S::Error>
1250    where
1251        S: Serializer,
1252    {
1253        let encoded = BASE64_STANDARD.encode(bytes);
1254        serializer.serialize_str(&encoded)
1255    }
1256
1257    pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
1258    where
1259        D: Deserializer<'de>,
1260    {
1261        let s = String::deserialize(deserializer)?;
1262        BASE64_STANDARD.decode(&s).map_err(serde::de::Error::custom)
1263    }
1264}
1265
1266#[cfg(test)]
1267mod tests {
1268    use super::*;
1269
1270    #[test]
1271    fn consensus_proposal_hash_from_hex_str_roundtrip() {
1272        let hex_str = "74657374";
1273        let hash = ConsensusProposalHash::from_hex(hex_str).expect("consensus hash hex");
1274        assert_eq!(hash.0, b"test".to_vec());
1275        assert_eq!(format!("{hash}"), hex_str);
1276        let json = serde_json::to_string(&hash).expect("serialize consensus hash");
1277        assert_eq!(json, "\"74657374\"");
1278        let decoded: ConsensusProposalHash =
1279            serde_json::from_str(&json).expect("deserialize consensus hash");
1280        assert_eq!(decoded, hash);
1281    }
1282
1283    #[test]
1284    fn txhash_from_hex_str_roundtrip() {
1285        let hex_str = "746573745f7478";
1286        let hash = TxHash::from_hex(hex_str).expect("txhash hex");
1287        assert_eq!(hash.0, b"test_tx".to_vec());
1288        assert_eq!(format!("{hash}"), hex_str);
1289        let json = serde_json::to_string(&hash).expect("serialize tx hash");
1290        assert_eq!(json, "\"746573745f7478\"");
1291        let decoded: TxHash = serde_json::from_str(&json).expect("deserialize tx hash");
1292        assert_eq!(decoded, hash);
1293    }
1294
1295    #[test]
1296    fn proof_data_hash_from_hex_str_roundtrip() {
1297        let hex_str = "0x74657374";
1298        let hash = ProofDataHash::from_hex(hex_str).expect("proof hash hex");
1299        assert_eq!(hash.0, b"test".to_vec());
1300        assert_eq!(hex::encode(&hash.0), "74657374");
1301        let json = serde_json::to_string(&hash).expect("serialize proof hash");
1302        assert_eq!(json, "\"74657374\"");
1303        let decoded: ProofDataHash = serde_json::from_str(&json).expect("deserialize proof hash");
1304        assert_eq!(decoded, hash);
1305    }
1306}