Skip to main content

WebAssembly in Nitro

WebAssembly (WASM) is a binary instruction format that enables high-performance execution of programs in the Nitro virtual machine. This guide explains how WASM works in the context of Arbitrum Nitro and Stylus smart contract development.

What is WebAssembly?​

WebAssembly is a portable, size-efficient binary format designed for safe execution at near-native speeds. Key characteristics include:

  • Binary format: Compact representation that's faster to parse than text-based formats
  • Stack-based VM: Simple execution model with operand stack
  • Sandboxed execution: Memory-safe by design with explicit bounds checking
  • Language-agnostic: Can be targeted by many programming languages (Rust, C, C++, etc.)

Why WebAssembly in Nitro?​

Nitro uses WebAssembly as its execution environment for several reasons:

  1. Performance: WASM compiles to native machine code for fast execution
  2. Security: Sandboxed environment prevents unauthorized access
  3. Portability: Same bytecode runs identically across all nodes
  4. Language flexibility: Developers can use Rust, C, C++, or any language that compiles to WASM
  5. Determinism: Guaranteed identical execution across all validators

WASM Execution Pipeline

Figure: WebAssembly execution pipeline in Arbitrum Nitro, from source code to native execution with access to blockchain state.

WASM compilation target​

Stylus contracts are compiled to the wasm32-unknown-unknown target, which means:

  • 32-bit addressing: Uses 32-bit pointers and memory addresses
  • Unknown OS: No operating system dependencies
  • Unknown environment: Minimal runtime assumptions (no std by default)

The .cargo/config.toml file in Stylus projects configures the WASM target:

[target.wasm32-unknown-unknown]
rustflags = [
"-C", "link-arg=-zstack-size=32768", # 32KB stack
"-C", "target-feature=-reference-types", # Disable reference types
"-C", "target-feature=+bulk-memory", # Enable bulk memory operations
]

Compilation flags​

  • Stack size: Limited to 32KB to ensure bounded memory usage
  • Bulk memory: Enables efficient memory.copy and memory.fill operations
  • No reference types: Keeps the WASM simpler and more compatible

WASM binary structure​

A Stylus WASM module consists of several sections:

WASM Module Structure

Figure: WASM module structure showing the main sections including exports, imports (hostio functions), memory, and code.

Exports​

Every Stylus contract exports a user_entrypoint function:

#[no_mangle]
pub extern "C" fn user_entrypoint(len: usize) -> usize {
// Entry point for all contract calls
// len: size of calldata in bytes
// returns: size of output data in bytes
}

This function is automatically generated by the #[entrypoint] macro and serves as the single entry point for all contract interactions.

Imports​

WASM modules import low-level functions from the vm_hooks module:

// Example hostio imports
extern "C" {
fn storage_load_bytes32(key: *const u8, dest: *mut u8);
fn storage_store_bytes32(key: *const u8, value: *const u8);
fn msg_sender(sender: *mut u8);
fn block_timestamp() -> u64;
// ... and many more
}

These imported functions (called "hostio" functions) provide access to blockchain state and functionality.

Memory​

WASM modules use linear memory, which is:

  • Contiguous: Single continuous address space starting at 0
  • Growable: Can expand at runtime (in 64KB pages)
  • Isolated: Each contract has its own memory space

Memory growth is explicitly metered:

// Exported function that must exist
#[no_mangle]
pub extern "C" fn pay_for_memory_grow(pages: u16) {
// Called before memory.grow to charge for new pages
// Each page is 64KB
}

Custom sections​

WASM supports custom sections for metadata:

// Example: Add version information
#[link_section = ".custom.stylus-version"]
static VERSION: [u8; 5] = *b"0.1.0";

Custom sections can store:

  • Contract version
  • Source code hashes
  • Compiler metadata
  • ABI information

VM runtime​

Stylus executes WebAssembly on a modified version of the Wasmer runtime. Wasmer is used unmodified at the runtime layer; Stylus extends the environment with host I/O imports (documented in hostio-exports.mdx) that let WASM programs call into native blockchain functions (storage, calls, logs, cryptography).

For Arbitrum-specific adaptations to Wasmer (gas metering, deterministic fraud-proof support), see Offchain Labs' fork: github.com/OffchainLabs/wasmer.

Compression and deployment​

Before deployment, Stylus contracts undergo compression:

Brotli compression​

// From stylus-tools/src/utils/wasm.rs
pub fn brotli_compress(wasm: impl Read, compression_level: u32) -> io::Result<Vec<u8>> {
let mut compressed = Vec::new();
let mut encoder = brotli::CompressorWriter::new(&mut compressed, 4096, compression_level, 22);
io::copy(&mut wasm, &mut encoder)?;
encoder.flush()?;
Ok(compressed)
}

Using Brotli compression typically reduces the WASM size by 50-70%. For more on reducing binary size before compression, see Optimizing binaries.

0xEFF00000 prefix​

Compressed WASM is prefixed with 0xEFF00000 (4 bytes, defined in the SDK as EOF_NO_DICT) to identify it as a Stylus program:

pub fn add_prefix(compressed_wasm: impl IntoIterator<Item = u8>, prefix: &str) -> Vec<u8> {
let prefix_bytes = hex::decode(prefix.strip_prefix("0x").unwrap_or(prefix)).unwrap();
prefix_bytes.into_iter().chain(compressed_wasm).collect()
}

This prefix allows the Nitro VM to distinguish Stylus contracts from EVM bytecode.

Contract activation​

After deployment, activating a contract is required before execution:

Activation process​

  1. Initial deployment: Contract code is stored onchain (compressed)
  2. Activation call: Special transaction invokes activateProgram
  3. Decompression: Brotli-compressed WASM is decompressed
  4. Validation: WASM is checked for:
    • Valid structure
    • Required exports (user_entrypoint)
    • Allowed imports (only vm_hooks)
    • Memory constraints
  5. Compilation: WASM is compiled to native machine code
  6. Caching: Compiled code is cached for future executions

One-time cost​

Activation incurs a one-time gas cost but provides benefits:

  • Fast execution: Native code runs 10-100x faster than interpreted
  • Persistent cache: Compilation happens once, benefits all future calls
  • Optimizations: Native compiler applies target-specific optimizations

Verification​

The activation process checks for the pay_for_memory_grow function to verify the correct entrypoint setup:

// From activation.rs
if !wasm::has_entrypoint(&wasm)? {
bail!("WASM is missing the entrypoint export");
}

Development workflow​

1. Write Rust code​

use stylus_sdk::{alloy_primitives::U256, prelude::*};

#[entrypoint]
#[storage]
pub struct Counter {
count: StorageU256,
}

#[public]
impl Counter {
pub fn increment(&mut self) {
let count = self.count.get() + U256::from(1);
self.count.set(count);
}
}

2. Compile to WASM​

cargo stylus build

This runs:

cargo build \
--lib \
--locked \
--release \
--target wasm32-unknown-unknown \
--target-dir target/wasm32-unknown-unknown/release

3. Optimize (optional)​

wasm-opt target/wasm32-unknown-unknown/release/my_contract.wasm \
-O3 \
--strip-debug \
-o optimized.wasm

Optimization can reduce the size by an additional 10-30%.

4. Deploy and activate​

# Deploy compressed WASM
cargo stylus deploy --private-key=$PRIVATE_KEY

# Activation happens automatically

Size limitations​

Nitro imposes limits on WASM contract size:

LimitValueReason
Uncompressed size~3-4 MBMemory and processing constraints
Compressed size24 KB (initial)Ethereum transaction size limit
Compressed size128 KB (with EIP-4844)Larger blob transactions

To stay within limits:

  • Use #[no_std] to avoid standard library bloat
  • Strip debug symbols with --strip-debug
  • Enable aggressive optimization (-O3)
  • Minimize dependencies
  • Use compact data structures

Memory model​

WASM Memory Model

Figure: WASM linear memory model showing the fixed 32KB stack and growable heap organized in 64KB pages.

Linear memory layout​

0x00000000 ┌─────────────────┐
│ Stack │ 32 KB fixed size
0x00008000 ├─────────────────┤
│ Heap/Data │ Grows upward
│ │
│ (Available) │
│ │
0xFFFFFFFF └─────────────────┘

Memory operations​

// Bulk memory operations (enabled by target config)
unsafe {
// Fast memory copy
core::ptr::copy_nonoverlapping(src, dst, len);

// Fast memory fill
core::ptr::write_bytes(ptr, value, len);
}

The bulk-memory feature flag enables efficient WASM instructions like memory.copy and memory.fill.

Advanced: WASM instructions​

Stylus uses WASM MVP (Minimum Viable Product) instructions plus bulk-memory operations:

Arithmetic​

  • i32.add, i32.sub, i32.mul, i32.div_s, i32.div_u
  • i64.add, i64.sub, i64.mul, i64.div_s, i64.div_u

Memory access​

  • i32.load, i32.store (32-bit load/store)
  • i64.load, i64.store (64-bit load/store)
  • memory.grow (expand memory)
  • memory.copy (bulk copy, requires flag)
  • memory.fill (bulk fill, requires flag)

Control flow​

  • call, call_indirect (function calls)
  • if, else, block, loop (structured control flow)
  • br, br_if (branching)

Not supported​

  • ❌ Floating point operations (f32, f64)
  • ❌ SIMD operations
  • ❌ Reference types
  • ❌ Multiple memories
  • ❌ Threads

Best practices​

1. Minimize binary size​

// Use #[no_std] when possible
#![no_std]
extern crate alloc;

// Avoid large dependencies
// Prefer: alloy-primitives
// Avoid: serde_json, regex (unless necessary)

2. Optimize memory usage​

// Stack allocate when possible
let small_buffer = [0u8; 32];

// Heap allocate only when necessary
let large_buffer = vec![0u8; 1024];

3. Profile before optimizing​

# Check binary size
ls -lh target/wasm32-unknown-unknown/release/*.wasm

# Analyze with twiggy
cargo install twiggy
twiggy top target/wasm32-unknown-unknown/release/my_contract.wasm

4. Test locally​

# Use cargo-stylus for local testing
cargo stylus check
cargo stylus export-abi

5. Validate before deployment​

// Ensure entrypoint exists
#[entrypoint]
#[storage]
pub struct MyContract { /* ... */ }

// Verify required exports
#[no_mangle]
pub extern "C" fn pay_for_memory_grow(pages: u16) {
// Generated automatically by SDK
}

Resources​