A Nostr-native cryptocurrency relay. coin-relay is a standard
NIP-01
WebSocket relay extended to validate custom transaction events, maintain
account balances in SQLite, require Proof-of-Work from block producers, and
gossip ledger state between peer relays.
See docs/ARCHITECTURE.md for the full design,
event schema, sequence diagram, and double-spend prevention model.
cargo buildThis is a Cargo workspace with a single member crate, coin-relay, which
produces both a library (coin_relay, used by integration tests) and a
binary (coin-relay).
cargo testCovers: schnorr signature verification (valid + tampered events), nonce / double-spend rejection, insufficient-balance rejection, PoW difficulty checking, mempool dedup/eviction, NIP-01 filter matching, and an integration test that submits two transfers through the full ingestion pipeline, seals a PoW block over them, and checks resulting balances plus independently re-verifies the block's PoW and hash linkage.
cp config.toml my-node.toml # edit listen_addr, sqlite_path, seed_peers, etc.
cargo run --release -- my-node.toml
# (defaults to ./config.toml if no path is given)On startup the relay:
- Opens/creates the SQLite ledger at
sqlite_path. - Generates an ephemeral relay identity keypair (used to sign sealed blocks and peer HELLOs) and logs its public key. v1 simplification: this key is not currently persisted across restarts.
- Connects out to each
seed_peersURL, performs a HELLO + catch-up + live-gossip subscription. - If
is_block_producer = true, spawns a background task that seals pending mempool transfers into a PoW block everyblock_interval_secs. - Serves the NIP-01 WebSocket endpoint at
ws://<listen_addr>/and a plain-text health check athttp://<listen_addr>/health.
# terminal 1: block-producer node
cat > node-a.toml <<'EOF'
relay_name = "node-a"
listen_addr = "127.0.0.1:7000"
sqlite_path = "node-a.sqlite"
seed_peers = []
pow_difficulty = 8
is_block_producer = true
block_interval_secs = 5
EOF
cargo run --release -- node-a.toml
# terminal 2: peer that syncs from node-a
cat > node-b.toml <<'EOF'
relay_name = "node-b"
listen_addr = "127.0.0.1:7001"
sqlite_path = "node-b.sqlite"
seed_peers = ["ws://127.0.0.1:7000/"]
pow_difficulty = 8
is_block_producer = false
EOF
cargo run --release -- node-b.tomlTransfers are ordinary NIP-01 EVENT messages of kind = 3000, whose
content is a JSON-encoded {"recipient", "amount", "nonce"} payload,
signed over the standard NIP-01 id computation
(sha256([0,pubkey,created_at,kind,tags,content])) with a BIP-340 schnorr
signature.
The example below uses websocat
plus a short Python helper (using only the standard library's hashlib,
and a lightweight secp256k1 schnorr binding) to build and sign the event.
If you don't have a signer handy, the easiest path is to adapt one of the
crate's own tests (coin-relay/src/event.rs::Event::new_signed) or use any
NIP-01-compatible Nostr client library in your language of choice - the
relay only depends on standard NIP-01 event framing and BIP-340 schnorr
signatures, nothing bespoke.
# Install websocat (or use any WebSocket client)
cargo install websocat
# Given a pre-signed event JSON file transfer_event.json, e.g.:
# ["EVENT", {"id": "...", "pubkey": "...", "created_at": 1700000000,
# "kind": 3000, "tags": [], "content": "{\"recipient\":\"<hex pubkey>\",\"amount\":10,\"nonce\":1}",
# "sig": "..."}]
cat transfer_event.json | websocat ws://127.0.0.1:7000/
# Expect a reply like:
# ["OK","<event id>",true,""]To subscribe and watch live ledger activity (transfers + sealed blocks):
echo '["REQ","sub1",{"kinds":[3000,3001]}]' | websocat ws://127.0.0.1:7000/A runnable Rust example that builds a signed Transfer, sends it, and
prints the relay's OK/EOSE replies is included at
coin-relay/examples/smoke_client.rs:
cargo run --release --example smoke_clientcoin-relay/
src/
main.rs - config load, starts the WS server + peer-sync + block producer tasks
lib.rs - re-exports modules for the binary and for integration tests
config.rs - TOML config struct
event.rs - NIP-01 Event struct, id computation, schnorr verify/sign
event_types.rs - Transfer / StateBlock / PeerAnnounce payloads (custom kinds 3000-3002)
validation.rs - signature + semantic (balance/nonce/PoW) validation
ledger.rs - SQLite-backed accounts/transactions/blocks/peers state
mempool.rs - in-memory pending-transaction pool
pow.rs - PoW difficulty check + mining loop
peer_sync.rs - outbound peer WebSocket client (HELLO, catch-up, live gossip)
ws_server.rs - axum WebSocket server, NIP-01 message dispatch, shared ingestion pipeline
tests/
integration_test.rs - end-to-end ingest -> validate -> apply -> seal -> verify test
config.toml - example relay configuration
docs/ARCHITECTURE.md - full design doc + sequence diagram
The relay uses a 210,000,000,000,000 base-unit supply cap and a 500,000,000-unit initial block reward, halving every 210,000 blocks. PoW starts at a deliberately low one-bit target and retargets every 2016 blocks toward 10-second blocks, with adjustments limited to 4x. A signed StateBlock event is limited to 100 KB; producers trim the oldest pending transfers to fit.