wasmtime 45.x (latest stable as of Feb 2026).
- Release cadence: new major on the 20th of each month.
- LTS every 12th version (24 months support). Nearest LTS: v36.
- Requires Rust 1.90.0+.
- Repo: https://github.com/bytecodealliance/wasmtime
| Criterion | wasmtime | wasmer | wasm3 |
|---|---|---|---|
| Rust-native embedding | First-class | Yes | C FFI |
| Async host functions | Yes | No | No |
| Component Model / WASI | Full | Partial | No |
| Fuel / epoch metering | Both | Fuel only | Injection |
| Production users | Fastly, Fermyon, Cloudflare, Zed | General | Embedded |
| Sandboxing | Proven | Similar | Similar |
The Component Model is production-viable in wasmtime 45 and gives us critical advantages over raw core modules:
-
Structural sandboxing. A component compiled against a WIT world with no filesystem import literally cannot access the filesystem — enforced at the type level, not just by omission of host functions. This is stronger than core module sandboxing where imports are stringly-typed.
-
Type-safe API contract. The WIT definition is the API spec. Both host and guest get generated bindings (
wasmtime::component::bindgen!on the host,wit_bindgen::generate!on the guest). No manual ABI wrangling, no serialisation disagreements. -
Resource types. Opaque handles with lifecycle management (constructors, methods, destructors via
ResourceTable). Ideal for subscription handles, RPC connections, etc. -
Multi-language guests from day 1. Module authors can use Rust, C/C++, Go, JavaScript (ComponentizeJS), or Python (componentize-py) — all producing valid components against the same WIT world. This dramatically lowers the barrier for community modules.
-
No WASI required. The Component Model and WASI are architecturally separate. We define a pure
nexum:hostworld with exactly our host APIs. Zero WASI imports means zero implicit capabilities. -
Acceptable overhead. The canonical ABI adds marshalling for strings/lists (memory copy across boundary), but for a plugin system with coarse-grained calls this is negligible.
InstancePrefront-loads validation costs.
- Tooling churn.
wit-bindgen(v0.57) andcargo-component(v0.21) are functional but APIs are not yet stable. Pin versions in the SDK. - Native async Component Model (
stream<T>,future<T>) is still evolving. We use basic async host functions (func_wrap_async) which are stable.
| Aspect | Risk |
|---|---|
bindgen! macro, custom worlds, resource types |
Low — stable, well-documented |
wit-bindgen guest bindings |
Medium — API churn between versions |
| Component Model native async (streams/futures) | High — not needed yet, avoid for now |
Global, thread-safe compilation environment. One per process.
let mut config = Config::new();
config.async_support(true);
config.consume_fuel(true);
config.epoch_interruption(true);
let engine = Engine::new(&config)?;Per-module execution context. Holds component instances, host state (NexumHostState), fuel counters, resource limits, and the ResourceTable for handle management.
let mut store = Store::new(&engine, NexumHostState {
table: ResourceTable::new(),
rpc: alloy_provider,
db: redb_handle,
// ...
});
store.set_fuel(10_000)?;
store.epoch_deadline_async_yield_and_update(10); // yield after 10 epochs (~1s at 100ms tick)- Component: compiled from
.wasmcomponent binary (expensive, cacheable, thread-safe). - Linker: binds host implementations of our WIT interfaces.
- InstancePre: pre-validated component + linker (reusable across stores).
- Instance: a live component in a specific store, from which we call exports.
let component = Component::from_file(&engine, "twap_monitor.wasm")?;
let mut linker = Linker::new(&engine);
EventModule::add_to_linker(&mut linker, |state| state)?;
// Pre-validate once, instantiate many times (one per store)
let pre = linker.instantiate_pre(&component)?;
let bindings = EventModule::instantiate_pre(&mut store, &pre)?;Nexum uses a two-layer WIT architecture. The universal package nexum:host defines platform-agnostic interfaces and the event-module world. The CoW-specific package shepherd:cow extends it with CoW Protocol interfaces and the shepherd world.
The nexum:host package is the single source of truth for the universal host-guest contract. It defines a custom world with no WASI imports:
package nexum:host@0.2.0;
interface types {
type chain-id = u64;
record block {
chain-id: chain-id,
number: u64,
hash: list<u8>,
timestamp: u64, // milliseconds since Unix epoch, UTC
}
record log {
chain-id: chain-id,
address: list<u8>,
topics: list<list<u8>>,
data: list<u8>,
block-number: u64,
transaction-hash: list<u8>,
log-index: u32,
}
record tick {
fired-at: u64, // milliseconds since Unix epoch, UTC
}
record message {
content-topic: string,
payload: list<u8>,
timestamp: u64, // milliseconds since Unix epoch, UTC
sender: option<list<u8>>,
}
variant event {
block(block),
logs(list<log>),
tick(tick),
message(message),
}
/// Opaque config from nexum.toml [config] section. All TOML scalars are
/// flattened to their string form by the host. A typed `config-value`
/// variant is on the 0.3 roadmap, bundled with the manifest parser work.
type config = list<tuple<string, string>>;
/// Unified error type returned by every host function in 0.2.
record host-error {
domain: string, // "chain" | "store" | "messaging" | "identity" | "cow" | ...
kind: host-error-kind, // normative discriminant
code: s32, // domain-specific
message: string,
data: option<string>, // JSON for richer context
}
variant host-error-kind {
unsupported, // host does not implement this capability
unavailable, // capability exists, backend is down/offline
denied, // user or policy rejected
rate-limited,
timeout,
invalid-input,
internal,
}
}
interface chain {
use types.{chain-id, host-error};
/// Execute a JSON-RPC request against the specified chain.
///
/// The host forwards the request to the configured alloy provider for
/// the given chain, applying timeout/retry/rate-limit/fallback middleware
/// transparently. Method includes the namespace prefix (e.g. "eth_call").
///
/// `params` and the success return value are JSON-encoded strings matching
/// the JSON-RPC spec. The host handles id/jsonrpc framing; the guest only
/// provides method + params and receives the `result` field.
///
/// See doc 07 (RPC Namespace Design) for the full design rationale: a
/// single generic function replaces per-method WIT functions, enabling
/// the SDK to implement alloy's Transport trait and expose the full
/// alloy Provider API (80+ methods) to guest modules with zero WIT churn.
///
/// Note: signing RPC methods (eth_sendTransaction, eth_accounts,
/// eth_signTypedData_v4, personal_sign) are intercepted by the host and
/// delegated to the identity backend. The module does not need to handle
/// key material directly when using chain for transactions.
request: func(chain-id: chain-id, method: string, params: string)
-> result<string, host-error>;
/// A single JSON-RPC request to be executed as part of a batch.
record rpc-request {
method: string,
params: string,
}
/// Result of a single request inside a batch. Each entry is independent;
/// one failing call does not abort the others.
variant rpc-result {
ok(string),
err(host-error),
}
/// Additive 0.2 method: batched JSON-RPC. The alloy-backed HostTransport
/// routes RequestPacket::Batch through this — `provider.multicall(...)`
/// actually batches on the wire in 0.2. Hosts that cannot batch natively
/// MUST fall back to sequential `request` calls; the returned list is
/// the same length as `requests` and in the same order.
request-batch: func(chain-id: chain-id, requests: list<rpc-request>)
-> result<list<rpc-result>, host-error>;
}
interface identity {
use types.{host-error};
/// Get available signing accounts (20-byte Ethereum addresses).
accounts: func() -> result<list<list<u8>>, host-error>;
/// Sign a message with `personal_sign` semantics. The host MUST prepend
/// the EIP-191 prefix (`\x19Ethereum Signed Message:\n<len>`) before
/// hashing and signing. Hosts MUST NOT expose a raw-bytes signing path
/// through this function — a raw signer can be tricked into signing
/// EIP-155 transactions or EIP-712 payloads disguised as plain bytes.
///
/// Returns a 65-byte ECDSA secp256k1 signature (r || s || v).
///
/// A separate raw-bytes signing primitive, gated by an explicit
/// capability, is on the 0.3 roadmap.
sign: func(account: list<u8>, message: list<u8>) -> result<list<u8>, host-error>;
/// Sign EIP-712 typed data with the specified account.
/// `typed-data` is the JSON-encoded EIP-712 TypedData structure.
sign-typed-data: func(account: list<u8>, typed-data: string) -> result<list<u8>, host-error>;
}
interface local-store {
use types.{host-error};
get: func(key: string) -> result<option<list<u8>>, host-error>;
set: func(key: string, value: list<u8>) -> result<_, host-error>;
delete: func(key: string) -> result<_, host-error>;
list-keys: func(prefix: string) -> result<list<string>, host-error>;
}
interface logging {
enum level { trace, debug, info, warn, error }
log: func(level: level, message: string);
}
/// The universal event-driven module world. Platform-agnostic: no CoW,
/// no domain-specific imports. Suitable for any web3 automation.
///
/// In 0.2 this imports all six primitives — the identity import was
/// missing from the 0.1 WIT despite being part of the documented primitive
/// taxonomy, and is now present.
world event-module {
import chain;
import identity;
import local-store;
import remote-store;
import messaging;
import logging;
/// Called once on load. Receives typed config from nexum.toml.
export init: func(config: types.config) -> result<_, host-error>;
/// Called for each subscribed event.
export on-event: func(event: types.event) -> result<_, host-error>;
}In addition to the six core imports, 0.2 publishes three additive optional capabilities — clock (now-ms / monotonic-ns), random (CSPRNG fill), and http (allowlisted outbound HTTP) — which modules can declare in their nexum.toml [capabilities] section. The migration guide carries the full WIT for each. 0.2 also publishes the experimental query-module world for request/response modules; the WIT is stable but no host implementation ships in 0.2, so it's a target for MockHost testing only.
The shepherd:cow package extends the universal world with CoW Protocol interfaces. In 0.2 the two 0.1 interfaces (cow + order) merge into a single cow-api interface to eliminate the cow::cow::request triple-stutter:
package shepherd:cow@0.2.0;
interface cow-api {
use nexum:host/types.{chain-id, host-error};
/// HTTP-style request to the CoW Protocol API.
///
/// The host routes to the correct CoW API base URL for the given chain.
/// `method`: "GET" | "POST" | "PUT" | "DELETE"
/// `path`: relative API path, e.g. "/api/v1/orders"
/// `body`: optional JSON request body
request: func(
chain-id: chain-id,
method: string,
path: string,
body: option<string>,
) -> result<string, host-error>;
/// Submit a serialised order to the CoW Protocol.
/// (Replaces the 0.1 `order::submit` interface.)
submit-order: func(chain-id: chain-id, order-data: list<u8>)
-> result<string, host-error>;
}
/// CoW Protocol module world. Extends the universal event-module
/// with CoW-specific imports.
world shepherd {
include nexum:host/event-module;
import cow-api;
}- No WASI — by default, modules cannot access FS, network, clocks, or random. The additive 0.2 capabilities (
clock,random,http) provide controlled access to time, entropy, and allowlisted HTTP — but only when declared in the manifest's[capabilities]section. - All I/O through our interfaces — RPC reads, identity/signing, CoW API, local-store, order submission, logging.
- Generic JSON-RPC passthrough — the
chaininterface exposes a singlerequestfunction (plus an additiverequest-batch). The SDK implements alloy'sTransporttrait on top of it, giving modules the full alloyProviderAPI. See doc 07 for details. - Identity as a first-class primitive — the
identityinterface provides key management and signing. Thechainhost implementation depends onidentityinternally: signing RPC methods (eth_sendTransaction,eth_accounts,eth_signTypedData_v4,personal_sign) are intercepted and delegated to the identity backend. Modules can also importidentitydirectly forpersonal_sign-style message signing, EIP-712 typed data signing, and listing accounts. (Raw-bytes signing, gated by an explicit capability, is on the 0.3 roadmap; the currentsignMUST prepend the EIP-191 prefix.) - Unified
host-errortaxonomy — every host function returnsresult<T, host-error>. The 0.1 per-protocol error types (json-rpc-error,identity-error,msg-error,store-error,api-error) are gone. Modules match onhost-error-kind(unsupported,unavailable,denied,rate-limited,timeout,invalid-input,internal) for retry/backoff decisions. list<u8>for raw bytes — local-store values, order payloads, signatures, accounts, etc. The SDK provides typed wrappers.- Resource types can be added later (e.g. subscription handles, cursor-based log iteration).
- Two worlds in 0.2's reference runtime —
nexum:host/event-modulefor platform-agnostic modules;shepherd:cow/shepherdfor CoW Protocol modules that need thecow-apiimport. The experimentalnexum:host/query-moduleworld is published but not yet hosted.
The host uses wasmtime::component::bindgen! to generate Rust traits from the WIT. For universal interfaces, the generated traits live under nexum::host::. For CoW-specific interfaces, they live under shepherd::cow::.
// Universal event-module world
wasmtime::component::bindgen!({
path: "wit/nexum-host",
world: "event-module",
async: true,
});
// CoW-specific shepherd world (extends event-module)
wasmtime::component::bindgen!({
path: "wit/shepherd-cow",
world: "shepherd",
async: true,
});The Identity trait abstracts key management and signing. Platform implementations vary (server uses keystore/KMS/HSM, mobile uses device keychain, WebView uses wallet extensions), but the trait is uniform:
trait Identity {
fn accounts(&self) -> Result<Vec<Address>>;
fn sign(&self, account: Address, data: &[u8]) -> Result<Signature>;
fn sign_typed_data(&self, account: Address, typed_data: &str) -> Result<Signature>;
}The chain host implementation depends on Identity internally. When a module calls a signing RPC method through chain::request (e.g. eth_sendTransaction, eth_accounts, eth_signTypedData_v4, personal_sign), the host intercepts the call and delegates to the identity backend instead of forwarding to the RPC provider:
impl nexum::host::chain::Host for NexumHostState {
async fn request(
&mut self,
chain_id: u64,
method: String,
params: String,
) -> Result<Result<String, HostError>> {
// Signing methods are intercepted and delegated to identity.
match method.as_str() {
"eth_accounts" => {
let accounts = self.identity.accounts()?;
let json = serde_json::to_string(&accounts)?;
return Ok(Ok(json));
}
"eth_sendTransaction" => {
// Parse tx, sign via identity, then submit signed tx to provider
let tx = serde_json::from_str(¶ms)?;
let signature = self.identity.sign(tx.from, &tx.signing_hash())?;
let signed = tx.with_signature(signature);
let provider = self.provider_for(chain_id)?;
let hash = provider.send_raw_transaction(&signed.encoded()).await?;
return Ok(Ok(serde_json::to_string(&hash)?));
}
"eth_signTypedData_v4" | "personal_sign" => {
// Delegate to identity for signing
let (account, data) = parse_sign_params(&method, ¶ms)?;
let sig = self.identity.sign(account, &data)?;
return Ok(Ok(serde_json::to_string(&sig)?));
}
_ => {}
}
if !self.is_method_allowed(&method) {
return Ok(Err(HostError {
domain: "chain".into(),
kind: HostErrorKind::Denied,
code: -32601,
message: format!("method not allowed: {method}"),
data: None,
}));
}
let provider = self.provider_for(chain_id)?;
let raw_params: Box<RawValue> = RawValue::from_string(params)?;
// One function handles the entire eth_ namespace — alloy's provider
// stack (timeout, retry, rate-limit, fallback) applies transparently.
match provider.raw_request_dyn(method.into(), &raw_params).await {
Ok(result) => Ok(Ok(result.get().to_string())),
Err(e) => Ok(Err(HostError::from_transport("chain", e))),
}
}
}The identity::Host implementation delegates to the platform-specific Identity trait. Errors map to the unified HostError:
impl nexum::host::identity::Host for NexumHostState {
async fn accounts(&mut self) -> Result<Result<Vec<Vec<u8>>, HostError>> {
match self.identity.accounts() {
Ok(addrs) => Ok(Ok(addrs.into_iter().map(|a| a.to_vec()).collect())),
Err(e) => Ok(Err(HostError {
domain: "identity".into(),
kind: HostErrorKind::Internal,
code: 1,
message: e.to_string(),
data: None,
})),
}
}
async fn sign(
&mut self,
account: Vec<u8>,
data: Vec<u8>,
) -> Result<Result<Vec<u8>, HostError>> {
let address = Address::from_slice(&account);
match self.identity.sign(address, &data) {
Ok(sig) => Ok(Ok(sig.to_vec())),
Err(IdentityBackendError::UserRejected) => Ok(Err(HostError {
domain: "identity".into(),
kind: HostErrorKind::Denied,
code: 2,
message: "user rejected".into(),
data: None,
})),
Err(e) => Ok(Err(HostError {
domain: "identity".into(),
kind: HostErrorKind::Internal,
code: 3,
message: e.to_string(),
data: None,
})),
}
}
// sign_typed_data follows the same pattern.
}impl nexum::host::local_store::Host for NexumHostState {
async fn get(&mut self, key: String) -> Result<Result<Option<Vec<u8>>, HostError>> {
// Read from the in-flight WriteTransaction (not a new ReadTransaction)
// so the module sees its own uncommitted writes within a single on_event.
let table = self.write_txn.open_table(self.local_store_table())?;
Ok(Ok(table.get(key.as_str())?.map(|v| v.value().to_vec())))
}
// ...
}
impl shepherd::cow::cow_api::Host for NexumHostState {
// CoW-specific host implementation
// ...
}See doc 07 for the full chain and cow-api host implementations, method allowlisting, and the HostTransport that bridges this to alloy's Provider API on the guest side.
Module authors targeting the universal event-module world add the nexum-sdk crate and use the #[nexum::module] proc macro. Modules can access identity for signing operations — either indirectly through chain (signing RPC methods are handled transparently) or directly via the identity interface for raw signing:
use nexum_sdk::prelude::*;
#[nexum::module]
struct BlockLogger;
impl BlockLogger {
fn init(config: Config) -> Result<()> {
info!("Block logger starting");
Ok(())
}
async fn on_block(block: Block, provider: &RootProvider) -> Result<()> {
let block_num = provider.get_block_number().await?;
info!("New block: {block_num}");
TypedState::set("last_block", &block_num)?;
Ok(())
}
}Module authors targeting the CoW-specific shepherd world add the shepherd-sdk crate and use the #[shepherd::module] proc macro. The macro provides named event handlers (on_block, on_logs, on_tick, on_message) — it generates the on_event match dispatch, WIT export wrapper, and optional provider injection. Handlers can be async fn for natural .await:
use shepherd_sdk::prelude::*;
sol! {
function getTradeableOrderWithSignature(
address owner, bytes32 ctx, bytes32 orderHash
) external view returns (bytes memory order, bytes memory signature);
}
#[shepherd::module]
struct TwapMonitor;
impl TwapMonitor {
fn init(config: Config) -> Result<()> {
info!("TWAP monitor starting");
Ok(())
}
// Named handler — macro generates on_event match dispatch.
// provider is injected from block.chain_id.
// async fn — macro wraps in block_on (single-poll, zero overhead).
async fn on_block(block: Block, provider: &RootProvider) -> Result<()> {
// Full alloy Provider API — natural .await
let block_num = provider.get_block_number().await?;
let balance = provider.get_balance(owner).latest().await?;
// Typed contract calls with sol! + EthCall builder
let tx = TransactionRequest::default()
.to(contract)
.input(getTradeableOrderWithSignatureCall {
owner, ctx, orderHash: order_hash,
}.abi_encode().into());
let result = provider.call(tx).latest().await?;
let decoded = getTradeableOrderWithSignatureCall::abi_decode_returns(&result)?;
// CoW API via typed client
let cow = Cow::new(block.chain_id);
cow.submit_order(&order)?;
// State persistence
TypedState::set("last_block", &block_num)?;
Ok(())
}
// Only define handlers for events you subscribe to.
// No on_logs, on_tick, or on_message → those events are silently ignored.
}Build with cargo component build --release (or cargo build --target wasm32-wasip2 + wasm-tools component new).
See doc 05 for the full macro design (named handlers, provider injection, escape hatch) and doc 07 for the HostTransport implementation and provider() constructor.
| Language | Tooling | Maturity |
|---|---|---|
| Rust | wit-bindgen + cargo-component |
Mature |
| C/C++ | wit-bindgen c + WASI SDK |
Mature |
| Go | wit-bindgen Go generator |
Maturing |
| JavaScript | ComponentizeJS (SpiderMonkey) | Maturing |
| Python | componentize-py (CPython) | Maturing |
| C# | wit-bindgen-csharp |
Emerging |
All produce valid components against the same WIT worlds (nexum:host/event-module for universal, shepherd:cow/shepherd for CoW).
Config::consume_fuel(true)— each WASM op consumes fuel; exhaustion traps.- Use for per-invocation budgets: cap a single
on_eventcallback.
Config::epoch_interruption(true)— background Tokio task callsengine.increment_epoch()on a fixed interval.- Stores yield at epoch boundaries via
epoch_deadline_async_yield_and_update. - Use for wall-clock fairness: prevent one module from starving others.
Both are needed: fuel for correctness, epochs for liveness.
Implement ResourceLimiter to cap per-module:
- Memory growth — target <10 MB default.
- Table growth — max entries.
- Instance count — max concurrent.
Enforced synchronously on every memory.grow / table.grow.
All RPC and CoW API I/O is async (alloy / reqwest on the host). wasmtime bridges this:
Config::async_support(true).- Host functions registered with
func_wrap_async(or viaasync: trueinbindgen!). - Guest exports called with
call_async. - wasmtime runs WASM on a separate native stack;
Future::polldrives execution. - Epoch yielding ensures cooperation with the Tokio scheduler.
Note: We use wasmtime's basic async support (stable), not the Component Model native async (stream<T>, future<T>) which is still evolving.
- WASI 0.2.1 is stable in wasmtime. WASI 0.3 (native async) is in preview.
- The
event-moduleworld imports zero WASI interfaces. - This is a security feature: components structurally cannot access FS/network/clocks via WASI.
- The 0.2 additive capabilities (
clock,random,http) cover the common needs that would otherwise drive a WASI import, but as first-class Nexum interfaces — capability-negotiated via the manifest, allowlisted (in the HTTP case), and consistent with the rest of the host surface (host-errorreturns, no panics on capability absence).
| Nexum Concept | wasmtime Primitive |
|---|---|
| Runtime process | Engine (one, shared) |
| Universal API contract | WIT world (nexum:host/event-module) |
| CoW API contract | WIT world (shepherd:cow/shepherd) |
| Compiled module | Component (cached, thread-safe) |
| Pre-validated module | InstancePre (linker + component) |
| Running instance | Store<NexumHostState> + Instance |
| Host API impl | Traits generated by bindgen! |
| Host identity | Identity trait (keystore/KMS/HSM on server) |
| Opaque handles | Resource<T> + ResourceTable |
| Per-call budget | Fuel |
| Wall-clock fairness | Epoch interruption |
| Memory/table caps | ResourceLimiter |
| Async RPC / CoW I/O | func_wrap_async + Tokio |
| Persistent state | redb (per-module database file, via local-store interface host fns) |