A Rust port of Gun.js - a realtime, decentralized, offline-first, graph data synchronization engine.
This is an unofficial, vibe-coded port of Gun.js to Rust. This project is:
- NOT for production use - Do not use in production environments
- NOT actively maintained - This is an experimental port
- For testing purposes only - Use at your own risk
- No guarantees - No warranties, no support, use at your own risk
This port was created for experimentation and learning purposes. If you need a production-ready solution, please use the official Gun.js implementation.
Gun.rs stores data as a directed graph where:
- Nodes are identified by unique souls (self-describing unique IDs)
- Properties are key-value pairs stored on nodes
- Relationships are formed through soul references
- No schema required - data is flexible and dynamic
// Example graph structure:
// {
// "user:alice": {
// "name": "Alice",
// "age": 30,
// "friend": "user:bob" // Reference to another node
// },
// "user:bob": {
// "name": "Bob",
// "age": 28
// }
// }The Chain API provides a fluent interface for navigating and manipulating the graph:
// Chain methods are chained together
gun.get("user").get("alice").get("name").put("Alice");
// ^^^^ ^^^^ ^^^^ ^^^^
// Chain Chain Chain ChainEach method returns a new Chain instance, allowing method chaining. Chains maintain context about their position in the graph hierarchy.
Souls are deterministic, unique identifiers for nodes. They are:
- Self-describing: Generated deterministically from node content
- Globally unique: No central authority needed
- Verifiable: Can be validated by any peer
- Stable: Same data generates the same soul
// Souls look like: "abc123def456..."
// They're used to reference nodes across the networkGun.rs uses Conflict-free Replicated Data Types (CRDTs) with state-based conflict resolution:
- HAM (Hypothetical Amnesia Machine) Algorithm: Resolves conflicts using timestamps and logical clocks
- Last-Write-Wins: With tie-breaking based on peer IDs
- Automatic merging: Conflicting updates are automatically resolved
- Eventual consistency: All peers eventually converge to the same state
The DAM protocol is Gun's custom P2P networking layer:
- Message routing: Efficient message broadcasting through the mesh
- Deduplication: Prevents message loops and duplicate processing
- Peer discovery: Automatic discovery of nearby peers
- NAT traversal: Works behind firewalls with relay support
- Cryptographic security: All messages are signed with BLS signatures and verified
Gun.rs is designed for offline-first operation:
- Local storage: Data is persisted locally using pluggable storage backends
- Sync on connect: Automatically syncs when peers become available
- Works offline: Full functionality available without network connectivity
- Conflict resolution: Handles conflicts when sync occurs
Multiple storage backends are available:
- MemoryStorage: In-memory storage (default, no persistence)
- LocalStorage: File-based storage (localStorage-like)
- SledStorage: High-performance embedded database (radisk mode)
Gun.rs uses BLS (Boneh-Lynn-Shacham) signatures for cryptographic security:
- Message signing: All outgoing messages are signed with the secret key
- Message verification: All incoming messages are verified using public keys
- Peer authentication: Each peer maintains a mapping of peer IDs to public keys
- Tamper detection: Invalid signatures cause messages to be rejected
- Message predicates: Optional custom filtering after signature verification
Add to your Cargo.toml:
[dependencies]
gun = { git = "https://github.com/DIG-Network/gun.rs" }
chia_bls = "12.2"
serde_json = "1.0"
tokio = { version = "1.0", features = ["full"] }Note: Gun.rs requires BLS (Boneh-Lynn-Shacham) key pairs for cryptographic security. All messages are signed and verified using BLS signatures.
use gun::Gun;
use chia_bls::{SecretKey, PublicKey};
// Generate BLS key pair
let secret_key = SecretKey::from_seed(&[0u8; 32]);
let public_key = secret_key.public_key();
// Simple instance (no networking, in-memory storage)
let gun = Gun::new(secret_key, public_key);
// With options (networking, storage, etc.)
use gun::GunOptions;
let secret_key = SecretKey::from_seed(&[1u8; 32]);
let public_key = secret_key.public_key();
let gun = Gun::with_options(secret_key, public_key, GunOptions {
peers: vec!["ws://relay.example.com/gun".to_string()],
localStorage: true,
storage_path: Some("./gun_data".to_string()),
..Default::default()
}).await?;use gun::Gun;
use chia_bls::{SecretKey, PublicKey};
use serde_json::json;
// Generate BLS key pair
let secret_key = SecretKey::from_seed(&[0u8; 32]);
let public_key = secret_key.public_key();
let gun = Gun::new(secret_key, public_key);
// Write data
gun.get("user").get("alice").put(json!("Alice")).await?;
// Read data once
gun.get("user").get("alice").once(|data, key| {
println!("User: {:?}", data);
}).await?;
// Subscribe to updates
gun.get("user").get("alice").on(|data, key| {
println!("Updated: {:?}", data);
});use serde_json::json;
// Put a complete object
gun.get("user").get("alice").put(json!({
"name": "Alice",
"age": 30,
"email": "alice@example.com"
})).await?;
// Update specific fields
gun.get("user").get("alice").get("age").put(json!(31)).await?;// Create two users
gun.get("user").get("alice").put(json!({
"name": "Alice"
})).await?;
gun.get("user").get("bob").put(json!({
"name": "Bob"
})).await?;
// Create a relationship (reference)
// Note: In practice, you'd get the soul from the created node
// This is a simplified example
let alice_soul = "..."; // Soul from Alice node
gun.get("user").get("bob").get("friend").put(json!(alice_soul)).await?;// Map over a collection
gun.get("users").map(|data, key| {
if let Some(name) = data.get("name").and_then(|v| v.as_str()) {
println!("User: {}", name);
}
});
// Add items to a collection
for i in 1..=10 {
gun.get("users").get(&format!("user_{}", i)).put(json!({
"id": i,
"name": format!("User {}", i)
})).await?;
}// Remove all listeners from a chain
gun.get("user").get("alice").off();
// Note: off() is chain-aware and removes listeners from the current chain point// Navigate back up the chain
let chain = gun.get("user").get("alice").get("name");
let parent = chain.back(Some(1)); // Go back 1 level -> "alice" chain
let root = chain.back(None); // Go back to root -> gun rootuse gun::{Gun, GunOptions};
use chia_bls::{SecretKey, PublicKey};
// Generate BLS key pair
let secret_key = SecretKey::from_seed(&[0u8; 32]);
let public_key = secret_key.public_key();
// Single relay
let gun = Gun::with_options(
secret_key.clone(),
public_key.clone(),
GunOptions::with_relay("ws://relay.example.com/gun")
).await?;
// Multiple relays for redundancy
let secret_key2 = SecretKey::from_seed(&[1u8; 32]);
let public_key2 = secret_key2.public_key();
let gun = Gun::with_options(
secret_key2,
public_key2,
GunOptions::with_peers(vec![
"ws://relay1.example.com/gun".to_string(),
"ws://relay2.example.com/gun".to_string(),
])
).await?;use gun::{Gun, GunOptions};
use chia_bls::{SecretKey, PublicKey};
// Generate BLS key pair
let secret_key = SecretKey::from_seed(&[0u8; 32]);
let public_key = secret_key.public_key();
// Start a relay server on port 8765
let gun = Gun::with_options(
secret_key,
public_key,
GunOptions::relay_server(8765)
).await?;
// Server will accept connections from other peersuse gun::{Gun, GunOptions};
use gun::webrtc::WebRTCOptions;
use chia_bls::{SecretKey, PublicKey};
// Generate BLS key pair
let secret_key = SecretKey::from_seed(&[0u8; 32]);
let public_key = secret_key.public_key();
let mut webrtc_opts = WebRTCOptions::default();
webrtc_opts.enabled = true;
webrtc_opts.max_connections = 10;
let mut opts = GunOptions::default();
opts.peers = vec!["ws://relay.example.com/gun".to_string()];
opts.webrtc = webrtc_opts;
let gun = Gun::with_options(secret_key, public_key, opts).await?;use gun::{Gun, GunOptions};
use chia_bls::{SecretKey, PublicKey};
// Generate BLS key pair
let secret_key = SecretKey::from_seed(&[0u8; 32]);
let public_key = secret_key.public_key();
let opts = GunOptions {
localStorage: true,
storage_path: Some("./gun_data".to_string()),
..Default::default()
};
let gun = Gun::with_options(secret_key, public_key, opts).await?;
// Data will be persisted to ./gun_data/use gun::{Gun, GunOptions};
use chia_bls::{SecretKey, PublicKey};
// Generate BLS key pair
let secret_key = SecretKey::from_seed(&[0u8; 32]);
let public_key = secret_key.public_key();
let opts = GunOptions {
localStorage: true,
radisk: true,
storage_path: Some("./gun_data".to_string()),
..Default::default()
};
let gun = Gun::with_options(secret_key, public_key, opts).await?;
// Uses high-performance sled database// Check connection status
let is_connected = gun.is_connected().await;
let peer_count = gun.connected_peer_count().await;
// Wait for connection with timeout
let connected = gun.wait_for_connection(5000).await; // 5 second timeout
// Graceful shutdown
gun.shutdown().await?;use gun::GunError;
match gun.get("key").put(data).await {
Ok(chain) => {
// Success
}
Err(GunError::InvalidData(msg)) => {
eprintln!("Invalid data: {}", msg);
}
Err(e) => {
eprintln!("Error: {}", e);
}
}The main entry point for the Gun.rs library.
Methods:
-
new(secret_key: SecretKey, public_key: PublicKey) -> Gun- Creates a new Gun instance with default settings (no networking, in-memory storage)
- Requires BLS key pair for cryptographic security
- All messages are signed with the secret key and verified with public keys
-
with_options(secret_key: SecretKey, public_key: PublicKey, options: GunOptions) -> GunResult<Gun>- Creates a Gun instance with custom options
- Requires BLS key pair for cryptographic security
- Async function - must be awaited
-
get(key: &str) -> Arc<Chain>- Returns a chain pointing to the specified key
- Entry point for navigating the graph
-
root() -> Arc<Chain>- Returns a chain pointing to the root of the graph
-
state() -> f64- Returns the current state timestamp (used for conflict resolution)
-
connected_peer_count() -> usize- Returns the number of currently connected peers
- Async function
-
is_connected() -> bool- Returns true if connected to at least one peer
- Async function
-
wait_for_connection(timeout_ms: u64) -> bool- Waits for a connection to be established
- Returns true if connected within timeout, false otherwise
- Async function
-
shutdown() -> GunResult<()>- Gracefully shuts down the Gun instance
- Stops servers and closes connections
- Async function
The fluent API for interacting with the graph. All chain methods return Arc<Chain> for method chaining.
Methods:
-
get(key: &str) -> Arc<Chain>- Navigate to a property or child node
- Returns a new chain with the key appended
-
put(data: Value) -> GunResult<Arc<Chain>>- Write data to the current chain position
- Accepts
serde_json::Value(numbers, strings, booleans, objects, arrays) - Objects are automatically expanded into the graph
- Returns error if data is invalid
- Async function
-
on<F>(callback: F) -> Arc<Chain>- Subscribe to updates at this chain position
- Callback signature:
Fn(Value, Option<String>) - Returns immediately (non-blocking)
- Returns the chain for further chaining
- Listeners persist until explicitly removed
-
once<F>(callback: F) -> GunResult<Arc<Chain>>- Execute callback once when data is available
- Callback signature:
Fn(Value, Option<String>) - Returns error if data is already available but callback fails
- Async function (waits for data)
-
map<F>(callback: F) -> Arc<Chain>- Iterate over child nodes/keys
- Callback signature:
Fn(Value, Option<String>) - Used for working with collections/arrays
- Returns immediately (non-blocking)
-
set(item: Value) -> GunResult<Arc<Chain>>- Add an item to a collection (similar to array.push)
- Generates a unique key for the item
- Returns the chain with the generated key appended
- Async function
-
back(amount: Option<usize>) -> Option<Arc<Chain>>- Navigate back up the chain
amount: Some(n)- go back n levelsamount: None- go back to root- Returns
Noneif cannot go back that far
-
off() -> Arc<Chain>- Remove all listeners from this chain position
- Does not remove listeners from parent/child chains
- Returns the chain for further operations
Configuration options for creating a Gun instance.
Fields:
-
peers: Vec<String>- List of WebSocket URLs to connect to (relay servers)
- Empty by default
-
localStorage: bool- Enable local storage persistence
- Default:
false
-
storage_path: Option<String>- Path for local storage
- Default:
None(uses "./gun_data" if localStorage is true)
-
radisk: bool- Use SledStorage instead of LocalStorage (high-performance mode)
- Default:
false
-
super_peer: bool- Enable relay server mode
- Default:
false
-
port: Option<u16>- Port to listen on (for relay server mode)
- Default:
None
-
webrtc: WebRTCOptions- WebRTC configuration (see
WebRTCOptionsbelow) - Default:
WebRTCOptions::default()
- WebRTC configuration (see
-
message_predicate: Option<MessagePredicate>- Optional predicate function to filter incoming messages
- Receives the entire message object and returns
trueto accept,falseto reject - Called after signature verification but before message processing
- Useful for implementing custom filtering, rate limiting, or access control
- Default:
None(all verified messages are accepted)
Methods:
-
default() -> GunOptions- Creates default options (no networking, no storage)
-
with_relay(relay_url: &str) -> GunOptions- Convenience method for single relay connection
-
with_peers(peers: Vec<String>) -> GunOptions- Convenience method for multiple peer connections
-
relay_server(port: u16) -> GunOptions- Convenience method for relay server configuration
Configuration for WebRTC peer-to-peer connections.
Fields:
-
ice_servers: Vec<RTCIceServer>- STUN/TURN servers for NAT traversal
- Default: Google and Cloudflare STUN servers
-
data_channel: RTCDataChannelInit- Data channel configuration
- Default: unordered, max retransmits 2
-
max_connections: usize- Maximum number of WebRTC connections
- Default:
55
-
room: Option<String>- Room name for peer discovery (optional)
- Default:
None
-
enabled: bool- Enable/disable WebRTC
- Default:
true
Methods:
default() -> WebRTCOptions- Creates default WebRTC options
Error type for Gun.rs operations.
Variants:
-
GunError::InvalidData(String)- Invalid data provided (e.g., invalid type, invalid structure)
-
GunError::Storage(sled::Error)- Storage operation failed (from sled database)
-
GunError::Serialization(serde_json::Error)- JSON serialization/deserialization failed
-
GunError::Network(String)- Network operation failed (connection lost, timeout, etc.)
-
GunError::InvalidSoul(String)- Invalid soul (node ID) format
-
GunError::NodeNotFound- Requested node doesn't exist in the graph
-
GunError::Io(std::io::Error)- I/O operation failed (file read/write, etc.)
-
GunError::UrlParseError(url::ParseError)- URL parsing failed (invalid peer URL)
-
GunError::WebRTC(String)- WebRTC operation failed (connection, signaling, etc.)
-
GunError::Crypto(String)- Cryptographic operation failed (encryption, signing, etc.)
Chain- Main chain API type
GunCore- Core graph database engine (internal)
Mesh- DAM protocol mesh networking (internal)
GunError- Error typesGunResult<T>- Result type alias:Result<T, GunError>
- Graph data structures (internal)
Storage- Storage traitMemoryStorage- In-memory storageLocalStorage- File-based storageSledStorage- Sled database storage
WebRTCOptions- WebRTC configurationWebRTCManager- WebRTC manager (internal)WebRTCPeer- WebRTC peer connection (internal)
- WebSocket client and server (internal)
- Security, Encryption, Authorization module (partial implementation)
MessagePredicate- Message filtering predicate type for custom message filtering
GunResult<T>=Result<T, GunError>MessagePredicate=Arc<dyn Fn(&serde_json::Value) -> bool + Send + Sync>- Function type for custom message filtering
None currently exported.
use gun::Gun;
use chia_bls::{SecretKey, PublicKey};
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Generate BLS key pair
let secret_key = SecretKey::from_seed(&[0u8; 32]);
let public_key = secret_key.public_key();
let gun = Gun::new(secret_key, public_key);
// Write data
gun.get("user").get("alice").put(json!("Alice")).await?;
// Read data
gun.get("user").get("alice").once(|data, _key| {
println!("User: {:?}", data);
}).await?;
Ok(())
}use gun::{Gun, GunOptions};
use chia_bls::{SecretKey, PublicKey};
use serde_json::json;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Generate BLS key pair
let secret_key = SecretKey::from_seed(&[0u8; 32]);
let public_key = secret_key.public_key();
let gun = Gun::with_options(
secret_key,
public_key,
GunOptions::with_relay("ws://relay.example.com/gun")
).await?;
// Subscribe to updates
gun.get("chat").get("messages").on(|data, key| {
if let Some(text) = data.get("text").and_then(|v| v.as_str()) {
println!("New message: {}", text);
}
});
// Send a message
gun.get("chat").get("messages").set(json!({
"text": "Hello, world!",
"author": "Alice"
})).await?;
// Keep running
tokio::time::sleep(tokio::time::Duration::from_secs(60)).await;
Ok(())
}See examples/two_clients_webrtc.rs for a complete WebRTC example demonstrating:
- Two clients with WebRTC enabled
- Direct peer-to-peer communication
- NAT traversal
See examples/two_clients.rs for a complete example demonstrating:
- Two clients connecting via relay
- Data synchronization
- Real-time updates
Run examples:
cargo run --example two_clients
cargo run --example two_clients_webrtc- DAM Protocol: See NETWORKING.md
- Relay Servers: See RELAY_SERVERS.md
- WebRTC Status: See WEBRTC_STATUS.md
See tests/ directory for comprehensive test suites:
- Unit tests
- Integration tests
- WebRTC tests
- Lock contention tests
- Stress tests
Important: Gun.js uses a custom DAM (Directed Acyclic Mesh) protocol over WebSocket, NOT libp2p. For 1:1 behavioral compatibility, we use:
tokio-tungstenitefor WebSocket transport (matches Gun.js)- Custom DAM protocol implementation (matches mesh.js)
- Message deduplication (matches dup.js)
MIT OR Apache-2.0 OR Zlib (same as Gun.js)