use std::{
fs,
io,
path::{Path, PathBuf},
};
use log::*;
use memmap2::Mmap;
use tari_engine_types::published_template::PublishedTemplate;
use tari_ootle_common_types::{
Epoch,
services::template_provider::{TemplateMetadataProvider, TemplateProvider, TemplateProviderMetadata},
};
use tari_template_builtin::is_builtin_template_address;
use tari_template_lib::types::TemplateAddress;
use crate::{
template::{LoadedTemplate, TemplateLoaderError},
wasm::WasmModule,
};
const LOG_TARGET: &str = "tari::engine::wasm::cache";
pub const ENGINE_FINGERPRINT: &str = "v1";
const HEADER_BYTES: usize = 8;
#[derive(Debug, Clone)]
pub struct WasmModuleCache {
dir: PathBuf,
}
impl WasmModuleCache {
pub fn open(dir: impl Into<PathBuf>) -> io::Result<Self> {
let dir = dir.into();
fs::create_dir_all(&dir)?;
Ok(Self { dir })
}
pub fn dir(&self) -> &Path {
&self.dir
}
fn path_for(&self, addr: &TemplateAddress) -> PathBuf {
self.dir.join(format!("{}_{}.bin", addr, ENGINE_FINGERPRINT))
}
pub fn try_load(&self, addr: &TemplateAddress) -> Option<LoadedTemplate> {
let path = self.path_for(addr);
let file = match fs::File::open(&path) {
Ok(f) => f,
Err(e) if e.kind() == io::ErrorKind::NotFound => return None,
Err(e) => {
warn!(
target: LOG_TARGET,
"Failed to open cache file {}: {}", path.display(), e,
);
return None;
},
};
let mmap = match unsafe { Mmap::map(&file) } {
Ok(m) => m,
Err(e) => {
warn!(
target: LOG_TARGET,
"Failed to mmap cache file {}: {}", path.display(), e,
);
return None;
},
};
if mmap.len() < HEADER_BYTES {
warn!(
target: LOG_TARGET,
"Cache file {} is shorter than the {}-byte header; removing.",
path.display(),
HEADER_BYTES,
);
drop(mmap);
let _ignore = fs::remove_file(&path);
return None;
}
let mut header = [0u8; HEADER_BYTES];
header.copy_from_slice(&mmap[..HEADER_BYTES]);
let code_size = u64::from_le_bytes(header) as usize;
let body = bytes::Bytes::from_owner(mmap).slice(HEADER_BYTES..);
match unsafe { WasmModule::load_template_from_serialized(body, code_size) } {
Ok(loaded) => {
debug!(target: LOG_TARGET, "Cache hit for template {}", addr);
Some(loaded)
},
Err(err) => {
warn!(
target: LOG_TARGET,
"Failed to deserialize cached module {}: {}; removing.",
path.display(),
err,
);
let _ignore = fs::remove_file(&path);
None
},
}
}
pub fn store(&self, addr: &TemplateAddress, loaded: &LoadedTemplate) {
let LoadedTemplate::Wasm(wasm) = loaded;
let serialized = match wasm.wasm_module().serialize() {
Ok(s) => s,
Err(e) => {
warn!(target: LOG_TARGET, "Failed to serialize module for {}: {}", addr, e);
return;
},
};
let path = self.path_for(addr);
let tmp = self.dir.join(format!(
"{}_{}.bin.tmp.{}",
addr,
ENGINE_FINGERPRINT,
std::process::id(),
));
let mut bytes = Vec::with_capacity(HEADER_BYTES + serialized.len());
bytes.extend_from_slice(&(wasm.code_size() as u64).to_le_bytes());
bytes.extend_from_slice(&serialized);
if let Err(e) = fs::write(&tmp, &bytes) {
warn!(target: LOG_TARGET, "Failed to write cache tempfile {}: {}", tmp.display(), e);
return;
}
if let Err(e) = fs::rename(&tmp, &path) {
warn!(
target: LOG_TARGET,
"Failed to rename {} -> {}: {}", tmp.display(), path.display(), e,
);
let _ignore = fs::remove_file(&tmp);
return;
}
debug!(
target: LOG_TARGET,
"Cached compiled module for template {} -> {}", addr, path.display(),
);
}
}
#[derive(Debug, Clone)]
pub struct DiskCachedWasmTemplateProvider<TStore> {
inner: TStore,
cache: WasmModuleCache,
}
impl<TStore> DiskCachedWasmTemplateProvider<TStore> {
pub fn new(inner: TStore, cache: WasmModuleCache) -> Self {
Self { inner, cache }
}
pub fn open(inner: TStore, path: impl Into<PathBuf>) -> io::Result<Self> {
let wasm_cache = WasmModuleCache::open(path)?;
Ok(Self::new(inner, wasm_cache))
}
}
impl<TStore> TemplateProvider for DiskCachedWasmTemplateProvider<TStore>
where TStore: TemplateProvider<Template = PublishedTemplate> + Clone + 'static
{
type Error = DiskCachedWasmTemplateProviderError;
type Template = LoadedTemplate;
fn get_template(&self, address: &TemplateAddress) -> Result<Option<Self::Template>, Self::Error> {
if is_builtin_template_address(address) {
let Some(published) = self
.inner
.get_template(address)
.map_err(|e| DiskCachedWasmTemplateProviderError::Inner(e.into()))?
else {
return Ok(None);
};
return Ok(Some(WasmModule::load_template_from_code(published.binary.as_slice())?));
}
if let Some(loaded) = self.cache.try_load(address) {
return Ok(Some(loaded));
}
let Some(published) = self
.inner
.get_template(address)
.map_err(|e| DiskCachedWasmTemplateProviderError::Inner(e.into()))?
else {
return Ok(None);
};
let loaded = WasmModule::load_template_from_code(published.binary.as_slice())?;
self.cache.store(address, &loaded);
Ok(Some(loaded))
}
fn has_template(&self, address: &TemplateAddress) -> Result<bool, Self::Error> {
if !is_builtin_template_address(address) && self.cache.path_for(address).exists() {
return Ok(true);
}
self.inner
.has_template(address)
.map_err(|e| DiskCachedWasmTemplateProviderError::Inner(e.into()))
}
}
impl<TStore> TemplateMetadataProvider for DiskCachedWasmTemplateProvider<TStore>
where TStore: TemplateProvider<Template = PublishedTemplate> + Clone + 'static
{
fn get_template_metadata(&self, id: &TemplateAddress) -> Result<Option<TemplateProviderMetadata>, Self::Error> {
let template = self
.inner
.get_template(id)
.map_err(|e| DiskCachedWasmTemplateProviderError::Inner(e.into()))?;
Ok(template.map(|t| TemplateProviderMetadata {
author: t.author,
binary_hash: t.to_binary_hash(),
epoch: Epoch(t.at_epoch),
metadata_hash: t.metadata_hash,
}))
}
}
#[derive(Debug, thiserror::Error)]
pub enum DiskCachedWasmTemplateProviderError {
#[error("Inner template provider error: {0}")]
Inner(#[source] Box<dyn std::error::Error + Send + Sync>),
#[error(transparent)]
TemplateLoader(#[from] TemplateLoaderError),
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use tari_engine_types::published_template::PublishedTemplate;
use tari_template_builtin::all_builtin_templates;
use tari_template_lib::types::crypto::RistrettoPublicKeyBytes;
use tempfile::TempDir;
use super::*;
#[derive(Clone)]
struct StaticStore {
templates: Arc<std::collections::HashMap<TemplateAddress, PublishedTemplate>>,
}
#[derive(Debug, thiserror::Error)]
#[error("not found")]
struct StaticStoreError;
impl TemplateProvider for StaticStore {
type Error = StaticStoreError;
type Template = PublishedTemplate;
fn get_template(&self, address: &TemplateAddress) -> Result<Option<Self::Template>, Self::Error> {
Ok(self.templates.get(address).cloned())
}
}
fn make_store() -> (StaticStore, TemplateAddress) {
let template = all_builtin_templates()
.iter()
.find(|t| t.name == "Account")
.expect("Account builtin");
let test_addr = TemplateAddress::from_array([
0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42,
0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42, 0x42,
]);
debug_assert!(
!is_builtin_template_address(&test_addr),
"test address must not collide with a builtin",
);
let mut map = std::collections::HashMap::new();
let published = PublishedTemplate {
template_name: template.name.try_into().expect("valid name"),
author: RistrettoPublicKeyBytes::default(),
binary: template.binary.to_vec().try_into().expect("template binary too large"),
at_epoch: 0,
metadata_hash: None,
};
map.insert(test_addr, published);
(
StaticStore {
templates: Arc::new(map),
},
test_addr,
)
}
#[test]
fn round_trip_compile_then_deserialize() {
let dir = TempDir::new().unwrap();
let cache = WasmModuleCache::open(dir.path()).unwrap();
let (store, addr) = make_store();
let provider = DiskCachedWasmTemplateProvider::new(store.clone(), cache.clone());
let first = provider.get_template(&addr).unwrap().expect("loaded");
assert!(cache.path_for(&addr).exists(), "store should write a file");
let second = provider.get_template(&addr).unwrap().expect("loaded");
assert_eq!(first.template_name(), second.template_name());
assert_eq!(first.code_size(), second.code_size());
assert_eq!(
first.template_def().functions().len(),
second.template_def().functions().len(),
);
}
#[test]
fn corrupt_cache_falls_back_to_recompile() {
let dir = TempDir::new().unwrap();
let cache = WasmModuleCache::open(dir.path()).unwrap();
let (store, addr) = make_store();
let path = cache.path_for(&addr);
fs::write(&path, b"this is not a wasmer artifact").unwrap();
assert!(path.exists());
assert!(cache.try_load(&addr).is_none());
assert!(!path.exists(), "corrupt file should be removed");
let provider = DiskCachedWasmTemplateProvider::new(store, cache.clone());
provider.get_template(&addr).unwrap().expect("loaded");
assert!(path.exists(), "fresh compile should re-populate the cache");
assert!(cache.try_load(&addr).is_some());
}
#[test]
fn fingerprint_mismatch_treated_as_miss() {
let dir = TempDir::new().unwrap();
let cache = WasmModuleCache::open(dir.path()).unwrap();
let (_store, addr) = make_store();
let alt = dir.path().join(format!("{}_v0.bin", addr));
fs::write(&alt, b"some bytes").unwrap();
assert!(cache.try_load(&addr).is_none());
assert!(alt.exists(), "files for other fingerprints are left alone");
}
}