Skip to main content

ave_core/evaluation/compiler/
mod.rs

1use std::{
2    collections::{HashMap, HashSet},
3    path::{Path, PathBuf},
4    process::Stdio,
5    sync::Arc,
6};
7
8use async_trait::async_trait;
9use ave_actors::{
10    Actor, ActorContext, ActorError, ActorPath, Handler, Message,
11    NotPersistentActor, Response,
12};
13use ave_common::{
14    ValueWrapper,
15    identity::{DigestIdentifier, HashAlgorithm, hash_borsh},
16};
17use base64::{Engine as Base64Engine, prelude::BASE64_STANDARD};
18use borsh::{BorshDeserialize, BorshSerialize, to_vec};
19use serde::{Deserialize, Serialize};
20use serde_json::Value;
21use tokio::{fs, process::Command, sync::RwLock};
22
23use tracing::{Span, debug, error, info_span};
24use wasmtime::{ExternType, Module, Store};
25
26use crate::model::common::contract::{
27    MAX_FUEL_COMPILATION, MemoryManager, WasmLimits, WasmRuntime,
28    generate_linker,
29};
30
31pub mod error;
32use error::*;
33
34#[derive(
35    Serialize, Deserialize, BorshSerialize, BorshDeserialize, Debug, Clone,
36)]
37pub struct ContractResult {
38    pub success: bool,
39    pub error: String,
40}
41
42#[derive(Clone, Debug, Serialize, Deserialize)]
43pub struct Compiler {
44    contract: DigestIdentifier,
45    hash: HashAlgorithm,
46}
47
48impl Compiler {
49    pub fn new(hash: HashAlgorithm) -> Self {
50        Self {
51            contract: DigestIdentifier::default(),
52            hash,
53        }
54    }
55
56    fn compilation_toml() -> String {
57        r#"
58    [package]
59    name = "contract"
60    version = "0.1.0"
61    edition = "2024"
62
63    [dependencies]
64    serde = { version = "1.0.219", features = ["derive"] }
65    serde_json = "1.0.140"
66    ave-contract-sdk = "0.7.0"
67
68    [profile.release]
69    strip = "debuginfo"
70    lto = true
71
72    [lib]
73    crate-type = ["cdylib"]
74
75    [workspace]
76      "#
77        .into()
78    }
79
80    async fn compile_contract(
81        contract: &str,
82        contract_path: &Path,
83    ) -> Result<(), CompilerError> {
84        // Write contract.
85        let decode_base64 = BASE64_STANDARD.decode(contract).map_err(|e| {
86            CompilerError::Base64DecodeFailed {
87                details: format!(
88                    "{} (path: {})",
89                    e,
90                    contract_path.to_string_lossy()
91                ),
92            }
93        })?;
94
95        let dir = contract_path.join("src");
96        if !Path::new(&dir).exists() {
97            fs::create_dir_all(&dir).await.map_err(|e| {
98                CompilerError::DirectoryCreationFailed {
99                    path: dir.to_string_lossy().to_string(),
100                    details: e.to_string(),
101                }
102            })?;
103        }
104
105        let toml: String = Self::compilation_toml();
106        let cargo = contract_path.join("Cargo.toml");
107        // We write cargo.toml
108        fs::write(&cargo, toml).await.map_err(|e| {
109            CompilerError::FileWriteFailed {
110                path: cargo.to_string_lossy().to_string(),
111                details: e.to_string(),
112            }
113        })?;
114
115        let lib_rs = contract_path.join("src").join("lib.rs");
116        fs::write(&lib_rs, decode_base64).await.map_err(|e| {
117            CompilerError::FileWriteFailed {
118                path: lib_rs.to_string_lossy().to_string(),
119                details: e.to_string(),
120            }
121        })?;
122
123        // Compiling contract
124        let status = Command::new("cargo")
125            .arg("build")
126            .arg(format!("--manifest-path={}", cargo.to_string_lossy()))
127            .arg("--target")
128            .arg("wasm32-unknown-unknown")
129            .arg("--release")
130            .stdout(Stdio::null())
131            .stderr(Stdio::null())
132            .status()
133            .await
134            .map_err(|e| CompilerError::CargoBuildFailed {
135                details: e.to_string(),
136            })?;
137
138        // Is success
139        if !status.success() {
140            return Err(CompilerError::CompilationFailed);
141        }
142
143        Ok(())
144    }
145
146    async fn check_wasm(
147        ctx: &ActorContext<Self>,
148        contract_path: &Path,
149        state: ValueWrapper,
150    ) -> Result<Vec<u8>, CompilerError> {
151        let Some(wasm_runtime) = ctx
152            .system()
153            .get_helper::<Arc<WasmRuntime>>("wasm_runtime")
154            .await
155        else {
156            return Err(CompilerError::MissingHelper {
157                name: "wasm_runtime",
158            });
159        };
160        // Read compile contract
161        let wasm_path = contract_path
162            .join("target")
163            .join("wasm32-unknown-unknown")
164            .join("release")
165            .join("contract.wasm");
166        let file = fs::read(&wasm_path).await.map_err(|e| {
167            CompilerError::FileReadFailed {
168                path: wasm_path.to_string_lossy().to_string(),
169                details: e.to_string(),
170            }
171        })?;
172
173        // Precompilation
174        let contract_bytes = wasm_runtime
175            .engine
176            .precompile_module(&file)
177            .map_err(|e| CompilerError::WasmPrecompileFailed {
178                details: e.to_string(),
179            })?;
180
181        drop(file);
182
183        // Module represents a precompiled WebAssembly program that is ready to be instantiated and executed.
184        // This function receives the previous input from Engine::precompile_module, that is why this function can be considered safe.
185        let module = unsafe {
186            Module::deserialize(&wasm_runtime.engine, &contract_bytes).map_err(
187                |e| CompilerError::WasmDeserializationFailed {
188                    details: e.to_string(),
189                },
190            )?
191        };
192
193        // Obtain imports
194        let imports = module.imports();
195        // get functions of sdk
196        let mut pending_sdk = Self::get_sdk_functions_identifier();
197
198        for import in imports {
199            // import must be a function
200            match import.ty() {
201                ExternType::Func(_) => {
202                    if !pending_sdk.remove(import.name()) {
203                        return Err(CompilerError::InvalidModule {
204                            kind: InvalidModuleKind::UnknownImportFunction {
205                                name: import.name().to_string(),
206                            },
207                        });
208                    }
209                }
210                extern_type => {
211                    return Err(CompilerError::InvalidModule {
212                        kind: InvalidModuleKind::NonFunctionImport {
213                            import_type: format!("{:?}", extern_type),
214                        },
215                    });
216                }
217            }
218        }
219        if !pending_sdk.is_empty() {
220            return Err(CompilerError::InvalidModule {
221                kind: InvalidModuleKind::MissingImports {
222                    missing: pending_sdk
223                        .into_iter()
224                        .map(|s| s.to_string())
225                        .collect(),
226                },
227            });
228        }
229
230        // We create a context from the state and the event.
231        let (context, state_ptr) =
232            Self::generate_context(state, &wasm_runtime.limits)?;
233
234        // Container to store and manage the global state of a WebAssembly instance during its execution.
235        let mut store = Store::new(&wasm_runtime.engine, context);
236
237        // Limit WASM linear memory and table growth to prevent resource exhaustion.
238        store.limiter(|data| &mut data.store_limits);
239
240        // Set fuel limit for compilation/validation (more generous than production)
241        store.set_fuel(MAX_FUEL_COMPILATION).map_err(|e| {
242            CompilerError::FuelLimitError {
243                details: e.to_string(),
244            }
245        })?;
246
247        // Responsible for combining several object files into a single WebAssembly executable file (.wasm).
248        let linker = generate_linker(&wasm_runtime.engine)?;
249
250        // Contract instance.
251        let instance =
252            linker.instantiate(&mut store, &module).map_err(|e| {
253                CompilerError::InstantiationFailed {
254                    details: e.to_string(),
255                }
256            })?;
257
258        // Get access to contract, only to check if main_function exist.
259        let _main_contract_entrypoint = instance
260            .get_typed_func::<(u32, u32, u32, u32), u32>(
261                &mut store,
262                "main_function",
263            )
264            .map_err(|_e| CompilerError::EntryPointNotFound {
265                function: "main_function",
266            })?;
267
268        // Get access to contract
269        let init_contract_entrypoint = instance
270            .get_typed_func::<u32, u32>(&mut store, "init_check_function")
271            .map_err(|_e| CompilerError::EntryPointNotFound {
272                function: "init_check_function",
273            })?;
274
275        // Contract execution
276        let result_ptr =
277            init_contract_entrypoint
278                .call(&mut store, state_ptr)
279                .map_err(|e| CompilerError::ContractExecutionFailed {
280                    details: e.to_string(),
281                })?;
282
283        Self::check_result(&store, result_ptr)?;
284
285        Ok(contract_bytes)
286    }
287
288    fn check_result(
289        store: &Store<MemoryManager>,
290        pointer: u32,
291    ) -> Result<(), CompilerError> {
292        let bytes = store.data().read_data(pointer as usize)?;
293        let contract_result: ContractResult =
294            BorshDeserialize::try_from_slice(bytes).map_err(|e| {
295                CompilerError::InvalidContractOutput {
296                    details: e.to_string(),
297                }
298            })?;
299
300        if contract_result.success {
301            Ok(())
302        } else {
303            Err(CompilerError::ContractCheckFailed {
304                error: contract_result.error,
305            })
306        }
307    }
308
309    fn generate_context(
310        state: ValueWrapper,
311        limits: &WasmLimits,
312    ) -> Result<(MemoryManager, u32), CompilerError> {
313        let mut context = MemoryManager::from_limits(limits);
314        let state_bytes =
315            to_vec(&state).map_err(|e| CompilerError::SerializationError {
316                context: "state serialization",
317                details: e.to_string(),
318            })?;
319        let state_ptr = context.add_data_raw(&state_bytes)?;
320        Ok((context, state_ptr as u32))
321    }
322
323    fn get_sdk_functions_identifier() -> HashSet<&'static str> {
324        ["alloc", "write_byte", "pointer_len", "read_byte"]
325            .into_iter()
326            .collect()
327    }
328}
329
330#[derive(Debug, Clone)]
331pub enum CompilerMessage {
332    TemporalCompile {
333        contract: String,
334        contract_name: String,
335        initial_value: Value,
336        contract_path: PathBuf,
337    },
338    Compile {
339        contract: String,
340        contract_name: String,
341        initial_value: Value,
342        contract_path: PathBuf,
343    },
344}
345
346impl Message for CompilerMessage {}
347
348#[derive(Debug, Clone)]
349pub enum CompilerResponse {
350    Ok,
351    Error(CompilerError),
352}
353
354impl Response for CompilerResponse {}
355
356impl NotPersistentActor for Compiler {}
357
358#[async_trait]
359impl Actor for Compiler {
360    type Event = ();
361    type Message = CompilerMessage;
362    type Response = CompilerResponse;
363
364    fn get_span(id: &str, parent_span: Option<Span>) -> tracing::Span {
365        parent_span.map_or_else(
366            || info_span!("Compiler", id),
367            |parent_span| info_span!(parent: parent_span, "Compiler", id),
368        )
369    }
370}
371
372#[async_trait]
373impl Handler<Self> for Compiler {
374    async fn handle_message(
375        &mut self,
376        _sender: ActorPath,
377        msg: CompilerMessage,
378        ctx: &mut ActorContext<Self>,
379    ) -> Result<CompilerResponse, ActorError> {
380        match msg {
381            CompilerMessage::TemporalCompile {
382                contract,
383                contract_name,
384                contract_path,
385                initial_value,
386            } => {
387                if let Err(e) =
388                    Self::compile_contract(&contract, &contract_path).await
389                {
390                    error!(
391                        msg_type = "TemporalCompile",
392                        error = %e,
393                        contract_name = %contract_name,
394                        path = %contract_path.display(),
395                        "Contract compilation failed"
396                    );
397                    let _ = fs::remove_dir_all(&contract_path).await;
398                    return Ok(CompilerResponse::Error(e));
399                };
400
401                if let Err(e) = Self::check_wasm(
402                    ctx,
403                    &contract_path,
404                    ValueWrapper(initial_value),
405                )
406                .await
407                {
408                    error!(
409                        msg_type = "TemporalCompile",
410                        error = %e,
411                        contract_name = %contract_name,
412                        "WASM validation failed"
413                    );
414                    let _ = fs::remove_dir_all(&contract_path).await;
415                    return Ok(CompilerResponse::Error(e));
416                }
417
418                if let Err(e) = fs::remove_dir_all(&contract_path).await {
419                    error!(
420                        msg_type = "TemporalCompile",
421                        error = %e,
422                        path = %contract_path.display(),
423                        "Failed to remove temporal contract directory"
424                    );
425                }
426
427                Ok(CompilerResponse::Ok)
428            }
429            CompilerMessage::Compile {
430                contract,
431                contract_name,
432                contract_path,
433                initial_value,
434            } => {
435                let contract_hash =
436                    match hash_borsh(&*self.hash.hasher(), &contract) {
437                        Ok(hash) => hash,
438                        Err(e) => {
439                            error!(
440                                msg_type = "Compile",
441                                error = %e,
442                                "Failed to hash contract"
443                            );
444                            return Err(ActorError::FunctionalCritical {
445                                description: format!(
446                                    "Can not hash contract: {}",
447                                    e
448                                ),
449                            });
450                        }
451                    };
452
453                if contract_hash != self.contract {
454                    if let Err(e) =
455                        Self::compile_contract(&contract, &contract_path).await
456                    {
457                        error!(
458                            msg_type = "Compile",
459                            error = %e,
460                            contract_name = %contract_name,
461                            path = %contract_path.display(),
462                            "Contract compilation failed"
463                        );
464                        return Ok(CompilerResponse::Error(e));
465                    };
466
467                    let contract = match Self::check_wasm(
468                        ctx,
469                        &contract_path,
470                        ValueWrapper(initial_value),
471                    )
472                    .await
473                    {
474                        Ok(contract) => contract,
475                        Err(e) => {
476                            error!(
477                                msg_type = "Compile",
478                                error = %e,
479                                contract_name = %contract_name,
480                                "WASM validation failed"
481                            );
482                            return Ok(CompilerResponse::Error(e));
483                        }
484                    };
485
486                    {
487                        let Some(contracts) = ctx.system().get_helper::<Arc<RwLock<HashMap<String, Vec<u8>>>>>("contracts").await else {
488                            error!(
489                                msg_type = "Compile",
490                                "Contracts helper not found"
491                            );
492                            return Err(ActorError::Helper { name: "contracts".to_string(), reason: "Not found".to_string() });
493                        };
494
495                        let mut contracts = contracts.write().await;
496                        contracts.insert(contract_name.clone(), contract);
497                    }
498
499                    self.contract = contract_hash.clone();
500
501                    debug!(
502                        msg_type = "Compile",
503                        contract_name = %contract_name,
504                        contract_hash = %contract_hash,
505                        "Contract compiled and validated successfully"
506                    );
507                } else {
508                    debug!(
509                        msg_type = "Compile",
510                        contract_name = %contract_name,
511                        "Contract already compiled, skipping"
512                    );
513                }
514
515                Ok(CompilerResponse::Ok)
516            }
517        }
518    }
519}