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>(®ister_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}