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