use super::{
RuntimeResult,
contract_store::ContractStore,
delegate_api::DelegateApiVersion,
delegate_store::DelegateStore,
engine::{BackendEngine, Engine, InstanceHandle, WasmEngine},
error::RuntimeInnerError,
native_api,
secrets_store::SecretsStore,
};
use freenet_stdlib::{
memory::{
WasmLinearMem,
buf::{BufferBuilder, BufferMut},
},
prelude::*,
};
use std::path::Path;
use std::sync::atomic::AtomicI64;
use std::sync::{Arc, Mutex};
use super::ModuleCache;
pub(crate) type SharedModuleCache<K> = Arc<Mutex<ModuleCache<K, <Engine as WasmEngine>::Module>>>;
fn wasm_code_hash(contract: &ContractContainer) -> RuntimeResult<CodeHash> {
match contract {
ContractContainer::Wasm(ContractWasmAPIVersion::V1(contract_v1)) => {
Ok(CodeHash::from_code(contract_v1.code().data()))
}
ContractContainer::Wasm(_) | _ => {
Err(anyhow::anyhow!("unsupported contract container version").into())
}
}
}
const SEEN_CAP: usize = 4096;
const _: () = assert!(
SEEN_CAP > 0 && SEEN_CAP <= 65_536,
"SEEN_CAP must be a real bound: `SEEN` is keyed on a contract code hash a \
remote PUT chooses, and .claude/rules/code-style.md forbids an unbounded \
per-key collection on externally-influenced data. A zero cap is also wrong \
— it records nothing, so every clock-reading contract re-warns on every \
module-cache miss."
);
fn warn_on_host_clock_import(key: &ContractKey, code_hash: &CodeHash, code: &[u8]) -> bool {
static SEEN: std::sync::OnceLock<Mutex<std::collections::HashSet<CodeHash>>> =
std::sync::OnceLock::new();
if !crate::conformance::imports_host_clock(code) {
return false;
}
let seen = SEEN.get_or_init(Default::default);
if !decide_host_clock_warning(seen, SEEN_CAP, code_hash, code) {
return false;
}
tracing::warn!(
contract = %key,
%code_hash,
namespace = crate::conformance::HOST_CLOCK_NAMESPACE,
function = crate::conformance::HOST_CLOCK_IMPORT,
docs = crate::conformance::HOST_CLOCK_DEPRECATION_DOC,
"this contract imports the host wall clock, which is DEPRECATED for \
contracts: a merge that reads the clock is not a function of its \
inputs, so replicas of this contract are not guaranteed to converge. \
In a future release the call will TRAP (issue #5465) — the contract \
will still load, but any actual call to the clock will fail that \
operation. A contract that imports the symbol without reaching it \
keeps working and needs no re-key. Delegates are unaffected. See the \
docs link for what to do instead"
);
true
}
fn decide_host_clock_warning(
seen: &Mutex<std::collections::HashSet<CodeHash>>,
cap: usize,
code_hash: &CodeHash,
code: &[u8],
) -> bool {
if !crate::conformance::imports_host_clock(code) {
return false;
}
let mut seen = seen.lock().unwrap_or_else(|e| e.into_inner());
if seen.contains(code_hash) {
return false;
}
if seen.len() < cap {
seen.insert(*code_hash);
}
true
}
static INSTANCE_ID: AtomicI64 = AtomicI64::new(0);
pub(super) struct RunningInstance {
pub id: i64,
pub handle: InstanceHandle,
pub supports_streaming: bool,
dropped_from_engine: bool,
}
impl RunningInstance {
fn new(
engine: &mut Engine,
module: &<Engine as WasmEngine>::Module,
key: Key,
req_bytes: usize,
) -> RuntimeResult<Self> {
let id = INSTANCE_ID.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
let handle = super::classify_result(engine.create_instance(module, id, req_bytes))?;
let (ptr, size) = engine.memory_info(&handle)?;
native_api::MEM_ADDR.insert(id, InstanceInfo::new(ptr as i64, size, key));
let supports_streaming = engine.module_has_streaming_io(module);
Ok(Self {
id,
handle,
supports_streaming,
dropped_from_engine: false,
})
}
}
impl Drop for RunningInstance {
fn drop(&mut self) {
if !self.dropped_from_engine {
tracing::debug!(
instance_id = self.id,
"RunningInstance dropped without engine cleanup — MEM_ADDR cleaned up, \
but WASM Instance will leak until engine is dropped"
);
}
let _ = native_api::MEM_ADDR.remove(&self.id);
}
}
pub(crate) struct InstanceInfo {
pub start_ptr: i64,
pub mem_size: usize,
key: Key,
}
impl InstanceInfo {
pub(crate) fn new(start_ptr: i64, mem_size: usize, key: Key) -> Self {
Self {
start_ptr,
mem_size,
key,
}
}
pub fn key(&self) -> String {
match &self.key {
Key::Contract(k) => k.encode(),
Key::Delegate(k) => k.encode(),
}
}
}
pub(super) enum Key {
Contract(ContractInstanceId),
Delegate(DelegateKey),
}
#[derive(thiserror::Error, Debug)]
pub enum ContractExecError {
#[error(transparent)]
ContractError(#[from] ContractError),
#[error("Attempted to perform a put for an already put contract ({0}), use update instead")]
DoublePut(ContractKey),
#[error("could not cast array length of {0} to max size (i32::MAX)")]
InvalidArrayLength(usize),
#[error("unexpected result from contract interface")]
UnexpectedResult,
#[error(
"The operation ran out of gas. This might be caused by an infinite loop or an inefficient computation."
)]
OutOfGas,
#[error("The operation exceeded the maximum allowed compute time")]
MaxComputeTimeExceeded,
#[error("The operation was queued too long on a saturated execution pool and never ran")]
SchedulerOverloaded,
#[error("module is not a contract: missing required export(s): {missing}")]
MissingContractExports { missing: String },
}
pub struct RuntimeConfig {
pub max_execution_seconds: f64,
pub cpu_cycles_per_second: Option<u64>,
pub safety_margin: f64,
pub enable_metering: bool,
pub module_cache_budget_bytes: usize,
pub offload_compilation: bool,
pub wasmtime_cache_dir: Option<std::path::PathBuf>,
pub wasmtime_cache_size_bytes: Option<u64>,
}
pub(crate) const MIN_WASMTIME_CACHE_SIZE_BYTES: u64 = 128 * 1024 * 1024;
pub(crate) const MAX_WASMTIME_CACHE_SIZE_BYTES: u64 = 512 * 1024 * 1024;
const WASMTIME_CACHE_RAM_DIVISOR: u64 = 8;
const WASMTIME_CACHE_FALLBACK_TOTAL_RAM_BYTES: u64 = 1024 * 1024 * 1024;
pub(crate) fn wasmtime_cache_size_for_ram(total_ram: u64) -> u64 {
(total_ram / WASMTIME_CACHE_RAM_DIVISOR)
.clamp(MIN_WASMTIME_CACHE_SIZE_BYTES, MAX_WASMTIME_CACHE_SIZE_BYTES)
}
const WASMTIME_CACHE_DISK_DIVISOR: u64 = 8;
const MIN_WASMTIME_CACHE_SIZE_BYTES_FOR_DISK: u64 = MIN_WASMTIME_CACHE_SIZE_BYTES / 4;
pub(crate) fn wasmtime_cache_size_for_disk(available_disk_bytes: u64) -> u64 {
(available_disk_bytes / WASMTIME_CACHE_DISK_DIVISOR).clamp(
MIN_WASMTIME_CACHE_SIZE_BYTES_FOR_DISK,
MAX_WASMTIME_CACHE_SIZE_BYTES,
)
}
pub(crate) fn combine_wasmtime_cache_size(
total_ram: u64,
available_disk_bytes: Option<u64>,
) -> u64 {
let ram_term = wasmtime_cache_size_for_ram(total_ram);
match available_disk_bytes {
Some(available) => ram_term.min(wasmtime_cache_size_for_disk(available)),
None => ram_term,
}
}
pub(crate) fn default_wasmtime_cache_size_bytes_for_dir(
dir: &Path,
hosting_disk_pct: f64,
max_hosting_disk: u64,
) -> u64 {
let total_ram = super::read_total_ram_bytes()
.map(|v| v as u64)
.unwrap_or(WASMTIME_CACHE_FALLBACK_TOTAL_RAM_BYTES);
let current_cache_bytes = crate::ring::disk_directory_size_bytes(dir);
let raw_available_disk_bytes = crate::ring::disk_available_bytes(dir);
let stabilized_available_disk_bytes =
stabilize_available_disk_bytes(raw_available_disk_bytes, current_cache_bytes);
let physical_term = combine_wasmtime_cache_size(total_ram, stabilized_available_disk_bytes);
let limit = bound_by_configured_disk_budget(
physical_term,
current_cache_bytes,
raw_available_disk_bytes,
hosting_disk_pct,
max_hosting_disk,
);
reconcile_existing_cache_dir(dir, current_cache_bytes, limit);
limit
}
const CONFIGURED_DISK_BUDGET_ALLOWANCE_DIVISOR: u64 = 4;
fn bound_by_configured_disk_budget(
physical_term: u64,
current_cache_bytes: u64,
raw_available_disk_bytes: Option<u64>,
hosting_disk_pct: f64,
max_hosting_disk: u64,
) -> u64 {
let Some(raw_available) = raw_available_disk_bytes else {
return physical_term;
};
let configured_budget = crate::ring::disk_budget_for_clamped(
current_cache_bytes,
raw_available,
hosting_disk_pct,
crate::ring::MIN_DEFAULT_HOSTING_BUDGET_BYTES,
max_hosting_disk,
);
physical_term.min(configured_budget / CONFIGURED_DISK_BUDGET_ALLOWANCE_DIVISOR)
}
fn stabilize_available_disk_bytes(
raw_available: Option<u64>,
current_cache_bytes: u64,
) -> Option<u64> {
raw_available.map(|raw| raw.saturating_add(current_cache_bytes))
}
fn reconcile_existing_cache_dir(dir: &Path, current_bytes: u64, new_soft_limit_bytes: u64) {
if current_bytes <= new_soft_limit_bytes {
return;
}
tracing::info!(
dir = %dir.display(),
current_bytes,
new_soft_limit_bytes,
"wasmtime compile cache exceeds the newly-computed disk-aware soft \
limit; clearing it for immediate relief (#5014)"
);
if let Err(error) = std::fs::remove_dir_all(dir) {
tracing::warn!(
dir = %dir.display(),
%error,
"failed to clear oversized wasmtime compile cache; falling back \
to wasmtime's own prune cycle"
);
}
}
impl Default for RuntimeConfig {
fn default() -> Self {
Self {
max_execution_seconds: 5.0,
cpu_cycles_per_second: None,
safety_margin: 0.2,
enable_metering: false,
module_cache_budget_bytes: super::default_module_cache_budget_bytes(),
offload_compilation: false,
wasmtime_cache_dir: None,
wasmtime_cache_size_bytes: None,
}
}
}
pub type StateWriteCallback =
Arc<dyn Fn(&freenet_stdlib::prelude::ContractKey, usize) + Send + Sync + 'static>;
pub type StateAdmitCallback = Arc<
dyn Fn(&freenet_stdlib::prelude::ContractKey, usize, bool) -> Result<(), String>
+ Send
+ Sync
+ 'static,
>;
pub struct Runtime {
pub(super) engine: Engine,
pub(super) secret_store: SecretsStore,
pub(super) delegate_store: DelegateStore,
pub(super) delegate_modules: SharedModuleCache<CodeHash>,
pub(super) delegate_contexts: super::native_api::DelegateContextCache,
pub(crate) created_delegates_count: super::native_api::SharedDelegateCounter,
pub(crate) inherited_origins: super::native_api::SharedInheritedOrigins,
pub(crate) contract_store: ContractStore,
pub(super) contract_modules: SharedModuleCache<CodeHash>,
pub(crate) state_store_db: Option<crate::contract::storages::Storage>,
pub(crate) state_write_callback: Option<StateWriteCallback>,
pub(crate) state_admit_callback: Option<StateAdmitCallback>,
}
impl Runtime {
pub fn is_healthy(&self) -> bool {
self.engine.is_healthy()
}
pub(crate) fn clone_backend_engine(&self) -> BackendEngine {
self.engine.clone_backend_engine()
}
pub fn set_state_store_db(&mut self, db: crate::contract::storages::Storage) {
self.state_store_db = Some(db);
}
pub fn set_state_write_callback(&mut self, cb: StateWriteCallback) {
self.state_write_callback = Some(cb);
}
pub fn set_state_admit_callback(&mut self, cb: StateAdmitCallback) {
self.state_admit_callback = Some(cb);
}
pub(crate) fn export_secret_bundle(
&self,
scope: super::secrets_store::SecretScope<'_>,
material: &super::secret_export::BundleKeyMaterial<'_>,
) -> Result<Vec<u8>, super::secret_export::ExportError> {
super::secret_export::export_bundle(&self.secret_store, scope, material)
}
pub(crate) fn import_secret_bundle(
&mut self,
bundle: &[u8],
material: &super::secret_export::BundleKeyMaterial<'_>,
target_scope: &super::secret_export::TargetScope,
overwrite: bool,
) -> Result<super::secret_export::ImportReport, super::secret_export::ExportError> {
super::secret_export::import_bundle(
&mut self.secret_store,
bundle,
material,
target_scope,
overwrite,
)
}
#[allow(dead_code)]
pub(crate) fn migrate_delegate_secrets(
&mut self,
predecessors: &[DelegateKey],
successor: &DelegateKey,
origin_contract: Option<[u8; 32]>,
) -> super::MigrationReport {
self.secret_store
.migrate_secrets(predecessors, successor, origin_contract)
}
pub(crate) fn record_delegate_registration_origin(
&self,
delegate: &DelegateKey,
origin: Option<[u8; 32]>,
) -> Result<(), super::SecretStoreError> {
self.secret_store
.record_delegate_registration_origin(delegate, origin)
}
pub fn build_with_config(
contract_store: ContractStore,
delegate_store: DelegateStore,
secret_store: SecretsStore,
host_mem: bool,
config: RuntimeConfig,
) -> RuntimeResult<Self> {
let budget = config.module_cache_budget_bytes;
let engine = Engine::new(&config, host_mem)?;
Ok(Self {
engine,
secret_store,
delegate_store,
contract_modules: Arc::new(Mutex::new(ModuleCache::new(budget))),
contract_store,
delegate_modules: Arc::new(Mutex::new(ModuleCache::new(budget))),
delegate_contexts: super::native_api::new_delegate_context_cache(),
created_delegates_count: super::native_api::new_delegate_counter(),
inherited_origins: super::native_api::new_inherited_origins(),
state_store_db: None,
state_write_callback: None,
state_admit_callback: None,
})
}
pub fn build(
contract_store: ContractStore,
delegate_store: DelegateStore,
secret_store: SecretsStore,
host_mem: bool,
) -> RuntimeResult<Self> {
Self::build_with_config(
contract_store,
delegate_store,
secret_store,
host_mem,
RuntimeConfig::default(),
)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn build_with_shared_module_caches(
contract_store: ContractStore,
delegate_store: DelegateStore,
secret_store: SecretsStore,
host_mem: bool,
contract_modules: SharedModuleCache<CodeHash>,
delegate_modules: SharedModuleCache<CodeHash>,
delegate_contexts: super::native_api::DelegateContextCache,
created_delegates_count: super::native_api::SharedDelegateCounter,
inherited_origins: super::native_api::SharedInheritedOrigins,
shared_backend: BackendEngine,
config: &RuntimeConfig,
) -> RuntimeResult<Self> {
let engine = Engine::new_with_shared_backend(config, host_mem, shared_backend)?;
Ok(Self {
engine,
secret_store,
delegate_store,
contract_modules,
contract_store,
delegate_modules,
delegate_contexts,
created_delegates_count,
inherited_origins,
state_store_db: None,
state_write_callback: None,
state_admit_callback: None,
})
}
pub(super) fn drop_running_instance(&mut self, running: &mut RunningInstance) {
self.engine.drop_instance(&running.handle);
running.dropped_from_engine = true;
}
pub(super) fn init_buf<T>(
&mut self,
handle: &InstanceHandle,
data: T,
) -> RuntimeResult<BufferMut<'_>>
where
T: AsRef<[u8]>,
{
let data = data.as_ref();
let builder_ptr =
super::classify_result(self.engine.initiate_buffer(handle, data.len() as u32))?;
let linear_mem = self.linear_mem(handle)?;
unsafe {
Ok(BufferMut::from_ptr(
builder_ptr as *mut BufferBuilder,
linear_mem,
))
}
}
pub(super) fn init_buf_with_capacity(
&mut self,
handle: &InstanceHandle,
capacity: usize,
) -> RuntimeResult<BufferMut<'_>> {
let builder_ptr =
super::classify_result(self.engine.initiate_buffer(handle, capacity as u32))?;
let linear_mem = self.linear_mem(handle)?;
unsafe {
Ok(BufferMut::from_ptr(
builder_ptr as *mut BufferBuilder,
linear_mem,
))
}
}
pub(super) fn write_streaming_buf(
&mut self,
handle: &InstanceHandle,
instance_id: i64,
data: &[u8],
max_cap: usize,
) -> RuntimeResult<*mut BufferBuilder> {
use super::native_api::{CONTRACT_IO, PendingContractData};
let header_size = 4usize;
debug_assert!(max_cap >= header_size, "max_cap must be >= {header_size}");
if data.len() > u32::MAX as usize {
return Err(super::ContractExecError::InvalidArrayLength(data.len()).into());
}
let buf_cap = max_cap.min(data.len().saturating_add(header_size));
let mut buf = self.init_buf_with_capacity(handle, buf_cap)?;
let total_len = data.len() as u32;
buf.write(total_len.to_le_bytes())?;
let first_chunk_size = data.len().min(buf_cap - header_size);
buf.write(&data[..first_chunk_size])?;
let ptr = buf.ptr();
if first_chunk_size < data.len() {
CONTRACT_IO.insert(
(instance_id, ptr as i64),
PendingContractData {
data: data[first_chunk_size..].to_vec(),
cursor: 0,
},
);
}
Ok(ptr)
}
pub(super) fn write_contract_buf(
&mut self,
running: &RunningInstance,
data: &[u8],
max_cap: usize,
) -> RuntimeResult<*mut BufferBuilder> {
if running.supports_streaming {
self.write_streaming_buf(&running.handle, running.id, data, max_cap)
} else {
let mut buf = self.init_buf(&running.handle, data)?;
buf.write(data)?;
Ok(buf.ptr())
}
}
pub(super) fn write_contract_buf_serialized<T: serde::Serialize + ?Sized>(
&mut self,
running: &RunningInstance,
value: &T,
max_cap: usize,
) -> RuntimeResult<*mut BufferBuilder> {
if running.supports_streaming {
let serialized = bincode::serialize(value)?;
self.write_streaming_buf(&running.handle, running.id, &serialized, max_cap)
} else {
let size = bincode::serialized_size(value)? as usize;
let mut buf = self.init_buf_with_capacity(&running.handle, size)?;
bincode::serialize_into(&mut buf, value)?;
Ok(buf.ptr())
}
}
pub(super) fn linear_mem(&mut self, handle: &InstanceHandle) -> RuntimeResult<WasmLinearMem> {
let (ptr, size) = self.engine.memory_info(handle)?;
Ok(unsafe { WasmLinearMem::new(ptr, size as u64) })
}
pub(crate) fn compile_check(
&mut self,
key: &ContractKey,
parameters: &Parameters<'_>,
) -> RuntimeResult<()> {
let mut running = self.prepare_contract_call(key, parameters, 0)?;
let missing = self.engine.missing_contract_exports_for(&running.handle);
self.drop_running_instance(&mut running);
if !missing.is_empty() {
return Err(ContractExecError::MissingContractExports {
missing: missing.join(", "),
}
.into());
}
Ok(())
}
pub(super) fn prepare_contract_call(
&mut self,
key: &ContractKey,
parameters: &Parameters,
req_bytes: usize,
) -> RuntimeResult<RunningInstance> {
self.prepare_contract_call_inner(key, parameters, req_bytes, None)
}
pub(super) fn prepare_contract_call_with_contract(
&mut self,
key: &ContractKey,
parameters: &Parameters,
req_bytes: usize,
already_fetched: &ContractContainer,
) -> RuntimeResult<RunningInstance> {
self.prepare_contract_call_inner(key, parameters, req_bytes, Some(already_fetched))
}
fn prepare_contract_call_inner(
&mut self,
key: &ContractKey,
parameters: &Parameters,
req_bytes: usize,
already_fetched: Option<&ContractContainer>,
) -> RuntimeResult<RunningInstance> {
let code_hash = match already_fetched {
Some(contract) => wasm_code_hash(contract)?,
None => self
.contract_store
.code_hash_from_id(key.id())
.ok_or_else(|| {
tracing::error!(
contract = %key,
phase = "prepare_contract_call_failed",
"Contract not indexed in store during WASM execution"
);
RuntimeInnerError::ContractNotFound(*key)
})?,
};
let cached = self
.contract_modules
.lock()
.unwrap()
.get(&code_hash)
.cloned();
let module = if let Some(module) = cached {
tracing::debug!(contract = %key, %code_hash, "Module cache hit");
module
} else {
tracing::info!(contract = %key, %code_hash, "Module cache miss — compiling");
let owned_contract;
let contract = match already_fetched {
Some(contract) => contract,
None => {
owned_contract = self
.contract_store
.fetch_contract(key, parameters)
.ok_or_else(|| {
tracing::error!(
contract = %key,
key_code_hash = ?key.code_hash(),
phase = "prepare_contract_call_failed",
"Contract not found in store during WASM execution"
);
RuntimeInnerError::ContractNotFound(*key)
})?;
&owned_contract
}
};
let code = match contract {
ContractContainer::Wasm(ContractWasmAPIVersion::V1(contract_v1)) => {
contract_v1.code().data().to_vec()
}
ContractContainer::Wasm(_) | _ => unimplemented!(),
};
warn_on_host_clock_import(key, &code_hash, &code);
let module = self.engine.compile(&code)?;
let compiled_size = self.engine.module_compiled_size(&module);
let mut cache = self.contract_modules.lock().unwrap();
if let Some(existing) = cache.get(&code_hash).cloned() {
existing
} else {
cache.insert(code_hash, module.clone(), compiled_size);
module
}
};
RunningInstance::new(
&mut self.engine,
&module,
Key::Contract(*key.id()),
req_bytes,
)
}
pub(super) fn prepare_delegate_call(
&mut self,
params: &Parameters,
key: &DelegateKey,
req_bytes: usize,
) -> RuntimeResult<(RunningInstance, DelegateApiVersion)> {
let code_hash = self
.delegate_store
.code_hash_from_key(key)
.ok_or_else(|| RuntimeInnerError::DelegateNotFound(key.clone()))?;
let cached = self
.delegate_modules
.lock()
.unwrap()
.get(&code_hash)
.cloned();
let module = if let Some(module) = cached {
tracing::debug!(delegate = %key, %code_hash, "Module cache hit");
module
} else {
tracing::info!(delegate = %key, %code_hash, "Module cache miss — compiling");
let delegate = self
.delegate_store
.fetch_delegate(key, params)
.ok_or_else(|| RuntimeInnerError::DelegateNotFound(key.clone()))?;
let code = delegate.code().as_ref().to_vec();
let module = self.engine.compile(&code)?;
let compiled_size = self.engine.module_compiled_size(&module);
let mut cache = self.delegate_modules.lock().unwrap();
if let Some(existing) = cache.get(&code_hash).cloned() {
existing
} else {
cache.insert(code_hash, module.clone(), compiled_size);
module
}
};
let api_version = if self.engine.module_has_async_imports(&module) {
DelegateApiVersion::V2
} else {
DelegateApiVersion::V1
};
let running = RunningInstance::new(
&mut self.engine,
&module,
Key::Delegate(key.clone()),
req_bytes,
)?;
Ok((running, api_version))
}
}
impl super::contract::ContractStoreBridge for Runtime {
fn code_hash_from_id(&self, id: &ContractInstanceId) -> Option<CodeHash> {
self.contract_store.code_hash_from_id(id)
}
fn fetch_contract_code(
&self,
key: &ContractKey,
params: &Parameters<'_>,
) -> Option<ContractContainer> {
self.contract_store.fetch_contract(key, params)
}
fn code_blob_stored(&self, code_hash: &CodeHash) -> bool {
self.contract_store.code_blob_stored(code_hash)
}
fn store_contract(&mut self, contract: ContractContainer) -> Result<(), anyhow::Error> {
self.contract_store.store_contract(contract)?;
Ok(())
}
fn remove_contract(&mut self, key: &ContractKey) -> Result<(), anyhow::Error> {
self.contract_store.remove_contract(key)?;
Ok(())
}
}
impl super::contract::ContractRuntimeBridge for Runtime {}
#[cfg(test)]
mod wasmtime_disk_cache_sizing_tests {
use super::{
MAX_WASMTIME_CACHE_SIZE_BYTES, MIN_WASMTIME_CACHE_SIZE_BYTES,
WASMTIME_CACHE_FALLBACK_TOTAL_RAM_BYTES, wasmtime_cache_size_for_ram,
};
use crate::ring::hosting_budget_for_ram;
const MIB: u64 = 1024 * 1024;
const GIB: u64 = 1024 * MIB;
const LEGACY_FLAT_SOFT_LIMIT_BYTES: u64 = 512 * MIB;
const MEASURED_P90_ARTIFACT_BYTES: u64 = 811 * 1024;
const MIN_ENTRIES_THE_FLOOR_MUST_HOLD: u64 = 150;
#[test]
fn compile_cache_scales_with_the_memory_the_node_may_use() {
assert_eq!(wasmtime_cache_size_for_ram(2 * GIB), 256 * MIB);
assert!(
wasmtime_cache_size_for_ram(2 * GIB) < LEGACY_FLAT_SOFT_LIMIT_BYTES,
"the containerized peer must get LESS than the old flat limit"
);
assert_eq!(wasmtime_cache_size_for_ram(15 * GIB), 512 * MIB);
assert_eq!(wasmtime_cache_size_for_ram(125 * GIB), 512 * MIB);
assert_eq!(wasmtime_cache_size_for_ram(3 * GIB), 384 * MIB);
assert!(wasmtime_cache_size_for_ram(2 * GIB) < wasmtime_cache_size_for_ram(3 * GIB));
assert!(wasmtime_cache_size_for_ram(3 * GIB) < wasmtime_cache_size_for_ram(4 * GIB));
}
#[test]
fn compile_cache_floor_binds_on_tiny_hosts() {
assert_eq!(wasmtime_cache_size_for_ram(0), 128 * MIB);
assert_eq!(wasmtime_cache_size_for_ram(1), 128 * MIB);
assert_eq!(wasmtime_cache_size_for_ram(512 * MIB), 128 * MIB);
assert_eq!(wasmtime_cache_size_for_ram(GIB), 128 * MIB);
assert_eq!(wasmtime_cache_size_for_ram(GIB + 8), 128 * MIB + 1);
}
#[test]
fn floor_holds_a_useful_entry_count_at_the_measured_artifact_size() {
let entries_at_p90 = MIN_WASMTIME_CACHE_SIZE_BYTES / MEASURED_P90_ARTIFACT_BYTES;
assert!(
entries_at_p90 >= MIN_ENTRIES_THE_FLOOR_MUST_HOLD,
"the {MIN_WASMTIME_CACHE_SIZE_BYTES}-byte floor holds only {entries_at_p90} \
artifacts at the measured p90 of {MEASURED_P90_ARTIFACT_BYTES} bytes; it must \
hold at least {MIN_ENTRIES_THE_FLOOR_MUST_HOLD}. Lowering the floor buys disk \
and pays for it in Cranelift recompiles."
);
}
#[test]
fn compile_cache_ceiling_binds_on_large_hosts() {
assert_eq!(wasmtime_cache_size_for_ram(4 * GIB - 8), 512 * MIB - 1);
assert_eq!(wasmtime_cache_size_for_ram(4 * GIB), 512 * MIB);
assert_eq!(wasmtime_cache_size_for_ram(8 * GIB), 512 * MIB);
assert_eq!(wasmtime_cache_size_for_ram(u64::MAX), 512 * MIB);
}
#[test]
fn compile_cache_default_never_exceeds_hosting_default() {
for total_ram in [
0,
1,
128 * MIB,
512 * MIB,
GIB,
2 * GIB, 3 * GIB,
4 * GIB,
8 * GIB,
15 * GIB, 32 * GIB,
125 * GIB, u64::MAX,
] {
let compile_cache = wasmtime_cache_size_for_ram(total_ram);
let state_budget = hosting_budget_for_ram(total_ram);
assert!(
compile_cache <= state_budget,
"at total_ram={total_ram} the DEFAULT on-disk compile cache \
({compile_cache}) must not exceed the DEFAULT contract-state budget \
({state_budget})"
);
}
assert_eq!(hosting_budget_for_ram(2 * GIB), 256 * MIB);
assert_eq!(wasmtime_cache_size_for_ram(2 * GIB), 256 * MIB);
assert!(LEGACY_FLAT_SOFT_LIMIT_BYTES > hosting_budget_for_ram(2 * GIB));
let operator_overridden_state_budget = 64 * MIB;
assert!(
wasmtime_cache_size_for_ram(4 * GIB) > operator_overridden_state_budget,
"an operator-overridden state budget CAN be smaller than the compile \
cache — the ordering holds between defaults, not between live budgets"
);
}
#[test]
fn default_soft_limit_reader_derives_from_ram_and_disk_signals() {
let src = include_str!("runtime.rs");
let body = src
.split("pub(crate) fn default_wasmtime_cache_size_bytes_for_dir(")
.nth(1)
.expect("default_wasmtime_cache_size_bytes_for_dir must exist")
.split("\n}\n")
.next()
.expect("end of default_wasmtime_cache_size_bytes_for_dir");
assert!(
body.contains("read_total_ram_bytes()"),
"the reader must consult the shared read_total_ram_bytes() signal, not \
a second notion of machine size"
);
assert!(
body.contains("disk_available_bytes("),
"the reader must consult a real disk-availability signal — a RAM-only \
reader is exactly the #5014 defect"
);
assert!(
body.contains("combine_wasmtime_cache_size("),
"the reader must delegate to the pure combiner so the RAM/disk \
interaction has exactly one implementation"
);
assert!(
body.contains("bound_by_configured_disk_budget("),
"the reader must ALSO bound the physical-disk term by the \
operator's configured hosting-disk budget — a physical-only \
bound leaves an operator-shrunk --max-hosting-disk wedged"
);
assert!(
body.contains("reconcile_existing_cache_dir("),
"the reader must reconcile an already-oversized cache directory, not \
just narrow the limit for future growth"
);
}
#[test]
fn fallback_ram_estimate_resolves_to_the_floor() {
assert_eq!(
wasmtime_cache_size_for_ram(WASMTIME_CACHE_FALLBACK_TOTAL_RAM_BYTES),
128 * MIB,
"a host whose RAM we cannot read must get the smallest sane cache"
);
}
#[test]
fn clamp_bounds_are_ordered_and_ceiling_is_the_historical_default() {
const _: () = assert!(MIN_WASMTIME_CACHE_SIZE_BYTES < MAX_WASMTIME_CACHE_SIZE_BYTES);
assert_eq!(MAX_WASMTIME_CACHE_SIZE_BYTES, LEGACY_FLAT_SOFT_LIMIT_BYTES);
}
}
#[cfg(test)]
mod wasmtime_disk_cache_disk_sizing_tests {
use super::{
MAX_WASMTIME_CACHE_SIZE_BYTES, MIN_WASMTIME_CACHE_SIZE_BYTES,
WASMTIME_CACHE_FALLBACK_TOTAL_RAM_BYTES, bound_by_configured_disk_budget,
combine_wasmtime_cache_size, default_wasmtime_cache_size_bytes_for_dir,
reconcile_existing_cache_dir, stabilize_available_disk_bytes, wasmtime_cache_size_for_disk,
wasmtime_cache_size_for_ram,
};
use std::io::Write;
const MIB: u64 = 1024 * 1024;
const GIB: u64 = 1024 * MIB;
#[test]
fn disk_term_floor_and_ceiling_bind() {
assert_eq!(wasmtime_cache_size_for_disk(0), 32 * MIB);
assert_eq!(wasmtime_cache_size_for_disk(1), 32 * MIB);
assert_eq!(wasmtime_cache_size_for_disk(256 * MIB), 32 * MIB);
assert_eq!(wasmtime_cache_size_for_disk(256 * MIB + 8), 32 * MIB + 1);
assert_eq!(wasmtime_cache_size_for_disk(4 * GIB), 512 * MIB);
assert_eq!(wasmtime_cache_size_for_disk(8 * GIB), 512 * MIB);
assert_eq!(wasmtime_cache_size_for_disk(u64::MAX), 512 * MIB);
}
#[test]
fn ram_rich_disk_tight_host_is_bounded_by_the_disk_term() {
let ram_only = wasmtime_cache_size_for_ram(16 * GIB);
assert_eq!(
ram_only,
512 * MIB,
"16 GiB RAM must hit the RAM-side ceiling"
);
let available_disk = 400 * MIB;
let combined = combine_wasmtime_cache_size(16 * GIB, Some(available_disk));
assert!(
combined < ram_only,
"a disk-tight host (400 MiB free) must get LESS than the RAM-only \
figure ({ram_only}); got {combined}"
);
assert_eq!(combined, 50 * MIB);
}
#[test]
fn combine_takes_the_tighter_of_the_two_terms() {
assert_eq!(
combine_wasmtime_cache_size(2 * GIB, Some(100 * GIB)),
wasmtime_cache_size_for_ram(2 * GIB),
);
assert_eq!(
combine_wasmtime_cache_size(100 * GIB, Some(2 * GIB)),
wasmtime_cache_size_for_disk(2 * GIB),
);
assert_eq!(
combine_wasmtime_cache_size(100 * GIB, Some(100 * GIB)),
MAX_WASMTIME_CACHE_SIZE_BYTES,
);
}
#[test]
fn unreadable_disk_signal_falls_back_to_ram_only() {
assert_eq!(
combine_wasmtime_cache_size(16 * GIB, None),
wasmtime_cache_size_for_ram(16 * GIB),
);
}
#[test]
fn reconcile_clears_a_directory_already_over_the_new_limit() {
let dir = tempfile::tempdir().unwrap();
let cache_dir = dir.path().join("wasmtime-cache");
std::fs::create_dir_all(&cache_dir).unwrap();
let mut f = std::fs::File::create(cache_dir.join("big.bin")).unwrap();
f.write_all(&vec![0u8; 200 * 1024 * 1024]).unwrap();
reconcile_existing_cache_dir(&cache_dir, 200 * MIB, 128 * MIB);
assert!(
!cache_dir.exists(),
"an over-limit cache directory must be cleared, not left for the \
~1h wasmtime prune cycle to catch up"
);
}
#[test]
fn reconcile_leaves_a_directory_under_the_limit_untouched() {
let dir = tempfile::tempdir().unwrap();
let cache_dir = dir.path().join("wasmtime-cache");
std::fs::create_dir_all(&cache_dir).unwrap();
let mut f = std::fs::File::create(cache_dir.join("small.bin")).unwrap();
f.write_all(&vec![0u8; 1024]).unwrap();
reconcile_existing_cache_dir(&cache_dir, 1024, 128 * MIB);
assert!(
cache_dir.join("small.bin").exists(),
"a directory already under the limit must not be touched"
);
}
#[test]
fn reconcile_is_a_no_op_on_a_missing_directory() {
let dir = tempfile::tempdir().unwrap();
let missing = dir.path().join("does-not-exist-yet");
reconcile_existing_cache_dir(&missing, 0, 128 * MIB); assert!(!missing.exists());
}
#[test]
fn default_for_dir_stays_within_bounds_when_the_directory_does_not_exist_yet() {
let dir = tempfile::tempdir().unwrap();
let missing = dir.path().join("wasmtime-cache");
let result = default_wasmtime_cache_size_bytes_for_dir(
&missing,
crate::ring::DEFAULT_HOSTING_DISK_PCT,
crate::ring::DEFAULT_MAX_HOSTING_DISK_BYTES,
);
assert!(
(MIN_WASMTIME_CACHE_SIZE_BYTES..=MAX_WASMTIME_CACHE_SIZE_BYTES).contains(&result),
"result {result} must stay within [{MIN_WASMTIME_CACHE_SIZE_BYTES}, \
{MAX_WASMTIME_CACHE_SIZE_BYTES}] regardless of this host's real RAM/disk"
);
}
#[test]
fn default_for_dir_uses_the_real_disk_reading_on_an_existing_directory() {
let dir = tempfile::tempdir().unwrap();
let cache_dir = dir.path().join("wasmtime-cache");
std::fs::create_dir_all(&cache_dir).unwrap();
let total_ram = crate::wasm_runtime::read_total_ram_bytes()
.map(|v| v as u64)
.unwrap_or(WASMTIME_CACHE_FALLBACK_TOTAL_RAM_BYTES);
let available_disk_bytes = crate::ring::disk_available_bytes(&cache_dir);
assert!(
available_disk_bytes.is_some(),
"statvfs on a directory that genuinely exists must succeed on this \
platform — if this fails, the test tempdir setup is wrong, not the \
production code"
);
let expected = combine_wasmtime_cache_size(total_ram, available_disk_bytes);
assert_eq!(
default_wasmtime_cache_size_bytes_for_dir(&cache_dir, 1.0, u64::MAX),
expected,
"the live entry point must match the pure combiner fed the SAME \
real RAM/disk signals — this pins that it actually reads a live \
Some(...) disk signal, not silently falling back to RAM-only"
);
}
#[test]
fn default_for_dir_is_bounded_by_a_tiny_configured_disk_budget() {
let dir = tempfile::tempdir().unwrap();
let cache_dir = dir.path().join("wasmtime-cache");
std::fs::create_dir_all(&cache_dir).unwrap();
let tiny_max_hosting_disk = 40 * MIB;
let result = default_wasmtime_cache_size_bytes_for_dir(
&cache_dir,
crate::ring::DEFAULT_HOSTING_DISK_PCT,
tiny_max_hosting_disk,
);
assert!(
result <= tiny_max_hosting_disk,
"a compile cache larger than the operator's OWN configured \
--max-hosting-disk ({tiny_max_hosting_disk}) defeats the whole \
point of the setting; got {result}"
);
}
#[test]
fn disk_term_always_leaves_positive_headroom_against_the_aggregate_disk_budget() {
for reachable_disk in [
0,
1,
MIB,
32 * MIB,
64 * MIB,
128 * MIB,
200 * MIB,
255 * MIB,
256 * MIB, 257 * MIB,
300 * MIB,
400 * MIB, 912 * MIB, GIB,
2 * GIB,
4 * GIB,
8 * GIB,
32 * GIB,
100 * GIB,
u64::MAX,
] {
let disk_term = wasmtime_cache_size_for_disk(reachable_disk);
let available = reachable_disk.saturating_sub(disk_term);
let disk_budget = crate::ring::disk_budget_for_clamped(
disk_term,
available,
crate::ring::DEFAULT_HOSTING_DISK_PCT,
crate::ring::MIN_DEFAULT_HOSTING_BUDGET_BYTES,
crate::ring::DEFAULT_MAX_HOSTING_DISK_BYTES,
);
assert!(
disk_budget > disk_term,
"at reachable_disk={reachable_disk} the compile cache's disk \
term ({disk_term}) must leave POSITIVE headroom under the \
aggregate disk budget ({disk_budget}) for real contract \
state — zero or negative headroom means the compile cache \
alone wedges admission"
);
}
}
#[test]
fn bound_by_configured_disk_budget_binds_only_when_tighter() {
let physical_term = 512 * MIB;
let tight = bound_by_configured_disk_budget(
physical_term,
0, Some(40 * MIB), crate::ring::DEFAULT_HOSTING_DISK_PCT,
crate::ring::MIN_DEFAULT_HOSTING_BUDGET_BYTES, );
assert!(
tight < physical_term,
"a tiny configured max_hosting_disk must pull the result below \
the physical term; got {tight}"
);
let generous = bound_by_configured_disk_budget(
physical_term,
0,
Some(100 * GIB),
crate::ring::DEFAULT_HOSTING_DISK_PCT,
crate::ring::DEFAULT_MAX_HOSTING_DISK_BYTES,
);
assert_eq!(
generous, physical_term,
"a generous configured budget must not tighten the physical term"
);
let unreadable = bound_by_configured_disk_budget(
physical_term,
0,
None,
crate::ring::DEFAULT_HOSTING_DISK_PCT,
crate::ring::MIN_DEFAULT_HOSTING_BUDGET_BYTES,
);
assert_eq!(
unreadable, physical_term,
"an unreadable disk signal must fall back to the physical term, \
not invent a budget projection from nothing"
);
}
#[test]
fn configured_budget_bound_always_leaves_positive_headroom() {
for reachable_disk in [0, MIB, 128 * MIB, 256 * MIB, GIB, 100 * GIB] {
for max_hosting_disk in [
crate::ring::MIN_DEFAULT_HOSTING_BUDGET_BYTES, 16 * GIB,
crate::ring::DEFAULT_MAX_HOSTING_DISK_BYTES,
] {
let physical_term = wasmtime_cache_size_for_disk(reachable_disk);
let available = reachable_disk.saturating_sub(physical_term);
let bound = bound_by_configured_disk_budget(
physical_term,
physical_term, Some(available),
crate::ring::DEFAULT_HOSTING_DISK_PCT,
max_hosting_disk,
);
let real_budget = crate::ring::disk_budget_for_clamped(
bound,
available,
crate::ring::DEFAULT_HOSTING_DISK_PCT,
crate::ring::MIN_DEFAULT_HOSTING_BUDGET_BYTES,
max_hosting_disk,
);
assert!(
real_budget > bound,
"at reachable_disk={reachable_disk}, \
max_hosting_disk={max_hosting_disk}: the configured-budget-\
bound compile cache ({bound}) must leave POSITIVE headroom \
under the real aggregate budget ({real_budget})"
);
}
}
}
#[test]
fn stabilize_available_disk_bytes_recovers_total_reachable_capacity() {
assert_eq!(
stabilize_available_disk_bytes(Some(350 * MIB), 50 * MIB),
Some(400 * MIB)
);
assert_eq!(
stabilize_available_disk_bytes(Some(400 * MIB), 0),
Some(400 * MIB)
);
assert_eq!(stabilize_available_disk_bytes(None, 50 * MIB), None);
assert_eq!(
stabilize_available_disk_bytes(Some(u64::MAX), 50 * MIB),
Some(u64::MAX)
);
}
#[test]
fn folding_the_caches_own_size_back_in_makes_the_limit_stable_across_simulated_restarts() {
let total_ram = 100 * GIB; let total_reachable_disk = 2 * GIB;
let limit1 = combine_wasmtime_cache_size(
total_ram,
stabilize_available_disk_bytes(Some(total_reachable_disk), 0),
);
let raw_available_boot2 = total_reachable_disk - limit1;
let limit2 = combine_wasmtime_cache_size(
total_ram,
stabilize_available_disk_bytes(Some(raw_available_boot2), limit1),
);
assert_eq!(
limit1, limit2,
"the computed limit must be STABLE across restarts when nothing \
other than the cache's own regrowth changed on disk"
);
let unstabilized_limit2 = combine_wasmtime_cache_size(total_ram, Some(raw_available_boot2));
assert!(
unstabilized_limit2 < limit1,
"sanity check failed: this scenario no longer demonstrates the \
bug the fold-back fixes, so it's not exercising anything — \
unstabilized_limit2={unstabilized_limit2}, limit1={limit1}"
);
}
}
#[cfg(test)]
mod host_clock_deprecation {
use super::*;
fn module_importing(marker: &str, imports: &[(&str, &str)]) -> Vec<u8> {
let mut wat = String::from("(module\n");
for (i, (namespace, name)) in imports.iter().enumerate() {
wat.push_str(&format!(
" (import \"{namespace}\" \"{name}\" (func $f{i} (param i64 i64)))\n"
));
}
wat.push_str(&format!(" (func (export \"{marker}\"))\n)\n"));
wat::parse_str(&wat).expect("test fixture is valid wat")
}
fn clock_module(marker: &str) -> Vec<u8> {
module_importing(
marker,
&[(
crate::conformance::HOST_CLOCK_NAMESPACE,
crate::conformance::HOST_CLOCK_IMPORT,
)],
)
}
fn key_for(code: &[u8]) -> (ContractKey, CodeHash) {
let contract = WrappedContract::new(
std::sync::Arc::new(ContractCode::from(code.to_vec())),
Parameters::from(vec![]),
);
let key = *contract.key();
let hash = *key.code_hash();
(key, hash)
}
#[test]
fn a_clock_importing_contract_warns() {
let code = clock_module("a_clock_importing_contract_warns");
let (key, hash) = key_for(&code);
assert!(
warn_on_host_clock_import(&key, &hash, &code),
"a contract importing the host clock must draw the deprecation warning"
);
}
#[test]
fn a_contract_that_does_not_read_the_clock_never_warns() {
let code = module_importing(
"a_contract_that_does_not_read_the_clock_never_warns",
&[("freenet_log", "__frnt__logger__info")],
);
let (key, hash) = key_for(&code);
assert!(
!warn_on_host_clock_import(&key, &hash, &code),
"warning on a contract that imports no clock would make the notice \
worthless: every contract would carry it"
);
}
#[test]
fn the_same_contract_warns_exactly_once_per_process() {
let code = clock_module("the_same_contract_warns_exactly_once_per_process");
let (key, hash) = key_for(&code);
assert!(warn_on_host_clock_import(&key, &hash, &code));
for _ in 0..5 {
assert!(
!warn_on_host_clock_import(&key, &hash, &code),
"the same contract warned more than once; the once-per-code-hash \
bound is gone and a module-cache thrash now floods the log"
);
}
}
#[test]
fn a_second_distinct_contract_still_warns() {
let first = clock_module("a_second_distinct_contract_still_warns_1");
let second = clock_module("a_second_distinct_contract_still_warns_2");
assert_ne!(first, second, "fixtures must be byte-distinct");
let (key_a, hash_a) = key_for(&first);
let (key_b, hash_b) = key_for(&second);
assert!(warn_on_host_clock_import(&key_a, &hash_a, &first));
assert!(
warn_on_host_clock_import(&key_b, &hash_b, &second),
"a DIFFERENT clock-reading contract was silenced by the first one's \
warning; the dedup is keyed on the wrong thing"
);
}
#[test]
fn the_dedup_set_stops_growing_at_its_cap() {
let seen = Mutex::new(std::collections::HashSet::new());
let first = clock_module("cap_1");
let second = clock_module("cap_2");
let third = clock_module("cap_3");
let (_, hash_a) = key_for(&first);
let (_, hash_b) = key_for(&second);
let (_, hash_c) = key_for(&third);
assert!(decide_host_clock_warning(&seen, 2, &hash_a, &first));
assert!(decide_host_clock_warning(&seen, 2, &hash_b, &second));
assert_eq!(
seen.lock().unwrap().len(),
2,
"the set did not fill as expected"
);
assert!(decide_host_clock_warning(&seen, 2, &hash_c, &third));
assert_eq!(
seen.lock().unwrap().len(),
2,
"the dedup set grew past its cap, so it is unbounded on \
externally-influenced keys after all"
);
}
#[test]
fn past_the_cap_the_warning_still_fires() {
let seen = Mutex::new(std::collections::HashSet::new());
let recorded = clock_module("past_cap_recorded");
let overflow = clock_module("past_cap_overflow");
let (_, hash_recorded) = key_for(&recorded);
let (_, hash_overflow) = key_for(&overflow);
assert!(decide_host_clock_warning(
&seen,
1,
&hash_recorded,
&recorded
));
for _ in 0..3 {
assert!(
decide_host_clock_warning(&seen, 1, &hash_overflow, &overflow),
"a contract past the dedup cap was silenced instead of merely \
re-warned; the cap must never suppress the notice"
);
}
assert!(!decide_host_clock_warning(
&seen,
1,
&hash_recorded,
&recorded
));
}
#[test]
fn concurrent_callers_for_one_contract_warn_exactly_once() {
const THREADS: usize = 8;
const ROUNDS: usize = 256;
for round in 0..ROUNDS {
let code = clock_module(&format!("concurrent_round_{round}"));
let (_, hash) = key_for(&code);
let seen = Mutex::new(std::collections::HashSet::new());
let barrier = std::sync::Barrier::new(THREADS);
let warned = std::thread::scope(|scope| {
let handles: Vec<_> = (0..THREADS)
.map(|_| {
scope.spawn(|| {
barrier.wait();
decide_host_clock_warning(&seen, SEEN_CAP, &hash, &code)
})
})
.collect();
handles
.into_iter()
.map(|h| h.join().expect("no thread may panic"))
.filter(|warned| *warned)
.count()
});
assert_eq!(
warned, 1,
"round {round}: {THREADS} threads loading the SAME contract \
produced {warned} warnings, not 1. The membership check and the \
insert are no longer atomic with respect to each other, so every \
racer sees the code hash as unseen and the once-per-contract \
bound is gone."
);
assert_eq!(
seen.lock().unwrap().len(),
1,
"round {round}: the dedup set holds more or fewer than the one \
code hash these threads all shared"
);
}
}
#[test]
fn a_full_dedup_set_does_not_warn_about_a_clockless_contract() {
let seen = Mutex::new(std::collections::HashSet::new());
let clock = clock_module("full_set_clock");
let (_, clock_hash) = key_for(&clock);
assert!(decide_host_clock_warning(&seen, 1, &clock_hash, &clock));
let clockless = module_importing(
"full_set_clockless",
&[("freenet_log", "__frnt__logger__info")],
);
let (_, clockless_hash) = key_for(&clockless);
assert!(
!decide_host_clock_warning(&seen, 1, &clockless_hash, &clockless),
"a contract that never reads the clock was warned about because the \
dedup set happened to be full"
);
}
}
#[cfg(test)]
mod host_clock_warning_call_site_pin {
fn blank_literals(src: &str) -> String {
fn excerpt(src: &str, at: usize) -> &str {
let end = (at + 48).min(src.len());
src.get(at..end).unwrap_or("<not a char boundary>")
}
fn char_literal_len(bytes: &[u8], at: usize) -> Option<usize> {
let escaped = bytes.get(at + 1) == Some(&b'\\');
let body_start = if escaped { at + 2 } else { at + 1 };
for (end, byte) in bytes.iter().enumerate().skip(body_start).take(4) {
if *byte == b'\'' {
return (end > body_start).then_some(end - at + 1);
}
}
None
}
let bytes = src.as_bytes();
let mut out = String::with_capacity(src.len());
let mut i = 0usize;
while i < bytes.len() {
match bytes[i] {
b'r' if bytes[i + 1..].starts_with(b"\"") || bytes[i + 1..].starts_with(b"#") => {
panic!(
"blank_literals cannot mask a raw string, so the brace count \
it feeds would be wrong and the scrape would silently cover \
the wrong region. EXTEND this function to handle raw strings; \
do not delete the call. At byte {i} of the scraped region: {:?}",
excerpt(src, i)
);
}
b'/' if bytes[i + 1..].starts_with(b"*") => {
panic!(
"blank_literals cannot mask a block comment, so the brace count \
it feeds would be wrong and the scrape would silently cover \
the wrong region. EXTEND this function to handle block \
comments; do not delete the call. At byte {i} of the scraped \
region: {:?}",
excerpt(src, i)
);
}
b'/' if bytes[i + 1..].starts_with(b"/") => {
while i < bytes.len() && bytes[i] != b'\n' {
out.push(' ');
i += 1;
}
}
b'"' => {
out.push(' ');
i += 1;
while i < bytes.len() && bytes[i] != b'"' {
if bytes[i] == b'\\' {
out.push(' ');
i += 1;
}
if i < bytes.len() {
out.push(' ');
i += 1;
}
}
assert!(i < bytes.len(), "unterminated string literal");
out.push(' ');
i += 1;
}
b'\'' if char_literal_len(bytes, i).is_some() => {
let len = char_literal_len(bytes, i).expect("just checked");
for _ in 0..len {
out.push(' ');
}
i += len;
}
_ => {
let ch = src[i..].chars().next().expect("in bounds");
out.push(ch);
i += ch.len_utf8();
}
}
}
debug_assert_eq!(out.len(), src.len(), "blank_literals must preserve offsets");
out
}
fn call_site_code() -> String {
let src = include_str!("runtime.rs");
let signature = "fn prepare_contract_call_inner(";
let start = src
.find(signature)
.expect("prepare_contract_call_inner not found in runtime.rs");
let first_test_mod = src
.find("\n#[cfg(test)]")
.expect("runtime.rs has no test module");
assert!(
start < first_test_mod,
"the signature matched only inside a test module, so this pin would \
be scoped to a test rather than to production code"
);
let after = &src[start..];
let open = after.find('{').expect("signature has no body");
let masked = blank_literals(&after[open..]);
let mut depth = 0usize;
let mut end = None;
for (offset, ch) in masked.char_indices() {
match ch {
'{' => depth += 1,
'}' => {
depth -= 1;
if depth == 0 {
end = Some(open + offset + 1);
break;
}
}
_ => {}
}
}
after[..end.expect("body is not brace-balanced")]
.lines()
.filter(|line| !line.trim_start().starts_with("//"))
.collect::<Vec<_>>()
.join("\n")
}
#[test]
fn contract_load_calls_the_host_clock_warning() {
let body = call_site_code();
assert_eq!(
body.matches("warn_on_host_clock_import(").count(),
1,
"the host-clock deprecation warning is no longer called (exactly once) \
from the contract module-cache miss path, so no contract will ever \
draw the #5465 notice:\n{body}"
);
}
#[test]
fn the_scrape_sees_real_code() {
let body = call_site_code();
assert!(
body.contains("self.engine.compile(&code)?"),
"the scraped region is not prepare_contract_call_inner's body any more"
);
assert!(
!body.contains("// Deprecation notice for #5465"),
"comment stripping stopped working, so the pin can be satisfied by a \
comment naming the function instead of by a call to it"
);
}
#[test]
fn braces_inside_literals_are_not_counted_as_structure() {
let masked = blank_literals("{ f(\"}}}{\"); g('{'); }");
assert_eq!(
masked.matches('{').count(),
1,
"a brace inside a string or char literal was counted as structure: {masked}"
);
assert_eq!(masked.matches('}').count(), 1, "{masked}");
assert_eq!(
masked.len(),
"{ f(\"}}}{\"); g('{'); }".len(),
"the mask changed byte offsets, so they no longer index the original"
);
}
#[test]
fn comments_and_escaped_quotes_are_handled() {
let masked = blank_literals("{ // }}}\n f(\"a\\\"}\"); }");
assert_eq!(masked.matches('{').count(), 1, "{masked}");
assert_eq!(masked.matches('}').count(), 1, "{masked}");
}
#[test]
fn a_lifetime_is_not_mistaken_for_a_char_literal() {
let src = "{ fn f<'a>(x: &'a str) -> &'a str { x } }";
assert_eq!(blank_literals(src), src);
}
#[test]
fn a_char_literal_holding_a_quote_does_not_open_a_string() {
let masked = blank_literals("{ let _q = '\"'; f(); }");
assert_eq!(
masked.matches('{').count(),
1,
"structure was lost after a quote char literal: {masked}"
);
assert_eq!(masked.matches('}').count(), 1, "{masked}");
assert!(
masked.contains("f()"),
"the code after a quote char literal was blanked as if it were \
inside a string: {masked}"
);
assert_eq!(masked.len(), "{ let _q = '\"'; f(); }".len());
}
#[test]
fn a_byte_char_literal_holding_a_quote_does_not_open_a_string() {
let masked = blank_literals("{ if c == b'\"' { g(); } }");
assert_eq!(
masked.matches('{').count(),
2,
"structure was lost after a byte quote literal: {masked}"
);
assert_eq!(masked.matches('}').count(), 2, "{masked}");
}
#[test]
fn escaped_char_literals_are_masked_whole() {
let masked = blank_literals("{ a('\\''); b('\\\\'); c('\\n'); d(); }");
assert_eq!(masked.matches('{').count(), 1, "{masked}");
assert_eq!(masked.matches('}').count(), 1, "{masked}");
assert!(masked.contains("d()"), "code after was blanked: {masked}");
}
#[test]
fn ordinary_char_literals_are_masked_without_losing_structure() {
let src = "{ m(' '); n('x'); o('é'); }";
let masked = blank_literals(src);
assert_eq!(masked.matches('{').count(), 1, "{masked}");
assert_eq!(masked.matches('}').count(), 1, "{masked}");
assert_eq!(
masked.len(),
src.len(),
"masking a multi-byte char literal changed byte offsets"
);
}
#[test]
#[should_panic(expected = "raw string")]
fn a_raw_string_fails_closed() {
blank_literals("{ let s = r\"}{\"; }");
}
#[test]
#[should_panic(expected = "block comment")]
fn a_block_comment_fails_closed() {
blank_literals("{ /* } */ }");
}
}