How Rust NIFs Power the Amadeus Protocol Node: High-Performance Storage and Consensus in Elixir
Rust NIFs serve as the critical bridge that lets the Amadeus Protocol node's Elixir/Erlang runtime execute performance-critical storage and consensus operations in native code, combining BEAM concurrency with Rust's speed and memory safety.
The Amadeus Protocol node is architected primarily in Elixir, running on the Erlang VM (BEAM). Yet blockchain nodes demand extreme efficiency for on-disk storage and cryptographic verification. Rather than compromise the developer experience or rewrite the entire system, the Amadeus team uses Rust Native Implemented Functions (NIFs) — compiled Rust libraries that the BEAM can call directly. This article examines exactly how these NIFs function in the amadeusprotocol/node repository based on the source code itself.
What Are Rust NIFs and Why They Matter for Blockchain Nodes
Rust NIFs are dynamic libraries loaded into the BEAM that expose Rust functions as if they were native Erlang functions. For the Amadeus node, this architecture solves three hard problems:
- Storage latency — RocksDB operations must not block lightweight Erlang processes
- Cryptographic throughput — BLS12-381 signatures and proof-of-work verification need raw compute performance
- Memory safety — Native code handling financial data cannot tolerate use-after-free or data races
The tradeoff is worth it: Rust NIFs give the Amadeus node native speed without sacrificing the fault-tolerance and distribution primitives that make Elixir ideal for networked systems.
Core Storage NIFs: RocksDB Operations in lib.rs
The heart of the Amadeus Protocol node's Rust NIF layer lives in ex/native/rdb/src/lib.rs. This file registers the NIF module Elixir.RDB and implements the complete RocksDB interface used by the node.
NIF Registration and Function Exports
At lines 14-15 of lib.rs, the module declares its exported functions using the rustler macro system:
#[rustler::nif]
fn open_transaction_db<'a>(env: Env<'a>, path: String, cf_names: Vec<String>) -> NifResult<Term<'a>> {
// Implementation opens RocksDB with column families
}
The exposed functions include:
open_transaction_db/2— Initialize database with named column familiesput/3,get/2,delete/2— Basic key-value operationsbatch_write/2— Atomic multi-key updates- Iterator functions for range scans
Each function returns NifResult<T>, a Rustler type that automatically converts Rust Result types into Erlang {:ok, term} or {:error, reason} tuples.
Dirty Scheduler Integration for Non-Blocking Execution
A blockchain node cannot afford scheduler starvation. The Amadeus NIFs declare execution constraints explicitly using schedule attributes. Throughout lib.rs, you'll find patterns like these:
#[rustler::nif(schedule = "DirtyCpu")]
fn compute-intensive-op(...) { ... }
#[rustler::nif(schedule = "DirtyIo")]
fn disk-heavy-op(...) { ... }
Specific locations include:
- Lines 227-229 — DirtyCpu scheduling for cryptographic operations
- Lines 240-242 — DirtyIo scheduling for large batch writes
- Lines 365-367 — DirtyCpu for iterator-heavy range queries
- Lines 721-723 — DirtyIo for compaction-triggering deletes
DirtyCpu runs the NIF on a separate CPU-bound thread pool, while DirtyIo uses I/O-bound threads. This ensures long-running RocksDB compactions or signature verifications never pause the BEAM's main schedulers.
Safe Resource Management with ResourceArc
Rust's ownership model integrates with the BEAM's garbage collector through the rustler::resource! macro. In lib.rs lines 99-102, the module registers resource types:
fn on_load(env: Env, _info: Term) -> bool {
rustler::resource!(DbResource, env);
rustler::resource!(CfResource, env);
rustler::resource!(TxResource, env);
rustler::resource!(ItResource, env);
true
}
These correspond to:
| Resource | Rust Type | Purpose |
|---|---|---|
DbResource |
Arc<DB> |
Open RocksDB database handle |
CfResource |
Arc<ColumnFamily> |
Reference to column family |
TxResource |
Transaction |
Active transaction guard |
ItResource |
DBIterator |
Live iterator over key range |
When Elixir passes a database reference to put/3, it actually passes an opaque term containing a ResourceArc<DbResource>. The Rust side borrows the underlying DB, operates on it, and returns. If the Elixir process terminates or the reference goes out of scope, the ResourceArc drops to zero and Rust automatically closes handles — no manual cleanup, no leaks, no use-after-free.
Consensus and Cryptography NIFs
Beyond storage, the Amadeus Protocol node delegates consensus-critical work to Rust. The ex/native/rdb/src/consensus/ directory contains additional NIF-exposed modules:
bls12_381.rs— BLS signature aggregation and verificationbic/*.rs— Proof-of-work verification and block validation helpers
These compile into the same NIF library as the storage layer. When the Elixir consensus engine processes a new block, it calls these functions like any other Erlang function, but execution happens in optimized Rust with constant-time cryptographic primitives.
The scheduling attributes apply here too: signature batch verification uses schedule = "DirtyCpu" to saturate available cores without blocking message passing.
The Elixir API: Thin Wrappers Over Native Code
From the Elixir developer's perspective, Rust NIFs disappear behind idiomatic module boundaries. A typical wrapper looks like:
defmodule AmadeusRDB do
use Rustler, otp_app: :amadeusd, crate: "rdb"
# NIF stubs — actual implementation is in Rust
def open_transaction_db(_path, _cf_names), do: :erlang.nif_error(:nif_not_loaded)
def put(_db, _key, _value), do: :erlang.nif_error(:nif_not_loaded)
def get(_db, _key), do: :erlang.nif_error(:nif_not_loaded)
def delete(_db, _key), do: :erlang.nif_error(:nif_not_loaded)
end
The use Rustler line configures:
otp_app: :amadeusd— Application name for path resolutioncrate: "rdb"— Cargo crate name inex/native/rdb/Cargo.toml
At runtime, the compiled .so or .dll is loaded automatically. The :nif_not_loaded stubs are replaced with actual function pointers, and calls become direct jumps into Rust code.
Complete Usage Example
# Open database with column families
{:ok, db} = AmadeusRDB.open_transaction_db("/var/lib/amadeusd/db", ["default", "index"])
# Write operation (executes in Rust, scheduled on DirtyIo)
:ok = AmadeusRDB.put(db, "block:1000", serialized_block)
# Read with automatic deserialization
{:ok, block_data} = AmadeusRDB.get(db, "block:1000")
Key Source Files for Understanding Rust NIFs in Amadeus
| File | Role |
|---|---|
ex/native/rdb/src/lib.rs |
Core NIF library with RocksDB operations, resource registration, and scheduler hints |
ex/native/rdb/src/atoms.rs |
Erlang atom declarations (:ok, :error, :not_found) for NIF return values |
ex/native/rdb/src/tx_filter.rs |
Transaction filtering utilities before Elixir handoff |
ex/native/rdb/src/consensus/* |
Cryptographic and consensus NIFs (BLS, proof-of-work) |
These files are compiled together via Cargo and linked as a single NIF library loaded by the BEAM at application startup.
Summary
Rust NIFs in the Amadeus Protocol node deliver:
- Native RocksDB performance through direct
lib.rsimplementations ofput,get,delete, and iterator operations - Scheduler isolation via
DirtyCpuandDirtyIoattributes that prevent storage and crypto work from blocking lightweight processes - Memory safety guarantees through
ResourceArcwrappers around database handles, transactions, and iterators - Ecosystem consistency — consensus cryptography and storage share the same Rust codebase and Elixir calling conventions
The result is a node that runs Erlang's battle-tested networking and supervision trees alongside Rust's zero-cost abstractions, without FFI complexity or process boundary overhead.
Frequently Asked Questions
What is a NIF in Erlang/Elixir?
A NIF (Native Implemented Function) is a function written in C, C++, or Rust that the BEAM virtual machine can call directly as if it were a built-in Erlang function. NIFs run in the same OS process as the VM but execute native machine code, making them ideal for performance-critical operations that would be too slow in pure Erlang.
Why does the Amadeus node use Rust instead of C for NIFs?
Rust provides memory safety without garbage collection, which is critical for a blockchain node handling financial data. The rustler crate automates NIF boilerplate and resource management, making Rust NIFs less error-prone than manual C implementations while matching C performance. The Amadeus source uses rustler::resource! and ResourceArc to eliminate entire classes of memory bugs.
How do DirtyCpu and DirtyIo schedulers prevent node freezing?
The BEVM has limited scheduler threads for running Erlang processes. If a NIF runs too long on these threads, it blocks all process execution on that scheduler. DirtyCpu and DirtyIo are separate thread pools: CPU-bound NIFs (cryptography, complex calculations) run on DirtyCpu threads, while I/O-bound NIFs (large disk writes) run on DirtyIo threads. The Amadeus node marks its RocksDB and consensus NIFs appropriately to maintain responsive message passing.
Can Rust NIFs crash the entire Erlang VM?
Yes — NIFs share memory space with the BEAM, so a segfault or panic in Rust will terminate the entire node. The Amadeus codebase mitigates this through Rust's type system and by avoiding unsafe code in NIF implementations. All database handle access goes through ResourceArc, which guarantees valid pointers, and error handling uses NifResult to convert Rust errors into catchable Erlang exceptions rather than panics.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →