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 families
  • put/3, get/2, delete/2 — Basic key-value operations
  • batch_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 verification
  • bic/*.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 resolution
  • crate: "rdb" — Cargo crate name in ex/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.rs implementations of put, get, delete, and iterator operations
  • Scheduler isolation via DirtyCpu and DirtyIo attributes that prevent storage and crypto work from blocking lightweight processes
  • Memory safety guarantees through ResourceArc wrappers 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →