De-MLS
Decentralized MLS proof-of-concept that coordinates secure group membership through
off-chain consensus and a Waku relay.
This repository now ships a native desktop client built with Dioxus that drives the MLS core directly.
What's Included
| Crate / Path | Description |
|---|---|
de-mls (src/) |
Core library — MLS groups, consensus, and the delivery service layer |
| crates/de_mls_gateway | Bridges UI commands (AppCmd) to the core runtime and streams AppEvents back |
| crates/ui_bridge | Bootstrap glue that hosts the async command loop for desktop clients |
| apps/de_mls_desktop_ui | Dioxus desktop UI with login, chat, stewardship, and voting flows |
| tests/ | Integration tests for the MLS state machine and consensus paths |
Prerequisites
- Rust (stable toolchain)
- Nim (for building libwaku) —
brew install nim
Building libwaku
The project uses a local libwaku.dylib built from logos-messaging-nim:
make # clones, builds, and copies libwaku.dylib into ./libs/
build.rs links against libs/libwaku.dylib (or libwaku.so on Linux) and embeds an rpath automatically.
Feature Flags
| Feature | Default | Description |
|---|---|---|
waku |
off | Enables the Waku relay transport (WakuDeliveryService) and links to libwaku |
The core library (de_mls) compiles and tests without libwaku present.
The waku feature is required only when you need the concrete WakuDeliveryService
implementation — the gateway and desktop crates enable it automatically.
# Use the transport-agnostic types only (no libwaku needed):
de_mls = { path = "..." }
# Enable the Waku transport (requires libwaku):
de_mls = { path = "...", features = ["waku"] }
Delivery Service (src/ds/)
The delivery service (DS) is the transport layer that sits between the MLS core and the network. It is fully synchronous — no tokio dependency — so it can be used from any Rust context.
Module layout
src/ds/
├── mod.rs Public API re-exports
├── transport.rs DeliveryService trait, OutboundPacket, InboundPacket
├── error.rs DeliveryServiceError
├── topic_filter.rs TopicFilter — async HashSet-based allowlist for inbound routing
└── waku/ Waku relay implementation (libwaku FFI)
├── mod.rs WakuDeliveryService, WakuConfig, content-topic helpers
├── sys.rs Raw C FFI bindings (trampoline pattern)
└── wrapper.rs Safe WakuNodeCtx wrapper (Drop calls waku_stop)
Key types
| Type | Feature | Description |
|---|---|---|
DeliveryService |
— | Trait — send() and subscribe(), both synchronous |
OutboundPacket |
— | Payload + group id + subtopic + app id (self-message filter) |
InboundPacket |
— | Payload + group id + subtopic + app id + timestamp |
TopicFilter |
— | Async allowlist used by the gateway to filter inbound packets by group |
WakuDeliveryService |
waku |
Concrete impl — runs an embedded Waku node on a background std::thread |
WakuConfig |
waku |
Node port, discv5 settings |
WakuStartResult |
waku |
Returned by start() — contains the service + optional local ENR |
Basic usage
use de_mls::ds::{WakuDeliveryService, WakuConfig, DeliveryService, OutboundPacket};
let result = WakuDeliveryService::start(WakuConfig {
node_port: 60000,
discv5: true,
discv5_udp_port: 61000,
..Default::default()
})?;
let ds = result.service;
// Subscribe (can be called multiple times for fan-out).
let rx = ds.subscribe();
std::thread::spawn(move || {
while let Ok(pkt) = rx.recv() {
println!("{} bytes from group {}", pkt.payload.len(), pkt.group_id);
}
});
// Send a message.
ds.send(OutboundPacket::new(
b"hello".to_vec(), "app_msg", "my-group", b"instance-id",
))?;
// Shut down explicitly, or just drop all clones.
ds.shutdown();
Threading model
WakuDeliveryService::start() spawns a "waku-node" thread that owns the
libwaku context. Outbound messages are queued via std::sync::mpsc; inbound
events are fanned out to all subscribers. The background thread is wrapped in
catch_unwind so a panic in the FFI layer won't crash the process.
In async code (e.g. the gateway), wrap ds.send() in
tokio::task::spawn_blocking to avoid blocking the tokio runtime.
Content topics
Messages are routed by Waku content topics with the format:
/{group_name}/{version}/{subtopic}/proto
Two subtopics are used: app_msg (application messages) and welcome
(MLS Welcome messages for group joins). The pubsub topic is fixed to
/waku/2/rs/15/1 (cluster 15, shard 1).
Self-message filtering
Each group handle generates a random UUID (app_id) stored in the Waku message
meta field. On receive, the application compares packet.app_id against the
local handle's id and drops self-originated messages. Waku relay (gossipsub)
does not filter self-messages natively.
Quick Start
Environment Variables
| Variable | Required | Description |
|---|---|---|
NODE_PORT |
Yes | TCP port for the embedded Waku node |
DISCV5_BOOTSTRAP_ENRS |
No | Comma-separated ENR strings for discv5 bootstrap |
discv5 peer discovery is always enabled. The discv5 UDP port is derived automatically (NODE_PORT + 1000).
Use a unique NODE_PORT per local client so the embedded Waku nodes do not collide.
Running Multiple Nodes For Example
Nodes on the same clusterId/shard discover each other automatically via discv5.
No external relay required — just run multiple local nodes.
Node 1 (bootstrap node):
NODE_PORT=60001 cargo run -p de_mls_desktop_ui
Copy the Local ENR: enr:-QE... line from the logs.
Nodes 2–4 (bootstrap off node 1):
NODE_PORT=60002 DISCV5_BOOTSTRAP_ENRS="enr:-QE..." cargo run -p de_mls_desktop_ui
NODE_PORT=60003 DISCV5_BOOTSTRAP_ENRS="enr:-QE..." cargo run -p de_mls_desktop_ui
NODE_PORT=60004 DISCV5_BOOTSTRAP_ENRS="enr:-QE..." cargo run -p de_mls_desktop_ui
All nodes discover each other via the DHT and form a gossipsub relay mesh automatically.
Using the Desktop UI
-
Login screen – paste an Ethereum-compatible secp256k1 private key (hex, with or without
0x) and clickEnter.
On success the app derives your wallet address, stores it in session state, and navigates to the home layout. -
Header bar – shows the derived address and allows runtime log-level changes (
error→trace).
Log files rotate daily underapps/de_mls_desktop_ui/logs/. -
Groups panel – lists every MLS group returned by the gateway.
UseCreateorJointo open a modal, enter the group name, and the UI automatically refreshes the list and opens the group. -
Chat panel – displays live conversation messages for the active group.
Compose text messages at the bottom; the UI also offers:Leave groupto request a self-ban (the backend fills in your address)Request banto request ban for another user Member lists are fetched automatically when a group is opened so you can pick existing members from the ban modal.
-
Consensus panel – keeps stewards and members aligned:
- Shows whether you are a steward for the active group
- Lists pending steward requests collected during the current epoch
- Surfaces the proposal currently open for voting with
YES/NObuttons - Stores the latest proposal decisions with timestamps for quick auditing
Development Tips
cargo test -p de_mls– runs core tests (no libwaku required)cargo test -p de_mls --features waku– includes Waku transport tests (needs libwaku inlibs/)cargo fmt --all --check/cargo clippy– keep formatting and linting consistent with the codebaseRUST_BACKTRACE=full– helpful when debugging state-machine transitions during development
Logs for the desktop UI live in apps/de_mls_desktop_ui/logs/; core logs are emitted to stdout as well.
Contributing
Issues and pull requests are welcome. Please include reproduction steps, relevant logs, and test coverage where possible.