Skip to main content

ave_core/evaluation/compiler/
mod.rs

1#[cfg(feature = "test")]
2use std::env;
3#[cfg(feature = "test")]
4use std::io::ErrorKind;
5use std::{
6    collections::{HashMap, HashSet},
7    hash::{DefaultHasher, Hash as StdHash, Hasher},
8    path::{Path, PathBuf},
9    process::Stdio,
10    sync::Arc,
11    time::Instant,
12};
13
14use ave_actors::{Actor, ActorContext, ActorError, ActorPath, Response};
15use ave_common::{
16    ValueWrapper,
17    identity::{DigestIdentifier, HashAlgorithm, hash_borsh},
18};
19use base64::{Engine as Base64Engine, prelude::BASE64_STANDARD};
20use borsh::{BorshDeserialize, BorshSerialize, to_vec};
21use serde::{Deserialize, Serialize};
22use serde_json::Value;
23#[cfg(feature = "test")]
24use tokio::time::{Duration, sleep};
25use tokio::{fs, process::Command, sync::RwLock};
26use tracing::debug;
27use wasmtime::{ExternType, Module, Store};
28
29use crate::model::common::contract::{
30    MAX_FUEL_COMPILATION, MemoryManager, WasmLimits, WasmRuntime,
31    generate_linker,
32};
33use crate::{
34    governance::contract_register::{
35        ContractRegister, ContractRegisterMessage, ContractRegisterResponse,
36    },
37    metrics::try_core_metrics,
38};
39
40pub mod contract_compiler;
41pub mod error;
42pub mod temp_compiler;
43
44pub use contract_compiler::{ContractCompiler, ContractCompilerMessage};
45pub use temp_compiler::{TempCompiler, TempCompilerMessage};
46
47use error::*;
48
49#[derive(
50    Serialize, Deserialize, BorshSerialize, BorshDeserialize, Debug, Clone,
51)]
52pub struct ContractResult {
53    pub success: bool,
54    pub error: String,
55}
56
57#[derive(
58    Debug, Clone, Serialize, Deserialize, BorshSerialize, BorshDeserialize,
59)]
60pub struct ContractArtifactRecord {
61    pub contract_hash: DigestIdentifier,
62    pub manifest_hash: DigestIdentifier,
63    pub wasm_hash: DigestIdentifier,
64    pub cwasm_hash: DigestIdentifier,
65    pub engine_fingerprint: DigestIdentifier,
66    pub toolchain_fingerprint: DigestIdentifier,
67}
68
69#[derive(Debug, Clone)]
70pub enum CompilerResponse {
71    Ok,
72    Error(CompilerError),
73}
74
75impl Response for CompilerResponse {}
76
77struct CompilerSupport;
78
79impl CompilerSupport {
80    const SHARED_TARGET_DIR: &'static str = "target";
81    const VENDOR_DIR: &'static str = "vendor";
82    const ARTIFACT_WASM: &'static str = "contract.wasm";
83    const ARTIFACT_PRECOMPILED: &'static str = "contract.cwasm";
84    const LEGACY_ARTIFACT_METADATA: &'static str = "contract.json";
85    #[cfg(feature = "test")]
86    const GLOBAL_CACHE_DIR: &'static str = "ave-contract-artifacts";
87    #[cfg(feature = "test")]
88    const GLOBAL_CACHE_METADATA: &'static str = "metadata.borsh";
89
90    fn observe_contract_prepare(
91        kind: &'static str,
92        result: &'static str,
93        started_at: Instant,
94    ) {
95        if let Some(metrics) = try_core_metrics() {
96            metrics.observe_contract_prepare(
97                kind,
98                result,
99                started_at.elapsed(),
100            );
101        }
102    }
103
104    fn compilation_toml() -> String {
105        include_str!("contract_Cargo.toml").to_owned()
106    }
107
108    fn contracts_root(contract_path: &Path) -> Result<PathBuf, CompilerError> {
109        contract_path
110            .parent()
111            .and_then(Path::parent)
112            .map(Path::to_path_buf)
113            .ok_or_else(|| CompilerError::InvalidContractPath {
114                path: contract_path.to_string_lossy().to_string(),
115                details:
116                    "expected contract path under <contracts_path>/contracts/<name>"
117                        .to_owned(),
118            })
119    }
120
121    #[cfg(feature = "test")]
122    fn artifact_wasm_path_in(base_path: &Path) -> PathBuf {
123        base_path.join(Self::ARTIFACT_WASM)
124    }
125
126    fn artifact_wasm_path(contract_path: &Path) -> PathBuf {
127        contract_path.join(Self::ARTIFACT_WASM)
128    }
129
130    #[cfg(feature = "test")]
131    fn artifact_precompiled_path_in(base_path: &Path) -> PathBuf {
132        base_path.join(Self::ARTIFACT_PRECOMPILED)
133    }
134
135    fn artifact_precompiled_path(contract_path: &Path) -> PathBuf {
136        contract_path.join(Self::ARTIFACT_PRECOMPILED)
137    }
138
139    fn legacy_artifact_metadata_path(contract_path: &Path) -> PathBuf {
140        contract_path.join(Self::LEGACY_ARTIFACT_METADATA)
141    }
142
143    #[cfg(feature = "test")]
144    fn global_cache_root() -> PathBuf {
145        env::temp_dir().join(Self::GLOBAL_CACHE_DIR)
146    }
147
148    #[cfg(feature = "test")]
149    fn global_cache_entry_dir(
150        contract_hash: &DigestIdentifier,
151        manifest_hash: &DigestIdentifier,
152        engine_fingerprint: &DigestIdentifier,
153        toolchain_fingerprint: &DigestIdentifier,
154    ) -> PathBuf {
155        Self::global_cache_root().join(format!(
156            "{contract_hash}_{manifest_hash}_{engine_fingerprint}_{toolchain_fingerprint}"
157        ))
158    }
159
160    #[cfg(feature = "test")]
161    fn global_cache_metadata_path(cache_dir: &Path) -> PathBuf {
162        cache_dir.join(Self::GLOBAL_CACHE_METADATA)
163    }
164
165    #[cfg(feature = "test")]
166    fn global_cache_lock_path(cache_dir: &Path) -> PathBuf {
167        cache_dir.with_extension("lock")
168    }
169
170    fn cargo_config_path(contract_path: &Path) -> PathBuf {
171        contract_path.join(".cargo").join("config.toml")
172    }
173
174    fn shared_target_dir_for_contract() -> PathBuf {
175        PathBuf::from(".")
176            .join("..")
177            .join("..")
178            .join(Self::SHARED_TARGET_DIR)
179    }
180
181    fn vendor_dir_for_contract() -> PathBuf {
182        PathBuf::from(".").join("..").join("..").join(Self::VENDOR_DIR)
183    }
184
185    fn build_output_wasm_path(contracts_root: &Path) -> PathBuf {
186        contracts_root
187            .join(Self::SHARED_TARGET_DIR)
188            .join("wasm32-unknown-unknown")
189            .join("release")
190            .join(Self::ARTIFACT_WASM)
191    }
192
193    fn cargo_config(
194        shared_target_dir: &Path,
195        vendor_dir: Option<&Path>,
196    ) -> String {
197        let mut config = format!(
198            "[build]\ntarget-dir = \"{}\"\n",
199            shared_target_dir.to_string_lossy()
200        );
201
202        if let Some(vendor_dir) = vendor_dir {
203            config.push_str(&format!(
204                "\n[net]\noffline = true\n\n[source.crates-io]\nreplace-with = \"vendored-sources\"\n\n[source.vendored-sources]\ndirectory = \"{}\"\n",
205                vendor_dir.to_string_lossy()
206            ));
207        }
208
209        config
210    }
211
212    async fn build_contract(
213        contract_path: &Path,
214        offline: bool,
215    ) -> Result<(), CompilerError> {
216        let cargo = contract_path.join("Cargo.toml");
217        let cargo_config = Self::cargo_config_path(contract_path);
218        let mut command = Command::new("cargo");
219        command
220            .arg("build")
221            .arg(format!("--manifest-path={}", cargo.to_string_lossy()))
222            .arg("--target")
223            .arg("wasm32-unknown-unknown")
224            .arg("--release")
225            .env("CARGO_HOME", contract_path.join(".cargo"))
226            .env("CARGO_CONFIG", &cargo_config)
227            .stdout(Stdio::null())
228            .stderr(Stdio::null());
229
230        if offline {
231            command.arg("--offline");
232        }
233
234        let status = command.status().await.map_err(|e| {
235            CompilerError::CargoBuildFailed {
236                details: e.to_string(),
237            }
238        })?;
239
240        if !status.success() {
241            return Err(CompilerError::CompilationFailed);
242        }
243
244        Ok(())
245    }
246
247    async fn prepare_contract_project(
248        contract: &str,
249        contract_path: &Path,
250    ) -> Result<(), CompilerError> {
251        let decode_base64 = BASE64_STANDARD.decode(contract).map_err(|e| {
252            CompilerError::Base64DecodeFailed {
253                details: format!(
254                    "{} (path: {})",
255                    e,
256                    contract_path.to_string_lossy()
257                ),
258            }
259        })?;
260
261        let contracts_root = Self::contracts_root(contract_path)?;
262        let dir = contract_path.join("src");
263        if !Path::new(&dir).exists() {
264            fs::create_dir_all(&dir).await.map_err(|e| {
265                CompilerError::DirectoryCreationFailed {
266                    path: dir.to_string_lossy().to_string(),
267                    details: e.to_string(),
268                }
269            })?;
270        }
271
272        let cargo_config_dir = contract_path.join(".cargo");
273        if !Path::new(&cargo_config_dir).exists() {
274            fs::create_dir_all(&cargo_config_dir).await.map_err(|e| {
275                CompilerError::DirectoryCreationFailed {
276                    path: cargo_config_dir.to_string_lossy().to_string(),
277                    details: e.to_string(),
278                }
279            })?;
280        }
281
282        let toml = Self::compilation_toml();
283        let cargo = contract_path.join("Cargo.toml");
284        fs::write(&cargo, toml).await.map_err(|e| {
285            CompilerError::FileWriteFailed {
286                path: cargo.to_string_lossy().to_string(),
287                details: e.to_string(),
288            }
289        })?;
290
291        let lib_rs = contract_path.join("src").join("lib.rs");
292        fs::write(&lib_rs, decode_base64).await.map_err(|e| {
293            CompilerError::FileWriteFailed {
294                path: lib_rs.to_string_lossy().to_string(),
295                details: e.to_string(),
296            }
297        })?;
298
299        let vendor_dir = contracts_root.join(Self::VENDOR_DIR);
300        let cargo_config = Self::cargo_config(
301            &Self::shared_target_dir_for_contract(),
302            vendor_dir
303                .exists()
304                .then(|| Self::vendor_dir_for_contract())
305                .as_deref(),
306        );
307        let cargo_config_path = Self::cargo_config_path(contract_path);
308        fs::write(&cargo_config_path, cargo_config)
309            .await
310            .map_err(|e| CompilerError::FileWriteFailed {
311                path: cargo_config_path.to_string_lossy().to_string(),
312                details: e.to_string(),
313            })?;
314
315        Ok(())
316    }
317
318    async fn load_artifact_wasm(
319        contract_path: &Path,
320    ) -> Result<Vec<u8>, CompilerError> {
321        let wasm_path = Self::artifact_wasm_path(contract_path);
322        fs::read(&wasm_path)
323            .await
324            .map_err(|e| CompilerError::FileReadFailed {
325                path: wasm_path.to_string_lossy().to_string(),
326                details: e.to_string(),
327            })
328    }
329
330    #[cfg(feature = "test")]
331    async fn load_artifact_wasm_from(
332        base_path: &Path,
333    ) -> Result<Vec<u8>, CompilerError> {
334        let wasm_path = Self::artifact_wasm_path_in(base_path);
335        fs::read(&wasm_path)
336            .await
337            .map_err(|e| CompilerError::FileReadFailed {
338                path: wasm_path.to_string_lossy().to_string(),
339                details: e.to_string(),
340            })
341    }
342
343    async fn load_artifact_precompiled(
344        contract_path: &Path,
345    ) -> Result<Vec<u8>, CompilerError> {
346        let precompiled_path = Self::artifact_precompiled_path(contract_path);
347        fs::read(&precompiled_path).await.map_err(|e| {
348            CompilerError::FileReadFailed {
349                path: precompiled_path.to_string_lossy().to_string(),
350                details: e.to_string(),
351            }
352        })
353    }
354
355    #[cfg(feature = "test")]
356    async fn load_artifact_precompiled_from(
357        base_path: &Path,
358    ) -> Result<Vec<u8>, CompilerError> {
359        let precompiled_path = Self::artifact_precompiled_path_in(base_path);
360        fs::read(&precompiled_path).await.map_err(|e| {
361            CompilerError::FileReadFailed {
362                path: precompiled_path.to_string_lossy().to_string(),
363                details: e.to_string(),
364            }
365        })
366    }
367
368    async fn load_compiled_wasm(
369        contracts_root: &Path,
370    ) -> Result<Vec<u8>, CompilerError> {
371        let wasm_path = Self::build_output_wasm_path(contracts_root);
372        fs::read(&wasm_path)
373            .await
374            .map_err(|e| CompilerError::FileReadFailed {
375                path: wasm_path.to_string_lossy().to_string(),
376                details: e.to_string(),
377            })
378    }
379
380    async fn persist_artifact(
381        contract_path: &Path,
382        wasm_bytes: &[u8],
383        precompiled_bytes: &[u8],
384    ) -> Result<(), CompilerError> {
385        let artifact_path = Self::artifact_wasm_path(contract_path);
386        fs::write(&artifact_path, wasm_bytes).await.map_err(|e| {
387            CompilerError::FileWriteFailed {
388                path: artifact_path.to_string_lossy().to_string(),
389                details: e.to_string(),
390            }
391        })?;
392
393        let precompiled_path = Self::artifact_precompiled_path(contract_path);
394        fs::write(&precompiled_path, precompiled_bytes)
395            .await
396            .map_err(|e| CompilerError::FileWriteFailed {
397                path: precompiled_path.to_string_lossy().to_string(),
398                details: e.to_string(),
399            })?;
400
401        let legacy_metadata_path =
402            Self::legacy_artifact_metadata_path(contract_path);
403        let _ = fs::remove_file(&legacy_metadata_path).await;
404
405        Ok(())
406    }
407
408    #[cfg(feature = "test")]
409    async fn persist_global_cache_artifact(
410        cache_dir: &Path,
411        metadata: &ContractArtifactRecord,
412        wasm_bytes: &[u8],
413        precompiled_bytes: &[u8],
414    ) -> Result<(), CompilerError> {
415        fs::create_dir_all(cache_dir).await.map_err(|e| {
416            CompilerError::DirectoryCreationFailed {
417                path: cache_dir.to_string_lossy().to_string(),
418                details: e.to_string(),
419            }
420        })?;
421
422        let artifact_path = Self::artifact_wasm_path_in(cache_dir);
423        fs::write(&artifact_path, wasm_bytes).await.map_err(|e| {
424            CompilerError::FileWriteFailed {
425                path: artifact_path.to_string_lossy().to_string(),
426                details: e.to_string(),
427            }
428        })?;
429
430        let precompiled_path = Self::artifact_precompiled_path_in(cache_dir);
431        fs::write(&precompiled_path, precompiled_bytes)
432            .await
433            .map_err(|e| CompilerError::FileWriteFailed {
434                path: precompiled_path.to_string_lossy().to_string(),
435                details: e.to_string(),
436            })?;
437
438        let metadata_path = Self::global_cache_metadata_path(cache_dir);
439        fs::write(
440            &metadata_path,
441            to_vec(metadata).map_err(|e| {
442                CompilerError::SerializationError {
443                    context: "global cache metadata",
444                    details: e.to_string(),
445                }
446            })?,
447        )
448        .await
449        .map_err(|e| CompilerError::FileWriteFailed {
450            path: metadata_path.to_string_lossy().to_string(),
451            details: e.to_string(),
452        })?;
453
454        Ok(())
455    }
456
457    #[cfg(feature = "test")]
458    async fn try_acquire_global_cache_lock(
459        cache_dir: &Path,
460    ) -> Result<Option<GlobalCacheLock>, CompilerError> {
461        fs::create_dir_all(Self::global_cache_root())
462            .await
463            .map_err(|e| CompilerError::DirectoryCreationFailed {
464                path: Self::global_cache_root().to_string_lossy().to_string(),
465                details: e.to_string(),
466            })?;
467
468        let lock_path = Self::global_cache_lock_path(cache_dir);
469        match fs::OpenOptions::new()
470            .write(true)
471            .create_new(true)
472            .open(&lock_path)
473            .await
474        {
475            Ok(_) => Ok(Some(GlobalCacheLock { path: lock_path })),
476            Err(error) if error.kind() == ErrorKind::AlreadyExists => Ok(None),
477            Err(error) => Err(CompilerError::FileWriteFailed {
478                path: lock_path.to_string_lossy().to_string(),
479                details: error.to_string(),
480            }),
481        }
482    }
483
484    #[cfg(feature = "test")]
485    async fn wait_for_global_cache<A: Actor>(
486        hash: HashAlgorithm,
487        ctx: &ActorContext<A>,
488        initial_value: Value,
489        contract_hash: &DigestIdentifier,
490        manifest_hash: &DigestIdentifier,
491        engine_fingerprint: &DigestIdentifier,
492        toolchain_fingerprint: &DigestIdentifier,
493    ) -> Result<
494        Option<(
495            Arc<Module>,
496            ContractArtifactRecord,
497            &'static str,
498            Vec<u8>,
499            Vec<u8>,
500        )>,
501        CompilerError,
502    > {
503        let cache_dir = Self::global_cache_entry_dir(
504            contract_hash,
505            manifest_hash,
506            engine_fingerprint,
507            toolchain_fingerprint,
508        );
509        let lock_path = Self::global_cache_lock_path(&cache_dir);
510
511        loop {
512            if let Some(hit) = Self::try_load_global_cache(
513                hash,
514                ctx,
515                initial_value.clone(),
516                contract_hash,
517                manifest_hash,
518                engine_fingerprint,
519                toolchain_fingerprint,
520            )
521            .await?
522            {
523                return Ok(Some(hit));
524            }
525
526            if fs::metadata(&lock_path).await.is_err() {
527                return Ok(None);
528            }
529
530            sleep(Duration::from_millis(50)).await;
531        }
532    }
533
534    #[cfg(feature = "test")]
535    async fn load_global_cache_metadata(
536        cache_dir: &Path,
537    ) -> Result<ContractArtifactRecord, CompilerError> {
538        let metadata_path = Self::global_cache_metadata_path(cache_dir);
539        let metadata_bytes = fs::read(&metadata_path).await.map_err(|e| {
540            CompilerError::FileReadFailed {
541                path: metadata_path.to_string_lossy().to_string(),
542                details: e.to_string(),
543            }
544        })?;
545
546        ContractArtifactRecord::try_from_slice(&metadata_bytes).map_err(|e| {
547            CompilerError::SerializationError {
548                context: "global cache metadata",
549                details: e.to_string(),
550            }
551        })
552    }
553
554    fn deserialize_precompiled(
555        wasm_runtime: &WasmRuntime,
556        precompiled_bytes: &[u8],
557    ) -> Result<Module, CompilerError> {
558        unsafe {
559            Module::deserialize(&wasm_runtime.engine, precompiled_bytes)
560                .map_err(|e| CompilerError::WasmDeserializationFailed {
561                    details: e.to_string(),
562                })
563        }
564    }
565
566    fn precompile_module(
567        wasm_runtime: &WasmRuntime,
568        wasm_bytes: &[u8],
569    ) -> Result<(Vec<u8>, Module), CompilerError> {
570        let precompiled_bytes = wasm_runtime
571            .engine
572            .precompile_module(wasm_bytes)
573            .map_err(|e| CompilerError::WasmPrecompileFailed {
574                details: e.to_string(),
575            })?;
576
577        let module =
578            Self::deserialize_precompiled(wasm_runtime, &precompiled_bytes)?;
579
580        Ok((precompiled_bytes, module))
581    }
582
583    async fn validate_module<A: Actor>(
584        ctx: &ActorContext<A>,
585        module: &Module,
586        state: ValueWrapper,
587    ) -> Result<(), CompilerError> {
588        let wasm_runtime = Self::wasm_runtime(ctx).await?;
589
590        let imports = module.imports();
591        let mut pending_sdk = Self::get_sdk_functions_identifier();
592
593        for import in imports {
594            match import.ty() {
595                ExternType::Func(_) => {
596                    if !pending_sdk.remove(import.name()) {
597                        return Err(CompilerError::InvalidModule {
598                            kind: InvalidModuleKind::UnknownImportFunction {
599                                name: import.name().to_string(),
600                            },
601                        });
602                    }
603                }
604                extern_type => {
605                    return Err(CompilerError::InvalidModule {
606                        kind: InvalidModuleKind::NonFunctionImport {
607                            import_type: format!("{:?}", extern_type),
608                        },
609                    });
610                }
611            }
612        }
613        if !pending_sdk.is_empty() {
614            return Err(CompilerError::InvalidModule {
615                kind: InvalidModuleKind::MissingImports {
616                    missing: pending_sdk
617                        .into_iter()
618                        .map(|s| s.to_string())
619                        .collect(),
620                },
621            });
622        }
623
624        let (context, state_ptr) =
625            Self::generate_context(state, &wasm_runtime.limits)?;
626        let mut store = Store::new(&wasm_runtime.engine, context);
627
628        store.limiter(|data| &mut data.store_limits);
629        store.set_fuel(MAX_FUEL_COMPILATION).map_err(|e| {
630            CompilerError::FuelLimitError {
631                details: e.to_string(),
632            }
633        })?;
634
635        let linker = generate_linker(&wasm_runtime.engine)?;
636        let instance = linker.instantiate(&mut store, module).map_err(|e| {
637            CompilerError::InstantiationFailed {
638                details: e.to_string(),
639            }
640        })?;
641
642        let _main_contract_entrypoint = instance
643            .get_typed_func::<(u32, u32, u32, u32), u32>(
644                &mut store,
645                "main_function",
646            )
647            .map_err(|_e| CompilerError::EntryPointNotFound {
648                function: "main_function",
649            })?;
650
651        let init_contract_entrypoint = instance
652            .get_typed_func::<u32, u32>(&mut store, "init_check_function")
653            .map_err(|_e| CompilerError::EntryPointNotFound {
654                function: "init_check_function",
655            })?;
656
657        let result_ptr =
658            init_contract_entrypoint
659                .call(&mut store, state_ptr)
660                .map_err(|e| CompilerError::ContractExecutionFailed {
661                    details: e.to_string(),
662                })?;
663
664        Self::check_result(&store, result_ptr)?;
665
666        Ok(())
667    }
668
669    async fn wasm_runtime<A: Actor>(
670        ctx: &ActorContext<A>,
671    ) -> Result<Arc<WasmRuntime>, CompilerError> {
672        ctx.system()
673            .get_helper::<Arc<WasmRuntime>>("wasm_runtime")
674            .await
675            .ok_or(CompilerError::MissingHelper {
676                name: "wasm_runtime",
677            })
678    }
679
680    async fn contracts_helper<A: Actor>(
681        ctx: &ActorContext<A>,
682    ) -> Result<Arc<RwLock<HashMap<String, Arc<Module>>>>, ActorError> {
683        ctx.system()
684            .get_helper::<Arc<RwLock<HashMap<String, Arc<Module>>>>>(
685                "contracts",
686            )
687            .await
688            .ok_or_else(|| ActorError::Helper {
689                name: "contracts".to_owned(),
690                reason: "Not found".to_owned(),
691            })
692    }
693
694    fn build_contract_record(
695        hash: HashAlgorithm,
696        contract_hash: DigestIdentifier,
697        manifest_hash: DigestIdentifier,
698        wasm_bytes: &[u8],
699        precompiled_bytes: &[u8],
700        engine_fingerprint: DigestIdentifier,
701        toolchain_fingerprint: DigestIdentifier,
702    ) -> Result<ContractArtifactRecord, CompilerError> {
703        let wasm_hash = Self::hash_bytes(hash, wasm_bytes, "wasm artifact")?;
704        let cwasm_hash =
705            Self::hash_bytes(hash, precompiled_bytes, "cwasm artifact")?;
706
707        Ok(ContractArtifactRecord {
708            contract_hash,
709            manifest_hash,
710            wasm_hash,
711            cwasm_hash,
712            engine_fingerprint,
713            toolchain_fingerprint,
714        })
715    }
716
717    fn hash_bytes(
718        hash: HashAlgorithm,
719        bytes: &[u8],
720        context: &'static str,
721    ) -> Result<DigestIdentifier, CompilerError> {
722        hash_borsh(&*hash.hasher(), &bytes.to_vec()).map_err(|e| {
723            CompilerError::SerializationError {
724                context,
725                details: e.to_string(),
726            }
727        })
728    }
729
730    fn engine_fingerprint(
731        hash: HashAlgorithm,
732        wasm_runtime: &WasmRuntime,
733    ) -> Result<DigestIdentifier, CompilerError> {
734        let mut hasher = DefaultHasher::new();
735        wasm_runtime
736            .engine
737            .precompile_compatibility_hash()
738            .hash(&mut hasher);
739        hash_borsh(&*hash.hasher(), &hasher.finish()).map_err(|e| {
740            CompilerError::SerializationError {
741                context: "engine fingerprint",
742                details: e.to_string(),
743            }
744        })
745    }
746
747    async fn toolchain_fingerprint(
748        hash: HashAlgorithm,
749    ) -> Result<DigestIdentifier, CompilerError> {
750        let output = Command::new("rustc")
751            .arg("--version")
752            .arg("--verbose")
753            .output()
754            .await
755            .map_err(|e| CompilerError::ToolchainFingerprintFailed {
756                details: e.to_string(),
757            })?;
758
759        if !output.status.success() {
760            return Err(CompilerError::ToolchainFingerprintFailed {
761                details: String::from_utf8_lossy(&output.stderr).to_string(),
762            });
763        }
764
765        let fingerprint_input =
766            String::from_utf8_lossy(&output.stdout).to_string();
767        hash_borsh(&*hash.hasher(), &fingerprint_input).map_err(|e| {
768            CompilerError::SerializationError {
769                context: "toolchain fingerprint",
770                details: e.to_string(),
771            }
772        })
773    }
774
775    async fn contract_environment(
776        hash: HashAlgorithm,
777        wasm_runtime: &Arc<WasmRuntime>,
778    ) -> Result<(DigestIdentifier, DigestIdentifier), CompilerError> {
779        let engine_fingerprint = Self::engine_fingerprint(hash, wasm_runtime)?;
780        let toolchain_fingerprint = Self::toolchain_fingerprint(hash).await?;
781        Ok((engine_fingerprint, toolchain_fingerprint))
782    }
783
784    fn metadata_matches(
785        persisted: &ContractArtifactRecord,
786        expected_contract_hash: &DigestIdentifier,
787        expected_manifest_hash: &DigestIdentifier,
788        expected_engine_fingerprint: &DigestIdentifier,
789        expected_toolchain_fingerprint: &DigestIdentifier,
790    ) -> bool {
791        persisted.contract_hash == *expected_contract_hash
792            && persisted.manifest_hash == *expected_manifest_hash
793            && persisted.engine_fingerprint == *expected_engine_fingerprint
794            && persisted.toolchain_fingerprint
795                == *expected_toolchain_fingerprint
796    }
797
798    async fn compile_fresh<A: Actor>(
799        hash: HashAlgorithm,
800        ctx: &ActorContext<A>,
801        contract: &str,
802        contract_path: &Path,
803        initial_value: Value,
804    ) -> Result<(Arc<Module>, ContractArtifactRecord), CompilerError> {
805        let contract_hash =
806            hash_borsh(&*hash.hasher(), &contract).map_err(|e| {
807                CompilerError::SerializationError {
808                    context: "contract hash",
809                    details: e.to_string(),
810                }
811            })?;
812        let manifest = Self::compilation_toml();
813        let manifest_hash =
814            hash_borsh(&*hash.hasher(), &manifest).map_err(|e| {
815                CompilerError::SerializationError {
816                    context: "contract manifest hash",
817                    details: e.to_string(),
818                }
819            })?;
820
821        Self::prepare_contract_project(contract, contract_path).await?;
822
823        let wasm_runtime = Self::wasm_runtime(ctx).await?;
824        let (engine_fingerprint, toolchain_fingerprint) =
825            Self::contract_environment(hash, &wasm_runtime).await?;
826
827        let contracts_root = Self::contracts_root(contract_path)?;
828        Self::build_contract(
829            contract_path,
830            contracts_root.join(Self::VENDOR_DIR).exists(),
831        )
832        .await?;
833
834        let wasm_bytes = Self::load_compiled_wasm(&contracts_root).await?;
835        let (precompiled_bytes, module) =
836            Self::precompile_module(&wasm_runtime, &wasm_bytes)?;
837        Self::validate_module(ctx, &module, ValueWrapper(initial_value))
838            .await?;
839        Self::persist_artifact(contract_path, &wasm_bytes, &precompiled_bytes)
840            .await?;
841
842        let metadata = Self::build_contract_record(
843            hash,
844            contract_hash,
845            manifest_hash,
846            &wasm_bytes,
847            &precompiled_bytes,
848            engine_fingerprint,
849            toolchain_fingerprint,
850        )?;
851
852        #[cfg(feature = "test")]
853        {
854            let global_cache_dir = Self::global_cache_entry_dir(
855                &metadata.contract_hash,
856                &metadata.manifest_hash,
857                &metadata.engine_fingerprint,
858                &metadata.toolchain_fingerprint,
859            );
860            if let Err(error) = Self::persist_global_cache_artifact(
861                &global_cache_dir,
862                &metadata,
863                &wasm_bytes,
864                &precompiled_bytes,
865            )
866            .await
867            {
868                debug!(
869                    error = %error,
870                    path = %global_cache_dir.display(),
871                    "Failed to persist global contract cache artifact"
872                );
873            }
874        }
875
876        Ok((Arc::new(module), metadata))
877    }
878
879    #[cfg(feature = "test")]
880    async fn try_load_global_cache<A: Actor>(
881        hash: HashAlgorithm,
882        ctx: &ActorContext<A>,
883        initial_value: Value,
884        contract_hash: &DigestIdentifier,
885        manifest_hash: &DigestIdentifier,
886        engine_fingerprint: &DigestIdentifier,
887        toolchain_fingerprint: &DigestIdentifier,
888    ) -> Result<
889        Option<(
890            Arc<Module>,
891            ContractArtifactRecord,
892            &'static str,
893            Vec<u8>,
894            Vec<u8>,
895        )>,
896        CompilerError,
897    > {
898        let cache_dir = Self::global_cache_entry_dir(
899            contract_hash,
900            manifest_hash,
901            engine_fingerprint,
902            toolchain_fingerprint,
903        );
904
905        let persisted = match Self::load_global_cache_metadata(&cache_dir).await
906        {
907            Ok(metadata) => metadata,
908            Err(error) => {
909                debug!(
910                    error = %error,
911                    path = %cache_dir.display(),
912                    "Global contract cache metadata unavailable"
913                );
914                return Ok(None);
915            }
916        };
917
918        if !Self::metadata_matches(
919            &persisted,
920            contract_hash,
921            manifest_hash,
922            engine_fingerprint,
923            toolchain_fingerprint,
924        ) {
925            return Ok(None);
926        }
927
928        let wasm_runtime = Self::wasm_runtime(ctx).await?;
929        let wasm_bytes = match Self::load_artifact_wasm_from(&cache_dir).await {
930            Ok(bytes) => bytes,
931            Err(error) => {
932                debug!(
933                    error = %error,
934                    path = %cache_dir.display(),
935                    "Global contract cache wasm artifact unavailable"
936                );
937                return Ok(None);
938            }
939        };
940
941        let wasm_hash =
942            Self::hash_bytes(hash, &wasm_bytes, "global cache wasm artifact")?;
943        if wasm_hash != persisted.wasm_hash {
944            debug!(
945                expected = %persisted.wasm_hash,
946                actual = %wasm_hash,
947                path = %cache_dir.display(),
948                "Global cache wasm artifact hash mismatch"
949            );
950            return Ok(None);
951        }
952
953        if let Ok(precompiled_bytes) =
954            Self::load_artifact_precompiled_from(&cache_dir).await
955        {
956            let precompiled_hash = Self::hash_bytes(
957                hash,
958                &precompiled_bytes,
959                "global cache cwasm artifact",
960            )?;
961            if precompiled_hash == persisted.cwasm_hash
962                && let Ok(module) = Self::deserialize_precompiled(
963                    &wasm_runtime,
964                    &precompiled_bytes,
965                )
966                && Self::validate_module(
967                    ctx,
968                    &module,
969                    ValueWrapper(initial_value.clone()),
970                )
971                .await
972                .is_ok()
973            {
974                return Ok(Some((
975                    Arc::new(module),
976                    persisted,
977                    "global_cwasm_hit",
978                    wasm_bytes,
979                    precompiled_bytes,
980                )));
981            }
982        }
983
984        if let Ok((precompiled_bytes, module)) =
985            Self::precompile_module(&wasm_runtime, &wasm_bytes)
986            && Self::validate_module(ctx, &module, ValueWrapper(initial_value))
987                .await
988                .is_ok()
989        {
990            let refreshed_record = Self::build_contract_record(
991                hash,
992                contract_hash.clone(),
993                manifest_hash.clone(),
994                &wasm_bytes,
995                &precompiled_bytes,
996                engine_fingerprint.clone(),
997                toolchain_fingerprint.clone(),
998            )?;
999
1000            if let Err(error) = Self::persist_global_cache_artifact(
1001                &cache_dir,
1002                &refreshed_record,
1003                &wasm_bytes,
1004                &precompiled_bytes,
1005            )
1006            .await
1007            {
1008                debug!(
1009                    error = %error,
1010                    path = %cache_dir.display(),
1011                    "Failed to refresh global contract cache artifact"
1012                );
1013            }
1014
1015            return Ok(Some((
1016                Arc::new(module),
1017                refreshed_record,
1018                "global_wasm_hit",
1019                wasm_bytes,
1020                precompiled_bytes,
1021            )));
1022        }
1023
1024        Ok(None)
1025    }
1026
1027    async fn compile_or_load_registered<A: Actor>(
1028        hash: HashAlgorithm,
1029        ctx: &ActorContext<A>,
1030        contract_name: &str,
1031        contract: &str,
1032        contract_path: &Path,
1033        initial_value: Value,
1034    ) -> Result<(Arc<Module>, ContractArtifactRecord), CompilerError> {
1035        let started_at = Instant::now();
1036        let result = async {
1037            let contract_hash =
1038                hash_borsh(&*hash.hasher(), &contract).map_err(|e| {
1039                    CompilerError::SerializationError {
1040                        context: "contract hash",
1041                        details: e.to_string(),
1042                    }
1043                })?;
1044            let manifest = Self::compilation_toml();
1045            let manifest_hash =
1046                hash_borsh(&*hash.hasher(), &manifest).map_err(|e| {
1047                    CompilerError::SerializationError {
1048                        context: "contract manifest hash",
1049                        details: e.to_string(),
1050                    }
1051                })?;
1052
1053            Self::prepare_contract_project(contract, contract_path).await?;
1054
1055            let wasm_runtime = Self::wasm_runtime(ctx).await?;
1056            let (engine_fingerprint, toolchain_fingerprint) =
1057                Self::contract_environment(hash, &wasm_runtime).await?;
1058
1059            let parent_path = ctx.path().parent();
1060            let register_path =
1061                ActorPath::from(format!("{}/contract_register", parent_path));
1062            let register = ctx
1063                .system()
1064                .get_actor::<ContractRegister>(&register_path)
1065                .await
1066                .map_err(|e| CompilerError::ContractRegisterFailed {
1067                    details: e.to_string(),
1068                })?;
1069
1070            let persisted = match register
1071                .ask(ContractRegisterMessage::GetMetadata {
1072                    contract_name: contract_name.to_owned(),
1073                })
1074                .await
1075            {
1076                Ok(ContractRegisterResponse::Metadata(metadata)) => metadata,
1077                Ok(ContractRegisterResponse::Contracts(_)) => None,
1078                Ok(ContractRegisterResponse::Ok) => None,
1079                Err(e) => {
1080                    return Err(CompilerError::ContractRegisterFailed {
1081                        details: e.to_string(),
1082                    });
1083                }
1084            };
1085
1086            if let Some(persisted) = persisted
1087                && Self::metadata_matches(
1088                    &persisted,
1089                    &contract_hash,
1090                    &manifest_hash,
1091                    &engine_fingerprint,
1092                    &toolchain_fingerprint,
1093                )
1094            {
1095                match Self::load_artifact_precompiled(contract_path).await {
1096                    Ok(precompiled_bytes) => {
1097                        let precompiled_hash = Self::hash_bytes(
1098                            hash,
1099                            &precompiled_bytes,
1100                            "persisted cwasm artifact",
1101                        )?;
1102                        if precompiled_hash == persisted.cwasm_hash {
1103                            match Self::deserialize_precompiled(
1104                                &wasm_runtime,
1105                                &precompiled_bytes,
1106                            ) {
1107                                Ok(module) => {
1108                                    match Self::validate_module(
1109                                        ctx,
1110                                        &module,
1111                                        ValueWrapper(initial_value.clone()),
1112                                    )
1113                                    .await
1114                                    {
1115                                        Ok(()) => {
1116                                            return Ok((
1117                                                Arc::new(module),
1118                                                persisted,
1119                                                "cwasm_hit",
1120                                            ));
1121                                        }
1122                                        Err(error) => {
1123                                            debug!(
1124                                                error = %error,
1125                                                path = %contract_path.display(),
1126                                                "Persisted precompiled contract is invalid, retrying from wasm artifact"
1127                                            );
1128                                        }
1129                                    }
1130                                }
1131                                Err(error) => {
1132                                    debug!(
1133                                        error = %error,
1134                                        path = %contract_path.display(),
1135                                        "Persisted precompiled contract can not be deserialized, retrying from wasm artifact"
1136                                    );
1137                                }
1138                            }
1139                        } else {
1140                            debug!(
1141                                expected = %persisted.cwasm_hash,
1142                                actual = %precompiled_hash,
1143                                path = %contract_path.display(),
1144                                "Persisted precompiled artifact hash mismatch, retrying from wasm artifact"
1145                            );
1146                        }
1147                    }
1148                    Err(error) => {
1149                        debug!(
1150                            error = %error,
1151                            path = %contract_path.display(),
1152                            "Persisted precompiled artifact can not be read, retrying from wasm artifact"
1153                        );
1154                    }
1155                }
1156
1157                match Self::load_artifact_wasm(contract_path).await {
1158                    Ok(wasm_bytes) => {
1159                        let wasm_hash = Self::hash_bytes(
1160                            hash,
1161                            &wasm_bytes,
1162                            "persisted wasm artifact",
1163                        )?;
1164                        if wasm_hash == persisted.wasm_hash {
1165                            match Self::precompile_module(
1166                                &wasm_runtime,
1167                                &wasm_bytes,
1168                            ) {
1169                                Ok((precompiled_bytes, module)) => {
1170                                    match Self::validate_module(
1171                                        ctx,
1172                                        &module,
1173                                        ValueWrapper(initial_value.clone()),
1174                                    )
1175                                    .await
1176                                    {
1177                                        Ok(()) => {
1178                                            Self::persist_artifact(
1179                                                contract_path,
1180                                                &wasm_bytes,
1181                                                &precompiled_bytes,
1182                                            )
1183                                            .await?;
1184                                            let refreshed_record =
1185                                                Self::build_contract_record(
1186                                                    hash,
1187                                                    contract_hash.clone(),
1188                                                    manifest_hash.clone(),
1189                                                    &wasm_bytes,
1190                                                    &precompiled_bytes,
1191                                                    engine_fingerprint.clone(),
1192                                                    toolchain_fingerprint
1193                                                        .clone(),
1194                                                )?;
1195
1196                                            register
1197                                                .tell(
1198                                                    ContractRegisterMessage::SetMetadata {
1199                                                        contract_name: contract_name
1200                                                            .to_owned(),
1201                                                        metadata: refreshed_record
1202                                                            .clone(),
1203                                                    },
1204                                                )
1205                                                .await
1206                                                .map_err(|e| {
1207                                                    CompilerError::ContractRegisterFailed {
1208                                                        details: e.to_string(),
1209                                                    }
1210                                                })?;
1211
1212                                            return Ok((
1213                                                Arc::new(module),
1214                                                refreshed_record,
1215                                                "wasm_hit",
1216                                            ));
1217                                        }
1218                                        Err(error) => {
1219                                            debug!(
1220                                                error = %error,
1221                                                path = %contract_path.display(),
1222                                                "Persisted wasm artifact is invalid, recompiling"
1223                                            );
1224                                        }
1225                                    }
1226                                }
1227                                Err(error) => {
1228                                    debug!(
1229                                        error = %error,
1230                                        path = %contract_path.display(),
1231                                        "Persisted wasm artifact can not be precompiled, recompiling"
1232                                    );
1233                                }
1234                            }
1235                        } else {
1236                            debug!(
1237                                expected = %persisted.wasm_hash,
1238                                actual = %wasm_hash,
1239                                path = %contract_path.display(),
1240                                "Persisted wasm artifact hash mismatch, recompiling"
1241                            );
1242                        }
1243                    }
1244                    Err(error) => {
1245                        debug!(
1246                            error = %error,
1247                            path = %contract_path.display(),
1248                            "Persisted contract artifact can not be read, recompiling"
1249                        );
1250                    }
1251                }
1252            }
1253
1254            #[cfg(feature = "test")]
1255            let cache_dir = Self::global_cache_entry_dir(
1256                &contract_hash,
1257                &manifest_hash,
1258                &engine_fingerprint,
1259                &toolchain_fingerprint,
1260            );
1261
1262            #[cfg(feature = "test")]
1263            if let Some((
1264                module,
1265                metadata,
1266                prepare_result,
1267                wasm_bytes,
1268                precompiled_bytes,
1269            )) = Self::try_load_global_cache(
1270                hash,
1271                ctx,
1272                initial_value.clone(),
1273                &contract_hash,
1274                &manifest_hash,
1275                &engine_fingerprint,
1276                &toolchain_fingerprint,
1277            )
1278            .await?
1279            {
1280                Self::persist_artifact(
1281                    contract_path,
1282                    &wasm_bytes,
1283                    &precompiled_bytes,
1284                )
1285                .await?;
1286
1287                register
1288                    .tell(ContractRegisterMessage::SetMetadata {
1289                        contract_name: contract_name.to_owned(),
1290                        metadata: metadata.clone(),
1291                    })
1292                    .await
1293                    .map_err(|e| CompilerError::ContractRegisterFailed {
1294                        details: e.to_string(),
1295                    })?;
1296
1297                return Ok((module, metadata, prepare_result));
1298            }
1299
1300            #[cfg(feature = "test")]
1301            let global_cache_lock =
1302                match Self::try_acquire_global_cache_lock(&cache_dir).await? {
1303                    Some(lock) => Some(lock),
1304                    None => {
1305                        if let Some((
1306                            module,
1307                            metadata,
1308                            prepare_result,
1309                            wasm_bytes,
1310                            precompiled_bytes,
1311                        )) = Self::wait_for_global_cache(
1312                            hash,
1313                            ctx,
1314                            initial_value.clone(),
1315                            &contract_hash,
1316                            &manifest_hash,
1317                            &engine_fingerprint,
1318                            &toolchain_fingerprint,
1319                        )
1320                        .await?
1321                        {
1322                            Self::persist_artifact(
1323                                contract_path,
1324                                &wasm_bytes,
1325                                &precompiled_bytes,
1326                            )
1327                            .await?;
1328
1329                            register
1330                                .tell(ContractRegisterMessage::SetMetadata {
1331                                    contract_name: contract_name.to_owned(),
1332                                    metadata: metadata.clone(),
1333                                })
1334                                .await
1335                                .map_err(|e| {
1336                                    CompilerError::ContractRegisterFailed {
1337                                        details: e.to_string(),
1338                                    }
1339                                })?;
1340
1341                            return Ok((module, metadata, prepare_result));
1342                        }
1343
1344                        Self::try_acquire_global_cache_lock(&cache_dir).await?
1345                    }
1346                };
1347
1348            let (module, metadata) = Self::compile_fresh(
1349                hash,
1350                ctx,
1351                contract,
1352                contract_path,
1353                initial_value,
1354            )
1355            .await?;
1356
1357            #[cfg(feature = "test")]
1358            drop(global_cache_lock);
1359
1360            register
1361                .tell(ContractRegisterMessage::SetMetadata {
1362                    contract_name: contract_name.to_owned(),
1363                    metadata: metadata.clone(),
1364                })
1365                .await
1366                .map_err(|e| CompilerError::ContractRegisterFailed {
1367                    details: e.to_string(),
1368                })?;
1369
1370            Ok((module, metadata, "recompiled"))
1371        }
1372        .await;
1373
1374        match result {
1375            Ok((module, metadata, prepare_result)) => {
1376                Self::observe_contract_prepare(
1377                    "registered",
1378                    prepare_result,
1379                    started_at,
1380                );
1381                Ok((module, metadata))
1382            }
1383            Err(error) => {
1384                Self::observe_contract_prepare(
1385                    "registered",
1386                    "error",
1387                    started_at,
1388                );
1389                Err(error)
1390            }
1391        }
1392    }
1393
1394    fn check_result(
1395        store: &Store<MemoryManager>,
1396        pointer: u32,
1397    ) -> Result<(), CompilerError> {
1398        let bytes = store.data().read_data(pointer as usize)?;
1399        let contract_result: ContractResult =
1400            BorshDeserialize::try_from_slice(bytes).map_err(|e| {
1401                CompilerError::InvalidContractOutput {
1402                    details: e.to_string(),
1403                }
1404            })?;
1405
1406        if contract_result.success {
1407            Ok(())
1408        } else {
1409            Err(CompilerError::ContractCheckFailed {
1410                error: contract_result.error,
1411            })
1412        }
1413    }
1414
1415    fn generate_context(
1416        state: ValueWrapper,
1417        limits: &WasmLimits,
1418    ) -> Result<(MemoryManager, u32), CompilerError> {
1419        let mut context = MemoryManager::from_limits(limits);
1420        let state_bytes =
1421            to_vec(&state).map_err(|e| CompilerError::SerializationError {
1422                context: "state serialization",
1423                details: e.to_string(),
1424            })?;
1425        let state_ptr = context.add_data_raw(&state_bytes)?;
1426        Ok((context, state_ptr as u32))
1427    }
1428
1429    fn get_sdk_functions_identifier() -> HashSet<&'static str> {
1430        ["alloc", "write_byte", "pointer_len", "read_byte"]
1431            .into_iter()
1432            .collect()
1433    }
1434}
1435
1436#[cfg(feature = "test")]
1437struct GlobalCacheLock {
1438    path: PathBuf,
1439}
1440
1441#[cfg(feature = "test")]
1442impl Drop for GlobalCacheLock {
1443    fn drop(&mut self) {
1444        let _ = std::fs::remove_file(&self.path);
1445    }
1446}