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