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:
- Performance: WASM compiles to native machine code for fast execution
- Security: Sandboxed environment prevents unauthorized access
- Portability: Same bytecode runs identically across all nodes
- Language flexibility: Developers can use Rust, C, C++, or any language that compiles to WASM
- 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.copyandmemory.filloperations - 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
- Initial deployment: Contract code is stored on-chain (compressed)
- Activation call: Special transaction invokes
activateProgram - Decompression: Brotli-compressed WASM is decompressed
- Validation: WASM is checked for:
- Valid structure
- Required exports (
user_entrypoint) - Allowed imports (only
vm_hooks) - Memory constraints
- Compilation: WASM is compiled to native machine code
- 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:
| Limit | Value | Reason |
|---|---|---|
| Uncompressed size | ~3-4 MB | Memory and processing constraints |
| Compressed size | 24 KB (initial) | Ethereum transaction size limit |
| Compressed size | 128 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_ui64.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
}