Skip to main content

hyli_contract_sdk/
lib.rs

1//! # Hyli Contract SDK
2//!
3//! This crate provides helper tools for writing smart contracts that run inside a Rust-based zkVM,
4//! such as [Risc0](https://github.com/risc0/risc0) or [SP1](https://github.com/succinctlabs/sp1).
5//!
6//! ## Building a contract on Hyli
7//!
8//! To write a contract for Hyli:
9//!
10//! - Create a library crate that defines a struct implementing the [`ZkContract`] trait.
11//! - Build a corresponding zkVM binary that runs your contract logic.
12//!
13//! See [`crate::guest`] for details on how to configure and run your contract inside the zkVM.
14//!
15//! You can use our app scaffold to get started:
16//!
17//! - [scaffold](https://github.com/hyli-org/app-scaffold/)
18//! - [Quickstart documentation](https://docs.hyli.org/quickstart/)
19//!
20//! ## Contract interoperability
21//!
22//! If your contract needs to exchange data with others, refer to [`StructuredBlobData`].
23//! More documentation about this will follow.
24
25#![cfg_attr(not(test), no_std)]
26
27extern crate alloc;
28
29use alloc::string::String;
30use alloc::vec::Vec;
31
32pub mod caller;
33pub mod guest;
34#[cfg(feature = "smt")]
35pub mod merkle_utils;
36pub mod secp256k1;
37pub mod utils;
38
39use caller::ExecutionContext;
40// re-export hyli-model
41pub use hyli_model::*;
42
43pub use hyli_model::utils as hyli_model_utils;
44
45#[cfg(feature = "tracing")]
46pub use tracing;
47
48// Si la feature "tracing" est activée, on redirige vers `tracing::info!`
49#[cfg(feature = "tracing")]
50#[macro_export]
51macro_rules! info {
52    ($($arg:tt)*) => {
53        $crate::tracing::info!($($arg)*);
54    }
55}
56
57// Si la feature "tracing" est activée, on redirige vers `tracing::error!`
58#[cfg(feature = "tracing")]
59#[macro_export]
60macro_rules! error {
61    ($($arg:tt)*) => {
62        $crate::tracing::error!($($arg)*);
63    }
64}
65
66// Si la feature "tracing" n'est pas activée, on redirige vers la fonction env::log
67#[cfg(all(not(feature = "tracing"), feature = "risc0"))]
68#[macro_export]
69macro_rules! info {
70    ($($arg:tt)*) => {
71        risc0_zkvm::guest::env::log(&format!($($arg)*));
72    }
73}
74
75#[cfg(all(not(feature = "tracing"), not(feature = "risc0")))]
76#[macro_export]
77macro_rules! info {
78    ($($arg:tt)*) => {
79        // no_std: no-op; in test builds (which have std), print normally
80        #[cfg(test)]
81        println!($($arg)*);
82    }
83}
84
85// Si la feature "tracing" n'est pas activée, on redirige vers la fonction env::log
86#[cfg(all(not(feature = "tracing"), feature = "risc0"))]
87#[macro_export]
88macro_rules! error {
89    ($($arg:tt)*) => {
90        risc0_zkvm::guest::env::log(&format!($($arg)*));
91    }
92}
93
94#[cfg(all(not(feature = "tracing"), not(feature = "risc0")))]
95#[macro_export]
96macro_rules! error {
97    ($($arg:tt)*) => {
98        // no_std: no-op; in test builds (which have std), print normally
99        #[cfg(test)]
100        println!($($arg)*);
101    }
102}
103
104pub type RunResult = Result<(Vec<u8>, ExecutionContext, Vec<OnchainEffect>), String>;
105
106/**
107This trait is used to define the contract's entrypoint.
108By using it and the [execute](function@crate::guest::execute) function, you let the sdk
109generate for you the [HyliOutput] struct with correct fields.
110
111The [Calldata] struct is built by the application backend and given as input to
112the program that runs in the zkvm.
113
114The calldata is generic to any contract, and holds all the blobs of the blob transaction
115being proved. These blobs are stored as vec of bytes, so contract need to parse them into the
116expected type. For this, it can call either [utils::parse_raw_calldata] or
117[utils::parse_calldata]. Check the [utils] documentation for details on these functions.
118
119## Example of execute implementation:
120
121```rust
122use hyli_contract_sdk::{StateCommitment, RunResult, ZkContract};
123use hyli_contract_sdk::utils::parse_raw_calldata;
124use hyli_model::Calldata;
125
126use borsh::{BorshSerialize, BorshDeserialize};
127
128struct MyContract{}
129#[derive(BorshSerialize, BorshDeserialize)]
130enum MyContractAction{
131    DoSomething
132}
133
134impl ZkContract for MyContract {
135    fn execute(&mut self, calldata: &Calldata) -> RunResult {
136        let (action, exec_ctx) = parse_raw_calldata(calldata)?;
137
138        let output = self.execute_action(action)?;
139
140        Ok((output.into_bytes(), exec_ctx, vec![]))
141    }
142    fn commit(&self) -> StateCommitment {
143        StateCommitment(vec![])
144    }
145}
146
147impl MyContract {
148    fn execute_action(&mut self, action: MyContractAction) -> Result<String, String> {
149        /// Execute contract's logic
150        Ok("Done.".to_string())
151    }
152}
153
154```
155*/
156pub trait ZkContract {
157    /// Entry point of the contract
158    /// Execution is based solely on the contract's commitment metadata.
159    /// Exemple: the merkle root for a contract's state based on a MerkleTrie. The execute function will only update the roothash of the trie.
160    fn execute(&mut self, calldata: &Calldata) -> RunResult;
161
162    fn commit(&self) -> StateCommitment;
163
164    /// A function executed before the contract is executed.
165    /// This might be used to do verifications on the state before validating calldatas.
166    fn initialize(&mut self) -> Result<(), String> {
167        Ok(())
168    }
169}
170
171pub trait TransactionalZkContract
172where
173    Self: ZkContract,
174{
175    type State;
176    fn initial_state(&self) -> Self::State;
177
178    fn revert(&mut self, initial_state: Self::State);
179
180    fn on_success(&mut self) -> StateCommitment {
181        self.commit()
182    }
183}
184
185pub trait FullStateRevert {}
186
187impl<T> TransactionalZkContract for T
188where
189    T: FullStateRevert,
190    Self: Sized + Clone + ZkContract,
191{
192    type State = Self;
193
194    fn initial_state(&self) -> Self::State {
195        self.clone()
196    }
197
198    fn revert(&mut self, initial_state: Self::State) {
199        *self = initial_state;
200    }
201}
202
203pub const fn to_u8_array(val: &[u32; 8]) -> [u8; 32] {
204    [
205        (val[0] & 0xFF) as u8,
206        ((val[0] >> 8) & 0xFF) as u8,
207        ((val[0] >> 16) & 0xFF) as u8,
208        ((val[0] >> 24) & 0xFF) as u8,
209        (val[1] & 0xFF) as u8,
210        ((val[1] >> 8) & 0xFF) as u8,
211        ((val[1] >> 16) & 0xFF) as u8,
212        ((val[1] >> 24) & 0xFF) as u8,
213        (val[2] & 0xFF) as u8,
214        ((val[2] >> 8) & 0xFF) as u8,
215        ((val[2] >> 16) & 0xFF) as u8,
216        ((val[2] >> 24) & 0xFF) as u8,
217        (val[3] & 0xFF) as u8,
218        ((val[3] >> 8) & 0xFF) as u8,
219        ((val[3] >> 16) & 0xFF) as u8,
220        ((val[3] >> 24) & 0xFF) as u8,
221        (val[4] & 0xFF) as u8,
222        ((val[4] >> 8) & 0xFF) as u8,
223        ((val[4] >> 16) & 0xFF) as u8,
224        ((val[4] >> 24) & 0xFF) as u8,
225        (val[5] & 0xFF) as u8,
226        ((val[5] >> 8) & 0xFF) as u8,
227        ((val[5] >> 16) & 0xFF) as u8,
228        ((val[5] >> 24) & 0xFF) as u8,
229        (val[6] & 0xFF) as u8,
230        ((val[6] >> 8) & 0xFF) as u8,
231        ((val[6] >> 16) & 0xFF) as u8,
232        ((val[6] >> 24) & 0xFF) as u8,
233        (val[7] & 0xFF) as u8,
234        ((val[7] >> 8) & 0xFF) as u8,
235        ((val[7] >> 16) & 0xFF) as u8,
236        ((val[7] >> 24) & 0xFF) as u8,
237    ]
238}
239
240const fn byte_to_u8(byte: u8) -> u8 {
241    match byte {
242        b'0'..=b'9' => byte - b'0',
243        b'a'..=b'f' => byte - b'a' + 10,
244        b'A'..=b'F' => byte - b'A' + 10,
245        _ => 0,
246    }
247}
248
249#[allow(
250    clippy::indexing_slicing,
251    reason = "const block, shouldn't be used at runtime."
252)]
253pub const fn str_to_u8(s: &str) -> [u8; 32] {
254    let mut bytes = [0u8; 32];
255    let chrs = s.as_bytes();
256    let mut i = 0;
257    while i < 32 {
258        bytes[i] = (byte_to_u8(chrs[i * 2]) << 4) | byte_to_u8(chrs[i * 2 + 1]);
259        i += 1;
260    }
261    bytes
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267    use alloc::{format, string::ToString, vec};
268
269    #[test]
270    fn test_identity_from_string() {
271        let identity_str = "test_identity".to_string();
272        let identity = Identity::from(identity_str.clone());
273        assert_eq!(identity.0, identity_str);
274    }
275
276    #[test]
277    fn test_identity_from_str() {
278        let identity_str = "test_identity";
279        let identity = Identity::from(identity_str);
280        assert_eq!(identity.0, identity_str.to_string());
281    }
282
283    #[test]
284    fn test_txhash_from_string() {
285        let txhash_str = "746573745f747868617368".to_string();
286        let txhash = TxHash::from_hex(&txhash_str).expect("txhash hex");
287        assert_eq!(txhash.0, b"test_txhash".to_vec());
288    }
289
290    #[test]
291    fn test_txhash_from_str() {
292        let txhash_str = "746573745f747868617368";
293        let txhash = TxHash::from_hex(txhash_str).expect("txhash hex");
294        assert_eq!(txhash.0, b"test_txhash".to_vec());
295    }
296
297    #[test]
298    fn test_txhash_new() {
299        let txhash = TxHash::new(b"test_txhash".to_vec());
300        assert_eq!(txhash.0, b"test_txhash".to_vec());
301    }
302
303    #[test]
304    fn test_blobindex_from_u32() {
305        let index = 42;
306        let blob_index = BlobIndex::from(index);
307        assert_eq!(blob_index.0, index);
308    }
309
310    #[test]
311    fn test_txhash_display() {
312        let txhash_str = "746573745f747868617368";
313        let txhash = TxHash::from_hex(txhash_str).expect("txhash hex");
314        assert_eq!(format!("{txhash}"), txhash_str);
315    }
316
317    #[test]
318    fn test_blobindex_display() {
319        let index = 42;
320        let blob_index = BlobIndex::from(index);
321        assert_eq!(format!("{blob_index}"), index.to_string());
322    }
323
324    #[test]
325    fn test_state_commitment_encoding() {
326        let state_commitment = StateCommitment(vec![1, 2, 3, 4]);
327        let encoded = borsh::to_vec(&state_commitment).expect("Failed to encode StateCommitment");
328        let decoded: StateCommitment =
329            borsh::from_slice(&encoded).expect("Failed to decode StateCommitment");
330        assert_eq!(state_commitment, decoded);
331    }
332
333    #[test]
334    fn test_identity_encoding() {
335        let identity = Identity::new("test_identity");
336        let encoded = borsh::to_vec(&identity).expect("Failed to encode Identity");
337        let decoded: Identity = borsh::from_slice(&encoded).expect("Failed to decode Identity");
338        assert_eq!(identity, decoded);
339    }
340
341    #[test]
342    fn test_txhash_encoding() {
343        let txhash = TxHash::from_hex("746573745f747868617368").expect("txhash hex");
344        let encoded = borsh::to_vec(&txhash).expect("Failed to encode TxHash");
345        let decoded: TxHash = borsh::from_slice(&encoded).expect("Failed to decode TxHash");
346        assert_eq!(txhash, decoded);
347    }
348
349    #[test]
350    fn test_blobindex_encoding() {
351        let blob_index = BlobIndex(42);
352        let encoded = borsh::to_vec(&blob_index).expect("Failed to encode BlobIndex");
353        let decoded: BlobIndex = borsh::from_slice(&encoded).expect("Failed to decode BlobIndex");
354        assert_eq!(blob_index, decoded);
355    }
356
357    #[test]
358    fn test_blobdata_encoding() {
359        let blob_data = BlobData(vec![1, 2, 3, 4]);
360        let encoded = borsh::to_vec(&blob_data).expect("Failed to encode BlobData");
361        let decoded: BlobData = borsh::from_slice(&encoded).expect("Failed to decode BlobData");
362        assert_eq!(blob_data, decoded);
363    }
364}