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 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:

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

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)
}

Brotli compression typically reduces WASM size by 50-70%.

0xEFF000 prefix

Compressed WASM is prefixed with 0xEFF000 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, contracts must be activated before execution:

Activation process

  1. Initial deployment: Contract code is stored on-chain (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 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 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

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