Add Nostr-native architecture - Nostr as infrastructure backbone
Comprehensive redesign using Nostr for all ATM communication: Lightning.Pub as Core Server: - Replaces lamassu-server entirely - Nostr-native account system wrapping LND - Zero server config (no DNS/SSL/ports) - CLINK native, one-line deployment - Built-in liquidity management Private Nostr Relay: - NIP-42 authenticated relay (strfry/rnostr) - Whitelist: ATM npubs + Operator npubs only - Encrypted command/control channel - Negentropy sync for offline reconciliation Machine Identity: - Each ATM has Nostr keypair (nsec/npub) - Replaces client certificates - Cryptographic authentication - No PKI/CA required Replacing SMS (KYC vector elimination): - NIP-17 encrypted DMs for receipts - No phone numbers collected - User provides npub voluntarily - Operator alerts via Nostr events Event Schema: - Kind 21001-21003: CLINK protocol - Kind 30078: Machine status (replaceable) - Kind 30079: Transaction records - Kind 14: Encrypted receipt DMs This is the logical extension of CLINK - if we're using Nostr for payments, why not use it for everything? Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
108cd83d69
commit
af358bf158
3 changed files with 838 additions and 2 deletions
|
|
@ -668,8 +668,9 @@ Features: Machine pairing, balance check, settings
|
||||||
|
|
||||||
## Related Notes
|
## Related Notes
|
||||||
|
|
||||||
|
- [[nostr-native-architecture]] - **Nostr as infrastructure backbone**
|
||||||
- [[modernization-plan]] - Original tech stack decisions
|
- [[modernization-plan]] - Original tech stack decisions
|
||||||
- [[lnbits-integration]] - Current LNbits docs (to be revised)
|
- [[lnbits-integration]] - LNbits as alternative to Lightning.Pub
|
||||||
- [[membership-lightning-integration]] - Membership feature (simplify)
|
- [[membership-lightning-integration]] - Membership feature (simplify)
|
||||||
- [[hardware-recommendations]] - Hardware choices
|
- [[hardware-recommendations]] - Hardware choices
|
||||||
- [[machine-ui-modernization]] - Vue 3 UI migration
|
- [[machine-ui-modernization]] - Vue 3 UI migration
|
||||||
|
|
@ -700,8 +701,14 @@ Features: Machine pairing, balance check, settings
|
||||||
- [BOLT Cards](https://bolt.cards/)
|
- [BOLT Cards](https://bolt.cards/)
|
||||||
- [LNbits BoltCards Extension](https://github.com/lnbits/lnbits/tree/main/lnbits/extensions/boltcards)
|
- [LNbits BoltCards Extension](https://github.com/lnbits/lnbits/tree/main/lnbits/extensions/boltcards)
|
||||||
|
|
||||||
|
### Nostr Infrastructure
|
||||||
|
- [strfry](https://github.com/hoytech/strfry) - High-performance relay
|
||||||
|
- [rnostr](https://github.com/rnostr/rnostr) - Rust relay with NIP-42
|
||||||
|
- [NIP-42: Auth](https://github.com/nostr-protocol/nips/blob/master/42.md)
|
||||||
|
- [NIP-44: Encryption](https://github.com/paulmillr/nip44)
|
||||||
|
- [NIP-17: Private DMs](https://nips.nostr.com/17)
|
||||||
|
|
||||||
### Reference Implementations
|
### Reference Implementations
|
||||||
- [FOSSA ATM](https://github.com/lnbits/fossa) - LNbits Lightning ATM
|
- [FOSSA ATM](https://github.com/lnbits/fossa) - LNbits Lightning ATM
|
||||||
- [Bleskomat](https://github.com/samotari/bleskomat) - Minimal Lightning ATM
|
- [Bleskomat](https://github.com/samotari/bleskomat) - Minimal Lightning ATM
|
||||||
- [RoboSats](https://github.com/RoboSats/robosats) - KYC-free P2P exchange
|
- [RoboSats](https://github.com/RoboSats/robosats) - KYC-free P2P exchange
|
||||||
- [RoboSats](https://github.com/RoboSats/robosats) - KYC-free P2P exchange
|
|
||||||
|
|
|
||||||
|
|
@ -514,6 +514,7 @@ test('user can complete transaction', async ({ page }) => {
|
||||||
## Related Notes
|
## Related Notes
|
||||||
|
|
||||||
- [[architecture-review]] - **KYC-free Lightning-first architecture review**
|
- [[architecture-review]] - **KYC-free Lightning-first architecture review**
|
||||||
|
- [[nostr-native-architecture]] - **Nostr as infrastructure backbone**
|
||||||
- [[CLAUDE]] - Claude Code guidance
|
- [[CLAUDE]] - Claude Code guidance
|
||||||
- [[admin-ui-modernization]] - Vue 3 migration for admin dashboard
|
- [[admin-ui-modernization]] - Vue 3 migration for admin dashboard
|
||||||
- [[machine-ui-modernization]] - Vue 3 migration for kiosk UI
|
- [[machine-ui-modernization]] - Vue 3 migration for kiosk UI
|
||||||
|
|
|
||||||
828
docs/nostr-native-architecture.md
Normal file
828
docs/nostr-native-architecture.md
Normal file
|
|
@ -0,0 +1,828 @@
|
||||||
|
---
|
||||||
|
title: Nostr-Native ATM Architecture
|
||||||
|
created: 2026-01-22
|
||||||
|
updated: 2026-01-22
|
||||||
|
tags:
|
||||||
|
- architecture
|
||||||
|
- nostr
|
||||||
|
- lightning-pub
|
||||||
|
- kyc-free
|
||||||
|
- decentralized
|
||||||
|
status: active
|
||||||
|
priority: critical
|
||||||
|
---
|
||||||
|
|
||||||
|
# Nostr-Native ATM Architecture
|
||||||
|
|
||||||
|
> [!abstract] Summary
|
||||||
|
> A radical rethinking of ATM infrastructure where **Nostr becomes the backbone** for identity, communication, and payments. Replaces traditional server infrastructure with Lightning.Pub and a private Nostr relay, eliminating KYC vectors like phone numbers while enabling a truly decentralized, censorship-resistant system.
|
||||||
|
|
||||||
|
## Quick Links
|
||||||
|
|
||||||
|
- [[#Vision: Nostr as Infrastructure]]
|
||||||
|
- [[#Lightning.Pub as Core Server]]
|
||||||
|
- [[#Private Relay Architecture]]
|
||||||
|
- [[#Machine Identity]]
|
||||||
|
- [[#Replacing SMS with Nostr]]
|
||||||
|
- [[#Event Schema]]
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Vision: Nostr as Infrastructure
|
||||||
|
|
||||||
|
### The Problem with Traditional ATM Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ TRADITIONAL LAMASSU ARCHITECTURE │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ATM ──HTTPS/WSS──► Server ──► PostgreSQL │
|
||||||
|
│ │ │
|
||||||
|
│ ├──► SMS Gateway (Twilio) │
|
||||||
|
│ ├──► Email Service │
|
||||||
|
│ ├──► KYC Provider │
|
||||||
|
│ └──► Lightning Node │
|
||||||
|
│ │
|
||||||
|
│ Problems: │
|
||||||
|
│ • Phone numbers = KYC vector │
|
||||||
|
│ • Complex server infrastructure │
|
||||||
|
│ • DNS, SSL, port forwarding required │
|
||||||
|
│ • Single point of failure │
|
||||||
|
│ • Centralized command/control │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### The Nostr-Native Solution
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ NOSTR-NATIVE ATM ARCHITECTURE │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
|
||||||
|
│ │ ATM 1 │ │ ATM 2 │ │ ATM N │ │
|
||||||
|
│ │ npub_1 │ │ npub_2 │ │ npub_n │ │
|
||||||
|
│ └────┬────┘ └────┬────┘ └────┬────┘ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ └────────────┼────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌────────────────────────┐ │
|
||||||
|
│ │ Private Nostr Relay │◄─── NIP-42 Auth │
|
||||||
|
│ │ (strfry / rnostr) │ Whitelist: ATMs + │
|
||||||
|
│ └───────────┬────────────┘ Operators only │
|
||||||
|
│ │ │
|
||||||
|
│ ┌───────────┼───────────┐ │
|
||||||
|
│ ▼ ▼ ▼ │
|
||||||
|
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
|
||||||
|
│ │Lightning │ │ Operator │ │ Public │ │
|
||||||
|
│ │ Pub │ │Dashboard │ │ Relays │ │
|
||||||
|
│ │(LND wrap)│ │ (Vue 3) │ │(fallback)│ │
|
||||||
|
│ └──────────┘ └──────────┘ └──────────┘ │
|
||||||
|
│ │
|
||||||
|
│ Benefits: │
|
||||||
|
│ • No phone numbers (Nostr DMs instead) │
|
||||||
|
│ • Zero server config (no DNS/SSL/ports) │
|
||||||
|
│ • Decentralized communication │
|
||||||
|
│ • Cryptographic machine identity │
|
||||||
|
│ • Censorship-resistant │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Lightning.Pub as Core Server
|
||||||
|
|
||||||
|
> [!decision] Lightning.Pub Replaces lamassu-server
|
||||||
|
> A Nostr-native account system that wraps LND and eliminates traditional server complexity.
|
||||||
|
|
||||||
|
### Why Lightning.Pub?
|
||||||
|
|
||||||
|
| Aspect | Traditional Server | Lightning.Pub |
|
||||||
|
|--------|-------------------|---------------|
|
||||||
|
| Network config | DNS, SSL, ports, firewall | Zero (uses Nostr relays) |
|
||||||
|
| Deployment | Complex | One-line install |
|
||||||
|
| Communication | HTTPS/WebSocket | Nostr events (NIP-44 encrypted) |
|
||||||
|
| Account system | Custom implementation | Built-in sublayers |
|
||||||
|
| CLINK support | Must implement | Native |
|
||||||
|
| Lightning | Separate integration | Wraps LND directly |
|
||||||
|
|
||||||
|
### One-Line Deployment
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Linux
|
||||||
|
wget -qO- https://deploy.lightning.pub | bash
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
curl -fsSL https://deploy.lightning.pub | bash
|
||||||
|
|
||||||
|
# Everything confined to ~/lightning_pub/
|
||||||
|
# No sudo, no root, no system changes
|
||||||
|
```
|
||||||
|
|
||||||
|
### Architecture
|
||||||
|
|
||||||
|
```
|
||||||
|
Lightning.Pub
|
||||||
|
├── LND (Lightning Network Daemon)
|
||||||
|
│ └── Neutrino (SPV Bitcoin)
|
||||||
|
├── Account System
|
||||||
|
│ ├── Application Pools (operator level)
|
||||||
|
│ └── User Accounts (ATM wallets)
|
||||||
|
├── CLINK Native
|
||||||
|
│ ├── noffer (static payment codes)
|
||||||
|
│ └── ndebit (authorized payments)
|
||||||
|
├── Nostr Communication
|
||||||
|
│ └── NIP-44 encrypted events
|
||||||
|
└── Optional: LNURL Bridge (legacy support)
|
||||||
|
```
|
||||||
|
|
||||||
|
### What Lightning.Pub Gives Us
|
||||||
|
|
||||||
|
1. **No Port Forwarding** - Nostr relays handle all communication
|
||||||
|
2. **Multi-User Accounts** - Each ATM gets its own account
|
||||||
|
3. **CLINK Native** - Static payment codes work out of the box
|
||||||
|
4. **Liquidity Management** - Auto-quotes from LSPs (Zeus, Voltage, Flashsats)
|
||||||
|
5. **Watchdog Security** - Monitors for drainage attacks
|
||||||
|
6. **Production Tested** - Years of real-world deployment
|
||||||
|
|
||||||
|
### Configuration for ATM Fleet
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# ~/lightning_pub/.env
|
||||||
|
|
||||||
|
# Private relay for machine communication
|
||||||
|
NOSTR_RELAYS="wss://relay.youratm.company wss://nos.lol"
|
||||||
|
|
||||||
|
# Disable bootstrap peering for full sovereignty
|
||||||
|
DISABLE_LIQUIDITY_PROVIDER=true
|
||||||
|
|
||||||
|
# Custom LNURL domain (optional, for legacy wallets)
|
||||||
|
SERVICE_URL=https://ln.youratm.company
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Private Relay Architecture
|
||||||
|
|
||||||
|
> [!decision] Run a Restricted Nostr Relay
|
||||||
|
> NIP-42 authenticated relay that only accepts events from known machines and operators.
|
||||||
|
|
||||||
|
### Why a Private Relay?
|
||||||
|
|
||||||
|
| Concern | Public Relay | Private Relay |
|
||||||
|
|---------|--------------|---------------|
|
||||||
|
| Who can read | Anyone | Whitelisted npubs only |
|
||||||
|
| Who can write | Anyone | Whitelisted npubs only |
|
||||||
|
| Machine commands | Exposed | Encrypted, restricted |
|
||||||
|
| Fleet data | Public | Private |
|
||||||
|
| Censorship | Relay can censor | You control |
|
||||||
|
|
||||||
|
### Relay Options
|
||||||
|
|
||||||
|
| Relay | Language | NIP-42 | Performance | Notes |
|
||||||
|
|-------|----------|--------|-------------|-------|
|
||||||
|
| **strfry** | C++ | Yes | Excellent | Plugin system, negentropy sync |
|
||||||
|
| **rnostr** | Rust | Yes | Excellent | LMDB storage, inspired by strfry |
|
||||||
|
| **nostr-rs-relay** | Rust | Yes | Good | SQLite/PostgreSQL |
|
||||||
|
|
||||||
|
### NIP-42 Authentication
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ NIP-42 AUTH FLOW │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ATM connects to relay │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ Relay sends AUTH challenge │
|
||||||
|
│ ["AUTH", "<random-challenge>"] │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ATM signs challenge with its nsec │
|
||||||
|
│ { │
|
||||||
|
│ "kind": 22242, │
|
||||||
|
│ "tags": [ │
|
||||||
|
│ ["relay", "wss://relay.youratm.company"], │
|
||||||
|
│ ["challenge", "<random-challenge>"] │
|
||||||
|
│ ], │
|
||||||
|
│ "content": "", │
|
||||||
|
│ "sig": "<signature>" │
|
||||||
|
│ } │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ Relay verifies npub is in whitelist │
|
||||||
|
│ │ │
|
||||||
|
│ ├── Yes → Connection allowed │
|
||||||
|
│ └── No → Connection rejected │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### strfry Configuration
|
||||||
|
|
||||||
|
```toml
|
||||||
|
# strfry.conf
|
||||||
|
|
||||||
|
[relay]
|
||||||
|
bind = "0.0.0.0"
|
||||||
|
port = 7777
|
||||||
|
realIpHeader = "X-Forwarded-For"
|
||||||
|
|
||||||
|
[relay.info]
|
||||||
|
name = "ATM Fleet Relay"
|
||||||
|
description = "Private relay for ATM communication"
|
||||||
|
contact = "operator@youratm.company"
|
||||||
|
|
||||||
|
# Require NIP-42 authentication
|
||||||
|
authRequired = true
|
||||||
|
|
||||||
|
[relay.writePolicy]
|
||||||
|
plugin = "./plugins/whitelist.js"
|
||||||
|
|
||||||
|
[relay.negentropy]
|
||||||
|
enabled = true
|
||||||
|
```
|
||||||
|
|
||||||
|
### Whitelist Plugin (noteguard style)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// plugins/whitelist.js
|
||||||
|
const ALLOWED_PUBKEYS = new Set([
|
||||||
|
'npub1_atm_001...', // ATM 1
|
||||||
|
'npub1_atm_002...', // ATM 2
|
||||||
|
'npub1_operator...', // Operator
|
||||||
|
])
|
||||||
|
|
||||||
|
export function writePolicy(event, sourceInfo) {
|
||||||
|
if (!sourceInfo.authedPubkey) {
|
||||||
|
return { action: 'reject', message: 'auth-required: authenticate first' }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ALLOWED_PUBKEYS.has(sourceInfo.authedPubkey)) {
|
||||||
|
return { action: 'reject', message: 'restricted: not authorized' }
|
||||||
|
}
|
||||||
|
|
||||||
|
return { action: 'accept' }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### NixOS Module for Relay
|
||||||
|
|
||||||
|
```nix
|
||||||
|
# relay.nix
|
||||||
|
{ config, pkgs, ... }:
|
||||||
|
{
|
||||||
|
services.strfry = {
|
||||||
|
enable = true;
|
||||||
|
settings = {
|
||||||
|
relay = {
|
||||||
|
bind = "127.0.0.1";
|
||||||
|
port = 7777;
|
||||||
|
info = {
|
||||||
|
name = "ATM Fleet Relay";
|
||||||
|
description = "Private NIP-42 authenticated relay";
|
||||||
|
};
|
||||||
|
authRequired = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# Nginx reverse proxy with SSL
|
||||||
|
services.nginx.virtualHosts."relay.youratm.company" = {
|
||||||
|
enableACME = true;
|
||||||
|
forceSSL = true;
|
||||||
|
locations."/" = {
|
||||||
|
proxyPass = "http://127.0.0.1:7777";
|
||||||
|
proxyWebsockets = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Machine Identity
|
||||||
|
|
||||||
|
> [!decision] Each ATM Has a Nostr Keypair
|
||||||
|
> Hardware-bound identity that replaces certificates and enables cryptographic authentication.
|
||||||
|
|
||||||
|
### Identity Model
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ MACHINE IDENTITY │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ Traditional: │
|
||||||
|
│ • Client certificate (complex PKI) │
|
||||||
|
│ • API keys (can be leaked) │
|
||||||
|
│ • IP-based auth (unreliable) │
|
||||||
|
│ │
|
||||||
|
│ Nostr-Native: │
|
||||||
|
│ • Machine has nsec (private key) │
|
||||||
|
│ • npub is machine identity │
|
||||||
|
│ • All events signed by machine │
|
||||||
|
│ • Operator whitelist controls access │
|
||||||
|
│ • No certificate authority needed │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Generation & Storage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Machine first boot - generate identity
|
||||||
|
import { generateSecretKey, getPublicKey } from 'nostr-tools'
|
||||||
|
import { writeFileSync } from 'fs'
|
||||||
|
|
||||||
|
function initMachineIdentity() {
|
||||||
|
const nsec = generateSecretKey()
|
||||||
|
const npub = getPublicKey(nsec)
|
||||||
|
|
||||||
|
// Store in secure location (TPM, encrypted file, etc.)
|
||||||
|
writeFileSync('/etc/lamassu/machine.nsec', nsec, { mode: 0o600 })
|
||||||
|
|
||||||
|
console.log(`Machine identity: ${npub}`)
|
||||||
|
console.log('Add this npub to operator whitelist')
|
||||||
|
|
||||||
|
return { nsec, npub }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Secure Key Storage Options
|
||||||
|
|
||||||
|
| Method | Security | Complexity | Best For |
|
||||||
|
|--------|----------|------------|----------|
|
||||||
|
| Encrypted file | Medium | Low | Development |
|
||||||
|
| TPM 2.0 | High | Medium | Production |
|
||||||
|
| Secure enclave | Highest | High | High-security |
|
||||||
|
| HSM | Highest | Highest | Enterprise |
|
||||||
|
|
||||||
|
### Tauri Integration
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// src-tauri/src/identity.rs
|
||||||
|
use nostr_sdk::prelude::*;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
|
pub struct MachineIdentity {
|
||||||
|
keys: Keys,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MachineIdentity {
|
||||||
|
pub fn load_or_create() -> Result<Self, Error> {
|
||||||
|
let nsec_path = "/etc/lamassu/machine.nsec";
|
||||||
|
|
||||||
|
let keys = if fs::metadata(nsec_path).is_ok() {
|
||||||
|
// Load existing
|
||||||
|
let nsec = fs::read_to_string(nsec_path)?;
|
||||||
|
Keys::parse(&nsec)?
|
||||||
|
} else {
|
||||||
|
// Generate new
|
||||||
|
let keys = Keys::generate();
|
||||||
|
fs::write(nsec_path, keys.secret_key()?.to_bech32()?)?;
|
||||||
|
keys
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(Self { keys })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn npub(&self) -> String {
|
||||||
|
self.keys.public_key().to_bech32().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sign_event(&self, event: UnsignedEvent) -> Result<Event, Error> {
|
||||||
|
event.sign(&self.keys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Replacing SMS with Nostr
|
||||||
|
|
||||||
|
> [!decision] Nostr DMs Replace Phone-Based Messaging
|
||||||
|
> No phone numbers = no KYC vector. Users provide npub for receipts.
|
||||||
|
|
||||||
|
### What SMS Was Used For (Old Lamassu)
|
||||||
|
|
||||||
|
| Use Case | Old Method | New Method |
|
||||||
|
|----------|------------|------------|
|
||||||
|
| Transaction receipt | SMS to phone | NIP-17 DM to npub |
|
||||||
|
| Verification code | SMS OTP | Not needed (no KYC) |
|
||||||
|
| Operator alerts | SMS/Email | Nostr events to operator npub |
|
||||||
|
| Customer notifications | SMS | Optional NIP-17 DM |
|
||||||
|
|
||||||
|
### NIP-17 Private Direct Messages
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Send encrypted receipt to user
|
||||||
|
import { nip44, nip59 } from 'nostr-tools'
|
||||||
|
|
||||||
|
async function sendReceipt(
|
||||||
|
userNpub: string,
|
||||||
|
receipt: TransactionReceipt
|
||||||
|
) {
|
||||||
|
const content = JSON.stringify({
|
||||||
|
type: 'transaction_receipt',
|
||||||
|
txid: receipt.txid,
|
||||||
|
amount: receipt.amountSats,
|
||||||
|
timestamp: receipt.timestamp,
|
||||||
|
atmId: receipt.atmNpub,
|
||||||
|
})
|
||||||
|
|
||||||
|
// NIP-17: Encrypted gift-wrapped message
|
||||||
|
const sealedEvent = await nip59.seal(
|
||||||
|
machineKeys,
|
||||||
|
userNpub,
|
||||||
|
{
|
||||||
|
kind: 14, // Direct message
|
||||||
|
content,
|
||||||
|
tags: [],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Publish to relay
|
||||||
|
await relay.publish(sealedEvent)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### User Flow (Optional Receipt)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────┐
|
||||||
|
│ OPTIONAL RECEIPT FLOW │
|
||||||
|
├─────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ 1. User completes transaction │
|
||||||
|
│ │
|
||||||
|
│ 2. ATM asks: "Want a receipt?" │
|
||||||
|
│ [No Thanks] [Yes, via Nostr] │
|
||||||
|
│ │
|
||||||
|
│ 3. If yes, user provides npub: │
|
||||||
|
│ • Scan NFC card with npub │
|
||||||
|
│ • Scan QR code of npub │
|
||||||
|
│ • Type npub manually │
|
||||||
|
│ │
|
||||||
|
│ 4. ATM sends NIP-17 encrypted DM │
|
||||||
|
│ • Only user can decrypt │
|
||||||
|
│ • Contains: amount, txid, timestamp │
|
||||||
|
│ • No phone number collected! │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Operator Alerts via Nostr
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Machine publishes alert event
|
||||||
|
async function sendOperatorAlert(
|
||||||
|
alertType: 'low_cash' | 'error' | 'offline',
|
||||||
|
details: object
|
||||||
|
) {
|
||||||
|
const event = {
|
||||||
|
kind: 30078, // Replaceable application-specific
|
||||||
|
pubkey: machineNpub,
|
||||||
|
content: nip44.encrypt(
|
||||||
|
machineNsec,
|
||||||
|
operatorNpub,
|
||||||
|
JSON.stringify({
|
||||||
|
type: alertType,
|
||||||
|
machineId: machineNpub,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
details,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
tags: [
|
||||||
|
['d', `alert:${machineNpub}`], // Replaceable identifier
|
||||||
|
['p', operatorNpub],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
await relay.publish(signEvent(event, machineNsec))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Event Schema
|
||||||
|
|
||||||
|
> [!tip] Custom Event Kinds for ATM Operations
|
||||||
|
> Define application-specific events for machine status, transactions, and commands.
|
||||||
|
|
||||||
|
### Event Kinds
|
||||||
|
|
||||||
|
| Kind | Type | Description |
|
||||||
|
|------|------|-------------|
|
||||||
|
| 21001 | CLINK | Offer Request/Response |
|
||||||
|
| 21002 | CLINK | Debit Request/Response |
|
||||||
|
| 21003 | CLINK | Management Delegation |
|
||||||
|
| 30078 | Replaceable | Machine Status |
|
||||||
|
| 30079 | Replaceable | Cash Levels |
|
||||||
|
| 14 | NIP-17 | Encrypted Receipt DM |
|
||||||
|
| 22242 | Ephemeral | NIP-42 Auth |
|
||||||
|
|
||||||
|
### Machine Status Event (Kind 30078)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface MachineStatusEvent {
|
||||||
|
kind: 30078
|
||||||
|
pubkey: string // Machine npub
|
||||||
|
content: string // NIP-44 encrypted JSON
|
||||||
|
tags: [
|
||||||
|
['d', 'status'], // Replaceable identifier
|
||||||
|
['p', string], // Operator npub
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypted content:
|
||||||
|
interface MachineStatus {
|
||||||
|
online: boolean
|
||||||
|
lastTransaction: number // timestamp
|
||||||
|
cashLevels: {
|
||||||
|
validator: number // bills in validator
|
||||||
|
dispenser: CassetteLevel[]
|
||||||
|
}
|
||||||
|
errors: string[]
|
||||||
|
version: string
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Transaction Record Event
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface TransactionEvent {
|
||||||
|
kind: 30079
|
||||||
|
pubkey: string // Machine npub
|
||||||
|
content: string // NIP-44 encrypted
|
||||||
|
tags: [
|
||||||
|
['d', `tx:${txid}`],
|
||||||
|
['p', string], // Operator npub
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypted content:
|
||||||
|
interface TransactionRecord {
|
||||||
|
txid: string
|
||||||
|
type: 'cash_in' | 'cash_out'
|
||||||
|
amountFiat: number
|
||||||
|
amountSats: number
|
||||||
|
fee: number
|
||||||
|
timestamp: number
|
||||||
|
paymentMethod: 'lnurl_withdraw' | 'clink_offer' | 'invoice' | 'cashu'
|
||||||
|
// No user identity stored!
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Operator Command Event
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface CommandEvent {
|
||||||
|
kind: 21003 // CLINK manage
|
||||||
|
pubkey: string // Operator npub
|
||||||
|
content: string // NIP-44 encrypted
|
||||||
|
tags: [
|
||||||
|
['p', string], // Target machine npub
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypted content:
|
||||||
|
interface OperatorCommand {
|
||||||
|
command: 'restart' | 'update' | 'disable' | 'enable' | 'set_limits'
|
||||||
|
params?: object
|
||||||
|
timestamp: number
|
||||||
|
signature: string // Operator signs command
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Full Stack Architecture
|
||||||
|
|
||||||
|
### Component Diagram
|
||||||
|
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────────────────────────────┐
|
||||||
|
│ NOSTR-NATIVE ATM STACK │
|
||||||
|
├─────────────────────────────────────────────────────────────────────┤
|
||||||
|
│ │
|
||||||
|
│ ┌─────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ ATM MACHINE │ │
|
||||||
|
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │ │
|
||||||
|
│ │ │ Vue 3 UI │ │ XState v5 │ │ Rust HAL │ │ │
|
||||||
|
│ │ │ (Tauri) │ │ (State) │ │ (Bill/Dispense) │ │ │
|
||||||
|
│ │ └──────┬──────┘ └──────┬──────┘ └──────────┬──────────┘ │ │
|
||||||
|
│ │ │ │ │ │ │
|
||||||
|
│ │ ┌──────┴────────────────┴─────────────────────┴──────────┐ │ │
|
||||||
|
│ │ │ Nostr Client │ │ │
|
||||||
|
│ │ │ • Machine nsec/npub identity │ │ │
|
||||||
|
│ │ │ • CLINK SDK for payments │ │ │
|
||||||
|
│ │ │ • NIP-44 encryption │ │ │
|
||||||
|
│ │ │ • Event publishing/subscription │ │ │
|
||||||
|
│ │ └────────────────────────┬───────────────────────────────┘ │ │
|
||||||
|
│ └───────────────────────────┼──────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ▼ │
|
||||||
|
│ ┌───────────────────────────────────────────────────────────────┐ │
|
||||||
|
│ │ PRIVATE NOSTR RELAY │ │
|
||||||
|
│ │ • strfry / rnostr │ │
|
||||||
|
│ │ • NIP-42 authentication required │ │
|
||||||
|
│ │ • Whitelist: ATM npubs + Operator npubs │ │
|
||||||
|
│ │ • Negentropy sync for offline reconciliation │ │
|
||||||
|
│ └────────────────────────────┬──────────────────────────────────┘ │
|
||||||
|
│ │ │
|
||||||
|
│ ┌────────────────────┼────────────────────┐ │
|
||||||
|
│ │ │ │ │
|
||||||
|
│ ▼ ▼ ▼ │
|
||||||
|
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────────┐ │
|
||||||
|
│ │ Lightning.Pub │ │ Operator │ │ Public Relays │ │
|
||||||
|
│ │ │ │ Dashboard │ │ (Fallback) │ │
|
||||||
|
│ │ • LND node │ │ │ │ │ │
|
||||||
|
│ │ • Accounts │ │ • Vue 3 app │ │ • nos.lol │ │
|
||||||
|
│ │ • CLINK native│ │ • Subscribe │ │ • relay.damus.io │ │
|
||||||
|
│ │ • Liquidity │ │ to events │ │ • For CLINK with │ │
|
||||||
|
│ └───────────────┘ │ • Send cmds │ │ external users │ │
|
||||||
|
│ └───────────────┘ └───────────────────┘ │
|
||||||
|
│ │
|
||||||
|
└─────────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
### Data Flow: Cash-In Transaction
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant User
|
||||||
|
participant ATM
|
||||||
|
participant Relay as Private Relay
|
||||||
|
participant LPub as Lightning.Pub
|
||||||
|
participant LN as Lightning Network
|
||||||
|
|
||||||
|
User->>ATM: Insert $50 cash
|
||||||
|
ATM->>ATM: Validate bills (HAL)
|
||||||
|
ATM->>Relay: Publish status event
|
||||||
|
|
||||||
|
ATM->>ATM: Generate CLINK offer (variable price)
|
||||||
|
ATM->>ATM: Display QR code
|
||||||
|
|
||||||
|
User->>User: Scan with ShockWallet
|
||||||
|
User->>Relay: CLINK offer request (Kind 21001)
|
||||||
|
Relay->>ATM: Forward request
|
||||||
|
|
||||||
|
ATM->>LPub: Request invoice (amount calculated)
|
||||||
|
LPub->>ATM: BOLT11 invoice
|
||||||
|
ATM->>Relay: CLINK response with invoice
|
||||||
|
Relay->>User: Forward response
|
||||||
|
|
||||||
|
User->>LN: Pay invoice
|
||||||
|
LN->>LPub: Payment received
|
||||||
|
LPub->>Relay: Payment confirmation event
|
||||||
|
Relay->>ATM: Forward confirmation
|
||||||
|
|
||||||
|
ATM->>ATM: Transaction complete
|
||||||
|
ATM->>Relay: Publish transaction record
|
||||||
|
|
||||||
|
opt User provided npub
|
||||||
|
ATM->>Relay: Send NIP-17 receipt DM
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Migration Path
|
||||||
|
|
||||||
|
### Phase 1: Add Nostr Layer
|
||||||
|
|
||||||
|
```
|
||||||
|
Existing Lamassu ──► Add Nostr client
|
||||||
|
Add private relay
|
||||||
|
Keep existing server (parallel)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 2: Lightning.Pub Integration
|
||||||
|
|
||||||
|
```
|
||||||
|
Add Lightning.Pub ──► Route payments through LPub
|
||||||
|
CLINK offers enabled
|
||||||
|
Account system active
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 3: Full Migration
|
||||||
|
|
||||||
|
```
|
||||||
|
Remove old server ──► Nostr-only communication
|
||||||
|
NIP-17 receipts (no SMS)
|
||||||
|
Private relay primary
|
||||||
|
```
|
||||||
|
|
||||||
|
### Phase 4: Optional Enhancements
|
||||||
|
|
||||||
|
```
|
||||||
|
Advanced features ──► Cashu ecash integration
|
||||||
|
Fedimint support
|
||||||
|
Multi-relay redundancy
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Security Considerations
|
||||||
|
|
||||||
|
### Threat Model
|
||||||
|
|
||||||
|
| Threat | Mitigation |
|
||||||
|
|--------|------------|
|
||||||
|
| Relay compromise | NIP-44 encryption (relay can't read) |
|
||||||
|
| Key theft | TPM/HSM storage, key rotation |
|
||||||
|
| Replay attacks | Timestamps, nonces in events |
|
||||||
|
| Rogue operator | Multi-sig commands (future) |
|
||||||
|
| Network sniffing | WebSocket over TLS, NIP-44 |
|
||||||
|
|
||||||
|
### Key Rotation
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// Periodic key rotation for machines
|
||||||
|
async function rotateMachineKey(oldNsec: string) {
|
||||||
|
const newKeys = generateKeys()
|
||||||
|
|
||||||
|
// Publish key rotation event (signed by old key)
|
||||||
|
const rotationEvent = {
|
||||||
|
kind: 30078,
|
||||||
|
content: nip44.encrypt(oldNsec, operatorNpub, JSON.stringify({
|
||||||
|
type: 'key_rotation',
|
||||||
|
oldPubkey: getPublicKey(oldNsec),
|
||||||
|
newPubkey: newKeys.npub,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
})),
|
||||||
|
tags: [
|
||||||
|
['d', 'key_rotation'],
|
||||||
|
['p', operatorNpub],
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
await relay.publish(signEvent(rotationEvent, oldNsec))
|
||||||
|
|
||||||
|
// Operator must update whitelist
|
||||||
|
// Then switch to new key
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Comparison: Old vs Nostr-Native
|
||||||
|
|
||||||
|
| Aspect | Old Lamassu | Nostr-Native |
|
||||||
|
|--------|-------------|--------------|
|
||||||
|
| Communication | HTTPS/WebSocket | Nostr events |
|
||||||
|
| Authentication | Client certs | NIP-42 + npub whitelist |
|
||||||
|
| Encryption | TLS | NIP-44 (content-level) |
|
||||||
|
| Identity | PKI certificates | Nostr keypairs |
|
||||||
|
| Receipts | SMS (phone = KYC) | NIP-17 DMs (npub) |
|
||||||
|
| Alerts | Email/SMS | Nostr events |
|
||||||
|
| Server config | DNS, SSL, ports | Zero config |
|
||||||
|
| Deployment | Complex | One-line |
|
||||||
|
| Censorship | Server can be seized | Relay-agnostic |
|
||||||
|
| Privacy | Phone numbers leaked | Pseudonymous npubs |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
1. **Relay redundancy** - Should machines connect to multiple relays?
|
||||||
|
2. **Offline operation** - How long can machine operate without relay?
|
||||||
|
3. **Key escrow** - How to recover if machine key is lost?
|
||||||
|
4. **Multi-operator** - Can multiple operators share a fleet?
|
||||||
|
5. **Cashu over Nostr** - Use Nostr for ecash token delivery?
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Related Notes
|
||||||
|
|
||||||
|
- [[architecture-review]] - Overall KYC-free architecture
|
||||||
|
- [[lnbits-integration]] - LNbits as alternative backend
|
||||||
|
- [[hardware-recommendations]] - Hardware choices
|
||||||
|
- [[machine-ui-modernization]] - Vue 3 UI migration
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
### Lightning.Pub
|
||||||
|
- [Lightning.Pub GitHub](https://github.com/shocknet/Lightning.Pub)
|
||||||
|
- [ShockWallet](https://github.com/shocknet/wallet2)
|
||||||
|
- [CLINK Protocol](https://github.com/shocknet/CLINK)
|
||||||
|
|
||||||
|
### Nostr Relays
|
||||||
|
- [strfry](https://github.com/hoytech/strfry)
|
||||||
|
- [rnostr](https://github.com/rnostr/rnostr)
|
||||||
|
- [nostr-rs-relay](https://sr.ht/~gheartsfield/nostr-rs-relay/)
|
||||||
|
- [noteguard](https://github.com/damus-io/noteguard) - strfry plugin system
|
||||||
|
|
||||||
|
### NIPs
|
||||||
|
- [NIP-42: Authentication](https://github.com/nostr-protocol/nips/blob/master/42.md)
|
||||||
|
- [NIP-44: Versioned Encryption](https://github.com/paulmillr/nip44)
|
||||||
|
- [NIP-17: Private Direct Messages](https://nips.nostr.com/17)
|
||||||
|
- [NIP-59: Gift Wraps](https://github.com/nostr-protocol/nips/blob/master/59.md)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue