Skip to main content

Overview

wacore is the core WhatsApp protocol implementation for whatsapp-rust. It’s designed to be platform-agnostic with no runtime dependencies on Tokio or specific databases, making it portable across different async runtimes and storage backends.
[dependencies]
wacore = "0.5"

Philosophy

wacore contains all the core logic for:
  • Binary protocol encoding/decoding
  • Cryptographic primitives (AES-GCM, Signal Protocol)
  • IQ protocol types and specifications
  • Runtime abstraction (Runtime trait for pluggable async executors)
  • Network abstractions (Transport, TransportFactory, HttpClient traits)
  • State management traits (Backend, SignalStore, AppSyncStore, etc.)
  • Message builders and parsers
It has zero runtime dependencies — no Tokio, no async-std, only futures, async-trait, async-lock, and async-channel for async primitives. This makes wacore portable to any async runtime, including WASM targets. The main whatsapp-rust crate provides concrete implementations (Tokio runtime, SQLite storage, ureq HTTP client, Tokio WebSocket transport).

Key Exports

Re-exported Crates

pub use aes_gcm;                      // AES-GCM encryption
pub use wacore_appstate as appstate;  // App state sync
pub use wacore_noise as noise;        // Noise Protocol
pub use wacore_libsignal as libsignal; // Signal Protocol

Derive Macros

pub use wacore_derive::{EmptyNode, ProtocolNode, StringEnum};
  • EmptyNode - For protocol nodes with only a tag (no attributes)
  • ProtocolNode - For protocol nodes with string attributes
  • StringEnum - For enums with string representations
See Binary Protocol for usage examples.

Framing

pub use wacore_noise::framing; // WebSocket frame encoding/decoding

Core Modules

Protocol & Binary

Binary protocol

Type-safe protocol node builders and parsers

xml

XML utilities for protocol nodes

Cryptography

Signal Protocol

Signal Protocol implementation for E2E encryption

noise

Noise Protocol XX for handshake encryption

IQ Protocol

pub mod iq;
Type-safe IQ request/response specifications:
  • blocklist - Block/unblock contacts
  • chatstate - Typing indicators, presence
  • contacts - Contact synchronization
  • dirty - Dirty bit checking
  • groups - Group management operations
  • keepalive - Connection keepalive
  • mediaconn - Media server connections
  • mex - Message Extension queries
  • passive - Passive IQ handling
  • prekeys - Prekey distribution
  • privacy - Privacy settings
  • props - Server properties and A/B experiment configs (includes config_codes for well-known experiment flags)
  • spam_report - Spam reporting
  • tctoken - Temporary client tokens
  • usync - User synchronization
See Architecture for the IQ protocol pattern.

Message Handling

messages

Message encryption and decryption

send

Message sending logic

download

Media download and decryption

upload

Media encryption and upload preparation

State Management

pub mod store;
  • Device - Core device state structure
  • DeviceCommand - State mutation commands
  • traits - Backend trait definitions (Backend, SessionStore, etc.)
  • ab_props - In-memory A/B experiment property cache (AbPropsCache), populated from fetch_props() on each connection. Features query this cache to check server-side experiment flags (e.g., privacy token attachment on group operations).
See State Management for the command pattern.

Runtime & Networking

pub mod runtime;  // Runtime trait, AbortHandle, timeout(), blocking()
pub mod net;      // Transport, TransportFactory, HttpClient, TransportEvent
The runtime module defines the Runtime trait that all async operations go through:
pub trait Runtime: Send + Sync + 'static {
    fn spawn(&self, future: Pin<Box<dyn Future<Output = ()> + Send + 'static>>) -> AbortHandle;
    fn sleep(&self, duration: Duration) -> Pin<Box<dyn Future<Output = ()> + Send>>;
    fn spawn_blocking(&self, f: Box<dyn FnOnce() + Send + 'static>) -> Pin<Box<dyn Future<Output = ()> + Send>>;
    fn yield_now(&self) -> Option<Pin<Box<dyn Future<Output = ()> + Send>>>;

    /// How often to yield in tight loops (every N items). Defaults to 10.
    fn yield_frequency(&self) -> u32 { 10 }
}
MethodPurpose
spawnSpawn a background task, returning an AbortHandle for cancellation. The handle is #[must_use] — dropping it aborts the task. Call .detach() for fire-and-forget tasks
sleepReturn a future that completes after a duration
spawn_blockingOffload a blocking closure to a thread pool
yield_nowCooperatively yield; return None if unnecessary (e.g., multi-threaded runtimes)
yield_frequencyHow many items to process before yielding in tight loops (default: 10)

Helper functions

The runtime module also provides two runtime-agnostic helper functions that work with any Runtime implementation: timeout — Race a future against a deadline using the runtime’s sleep implementation:
pub async fn timeout<F, T>(
    rt: &dyn Runtime,
    duration: Duration,
    future: F,
) -> Result<T, Elapsed>
where
    F: Future<Output = T>,
Returns Ok(value) if the future completes before the duration, or Err(Elapsed) if it times out. This is the runtime-agnostic replacement for tokio::time::timeout — the library uses it internally for phash validation, media retry timeouts, session establishment, and IQ response waiting. blocking — Offload a blocking closure and return its result:
pub async fn blocking<T: Send + 'static>(
    rt: &dyn Runtime,
    f: impl FnOnce() -> T + Send + 'static,
) -> T
Wraps Runtime::spawn_blocking with a oneshot channel to ferry the closure’s return value back to the caller. On WASM, the closure runs inline since there is only one thread. The net module defines the networking abstractions:
  • Transport — active connection for sending/receiving raw bytes
  • TransportFactory — creates new transport instances and event streams
  • HttpClient — HTTP request execution (buffered and streaming)
  • TransportEventConnected, DataReceived(Bytes), Disconnected
On WASM targets (target_arch = "wasm32"), all Send bounds are automatically removed.

Connection & Pairing

handshake

Noise Protocol handshake

pair

QR code pairing

pair_code

Phone number pairing

net

Transport and HTTP client traits

Specialized Features

appstate

App state synchronization (contacts, settings)

history_sync

Message history synchronization

usync

User device list synchronization

prekeys

Signal Protocol prekey generation

Time

pub mod time;
The time module centralizes all timestamp handling. By default it uses chrono::Utc::now(), but you can override the provider globally via set_time_provider for environments where std::time::SystemTime is unavailable (e.g., WASM) or for deterministic testing.

Functions

FunctionReturn typeDescription
now_millis()i64Current time in milliseconds since Unix epoch
now_secs()i64Current time in seconds since Unix epoch
now_utc()DateTime<Utc>Current time as a chrono::DateTime<Utc>
from_secs(ts)Option<DateTime<Utc>>Convert Unix timestamp (seconds) to DateTime<Utc>
from_secs_or_now(ts)DateTime<Utc>Like from_secs, falling back to now_utc() for out-of-range values
from_millis(ts)Option<DateTime<Utc>>Convert Unix timestamp (milliseconds) to DateTime<Utc>
from_millis_or_now(ts)DateTime<Utc>Like from_millis, falling back to now_utc() for out-of-range values

Custom time provider

Implement the TimeProvider trait and call set_time_provider before any time functions are used:
use wacore::time::{TimeProvider, set_time_provider};

struct FixedTime;

impl TimeProvider for FixedTime {
    fn now_millis(&self) -> i64 {
        1700000000000 // Fixed timestamp for testing
    }
}

// Must be called before any time functions are used
set_time_provider(FixedTime).expect("provider already set");
set_time_provider returns Err if a provider has already been set. The provider uses OnceLock internally, so it can only be configured once per process.

Instant

The module also provides a portable Instant type that replaces std::time::Instant, which is unavailable on wasm32-unknown-unknown. It uses now_millis() internally — not truly monotonic, but sufficient for elapsed-time measurement and timeout tracking.
use wacore::time::Instant;

let start = Instant::now();
// ... do work ...
let elapsed = start.elapsed(); // std::time::Duration
Instant supports Add<Duration> and Sub<Instant> (returning Duration), with saturating arithmetic to prevent overflow.

Utilities

  • client - Client context traits
  • ib - Identity byte utilities
  • proto_helpers - Protobuf conversion helpers
  • reporting_token - Reporting token generation
  • request - Request building utilities
  • stanza - Common stanza builders
  • sticker_pack - Sticker pack creation helpers (see sticker packs)
  • time - Pluggable time provider with portable Instant (see time above)
  • types - Common type definitions (JID, events, messages). Includes types::jid utilities for zero-allocation JID comparison (cmp_for_lock_order), buffer-reusing address formatting (write_protocol_address_to), and in-place sorted deduplication (sort_dedup_by_user, sort_dedup_by_device)
  • version - WhatsApp version constants
  • webp - WebP format utilities (animated sticker detection)

webp module

The webp module provides utilities for working with WebP image files. It is re-exported as whatsapp_rust::webp.

is_animated

Detects whether a WebP file contains animation frames by parsing RIFF/VP8X headers and scanning for ANIM/ANMF chunks.
pub fn is_animated(data: &[u8]) -> bool
data
&[u8]
required
Raw WebP file bytes.
Returns true if the WebP file is animated, false otherwise (including for invalid or too-short input). Example:
use whatsapp_rust::webp;

let webp_bytes = std::fs::read("sticker.webp")?;

if webp::is_animated(&webp_bytes) {
    println!("Animated sticker");
} else {
    println!("Static sticker");
}
This function is used internally by create_sticker_pack_zip to set the is_animated field on each sticker proto entry. You can also use it directly when you need to classify WebP files before processing.

Submodule Packages

wacore is split into several workspace crates:

wacore-binary

Location: wacore/binary Binary protocol encoding/decoding using WhatsApp’s custom format.
use wacore_binary::{
    CompactString,
    jid::Jid,
    node::Node,
    builder::NodeBuilder,
    marshal::{marshal, unmarshal_ref},
};

let node = NodeBuilder::new("message")
    .attr("type", "text")
    .attr("to", "15551234567@s.whatsapp.net")
    .build();

let bytes = marshal(&node)?;
let decoded = unmarshal_ref(&bytes)?;
Key exports:
  • CompactString - Re-export of compact_str::CompactString, used by Jid.user, NodeValue::String, and NodeContent::String
  • jid::{Jid, JidRef, Server, JidExt} - WhatsApp JID types. Server is an enum (#[repr(u8)]) with variants for all known WhatsApp server domains (Pn, Lid, Group, Broadcast, Newsletter, Hosted, HostedLid, Messenger, Interop, Bot, Legacy). JidExt provides helper methods (is_group(), is_newsletter(), etc.) for both owned and borrowed JID types
  • node::{Node, NodeRef, NodeStr, NodeValue, ValueRef, OwnedNodeRef} - Protocol node types. Node (owned) for building outgoing stanzas, NodeRef (borrowed) for reading received stanzas, NodeStr for borrowed-or-inline decoded strings, OwnedNodeRef for yoke-based zero-copy self-referential nodes shared as Arc<OwnedNodeRef>. The entire NodeRef type family (NodeRef, NodeStr, ValueRef, JidRef, NodeContentRef, OwnedNodeRef) implements serde::Serialize, producing output identical to their owned counterparts — enabling zero-copy serialization without converting to Node first
  • builder::NodeBuilder - Fluent node builder with new(&'static str) / new_dynamic(String), attr(), jid_attr(), children(), bytes(), string_content(), and apply_content() chaining methods
  • marshal::* - Binary marshaling functions
  • attrs::{AttrParser, AttrParserRef} - Attribute parsing utilities for owned Node and borrowed NodeRef respectively
  • token - Token dictionary

wacore-libsignal

Location: wacore/libsignal Signal Protocol implementation for end-to-end encryption.
use wacore::libsignal::{
    core::SessionCipher,
    protocol::{PreKeyBundle, PublicKey},
    store::SessionStore,
};
Key modules:
  • core - Core session cipher logic
  • crypto - Cryptographic primitives (HKDF, HMAC, AES)
  • protocol - Protocol message types
  • store - Store trait definitions
See Signal Protocol for encryption details.

wacore-noise

Location: wacore/noise Noise Protocol XX implementation for handshake encryption.
use wacore::noise::{
    NoiseHandshake,
    HandshakeUtils,
    build_handshake_header,
};
Key exports:
  • NoiseState - Generic Noise XX state machine
  • NoiseHandshake - WhatsApp-specific handshake wrapper
  • HandshakeUtils - Protocol message building/parsing
  • framing - WebSocket frame encoding
  • build_edge_routing_preintro - Edge routing helper

wacore-appstate

Location: wacore/appstate App state synchronization for contacts, settings, and metadata.
use wacore::appstate::{
    process_snapshot,
    process_patch,
    expand_app_state_keys,
    Mutation,
};
Key exports:
  • process_snapshot - Process full state snapshots
  • process_patch - Apply incremental patches
  • Mutation - State mutation records
  • LTHash - LTHash implementation for integrity
  • expand_app_state_keys - Key derivation

wacore-derive

Location: wacore/derive Procedural macros for protocol node generation.
use wacore::{EmptyNode, ProtocolNode, StringEnum};

#[derive(EmptyNode)]
#[protocol(tag = "participants")]
pub struct ParticipantsRequest;

#[derive(ProtocolNode)]
#[protocol(tag = "query")]
pub struct QueryRequest {
    #[attr(name = "request", default = "interactive")]
    pub request_type: String,
}

#[derive(StringEnum)]
pub enum Action {
    #[str = "block"]
    Block,
    #[str = "unblock"]
    Unblock,
}

Usage in Main Library

The main whatsapp-rust crate uses wacore modules throughout:
// Binary protocol
use wacore_binary::jid::Jid;
use wacore_binary::builder::NodeBuilder;

// Signal Protocol
use wacore::libsignal::store::SessionStore;
use wacore::libsignal::protocol::PreKeyBundle;

// App state
use wacore::appstate::{
    process_snapshot,
    Mutation,
};

// IQ specs
use wacore::iq::privacy as privacy_settings;

// Store traits
use wacore::store::traits::Backend;

// Protobuf helpers
use wacore::proto_helpers;

Design Principles

Platform-Agnostic

No dependencies on:
  • Tokio or any async runtime — uses only futures, async-trait, async-lock, async-channel
  • Specific database implementations
  • File system operations
This allows users to provide their own:
  • Runtime: Tokio (default), async-std, smol, WASM, etc. — implement Runtime (4 methods)
  • Storage: SQLite (default), PostgreSQL, in-memory, etc. — implement Backend (4 sub-traits)
  • Transport: Tokio WebSocket (default), custom protocols — implement TransportFactory + Transport
  • HTTP client: ureq (default), reqwest, surf, etc. — implement HttpClient

Type Safety

Strong typing throughout:
  • Jid with Server enum for WhatsApp identifiers — server type is an enum variant, not a string
  • Node / NodeRef for protocol messages (owned / borrowed)
  • Validated newtypes (e.g., GroupSubject with length limits)
  • Enum variants with StringEnum for protocol values

Zero-Copy Where Possible

  • Cow<'static, str> for owned Node.tag and Attrs keys — known protocol strings (from the token dictionary) are borrowed as static references with zero heap allocation, while unknown strings fall back to owned String
  • NodeStr<'a> for borrowed NodeRef.tag, AttrsRef keys, ValueRef::String, and JidRef.user — a borrowed-or-inline string type where the Owned variant uses CompactString (inline up to 24 bytes) instead of heap-allocated String, reducing allocation pressure during decoding
  • Server enum (#[repr(u8)]) for Jid.server — a Copy type that requires zero allocation, replacing the previous Cow<'static, str> string-based server field
  • OwnedNodeRef — yoke-based self-referential node that owns the decompressed network buffer while NodeRef borrows string/byte payloads directly from it. Received stanzas flow through the system as Arc<OwnedNodeRef> for cheap shared zero-copy access
  • Zero-copy Serialize — the entire NodeRef type family (NodeRef, NodeStr, ValueRef, JidRef, NodeContentRef, OwnedNodeRef) implements serde::Serialize, producing output identical to their owned counterparts. This allows serializing received stanzas directly from the network buffer without converting to owned Node types first. See Binary Protocol — Zero-copy serialization for details
  • NodeRef for borrowed node parsing
  • AttrParserRef for attribute iteration
  • marshal_ref for encoding without cloning

Benchmarks

The project includes two categories of benchmarks: protocol-level (iai-callgrind, deterministic instruction counts) and integration-level (real client operations with allocation tracking).

Protocol benchmarks (iai-callgrind)

wacore includes a suite of iai-callgrind benchmarks that measure instruction counts for core protocol operations. These benchmarks run under Valgrind’s Callgrind tool, producing deterministic, low-noise measurements that are suitable for CI regression tracking.

Prerequisites

Running the protocol benchmarks requires:
  • Nightly Rust (the project pins nightly-2026-04-05)
  • Valgrind installed on your system
  • iai-callgrind-runner (cargo install iai-callgrind-runner --version 0.16.1)

Available suites

SuiteLocationWhat it measures
send_receive_benchmarkwacore/benches/send_receive_benchmark.rsFull send/receive pipeline — DM send, DM receive, group send (steady-state skmsg), group send with SKDM distribution (10/50/256 participants), and group receive. Uses real prepare_peer_stanza and prepare_group_stanza functions with in-memory Signal stores
reporting_token_benchmarkwacore/benches/reporting_token_benchmark.rsReporting token generation — key derivation, token calculation, and full generation pipeline for simple and extended messages
binary_benchmarkwacore/binary/benches/binary_benchmark.rsBinary protocol encoding/decoding — marshal and unmarshal operations for various node sizes and structures
libsignal_benchmarkwacore/libsignal/benches/libsignal_benchmark.rsSignal Protocol operations — session establishment, message encrypt/decrypt, sender key distribution, and group cipher operations

Running protocol benchmarks

# Run all benchmarks across the workspace
cargo bench --workspace

# Run a specific benchmark suite
cargo bench -p wacore --bench send_receive_benchmark
cargo bench -p wacore --bench reporting_token_benchmark
cargo bench -p wacore-binary --bench binary_benchmark
cargo bench -p wacore-libsignal --bench libsignal_benchmark
Each benchmark produces instruction counts and optionally generates flamegraph SVGs for profiling hotspots.

Integration benchmarks

The bench-integration test suite (tests/bench-integration/) measures real-world client operations end-to-end, including wall-clock time and heap allocation counts. It uses a custom CountingAlloc global allocator that tracks every allocation and byte count, then produces customSmallerIsBetter JSON output for CI trend tracking.

Scenarios

ScenarioWhat it measures
connect_to_readyClient creation through Connected event — includes transport connect, Noise handshake, and initial sync
send_messageSingle DM send (sender side) — protobuf encoding, Signal encrypt, node marshal, and WebSocket write. Also measures amortized cost over 20 consecutive sends
send_and_receive_messageFull round-trip — send a DM and wait for delivery on the receiver. Also measures amortized cost over 20 round-trips
reconnectDisconnect and reconnect cycle — measures the time and allocations to re-establish a session
Each scenario reports three metrics: alloc_count (number of heap allocations), alloc_bytes (total bytes allocated), and wall_ms (elapsed wall-clock time). Amortized scenarios divide totals by the number of iterations for per-operation costs.

Running integration benchmarks

Integration benchmarks require a mock WhatsApp server (e.g., Bartender):
# Start the mock server (Docker)
docker run -d -p 8080:8080 ghcr.io/whiskeysockets-devtools/bartender:latest

# Run the benchmarks
MOCK_SERVER_URL="wss://127.0.0.1:8080/ws/chat" \
  cargo run -p bench-integration --release
The output is a JSON array of customSmallerIsBetter entries printed to stdout, suitable for consumption by github-action-benchmark.
Integration benchmarks require the danger-skip-tls-verify and debug-diagnostics features, which are enabled automatically via the bench-integration crate’s Cargo.toml. You can also enable DHAT heap profiling with --features dhat-heap for detailed allocation flamegraphs.

Allocation optimizations

The library includes several allocation-reduction strategies that the integration benchmarks track:
  • Thread-local zlib pool — The binary protocol decompressor (decompress_zlib_pooled) reuses a thread-local flate2::Decompress instance (~48 KB internal state) and output buffer across calls, avoiding per-frame heap allocations
  • CompactString for JIDs — JID user fields use compact_str::CompactString which stores strings up to 24 bytes inline (no heap allocation), covering the vast majority of phone numbers and LID identifiers
  • Server enum — JID server fields use a #[repr(u8)] enum instead of heap-allocated strings, making JID construction and comparison zero-allocation
  • Zero-copy node decoding — Received stanzas are decoded as NodeRef borrowing directly from the network buffer via OwnedNodeRef (yoke-based self-referential type), avoiding cloning string/byte payloads during decode
  • Pre-allocated buffers — History sync decompression uses a compressed_size_hint with a 4x multiplier for buffer pre-allocation, reducing Vec reallocation during decompression

CI integration

The repository includes GitHub Actions workflows for both benchmark types: Protocol benchmarks (.github/workflows/benchmark.yml):
  • Runs on every push to main and on pull requests
  • Uses github-action-benchmark to store baselines and generate trend charts on GitHub Pages
  • A custom parser script (.github/scripts/iai-to-benchmark-json.py) converts iai-callgrind instruction counts into the customSmallerIsBetter JSON format
  • Pull requests receive a collapsed PR comment (via .github/scripts/bench-comment.py) grouping benchmarks into regressions (>5% slower), improvements (>5% faster), and unchanged
  • The PR comment is idempotent — subsequent benchmark runs update the existing comment instead of creating duplicates
Integration benchmarks (.github/workflows/bench-integration.yml):
  • Runs on every push to main and on pull requests
  • Spins up a Bartender mock server as a Docker service container
  • Measures allocation counts, allocation bytes, and wall-clock time for each scenario
  • Pushes to main store the baseline for trend tracking
  • Pull requests compare against the baseline
Raw benchmark output for both suites is uploaded as artifacts (retained for 30 days) for debugging.

Next steps

waproto

Protocol Buffers message definitions

Binary Protocol

Type-safe protocol node pattern

Architecture

IqSpec request/response pairing

State Management

Device state and commands