docs: finish the LNbits-era doc sweep across docs/ + .claude/skills/

Second + final batch of the doc refresh. README + CLAUDE went out in
8c9ae29; deploy/nixos/README + obsolete-flow flags in 924844f. This
commit covers everything left.

docs/machine-installation.md
  Was describing a manual AppImage scp deploy + a `lamassu-kiosk`
  systemd unit that hasn't been the deployment path for months.
  Replaced with a high-level "what the pipeline does and why"
  overview that points at deploy/nixos/README.md for the full
  command-by-command walkthrough. Includes the BATM3 chassis-mod
  note (custom Dell OptiPlex retrofit, not a stock Dell).

docs/architecture-comparison.md
  Rewrote the comparison to be lamassu-server (≤ v8.1.5) vs bitSpire
  (LNbits-backed) instead of the original lamassu-server vs LP-backed
  lamassu-next framing. Updated the cash-out + cash-in flow diagrams
  to show the actual nostr-transport path (LNbits-bundled nostrrelay
  extension at ws://<host>:5001/nostrrelay/test, no separate strfry
  container). Replaced the migration-path section with a softer
  "when to choose what" framing that includes Lamassu's current
  commercial offering as a legitimate third option. Added a header
  pointer to the Acknowledgements section.

docs/business-model.md
  Light touch-ups: Lightning.Pub → LNbits where it appeared, swapped
  the [[ndebit-cash-in-flow]] link for [[architecture-comparison]],
  noted the kind-30078 service beacon for availability broadcasts.

docs/device-configuration.md
  Dropped "Lamassu" branding from the machine-model headings
  (Sintra / tejo / douro / batm3 are referenced by hardware identity
  here, not by Lamassu's product line). Added the Sintra-specific
  ttyS4-vs-placeholder-ttyS1..3 gotcha we hard-learned during the
  first flash. Corrected the BATM3 entry: stock GeneralBytes chassis
  with a Dell OptiPlex 9030 AIO motherboard physically grafted in,
  NOT a Dell out of the box. Updated the example /dev/ttyJ* symlink
  output to match what a healthy Sintra actually shows.

docs/adr/001-hal-architecture.md
  ADRs are historical artifacts — kept the original decision text
  intact. Added a postscript noting:
    - The package rename @lamassu/hal → @bitSpire/hal
    - The v8.1.5 boundary on any lamassu-machine source-tree
      references (Lamassu's 2024-01-26 license transition)
    - That the "Remaining Work" list is complete and the first
      successful Sintra hardware integration ran on 2026-05-13

.claude/skills/lightning-check.md
  Rewrote end-to-end. Was Lightning.Pub-flavoured with CLINK kinds
  21001/21002 as the primary flows; now validates the LNbits nostr-
  transport surface (kind-21000 envelope, NIP-44 v2 encryption,
  subscribe_payments filter discipline, lnurlw link composition).
  Preserved a --clink mode for the still-live kind-21003 operator-
  management surface. Includes a "what to check" rubric for cash-out
  vs cash-in flows that mirrors the actual code in
  apps/machine/src/services/lightning.ts.

.claude/skills/hal-check.md
  Two pivots: (1) acknowledge ADR-001's TypeScript-not-Rust choice
  and reframe all the safety checklists in TS-flavour (type safety,
  discriminated unions, single-writer serial, bounded emitters)
  instead of Rust-flavour (unsafe, borrow checker). (2) Add explicit
  v8.1.5 provenance boundary plus a "forbidden operations" section
  that prohibits diffing or porting from v8.1.6+ lamassu-machine
  source. Updated the port-validation source-reference table to
  list TS file paths under packages/hal/ instead of Rust paths.

.claude/skills/docs.md
  @lamassu/* → @bitSpire/*. Replaced the Lightning.Pub mermaid
  diagram with a current cash-out flow showing the nostr-transport
  RPC + subscribe_payments push path. Left the createOffer noffer
  example in the API-docs template section since it's illustrative
  ("here's what a good TSDoc block looks like") rather than current
  reference documentation.

.claude/skills/test.md
  One-line: @lamassu/nostr-client → @bitSpire/nostr-client in the
  pnpm-filter example.

deploy/nixos/README.md
  Single touch-up: clarified the douro/batm3 hardware-module comments
  to reflect that BATM3 is a custom-installed Dell board in a
  GeneralBytes BATM3 chassis (not a Dell OEM).

Files NOT touched in this sweep (intentionally):
  - packages/hal/src/**/*.ts attribution comments — those reference
    "lamassu-machine" in their port-source headers. Those are
    factually accurate (the drivers ARE ported from there, up to
    v8.1.5) and constitute necessary license/attribution metadata.
    Editing them would erase the provenance trail.
  - .claude/skills/{nostr-check,security}.md — already protocol-
    neutral, no LP/lamassu references to clean up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Padreug 2026-05-14 08:25:18 +02:00
commit 53b0d382e8
10 changed files with 692 additions and 792 deletions

View file

@ -25,10 +25,11 @@ docs/
│ ├── lightning-flow.md # Payment flows │ ├── lightning-flow.md # Payment flows
│ └── hardware-hal.md # HAL documentation │ └── hardware-hal.md # HAL documentation
├── api/ ├── api/
│ ├── nostr-client.md # @lamassu/nostr-client API │ ├── nostr-client.md # @bitSpire/nostr-client API
│ ├── clink.md # @lamassu/clink API │ ├── lnbits.md # @bitSpire/lnbits API (kind-21000 RPC surface)
│ ├── state-machine.md # @lamassu/state-machine API │ ├── clink.md # @bitSpire/clink API (dormant on dev; 21003 management still wired)
│ └── hal.md # @lamassu/hal API │ ├── state-machine.md # @bitSpire/state-machine API
│ └── hal.md # @bitSpire/hal API
├── guides/ ├── guides/
│ ├── development.md # Dev setup guide │ ├── development.md # Dev setup guide
│ ├── hardware-testing.md # Testing with real hardware │ ├── hardware-testing.md # Testing with real hardware
@ -106,22 +107,25 @@ Keep architecture diagrams current:
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
participant U as User participant U as Customer Wallet
participant A as ATM participant A as ATM
participant R as Relay participant R as Relay (LNbits-bundled nostrrelay)
participant L as Lightning.Pub participant L as LNbits (nostr-transport)
U->>A: Insert $20 bill Note over A,L: cash-out flow (customer pays ATM, gets cash)
A->>A: Validate bill A->>R: kind-21000 RPC "create_invoice"
A->>R: Publish CLINK offer R->>L: forward to LNbits server pubkey
U->>R: Send payment request L->>R: kind-21000 ACK with BOLT11 invoice
R->>A: Forward request R->>A: forward ACK
A->>L: Generate invoice A->>U: display BOLT11 QR
L->>A: Return invoice A->>R: kind-21000 "subscribe_payments" by payment_hash
A->>R: Send invoice to user R->>L: forward
U->>L: Pay invoice L->>R: kind-21000 ACK with subscription_id
L->>A: Payment confirmed R->>A: forward
A->>U: Display success U->>L: pay BOLT11 via Lightning Network
L->>R: kind-21000 push (settlement) tagged subscription_id
R->>A: forward
A->>A: dispense cash
``` ```
### Auto-update triggers ### Auto-update triggers

View file

@ -1,133 +1,162 @@
# /hal-check - Hardware Abstraction Layer Agent # /hal-check — Hardware Abstraction Layer Agent
## Purpose ## Purpose
Validate HAL driver implementations against existing lamassu-machine drivers and hardware specifications.
Validate HAL driver implementations in `packages/hal/` against published hardware protocol specs (JCM ID003, Fujitsu F56 DLE/STX, Puloon LCDM, MEI EBDS, etc.) and against the v8.1.5 release line of `lamassu-machine` — which is the **last** lamassu-machine release published under a fully-open license.
> **Provenance boundary.** Drivers in `packages/hal/` derive from `lamassu-machine` at v8.1.5 and earlier (plus hardware-vendor protocol specs). Lamassu Industries AG transitioned to a proprietary source-available license on 2024-01-26 with v8.1.6+ gated behind a paid Operator Support Agreement. **Do not** reference, port, or diff against v8.1.6+ — the only safe upstream tree for porting is `v8.1.5` or earlier. See [CLAUDE.md → Provenance + legal status](../../CLAUDE.md#provenance--legal-status) for the operating rules.
> **Language note.** ADR-001 selected TypeScript-in-Electron over Rust-in-Tauri for the HAL. Earlier versions of this skill referenced Rust patterns; that's obsolete. All checks below are TypeScript-flavored.
## Invocation ## Invocation
``` ```
/hal-check [command] [driver] /hal-check [command] [driver]
``` ```
Commands: Commands:
- `port` - Validate porting from lamassu-machine
- `protocol` - Check protocol implementation - `port` — Validate that a driver matches its lamassu-machine v8.1.5 reference (where the driver was ported from one)
- `safety` - Rust safety review - `protocol` — Check protocol implementation against published vendor specs
- `mock` - Validate mock implementation - `safety` — Type safety, error handling, hardware safety review
- `mock` — Validate mock implementation completeness
Drivers: Drivers:
- `id003`, `ccnet`, `cashflow`, `bnr`, `genmega`, `hcm2`, `gsr50` (validators)
- `puloon`, `f56`, `genmega`, `hcm2`, `gsr50` (dispensers)
- `nippon`, `zebra`, `genmega` (printers)
## Porting Validation - Validators: `id003`, `ccnet`, `cashflow_sc` (EBDS), `bnr_advance`, `genmega`, `hcm2`, `gsr50`
- Dispensers: `puloon`, `f56`, `genmega`, `hcm2`, `gsr50`
- Printers: `nippon`, `zebra`, `genmega`
### Source Reference ## Porting validation (`port` command)
Each Rust driver should map 1:1 with lamassu-machine JavaScript:
| Rust File | JavaScript Source | ### Source reference
|-----------|-------------------|
| `validators/id003.rs` | `lib/id003/*.js` | Each TS driver in `packages/hal/` maps to (at most) one JS source in lamassu-machine v8.1.5 (the last fully-open release):
| `validators/ccnet.rs` | `lib/ccnet/*.js` |
| `dispensers/puloon.rs` | `lib/puloon/*.js` | | TS Driver | JS Source (v8.1.5 release tree) |
| `dispensers/f56.rs` | `lib/f56/*.js` | |---|---|
| `validators/id003/*.ts` | `lib/id003/*.js` |
| `validators/ccnet/*.ts` | `lib/ccnet/*.js` |
| `validators/ebds/*.ts` | `lib/mei/cashflow_sc.js` + `lib/mei/*.js` |
| `dispensers/puloon/*.ts` | `lib/puloon/*.js` |
| `dispensers/f56/*.ts` | `lib/f56/*.js` |
If a driver in `packages/hal/` references a JS source NOT under v8.1.5 (or no JS source at all because it was re-derived from vendor specs), the attribution comment at the top of the TS file should say so.
### Porting checklist
### Porting Checklist
``` ```
/hal-check port id003 /hal-check port id003
``` ```
Validates: Validates:
- [ ] All protocol commands implemented
- [ ] State machine matches JS FSM
- [ ] CRC/checksum calculation identical
- [ ] Timeout values match
- [ ] Error codes mapped correctly
- [ ] Denomination tables match
### Protocol Commands - [ ] All protocol commands implemented (RESET, ENABLE, DISABLE, STACK, RETURN, STATUS — see ID003 spec or `lib/id003/id003rs232.js` from v8.1.5)
- [ ] State machine matches the JS FSM (`lib/id003/id003fsm.js`)
- [ ] CRC/checksum calculation byte-identical to the JS version
- [ ] Timeout values match the JS source
- [ ] Error codes mapped correctly to TypeScript enum/union
- [ ] Denomination tables match (currency-specific)
- [ ] Attribution comment at top of TS file points at the specific v8.1.5 file
### Protocol commands
#### ID003 (JCM) #### ID003 (JCM)
| Command | Code | JS Reference |
|---------|------|--------------| | Command | Code | JS Reference (v8.1.5) |
| RESET | 0x40 | id003.js:reset() | |---|---|---|
| RESET | 0x40 | id003rs232.js:reset() |
| ENABLE | 0x13 | id003fsm.js:enable | | ENABLE | 0x13 | id003fsm.js:enable |
| DISABLE | 0x14 | id003fsm.js:disable | | DISABLE | 0x14 | id003fsm.js:disable |
| STACK | 0x15 | id003fsm.js:stack | | STACK | 0x15 | id003fsm.js:stack |
| RETURN | 0x16 | id003fsm.js:return | | RETURN | 0x16 | id003fsm.js:return |
| STATUS | 0x10 | id003fsm.js:poll | | STATUS | 0x10 | id003fsm.js:poll |
#### Puloon #### Puloon LCDM
| Command | Code | JS Reference |
|---------|------|--------------| | Command | Code | JS Reference (v8.1.5) |
|---|---|---|
| RESET | 0x44 | puloonrs232.js | | RESET | 0x44 | puloonrs232.js |
| DISPENSE | 0x45 | puloonrs232.js | | DISPENSE | 0x45 | puloonrs232.js |
| STATUS | 0x46 | puloonrs232.js | | STATUS | 0x46 | puloonrs232.js |
## Safety Review (Rust-specific) ## Safety review (`safety` command)
### Memory Safety ### Type safety
- [ ] No `unsafe` without justification comment
- [ ] Buffer sizes validated before read/write
- [ ] No panic paths in production code
- [ ] Proper error propagation with Result
### Concurrency Safety - [ ] No `any` types in driver public API
- [ ] Serial port access properly synchronized - [ ] Discriminated unions for state machine states (not just string literals)
- [ ] Event channels bounded - [ ] Error types are typed `Result<T, DriverError>` (or equivalent) at the driver boundary — never `throw` raw strings
- [ ] No deadlock potential - [ ] Buffer reads are bounds-checked; offsets validated before slicing
- [ ] Timeout on all blocking operations
### Hardware Safety ### Concurrency safety
- [ ] Dispenser amounts validated (prevent over-dispense)
- [ ] Bill count cross-checked
- [ ] Error states trigger hardware reset
- [ ] Graceful degradation on hardware failure
## Mock Validation - [ ] Serial port access is single-writer (TypeScript single-threaded helps, but ensure no two `await write()` calls can interleave with each other's expected response)
- [ ] Event emitters are bounded (max listeners set; back-pressure handled)
- [ ] Timeouts on all blocking I/O (`Promise.race` with `setTimeout`)
- [ ] No unbounded queues — a runaway validator polling loop shouldn't fill RAM
### Hardware safety
- [ ] Dispenser amounts validated against cassette inventory before dispensing
- [ ] Bill count cross-checked between requested and dispensed
- [ ] Error states trigger hardware reset (`disable + reset + enable` cycle for validators)
- [ ] Graceful degradation: partial dispense reports correct dispensed-vs-rejected counts
- [ ] Validator goes to `disable` on app shutdown — bills inserted while the app is dead get returned, not stacked uncounted
## Mock validation (`mock` command)
### Mock requirements
Mocks under `packages/hal/src/**/mock.ts` must simulate:
### Mock Requirements
Mocks must simulate:
1. Normal operation flow 1. Normal operation flow
2. Error conditions 2. Error conditions (jam, empty cassette, comms failure)
3. Timing (realistic delays) 3. Realistic timing (validator escrow takes ~600ms; dispenser cycle takes ~2s)
4. State persistence 4. State persistence within a session
### Mock test coverage
### Mock Test Coverage
``` ```
/hal-check mock puloon /hal-check mock puloon
``` ```
Validates mock implements: Validates mock implements:
- [ ] `connect()` - Success and failure paths
- [ ] `dispense()` - Full and partial dispense
- [ ] `reset()` - Error recovery
- [ ] Event emission timing
- [ ] Cassette state tracking
## Protocol Analysis - [ ] `connect()` — success and failure paths
- [ ] `dispense()` — full, partial, and over-requested paths
- [ ] `reset()` — error recovery
- [ ] Event emission timing roughly matches real hardware
- [ ] Cassette state tracking (count decrements properly)
## Protocol analysis (`protocol` command)
### Packet structure validation
### Packet Structure Validation
``` ```
/hal-check protocol id003 /hal-check protocol id003
``` ```
Compares Rust packet building with JS: Compares TS packet building with the v8.1.5 JS reference:
```rust
// Rust implementation ```typescript
fn build_packet(&self, data: &[u8]) -> Vec<u8> { // TypeScript implementation
let mut packet = vec![0x02]; // SYNC function buildPacket(data: Uint8Array): Uint8Array {
packet.push(data.len() as u8 + 4); const packet = new Uint8Array(data.length + 4)
packet.extend_from_slice(data); packet[0] = 0x02 // SYNC
let crc = self.calculate_crc(&packet); packet[1] = data.length + 4 // length includes header + CRC
packet.push((crc & 0xFF) as u8); packet.set(data, 2)
packet.push((crc >> 8) as u8); const crc = calculateCrc(packet.slice(0, -2))
packet packet[packet.length - 2] = crc & 0xff
packet[packet.length - 1] = (crc >> 8) & 0xff
return packet
} }
``` ```
Against JavaScript: Against the v8.1.5 JS reference (line numbers may vary by tag):
```javascript ```javascript
// lamassu-machine/lib/id003/id003rs232.js // lamassu-machine v8.1.5 — lib/id003/id003rs232.js
function buildPacket(data) { function buildPacket(data) {
const buf = Buffer.alloc(data.length + 4) const buf = Buffer.alloc(data.length + 4)
buf[0] = 0x02 // SYNC buf[0] = 0x02 // SYNC
@ -139,62 +168,72 @@ function buildPacket(data) {
} }
``` ```
## Output Format A discrepancy here (different CRC polynomial, different framing, different endianness) is a serious port bug.
## Output format
### Port validation
### Port Validation
```markdown ```markdown
## HAL Port Validation: id003 ## HAL Port Validation: id003
### Command Coverage ### Command coverage
| Command | JS | Rust | Match | | Command | v8.1.5 JS | TS | Match |
|---------|-------|------|-------| |---|---|---|---|
| RESET | ✅ | ✅ | ✅ | | RESET | ✅ | ✅ | ✅ |
| ENABLE | ✅ | ✅ | ✅ | | ENABLE | ✅ | ✅ | ✅ |
| STATUS | ✅ | ⚠️ | Partial | | STATUS | ✅ | ⚠️ | Partial |
### Protocol Differences ### Protocol differences
- [ ] `id003.rs:78` - CRC uses different polynomial than JS - [ ] `id003-rs232.ts:78` — CRC uses different polynomial than v8.1.5 JS
### Missing Implementations ### Missing implementations
- [ ] `HOLD` command not implemented (used in id003fsm.js:holdBill) - [ ] HOLD command not implemented (v8.1.5 `id003fsm.js:holdBill`)
### Recommendations ### Attribution
1. Verify CRC calculation against test vectors from JS - [x] File header points at `lib/id003/id003rs232.js` (v8.1.5)
2. Add HOLD command for escrow mode
``` ```
### Safety Review ### Safety review
```markdown ```markdown
## HAL Safety Review: puloon ## HAL Safety Review: puloon
### Memory Safety ### Type safety
- [x] No unsafe blocks - [x] No `any` types in public surface
- [x] Buffer bounds checked - [x] Discriminated union for DispenserState
- [ ] `dispense()` at line 145 - unwrap() could panic
### Concurrency Safety ### Concurrency safety
- [x] Serial port mutex protected - [x] Single-writer over serial port
- [x] Event channel bounded (16) - [x] Event emitter has bounded listener count
- [ ] `puloon-rs232.ts:142` — no timeout on response wait; could hang forever
### Hardware Safety ### Hardware safety
- [x] Amount validation in dispense() - [x] Amount validated against cassette inventory in dispense()
- [ ] No cassette empty check before dispense - [ ] `puloon.ts:178` — partial dispense doesn't update cassette inventory
### Critical Issues ### Critical issues
1. `puloon.rs:145` - Replace unwrap() with proper error handling 1. `puloon-rs232.ts:142` — wrap response wait in `Promise.race([response, timeout(5000)])`
2. `puloon.rs:178` - Add cassette level check before dispensing 2. `puloon.ts:178` — decrement cassette count by actual dispensed, not requested
``` ```
## Example Usage ## Example usage
``` ```
/hal-check port id003 /hal-check port id003
``` /hal-check safety packages/hal/src/dispensers/puloon/
```
/hal-check safety packages/hal/src/dispensers/
```
```
/hal-check mock --all /hal-check mock --all
/hal-check protocol ebds # validates EBDS framing against v8.1.5 JS + MEI vendor spec
``` ```
## Related skills
- `/security` — broader Bitcoin/Lightning/ATM vulnerability scan
- `/test` — runs HAL-specific tests, generates coverage reports
- `/lightning-check` — validates the Lightning backend side (LNbits transport)
## Forbidden operations
- Diff or read `lamassu-machine` source at v8.1.6 or later. Only `v8.1.5` (and the historical commit range leading up to it) is permissible to reference.
- "Backport" any fix or feature from v8.1.6+ JS sources into TypeScript. If a bug fix is needed, implement from the protocol spec or hardware traces.
- Include attribution comments pointing at v8.1.6+ files even if the implementation is your own — readers should be able to trust file-header attributions as accurate.

View file

@ -1,168 +1,208 @@
# /lightning-check - Lightning.Pub Conformity Agent # /lightning-check — Lightning backend conformity
## Purpose ## Purpose
Validate Lightning.Pub integration and CLINK protocol conformity.
Validate the LNbits nostr-transport integration in bitSpire — wire envelope, encryption, subscription handling, invoice/withdraw correctness. Replaces the original Lightning.Pub-flavored version of this skill; the LP backend was removed from `dev` in commit `a51d7af`.
> The `--clink` mode is preserved for validating CLINK kind-21003 management commands, which are the one piece of CLINK still wired on `dev` (operator dispense commands). CLINK kinds 21001/21002 are dormant on `dev`.
## Invocation ## Invocation
``` ```
/lightning-check [target] [--clink] [--wallet] /lightning-check [target] [--lnbits] [--lnurlw] [--clink]
``` ```
Where: Where:
- `target` - File or directory to check
- `--clink` - Focus on CLINK offer/debit flow
- `--wallet` - Focus on wallet operations
## Lightning.Pub Integration - `target` — file or directory to check (default: `packages/lnbits/src/` + `apps/machine/src/services/lightning.ts`)
- `--lnbits` — focus on LNbits nostr-transport RPC + subscription surface
- `--lnurlw` — focus on LNURL-withdraw cash-in flow (link creation, callback URL composition, subscription)
- `--clink` — focus on the kind-21003 management surface (operator commands)
### Connection ## What to validate
Lightning.Pub uses Nostr for all communication:
### 1. Wire envelope (kind 21000)
Each RPC request/response is a kind-21000 event with NIP-44 v2-encrypted JSON in `content`. The plaintext shape is:
```jsonc
// Request
{
"rpc_name": "create_invoice",
"request_id": "create_invoice-7-ab12cd",
"wallet_id": "<uuid>", // present for AUTH_WALLET RPCs
"body": { /* RPC-specific */ },
"query": { /* RPC-specific */ } // optional
}
// One-shot reply
{
"status": "OK" | "ERROR",
"request_id": "create_invoice-7-ab12cd",
"data": { /* RPC-specific */ },
"error": "..." // present when status==ERROR
}
// Subscription push
{
"status": "OK",
"request_id": "sub-9-ef34gh",
"subscription_id": "<server-issued>",
"data": { "payment": { /* Payment */ } } |
{ "closed": true, "reason": "ttl"|"unsubscribed" }
}
```
Required checks:
- [ ] `request_id` is unique per outbound RPC (no reuse across sessions)
- [ ] The pending map is keyed by `request_id` and cleaned up on resolve/reject
- [ ] Subscription pushes are matched by `subscription_id` (top-level field, not in `data`)
- [ ] The handler resolves the subscription's `subscriptionId` only after the ACK arrives — pre-ACK pushes (rare but possible) are handled
- [ ] The reply listener decrypts with `decryptContentV2(identity, serverPubkey, ev.content)` and silently skips events that don't decrypt (wrong sender)
- [ ] Events are signed with `finalizeEvent` from `nostr-tools` using the ATM's `identity.privateKey` — server identifies the calling account from the signature alone
### 2. Encryption (NIP-44 v2)
LNbits transport requires NIP-44 v2, NOT NIP-04. Required checks:
- [ ] All kind-21000 content is `encryptContentV2`/`decryptContentV2` (not `encryptContent`/`decryptContent`, which are the v1/NIP-04 paths kept around for CLINK compatibility)
- [ ] The shared secret is derived from `identity.privateKey × serverPubkey` (or the reverse on the server side — symmetric)
- [ ] No code logs ciphertext or shared secrets
### 3. Authentication model
LNbits derives the calling account from the event signature. There is **no admin token, no API key, no client cert** on the ATM. Required checks:
- [ ] No code stores or references an admin token
- [ ] No code makes outbound HTTP to LNbits (other than via `VITE_LNBITS_HTTP_URL` for the LNURL-withdraw callback, which is the customer wallet's path — the ATM itself doesn't hit HTTP)
- [ ] The ATM's `identity.privateKey` is loaded once from `/var/lib/bitspire/.env` and never logged
### 4. Cash-out flow (`--lnbits`)
In `services/lightning.ts → generateInvoice / watchInvoice`:
```typescript ```typescript
// Connection via nprofile // Generate
const nprofile = 'nprofile1...' // Contains pubkey + relay hints const payment = await lnbits.createInvoice(walletId, {
amount: amountSats,
memo: 'bitSpire - Cash Out',
unit: 'sat',
})
// All operations are Nostr events to Lightning.Pub's pubkey // Watch — must filter by payment_hash to avoid acting on unrelated settlements
await relay.publish({ const decoded = await lnbits.decodePayment(invoice)
kind: 21002, // CLINK debit const paymentHash = decoded.payment_hash
content: encrypted_request, const subId = await lnbits.subscribePayments(walletId, {
tags: [['p', lightningPubPubkey]], payment_hash: paymentHash,
max_seconds: 600,
}, push => {
if (push.payment_hash !== paymentHash) return // belt-and-suspenders
if (push.status !== 'success') return
callback(push.preimage ?? 'payment-confirmed')
}) })
``` ```
### Required Checks Required checks:
- [ ] Using correct event kinds (21001, 21002, 21003)
- [ ] Proper NIP-44 encryption for requests
- [ ] Handling async responses via subscription
- [ ] Proper error handling for payment failures
## CLINK Protocol - [ ] `subscribePayments` filter always specifies `payment_hash` for invoice watching (filter-less subscriptions are server-rejected; broader filters expose us to acting on unrelated settlements)
- [ ] `max_seconds` is clamped — LNbits clamps server-side to [1, 600], so values outside that range are silently changed
- [ ] The callback checks `push.payment_hash` matches the expected one (defense in depth)
- [ ] The callback checks `push.status === 'success'` before treating as paid
- [ ] On cleanup, `unsubscribe(walletId, subId)` is called
### Offer Flow (Kind 21001) ### 5. Cash-in flow (`--lnurlw`)
```
User scans noffer → Wallet sends 21002 → ATM responds with invoice → User pays In `services/lightning.ts → generateLnurlWithdraw`:
```typescript
const link = await lnbits.createWithdrawLink(walletId, {
title: '...',
min_withdrawable: ctx.satsAmount,
max_withdrawable: ctx.satsAmount,
uses: 1,
wait_time: 1,
is_unique: false,
})
const callbackUrl = `${CONFIG.lnbitsHttpUrl.replace(/\/+$/, '')}/withdraw/api/v1/lnurl/${link.unique_hash}`
const lnurl = encodeLnurl(callbackUrl) // bech32 HRP="lnurl", uppercase
const subId = await lnbits.subscribePayments(walletId, {
tag: 'withdraw',
link_id: link.id,
max_seconds: 600,
}, push => { /* mark session claimed, dispense */ })
``` ```
Validation: Required checks:
- [ ] noffer encoding is valid
- [ ] Relays included in offer
- [ ] Price type correctly specified (fixed/variable/spontaneous)
- [ ] Amount bounds validated
### Debit Flow (Kind 21002) - [ ] `uses: 1` — single-use prevents replay
Request structure: - [ ] `min_withdrawable === max_withdrawable === ctx.satsAmount` — exact amount, no operator slippage
```json - [ ] The LNURL is composed from `VITE_LNBITS_HTTP_URL` + `link.unique_hash` (the transport `lnurlw_create_link` returns `link.lnurl === null` — those fields are filled by HTTP views, not the create RPC)
{ - [ ] bech32 encoding uses HRP `"lnurl"` and the result is uppercased per BOLT/LNURL convention
"method": "pay_invoice", - [ ] Subscription filter is `tag: 'withdraw'` + `link_id: link.id` — this is what the withdraw extension stamps on settlement events
"params": { - [ ] On session abort, the cleanup closure both unsubscribes AND deletes the withdraw link (so a half-completed session doesn't leave a redeemable QR alive on LNbits)
"invoice": "lnbc...",
"amount_msat": 100000
}
}
```
Response structure: ### 6. Operator commands (`--clink`, kind-21003)
```json
{
"result": {
"preimage": "...",
"fee_msat": 1000
}
}
```
Validation: The kind-21003 management surface is the one piece of CLINK still wired on `dev`. It's LP-independent (operators talk directly to the ATM over nostr) so it survived the migration.
- [ ] Invoice format valid (BOLT11)
- [ ] Amount matches invoice
- [ ] Preimage verified against payment hash
- [ ] Fee within acceptable bounds
- [ ] Timeout handling
### Manage Flow (Kind 21003) Required checks:
For operator commands:
```json
{
"method": "get_balance",
"params": {}
}
```
Validation: - [ ] `CLINKClient` is initialized with `operatorPubkey: CONFIG.operatorPubkeys` (no Lightning.Pub pubkey mixed in — that's LP-era)
- [ ] Only authorized operators can send - [ ] `clink.onManagement` handler routes through `handleManagementCommand` which dispatches by `request.command` (manual dispense, status query, etc.)
- [ ] Commands are properly authenticated - [ ] Operator authentication is by pubkey allowlist — not a separate token
- [ ] Responses handled securely - [ ] Sensitive ops (`dispenseCash`) only fire when the state machine is idle (`isIdle.value` check)
## Invoice Validation ## Output format
### BOLT11 Checks
- [ ] Valid bech32 encoding
- [ ] Expiry not passed
- [ ] Amount matches expected
- [ ] Description hash valid (if used)
- [ ] Payment hash extractable
### Security Checks
- [ ] Never pay same invoice twice
- [ ] Amount limits enforced
- [ ] Rate limiting on payments
- [ ] Proper logging (no sensitive data)
## Wallet Operations
### Balance Queries
- [ ] Cached appropriately (not every render)
- [ ] Error handling for offline
- [ ] Display in correct units (sats, not msat)
### Invoice Generation
- [ ] Unique payment hashes
- [ ] Reasonable expiry times
- [ ] Description for record-keeping
- [ ] Amount in correct units
## Output Format
```markdown ```markdown
## Lightning.Pub Conformity: [target] ## Lightning backend conformity: [target]
### CLINK Protocol ### LNbits transport
| Flow | Status | Notes | | Concern | Status | Notes |
|------|--------|-------| |---|---|---|
| Offer (21001) | ✅ | | | Wire envelope | ✅ | |
| Debit (21002) | ⚠️ | Missing timeout | | NIP-44 v2 encryption | ✅ | |
| Manage (21003) | ✅ | | | Signature-only auth | ✅ | No admin tokens found |
| Subscription handling | ⚠️ | Missing payment_hash filter on watchInvoice cleanup |
### Invoice Handling ### Cash-out / Cash-in flows
- [ ] `file:line` - Issue description - [ ] `file:line` — Issue description + fix
### Integration Issues ### Operator (kind-21003)
- [ ] Description with fix suggestion - [ ] `file:line` — Issue description
### Recommendations ### Recommendations
- [ ] Performance/UX improvements - [ ] Performance/UX improvements
``` ```
## Example Usage ## Example
``` ```
/lightning-check packages/lightning/src/ --clink /lightning-check packages/lnbits/src/ --lnbits
``` ```
Output: Output:
```markdown ```markdown
## Lightning.Pub Conformity: packages/lightning/src/ ## Lightning backend conformity: packages/lnbits/src/
### CLINK Protocol ### LNbits transport
| Flow | Status | Notes | | Concern | Status | Notes |
|------|--------|-------| |---|---|---|
| Offer (21001) | ✅ | Properly encoded | | Wire envelope | ✅ | request_id uniqueness OK, pending map cleaned up on timeout |
| Debit (21002) | ⚠️ | No timeout handling | | NIP-44 v2 encryption | ✅ | encryptContentV2/decryptContentV2 used exclusively |
| Subscription handling | ✅ | subscriptionId resolved after ACK, push handlers race-safe |
| Cleanup | ⚠️ | unsubscribe error swallowed silently — log at warn level |
### Issues Found ### Cash-out flow
- [ ] `client.ts:89` - Debit request has no timeout, could hang indefinitely - [ ] `packages/lnbits/src/client.ts:212` — watchInvoice resolves on first success push but doesn't unsubscribe until subIdRef is assigned; if push lands before subscribePayments resolves, leaks the subscription
- [ ] `client.ts:112` - Payment hash not verified against preimage
### Recommendations
- Add 30-second timeout for CLINK debit requests
- Implement invoice deduplication to prevent double-pay
``` ```
## Related skills
- `/nostr-check` — validates NIP-01 / NIP-44 conformance at the nostr-client layer
- `/security` — Bitcoin/Lightning vuln audit
- `/test` — runs vitest + flow validation

View file

@ -22,7 +22,7 @@ Location: `*.test.ts` or `*.spec.ts` alongside source files
```bash ```bash
pnpm test # All tests pnpm test # All tests
pnpm test --filter @lamassu/nostr-client # Specific package pnpm test --filter @bitSpire/nostr-client # Specific package
``` ```
### 2. Integration Tests ### 2. Integration Tests

View file

@ -12,8 +12,8 @@ deploy/nixos/
├── configuration.nix # Base system (NixOS 24.05, locale, kernel, packages, lamassu user) ├── configuration.nix # Base system (NixOS 24.05, locale, kernel, packages, lamassu user)
├── live.nix # Live USB variant (squashfs + tmpfs root) — used by mkLiveConfig ├── live.nix # Live USB variant (squashfs + tmpfs root) — used by mkLiveConfig
├── hardware/ ├── hardware/
│ ├── douro.nix # Dell OptiPlex 9030 AIO (batm3 sibling; SATA SSD, eGalax touch) │ ├── douro.nix # Dell OptiPlex 9030 AIO (stock Douro motherboard; SATA SSD, eGalax touch)
│ ├── batm3.nix # GeneralBytes BATM3 (Dell board, WireGuard wired in) │ ├── batm3.nix # GeneralBytes BATM3 chassis with a Dell OptiPlex 9030 AIO grafted in (custom mod; WireGuard wired in)
│ └── upboard.nix # Aaeon UP Board (Sintra + tejo; eMMC root via sdhci-acpi + mmc_block) │ └── upboard.nix # Aaeon UP Board (Sintra + tejo; eMMC root via sdhci-acpi + mmc_block)
├── udev/ ├── udev/
│ └── 99-lamassu-hardware.rules # additional udev rules (loaded via configuration.nix) │ └── 99-lamassu-hardware.rules # additional udev rules (loaded via configuration.nix)

View file

@ -134,7 +134,24 @@ If a compelling reason for Rust emerges (it likely won't), the HAL interface is
## References ## References
- `packages/hal/` - TypeScript HAL implementation - `packages/hal/` — TypeScript HAL implementation
- `packages/state-machine/src/types.ts` - ATMServices interface - `packages/state-machine/src/types.ts` — ATMServices interface
- `hardware/codebase/upboard/sintra/device_config.json` - Sintra device paths - `deploy/nixos/hardware/upboard.nix` — Sintra device paths and udev rules
- lamassu-machine `lib/id003/`, `lib/f56/` - Original JS drivers - lamassu-machine `lib/id003/`, `lib/f56/` — Original JS drivers (v8.1.5 release line and earlier; see postscript below)
---
## Postscript (2026-05-13)
This ADR is still in effect — TypeScript HAL in Electron remains the right call for the same reasons documented above. A few clarifications for readers in 2026 and beyond:
1. **Package naming.** The HAL package was originally `@lamassu/hal` (per the line above in "Completed"). It was renamed to `@bitSpire/hal` during the project rename from `lamassu-next` to `bitSpire` (commit `924844f` in the 2a sweep). All references to the package scope should now read `@bitSpire/hal`.
2. **Source-tree provenance, post-Lamassu license change.** The "Original JS drivers" reference above refers to lamassu-machine's `lib/id003/` and `lib/f56/` directories in the **v8.1.5 release line and earlier**, which were published under a fully-open license. Lamassu Industries AG transitioned to a proprietary source-available license on 2024-01-26 (v8.1.6+). bitSpire incorporates no code from v8.1.6 or later; the protocol-level reimplementations we have are derived from v8.1.5 + protocol specs published by JCM, Fujitsu, etc. See [CLAUDE.md → Provenance + legal status](../../CLAUDE.md#provenance--legal-status) for the operating rules going forward.
3. **Implementation status.** The "Remaining Work" list above is complete:
- `apps/machine` runs on Electron (the original Tauri scaffolding was removed early on)
- HAL is wired to the state machine via the ATMServices interface
- Device configuration ships via `deploy/nixos/hardware/upboard.nix` + the `services.bitspire` NixOS module
- First successful Sintra hardware integration test ran on 2026-05-13 (cash-out flow verified end-to-end against a regtest LNbits instance)

View file

@ -1,39 +1,41 @@
# Architecture Comparison: Nostr-Native vs Traditional Lamassu # Architecture Comparison: bitSpire vs Traditional Lamassu
This document compares the Nostr-native Lightning ATM architecture (Lamassu Next) with the traditional lamassu-server/machine implementation to help operators evaluate the transition. This document compares the bitSpire architecture — a Nostr-native Lightning ATM talking to LNbits over the nostr-native-transport — with the traditional `lamassu-server` + `lamassu-machine` architecture. The goal is to help an operator evaluating an open-source Lightning ATM stack understand what they're choosing between.
## Executive Summary > A note on prior art: bitSpire's hardware drivers and cash-flow state machine derive from Lamassu Industries AG's open-source `lamassu-machine` and `lamassu-server` projects up to and including the v8.1.5 release line (the last published under a fully-open license). Lamassu [transitioned to a proprietary source-available license](https://blog.lamassu.is/updates-to-our-lamassu-software-license/) on 2024-01-26; bitSpire does **not** incorporate code from v8.1.6 or later and is not affiliated with Lamassu Industries AG. See the [Acknowledgements](../README.md#acknowledgements) section of the top-level README.
| Aspect | Traditional (lamassu-server) | Nostr-Native (lamassu-next) | ## Executive summary
| ------------------------ | ---------------------------- | ---------------------------------- |
| **Communication** | Custom WebSocket protocol | Nostr relay (NIP-01) | | Aspect | Traditional (`lamassu-server` ≤ v8.1.5) | bitSpire |
| **Identity** | Server-issued credentials | Cryptographic keypairs (npub/nsec) | |---|---|---|
| **Account System** | PostgreSQL + custom auth | Lightning.Pub (Nostr-native) | | **Software license** | AGPL-3.0 (v8.1.5); v8.1.6+ is proprietary SLA | AGPL-3.0 |
| **Payment Protocol** | Direct LND RPC | CLINK protocol (kinds 21001-21003) | | **Communication** | Custom WebSocket + GraphQL over HTTPS | Nostr kind-21000 events (NIP-01, NIP-44 v2) over a relay |
| **Infrastructure** | Server + DB + Admin UI | Relay (optional self-hosted) | | **Identity** | Server-issued client certs | Nostr keypairs (`nsec`/`npub`) |
| **Wallet Compatibility** | Lamassu-specific | Any CLINK-compatible wallet | | **Account system** | PostgreSQL + custom auth | LNbits wallet auto-created from ATM's nostr pubkey |
| **Payment protocol** | Direct LND RPC | BOLT11 (cash-out) + LNURL-withdraw (cash-in) |
| **Server infrastructure** | App server + DB + Admin UI | A relay + an LNbits instance (both can be one container) |
| **Wallet compatibility** | Any Lightning wallet (cash-out); custom flow for cash-in | Any Lightning wallet (BOLT11 + LNURL-withdraw — both widely supported) |
| **State location** | Centralized in PostgreSQL | Distributed: LNbits holds Lightning state, ATM holds session state |
--- ---
## Traditional Architecture (lamassu-server/machine) ## Traditional architecture (lamassu-server / lamassu-machine, v8.1.5)
### Overview
``` ```
┌─────────────────┐ WebSocket ┌─────────────────────┐ ┌──────────────────┐ WebSocket ┌─────────────────────┐
│ ATM Machine │◄─────────────────────────►│ lamassu-server │ │ ATM (kiosk) │◄──────────────────────────►│ lamassu-server │
│ (lamassu-machine)│ │ │ │ lamassu-machine │ client-cert auth │ │
└─────────────────┘ │ ┌───────────────┐ │ └──────────────────┘ │ ┌───────────────┐ │
│ │ PostgreSQL │ │ │ │ PostgreSQL │ │
┌─────────────────┐ HTTPS │ └───────────────┘ │ ┌──────────────────┐ HTTPS │ └───────────────┘ │
│ Admin UI │◄─────────────────────────►│ │ │ Admin UI │◄──────────────────────────►│ │
│ (React SPA) │ GraphQL │ ┌───────────────┐ │ │ (React SPA) │ GraphQL │ ┌───────────────┐ │
└─────────────────┘ │ │ LND │ │ └──────────────────┘ │ │ LND │ │
│ └───────────────┘ │ │ └───────────────┘ │
┌─────────────────┐ │ │ ┌──────────────────┐ │ │
│ Customer Wallet │◄── Lightning Invoice ─────│ ┌───────────────┐ │ │ Customer Wallet │◄── Lightning Invoice ──────│ ┌───────────────┐ │
│ (any LN wallet) │ │ │ Compliance │ │ │ (any LN wallet) │ │ │ Compliance │ │
└─────────────────┘ │ │ Services │ │ └──────────────────┘ │ │ Services │ │
│ └───────────────┘ │ │ └───────────────┘ │
└─────────────────────┘ └─────────────────────┘
``` ```
@ -41,298 +43,264 @@ This document compares the Nostr-native Lightning ATM architecture (Lamassu Next
### Components ### Components
| Component | Description | | Component | Description |
| ------------------- | ------------------------------------------------ | |---|---|
| **lamassu-server** | Node.js Express + Apollo GraphQL backend | | `lamassu-server` | Node.js (Express + Apollo GraphQL) backend |
| **lamassu-machine** | Node.js kiosk application with hardware drivers | | `lamassu-machine` | Node.js kiosk app with hardware drivers |
| **PostgreSQL** | Central database for transactions, users, config | | PostgreSQL | Central DB for transactions, users, config |
| **Admin UI** | React dashboard for fleet management | | Admin UI | React dashboard for fleet management |
| **LND** | Lightning Network Daemon for payments | | LND | Lightning Network Daemon for payments |
### Communication Flow ### Cash-out flow
1. Machine boots and authenticates with server via client certificate 1. Machine boots, authenticates via client cert
2. Server pushes configuration and state via WebSocket 2. Server pushes config + state over WebSocket
3. Customer initiates transaction at machine 3. Customer initiates cash-out at machine
4. Machine requests invoice/address from server 4. Machine asks server for an invoice
5. Server creates invoice via LND, stores in PostgreSQL 5. Server creates invoice via LND, stores in PostgreSQL
6. Customer pays invoice 6. Customer pays the invoice
7. Server detects payment, updates database 7. Server polls LND, detects payment, updates DB
8. Server notifies machine to dispense cash 8. Server notifies machine to dispense
9. Transaction logged in PostgreSQL for compliance 9. Compliance services log the transaction
### Characteristics ### Characteristics
- **Centralized**: All state lives in server's PostgreSQL - **Centralized**: all state in `lamassu-server`'s PostgreSQL
- **Operator-managed**: Requires significant infrastructure - **Operator-managed**: requires real infrastructure (server, DB, certs, monitoring)
- **Custom protocol**: WebSocket messages are Lamassu-specific - **Tightly coupled**: machine doesn't transact if server is unreachable
- **Tight coupling**: Machine depends entirely on server availability - **Compliance-ready**: KYC/AML hooks built in (relevant in regulated jurisdictions)
- **Compliance-ready**: Built-in KYC/AML features
--- ---
## Nostr-Native Architecture (lamassu-next) ## bitSpire architecture
### Overview
``` ```
┌─────────────────┐ ┌─────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ ATM Machine │ │ Customer │ │ ATM (kiosk) │ │ Customer wallet │
│ (npub_atm) │ │ Wallet │ │ npub_atm │ │ (any LN wallet) │
└────────┬────────┘ │ (npub_user) │ └────────┬─────────┘ └────────┬─────────┘
│ └────────┬────────┘ │ kind-21000 │
│ NIP-01 events │ │ NIP-44 v2 encrypted │ BOLT11
│ NIP-44 encrypted │ CLINK protocol ▼ │ LNURL-withdraw
▼ ▼ ┌───────────────────────────────┐ │
┌─────────────────────────────────────────────────────────────────┐ │ Nostr Relay │ │
│ Nostr Relay │ │ (LNbits' bundled │ │
│ (strfry, nostr-rs, etc.) │ │ nostrrelay extension, or │ │
└─────────────────────────────────────────────────────────────────┘ │ any standalone relay) │ │
└────────────────┬──────────────┘ │
│ │ │ │
│ Kind 21000 (RPC) │ │ kind-21000 RPC + push │
│ Kind 21001-21003 (CLINK) │ ▼ │
▼ ▼ ┌───────────────────────┐ ◄──────────────────────┘
┌─────────────────┐ ┌─────────────────┐ │ LNbits │ BOLT11 settle / LNURLw redeem
│ Lightning.Pub │ │ Lightning.Pub │ │ ┌──────────────────┐ │
│ (ATM account) │◄── Lightning Payment ─────►│ (User account) │ │ │ FakeWallet (dev) │ │
└────────┬────────┘ └─────────────────┘ │ │ or LND/CLN/etc. │ │
│ │ └──────────────────┘ │
▼ └───────────────────────┘
┌─────────────────┐
│ LND │
└─────────────────┘
``` ```
### Components ### Components
| Component | Description | | Component | Description |
| ------------------- | ----------------------------------------------- | |---|---|
| **Nostr Relay** | Message broker (strfry, nostr-rs-relay) | | Nostr relay | Message broker. In the dev compose, this is LNbits's bundled `nostrrelay` extension at `ws://<host>:5001/nostrrelay/test`. Production can use any standalone relay both peers subscribe to |
| **Lightning.Pub** | Nostr-native account system wrapping LND | | LNbits | Lightning wallet platform; runs the `nostr-native-transport` branch. Auto-creates a wallet on first contact from any nostr pubkey |
| **ATM Machine** | Tauri + Vue 3 kiosk identified by npub | | ATM machine | Electron + Vue 3 kiosk identified by its `npub`. Signing key IS the credential to LNbits |
| **Customer Wallet** | Any CLINK-compatible wallet (ShockWallet, etc.) | | Customer wallet | Any LN wallet that handles BOLT11 (cash-out) and LNURL-withdraw (cash-in) — most do |
### Communication Flow (Cash-In Example) ### Cash-out flow (customer pays ATM, gets cash)
1. ATM generates ephemeral keypair or uses persistent npub 1. ATM publishes a kind-21000 event to LNbits: `{rpc_name: "create_invoice", body: {amount: <sats>, memo: "..."}}`
2. Customer scans QR containing `ndebit` (debit authorization) 2. LNbits replies on the same relay with the BOLT11 invoice
3. Customer's wallet connects to relay, finds ATM's Lightning.Pub 3. ATM displays the BOLT11 as a QR
4. Wallet creates invoice via Lightning.Pub RPC (kind 21000) 4. Customer scans, pays from any LN wallet
5. Wallet sends debit request (kind 21002) to ATM's npub 5. LNbits's Lightning backend (FakeWallet / LND / CLN / etc.) settles the payment
6. ATM verifies request, approves payment 6. LNbits pushes a kind-21000 settlement event to the ATM, filtered by `payment_hash`
7. Lightning.Pub pays invoice, returns preimage 7. ATM dispenses cash
8. ATM detects payment confirmation
9. ATM dispenses cash ### Cash-in flow (customer hands ATM cash, gets sats)
10. Transaction recorded as Nostr event (optional)
1. ATM publishes `lnurlw_create_link` (kind-21000) to LNbits with `{uses: 1, max_withdrawable: <sats>}`
2. LNbits replies with a `WithdrawLink` (a `unique_hash` plus metadata)
3. ATM composes the callback URL: `{LNBITS_HTTP_URL}/withdraw/api/v1/lnurl/{unique_hash}` and bech32-encodes it as an LNURL
4. ATM displays the LNURL as a QR
5. Customer scans with any LNURL-withdraw-capable wallet, redeems it
6. LNbits settles the withdrawal via its Lightning backend
7. LNbits pushes a kind-21000 settlement event to the ATM, filtered by `tag="withdraw"` + `link_id`
8. ATM marks the session complete (cash already physically accepted earlier in the flow)
### Characteristics ### Characteristics
- **Decentralized**: No single server owns state - **Decentralized message layer**: any nostr relay works, no proprietary protocol
- **Interoperable**: Standard protocols (Nostr, CLINK, Lightning) - **No admin tokens on the kiosk**: the ATM's signing key IS the credential — there are no HTTP API tokens or client certs to leak. LNbits derives the calling identity from the event signature.
- **Keypair identity**: ATM is identified by npub, not server account - **Loose coupling**: ATM and LNbits don't need to share a network segment; only a relay both reach
- **Loose coupling**: ATM can work with any relay/Lightning.Pub - **Privacy-first**: no central transaction logs unless the operator builds them. LNbits stores its own ledger, but bitSpire writes no user-identifying data anywhere on its side.
- **Privacy-first**: No central transaction logs required
--- ---
## Detailed Comparison ## Detailed comparison
### 1. Infrastructure Requirements ### 1. Infrastructure requirements
| Requirement | Traditional | Nostr-Native | | Requirement | Traditional | bitSpire |
| -------------------- | ------------------------------------ | -------------------------------- | |---|---|---|
| **Server Hardware** | Dedicated server (4+ GB RAM, SSD) | Optional (can use public relays) | | Server hardware | Dedicated server (4+ GB RAM, SSD) | An LNbits instance (1 GB RAM is plenty in production) |
| **Database** | PostgreSQL (backup, maintenance) | None required | | Database | PostgreSQL (operator managed) | LNbits's SQLite (or its configured DB; LNbits manages it) |
| **SSL Certificates** | Required (client certs for machines) | Not required | | Client TLS | Required (client certs per machine) | Not required (signature-based auth over a nostr relay) |
| **Domain Name** | Required | Optional | | Domain | Required | Optional (only if exposing LNbits HTTP for LNURL redemption from the public internet) |
| **Static IP** | Recommended | Not required | | Static IP | Recommended | Not required |
| **Firewall Rules** | Complex (multiple ports) | Simple (outbound WebSocket only) | | Firewall | Multiple inbound ports | Outbound WebSocket to a relay; inbound only on the LNbits HTTP port for LNURL callbacks |
**Advantage: Nostr-Native** - Dramatically simpler infrastructure. An operator can start with public relays and self-host later. **Edge: bitSpire.** A single-container LNbits with FakeWallet is enough for an operator to evaluate the whole stack end-to-end.
### 2. Security Model ### 2. Security model
| Aspect | Traditional | Nostr-Native | | Aspect | Traditional | bitSpire |
| -------------------- | ------------------------------- | ------------------------ | |---|---|---|
| **Machine Auth** | Client certificates | Keypair (nsec) | | Machine auth | Client certificates | Nostr signing key |
| **Message Security** | TLS | NIP-44 encryption | | Transport security | TLS | NIP-44 v2 (XChaCha20 + HMAC-SHA256) |
| **Identity Theft** | Steal cert + key | Steal nsec only | | Compromise scope | Server breach exposes all machines | One machine's `nsec` compromises only that machine |
| **Compromise Scope** | All machines if server breached | Individual machine only | | Key storage | Server manages | Machine manages its own keys (`/var/lib/bitspire/.env`, mode 0600) |
| **Key Storage** | Server manages | Machine manages own keys | | Revocation | CA-style cert revocation | Operator can stop honoring a pubkey at the LNbits layer; no fleet-wide blast radius |
**Advantage: Nostr-Native** - Compromise of one machine doesn't affect others. No central point of failure. **Edge: bitSpire.** No central authority that, if compromised, exposes the whole fleet. Per-machine compromise stays contained.
### 3. Payment Flow ### 3. Payment flow
| Aspect | Traditional | Nostr-Native | | Aspect | Traditional | bitSpire |
| ------------------------ | ---------------------- | ------------------------------ | |---|---|---|
| **Invoice Creation** | Server creates via LND | Lightning.Pub via Nostr RPC | | Invoice creation | Server creates via LND | LNbits creates via its Lightning backend, returned over kind-21000 |
| **Payment Detection** | Server polls LND | Subscription to payment events | | Payment detection | Server polls LND | LNbits pushes a settlement event over kind-21000 (`subscribe_payments` filtered by `payment_hash`) |
| **Wallet Compatibility** | Any Lightning wallet | CLINK-compatible wallets | | Cash-in detection | Custom flow | LNbits pushes a `tag="withdraw"` + `link_id` event when the LNURL is redeemed |
| **Offline Capability** | None | Cashu ecash (planned) | | Wallet compatibility | Any BOLT11 wallet | Any BOLT11 wallet (cash-out) + any LNURL-withdraw wallet (cash-in) |
| **Payment Protocol** | BOLT11 only | BOLT11 + CLINK + noffer | | Offline capability | None | Cashu ecash (placeholder package, not yet wired) |
**Mixed**: Traditional has broader wallet compatibility today. Nostr-Native enables advanced features (ndebit, noffer) but requires CLINK wallets. **Tied.** Both architectures end up using BOLT11 + LNURL-withdraw at the customer-wallet boundary. The difference is on the ATM side: traditional polls a server it owns; bitSpire subscribes to a push channel on a wallet it shares with anyone who can reach the relay.
### 4. Operator Experience ### 4. Operator experience
| Aspect | Traditional | Nostr-Native | | Aspect | Traditional | bitSpire |
| ----------------------- | ------------------------------- | -------------------------- | |---|---|---|
| **Setup Time** | Hours (server, DB, certs) | Minutes (connect to relay) | | Setup time | Hours (provision server, DB, TLS, cert each machine) | Minutes (run an LNbits instance + a relay; flash + provision each ATM) |
| **Maintenance** | DB backups, updates, monitoring | Minimal | | Maintenance | DB backups, server updates, monitoring | Update LNbits + the bitSpire ATMs (the latter auto-upgrades from the `dev` branch nightly) |
| **Fleet Management** | Admin UI (full-featured) | Dashboard (planned) | | Fleet management | Mature Admin UI | Per-ATM `journalctl`; centralized dashboard is planned, not built |
| **Transaction History** | PostgreSQL queries | Nostr event queries | | Transaction history | PostgreSQL queries | Per-ATM `/var/lib/bitspire/state.db` (SQLite); operator-side aggregation is on them to build |
| **Configuration** | Server-pushed | Local or relay-stored | | Configuration | Server-pushed | Per-ATM `.env` written via `provision-atm.sh` |
**Mixed**: Traditional has mature tooling. Nostr-Native is simpler but dashboard is still in development. **Mixed.** Traditional has mature operator tooling. bitSpire is simpler to stand up but the fleet-management story is currently "ssh + scripts" rather than a UI.
### 5. Customer Experience ### 5. Customer experience
| Aspect | Traditional | Nostr-Native | | Aspect | Traditional | bitSpire |
| ----------------------- | ------------------------ | ----------------------- | |---|---|---|
| **Cash-In** | Scan invoice, pay | Scan ndebit, authorize | | Cash-out | Scan BOLT11, pay | Scan BOLT11, pay |
| **Cash-Out** | Enter phone, receive SMS | Scan noffer, receive | | Cash-in | Varies by deployment (LNURL-withdraw common, sometimes custom) | Scan LNURL-withdraw QR, redeem |
| **Wallet Requirements** | Any Lightning wallet | CLINK-compatible wallet | | Wallet requirements | Any LN wallet for cash-out | Any LN wallet for cash-out + any LNURL-w wallet for cash-in |
| **Account Required** | Sometimes (compliance) | Never | | Account required | Sometimes (compliance) | Never |
| **Transaction Speed** | ~10 seconds | ~5 seconds | | Transaction speed | ~10 seconds | ~3-5 seconds (push-based; no server polling delay) |
**Advantage: Nostr-Native** - Faster, more private, no accounts. But requires specific wallet support. **Tied on protocol; slight edge to bitSpire on speed** because LNbits push events skip the polling cycle that traditional setups have between LND and the application server.
### 6. Compliance & Regulation ### 6. Compliance & regulation
| Aspect | Traditional | Nostr-Native | | Aspect | Traditional | bitSpire |
| -------------------- | ----------------------- | --------------------- | |---|---|---|
| **KYC Integration** | Built-in | Not included | | KYC integration | Built-in | Not included |
| **Transaction Logs** | PostgreSQL | Optional Nostr events | | Transaction logs | PostgreSQL (server) | Per-ATM SQLite + whatever LNbits records on its side |
| **Audit Trail** | Comprehensive | Operator-defined | | Audit trail | Comprehensive | Operator-defined; LNbits's ledger is the canonical Lightning-side record |
| **Reporting** | Admin UI exports | Custom tooling needed | | Reporting | Admin UI exports | Custom tooling needed |
| **Regulatory Fit** | Designed for compliance | Privacy-first design | | Regulatory fit | Designed for KYC/AML jurisdictions | Privacy-first design; suits jurisdictions without identity-collection requirements |
**Advantage: Traditional** - If you need KYC/AML, traditional is ready. Nostr-Native assumes jurisdictions without these requirements. **Edge: Traditional** if you need built-in compliance. bitSpire targets KYC-free operation; operators in regulated jurisdictions would need to add compliance hooks themselves.
### 7. Resilience & Availability ### 7. Resilience & availability
| Aspect | Traditional | Nostr-Native | | Aspect | Traditional | bitSpire |
| --------------------------- | ---------------------- | ------------------------- | |---|---|---|
| **Server Down** | All machines offline | Machines continue working | | Server down | All machines stop transacting | Machines stop transacting if LNbits is down; relay outage means RPCs hang |
| **Network Partition** | Transactions fail | Can use multiple relays | | Network partition | Transactions fail | Can connect to multiple relays for failover |
| **Database Corruption** | Catastrophic | No database to corrupt | | Database corruption | Catastrophic (cascading impact across fleet) | LNbits-side issue affects only the affected operator's wallet; per-ATM SQLite is local and independent |
| **Recovery** | Restore from backup | Re-sync from relay | | Recovery | Restore from backup | LNbits-side restore is its concern; ATMs come back up clean by re-reading `.env` |
| **Geographic Distribution** | Single server location | Relay anywhere | | Geographic distribution | Single server | LNbits can be regional; relays can be anywhere |
**Advantage: Nostr-Native** - No single point of failure. Machines are autonomous. **Edge: bitSpire** for blast-radius reasons. LNbits-side failures are no worse than `lamassu-server`-side failures, but bitSpire's per-ATM state is genuinely isolated.
### 8. Development & Extensibility ### 8. Development & extensibility
| Aspect | Traditional | Nostr-Native | | Aspect | Traditional | bitSpire |
| --------------------------- | ----------------- | ------------------------- | |---|---|---|
| **Codebase** | Large monolith | Modular packages | | Codebase | Large monolith (`lamassu-server` is hundreds of files) | Modular workspace; ATM, transport client, HAL, state machine are separate packages |
| **Protocol** | Proprietary | Open standards | | Protocol | Custom WebSocket schema | NIP-01 + NIP-44 v2 + a thin documented kind-21000 envelope |
| **Third-party Integration** | Custom API needed | Standard Nostr/CLINK | | Third-party integration | Custom API needed | Any nostr/Lightning client can talk to LNbits over the transport |
| **Community** | Lamassu operators | Nostr + Bitcoin ecosystem | | Community | Lamassu operators (and ex-operators after the license change) | Nostr + Bitcoin ecosystem; LNbits community |
| **Forkability** | Complex | Straightforward | | Forkability | Complex (server + machine are coupled) | Straightforward (replace any one package without touching the others) |
**Advantage: Nostr-Native** - Open protocols mean broader ecosystem participation. **Edge: bitSpire.** Open protocols mean a wider ecosystem of compatible clients, wallets, and tools.
--- ---
## Migration Path ## When to choose what
### Phase 1: Parallel Operation ### Choose bitSpire if
Run both systems simultaneously: - You operate in a jurisdiction without mandatory KYC/AML
- You want minimal infrastructure
- You value customer privacy (no PII collection, no central server-side ledger of who-paid-what)
- You're comfortable with emerging tooling (no admin UI yet)
- You want a license that stays open (AGPL-3.0)
- Existing machines continue with lamassu-server ### Stick with `lamassu-server` v8.1.5 if
- New machines or test units use lamassu-next
- Compare reliability and customer feedback
### Phase 2: Hybrid Mode
Bridge the systems:
- lamassu-server creates Nostr events for transactions
- Dashboard reads from both PostgreSQL and relay
- Gradual feature parity validation
### Phase 3: Full Migration
Complete transition:
- All machines run lamassu-next firmware
- Retire lamassu-server infrastructure
- Optional: Keep PostgreSQL as read-only archive
### Migration Considerations
| Consideration | Notes |
| -------------------------- | ----------------------------------------------- |
| **Hardware Compatibility** | Same bill validators/dispensers, new software |
| **Customer Education** | May need CLINK-compatible wallet guidance |
| **Operator Training** | Different mental model (events vs database) |
| **Regulatory Review** | Verify compliance in your jurisdiction |
| **Rollback Plan** | Keep lamassu-server available during transition |
---
## Trade-offs & Honest Assessment
### Where Nostr-Native Excels
1. **Simplicity**: No server to manage, no database to backup
2. **Privacy**: No central transaction logs
3. **Resilience**: No single point of failure
4. **Interoperability**: Works with any CLINK wallet
5. **Speed**: Direct relay communication is faster
6. **Cost**: No server hosting costs (can use public relays)
### Where Traditional May Be Better
1. **Compliance**: Built-in KYC/AML if required by law
2. **Wallet Support**: Works with any Lightning wallet today
3. **Maturity**: Battle-tested over years of operation
4. **Tooling**: Full-featured admin dashboard exists
5. **Support**: Established support channels and documentation
### Current Limitations of Nostr-Native
| Limitation | Status | Mitigation |
| ---------------------- | ------------- | ---------------------------------- |
| Dashboard not complete | In progress | Use relay queries directly |
| Limited wallet support | Growing | ShockWallet, others adopting CLINK |
| No offline mode yet | Cashu planned | Requires internet currently |
| Less documentation | Improving | This document helps |
---
## Conclusion
The Nostr-native architecture represents a fundamental shift from centralized to decentralized ATM operation. It trades the comprehensive compliance features of lamassu-server for simplicity, privacy, and resilience.
**Choose Nostr-Native if:**
- You operate in jurisdictions without KYC requirements
- You want minimal infrastructure overhead
- You value customer privacy
- You're comfortable with emerging technology
**Stick with Traditional if:**
- You need built-in compliance features - You need built-in compliance features
- You require extensive fleet management tools today - You need a mature admin dashboard *today*
- You need to support any Lightning wallet - You have existing infrastructure investment in PostgreSQL + LND tooling
- You prefer mature, battle-tested systems - You're comfortable being on a code line that no longer receives upstream updates (v8.1.5 is the last open release; bug fixes in v8.1.6+ are paid-license-only)
**Consider Hybrid if:** ### Choose Lamassu's current commercial offering if
- You want to evaluate both approaches - You want vendor support, official updates, and a commercial SLA
- You're planning a gradual migration - You're willing to pay the per-machine subscription
- You have mixed regulatory requirements across locations - You operate in a regulated jurisdiction where KYC tooling matters
This last option is outside the scope of this document — see [lamassu.is](https://lamassu.is) for their current commercial terms.
--- ---
## Further Reading ## Trade-offs & honest assessment
- [CLINK Protocol Specification](https://github.com/shocknet/clink) ### Where bitSpire excels
- [Lightning.Pub Documentation](https://github.com/shocknet/Lightning.Pub)
- [Nostr Protocol (NIP-01)](https://github.com/nostr-protocol/nips/blob/master/01.md) 1. **Simplicity** — LNbits + a relay is a much smaller surface than a stateful application server + DB + admin tooling
- [NIP-44 Encryption](https://github.com/nostr-protocol/nips/blob/master/44.md) 2. **Privacy** — no central transaction logs by design
- [ndebit Cash-In Flow](./ndebit-cash-in-flow.md) 3. **Resilience** — no single point of failure on the ATM side; LNbits is replaceable
4. **Interoperability** — works with any BOLT11 / LNURL-withdraw wallet
5. **Speed** — push-based settlement detection skips the LND-poll cycle
6. **Cost** — no Lamassu license fees; no commercial admin-server hosting
### Where traditional (`lamassu-server` ≤ v8.1.5) is still better
1. **Compliance** — built-in KYC/AML if your jurisdiction requires it
2. **Tooling** — mature admin dashboard, fleet management, transaction reporting
3. **Maturity** — battle-tested in production over many years
4. **Documentation** — established support channels for operators (community-maintained for v8.1.5; commercial for v8.1.6+)
### Current limitations of bitSpire
| Limitation | Status | Mitigation |
|---|---|---|
| No fleet dashboard | Planned, not built | Per-ATM `journalctl` + custom scripts |
| Limited transaction reporting | SQLite per ATM | Aggregate via operator-side ETL if needed |
| No offline mode | `@bitSpire/cashu` is a placeholder | Requires LNbits connectivity currently |
| Smaller operator community | Growing | This doc + the README walkthrough |
---
## Further reading
- [LNbits nostr-native-transport docs](https://git.atitlan.io/aiolabs/lnbits) — the wire spec for what's actually happening on kind-21000
- [NIP-01 Nostr basics](https://github.com/nostr-protocol/nips/blob/master/01.md)
- [NIP-44 v2 encryption](https://github.com/nostr-protocol/nips/blob/master/44.md)
- [LUD-03 LNURL-withdraw](https://github.com/lnurl/luds/blob/luds/03.md)
- [CLINK Protocol](./clink-protocol.md) — historical reference, dormant on `dev`
- [bitSpire deployment walkthrough](../deploy/nixos/README.md)
- [machine-installation.md](./machine-installation.md) — high-level deployment overview

View file

@ -33,7 +33,7 @@ The location buys (or finances) the machine and runs their own stack.
| Aspect | Details | | Aspect | Details |
| ----------------- | ---------------------------------------- | | ----------------- | ---------------------------------------- |
| Machine ownership | Them | | Machine ownership | Them |
| Liquidity | Their own Lightning node + Lightning.Pub | | Liquidity | Their own Lightning node + LNbits |
| Software updates | Them (we provide releases) | | Software updates | Them (we provide releases) |
| Cash management | Them | | Cash management | Them |
| Fee configuration | Them | | Fee configuration | Them |
@ -107,15 +107,15 @@ We're selective about where we place machines. The goal is to find locations wit
For the managed model to work at scale, we need: For the managed model to work at scale, we need:
- **Remote monitoring** — machine status, cash levels, error alerts over [[clink-protocol|CLINK]]/Nostr - **Remote monitoring** — machine status, cash levels, error alerts over nostr (kind-30078 service beacons + kind-21003 management commands)
- **Operator dashboard** — manage cassettes, view transactions, update config remotely - **Operator dashboard** — manage cassettes, view transactions, update config remotely (planned)
- **Multi-machine support** — one Lightning.Pub instance serving multiple ATMs - **Multi-machine support** — one LNbits instance can host wallets for many ATMs; each ATM auto-creates its own wallet from its nostr pubkey
- **Fee configuration** — per-machine fee % set by operator - **Fee configuration** — per-machine fee % set by operator
- **Availability broadcast** — public Nostr events indicating cash-in/cash-out availability (see [[ndebit-cash-in-flow|cash-in flow]]) - **Availability broadcast** — public kind-30078 events indicating cash-in/cash-out availability and reserves
## Related ## Related
- [[device-configuration]] — hardware setup for different machine models - [[device-configuration]] — hardware setup for different machine models
- [[machine-installation]] — NixOS installation on ATM hardware - [[machine-installation]] — deployment overview for ATM hardware
- [[clink-protocol]] — CLINK protocol for ATM-wallet communication - [Deploy/NixOS README](../deploy/nixos/README.md) — full deployment walkthrough
- [[ndebit-cash-in-flow]] — cash-in payment flow - [[architecture-comparison]] — bitSpire vs traditional lamassu-server

View file

@ -4,23 +4,23 @@ This document describes how to configure the ATM hardware for different machine
## Overview ## Overview
The ATM application supports multiple Lamassu machine models out of the box. Configuration is handled through: bitSpire ships built-in presets for the hardware platforms we've tested on. Configuration is handled through:
1. **Machine presets** - Built-in defaults for known hardware (Sintra, Gaia) 1. **Machine presets** — built-in defaults for known hardware (Sintra, tejo, douro, batm3)
2. **Environment variables** - Override any setting at runtime 2. **Environment variables** — override any setting at runtime
3. **Runtime overrides** - Programmatic configuration 3. **Runtime overrides** — programmatic configuration
## Supported Machine Models ## Supported machine models
### Sintra (Default) ### Sintra
The Lamassu Sintra (Gen 2) uses: The Sintra (Aaeon UP Board-based, Intel Atom x5-Z8350) uses:
| Device | Protocol | Path | | Device | Protocol | Path | Underlying device |
| -------------- | -------- | ------------ | |---|---|---|---|
| Bill Validator | ID003 | `/dev/ttyJ5` | | Bill Validator | ID003 | `/dev/ttyJ5` | FTDI USB-serial → `/dev/ttyUSB1` |
| Bill Dispenser | F56 | `/dev/ttyJ7` | | Bill Dispenser | F56 | `/dev/ttyJ7` | SoC MMIO UART → `/dev/ttyS4` |
| Printer | Nippon | `/dev/ttyJ4` | | Printer | Nippon | `/dev/ttyJ4` | FTDI USB-serial → `/dev/ttyUSB0` |
**Hardware:** **Hardware:**
@ -28,9 +28,19 @@ The Lamassu Sintra (Gen 2) uses:
- Validator: JCM iVIZION - Validator: JCM iVIZION
- Dispenser: Fujitsu F53/F56 - Dispenser: Fujitsu F53/F56
### Gaia **Sintra-specific gotcha:** the kernel allocates `ttyS0..ttyS3` as placeholder serial nodes that error on any I/O. Only `ttyS0` (legacy 8250 at I/O 0x3f8) and `ttyS4` (SoC MMIO 16550A at 0xa171b000) are real on this hardware. The `bitspire-atm` NixOS module's `upboard.nix` keeps the kernel console on `tty0` only (not `ttyS4`) so userspace can claim `ttyS4` for the F56 dispenser.
The Lamassu Gaia uses similar hardware with different device paths. Verify paths on your specific unit. ### tejo
The Tejo runs on the same Aaeon UP Board family as Sintra; same validator + dispenser layout. Uses the same `upboard.nix` hardware module.
### Douro
Runs on a Dell OptiPlex 9030 AIO (Intel i7-4790S, Intel HD 4600) — Lamassu's stock Douro motherboard. SATA SSD instead of eMMC, eGalax touchscreen, dispenser/validator on USB-attached FTDI bridges. See `deploy/nixos/hardware/douro.nix` for the specifics.
### BATM3
The "batm3" target in this repo is **not the stock GeneralBytes BATM3** — it's a custom modification where the original ARM/Android board has been physically replaced with a Dell OptiPlex 9030 AIO (same hardware family as the Douro). The chassis, cash-handling units (MEI SCR validator/recycler + Puloon F56 dispenser), and screen are stock BATM3; everything compute-side is grafted in. See `deploy/nixos/hardware/batm3.nix` for the boot configuration; functionally it shares the Dell-board layout with Douro but with different cash hardware on the inside.
## Environment Variables ## Environment Variables
@ -155,15 +165,19 @@ On a Sintra running Linux, verify the serial devices exist:
ls -la /dev/ttyJ* ls -la /dev/ttyJ*
``` ```
Expected output: Expected output on a working Sintra:
``` ```
lrwxrwxrwx 1 root root 10 Jan 29 12:00 /dev/ttyJ4 -> ttyS4 lrwxrwxrwx 1 root root 7 May 13 09:22 /dev/ttyJ4 -> ttyUSB0
lrwxrwxrwx 1 root root 10 Jan 29 12:00 /dev/ttyJ5 -> ttyS5 lrwxrwxrwx 1 root root 7 May 13 09:22 /dev/ttyJ5 -> ttyUSB1
lrwxrwxrwx 1 root root 10 Jan 29 12:00 /dev/ttyJ7 -> ttyS7 lrwxrwxrwx 1 root root 5 May 13 09:22 /dev/ttyJ7 -> ttyS4
``` ```
If the symlinks don't exist, check the udev rules or use the underlying `/dev/ttyS*` devices directly. (`ttyUSB0`/`ttyUSB1` are the two FTDI USB-serial bridges — printer + validator. `ttyS4` is the SoC's on-carrier MMIO UART used for the F56 dispenser.)
If the symlinks don't exist, check the udev rules in `deploy/nixos/hardware/upboard.nix`, then run `mdev -s` (or `udevadm trigger` on a non-Alpine system) to repopulate `/dev`.
If you see a symlink pointing at `ttyS1`/`ttyS2`/`ttyS3` instead, those are kernel-allocated placeholder nodes that always error on I/O — the udev rule needs to map to `ttyS4` for Sintra. The current `upboard.nix` covers all the cases (`ttyS1`, `ttyS4`, `ttyS5`) so whichever real device shows up gets the `ttyJ7` alias.
## Troubleshooting ## Troubleshooting

View file

@ -1,278 +1,96 @@
# Machine Installation # Deploying bitSpire to a Sintra (or other ATM)
This document describes how to deploy the bitSpire ATM software to a Sintra machine. (Historical name: "Lamassu Next" — renamed on the `dev` branch as part of the LNbits-backend transition.) This document gives the high-level shape of an ATM deployment. **For the step-by-step walkthrough — every command from `nix build` to a kiosk on a real Sintra — see [deploy/nixos/README.md](../deploy/nixos/README.md).** This file is the orientation doc that explains *why* the pipeline looks the way it does.
## Prerequisites > The pre-NixOS workflow described in earlier versions of this file (manual AppImage scp, hand-written systemd unit, ad-hoc env files) has been retired. NixOS is the supported deployment path on `dev`. The historical AppImage build still exists for one-off testing on non-NixOS dev boxes, but it is not how production ATMs are installed.
### On the Sintra ## The pipeline at a glance
- Linux OS with X11 display server
- Network connectivity (WiFi or Ethernet)
- User account with `dialout` group membership (for serial port access)
### Backend Services
The ATM requires network access to:
| Service | Purpose | Required |
| ------------- | ------------------- | -------- |
| Nostr relay | CLINK communication | Yes |
| Lightning.Pub | Payment processing | Yes |
These can be self-hosted or provided by a third party.
## Building the Application
### Development Machine Setup
1. Enter the development environment:
```bash
cd lamassu-next
devenv shell
```
2. Build the Electron application:
```bash
cd apps/machine
pnpm build:electron
```
3. Build artifacts are created in `apps/machine/release/`:
``` ```
release/ ┌─────────────────┐ nix build .# ┌─────────────────────────┐
├── bitSpire-0.1.0.AppImage # Portable executable (recommended) │ dev box │ ─disk-image-sintra──▶ │ result/nixos.img │
└── linux-unpacked/ # Unpacked application directory │ (Nix + flake) │ │ (8.5 GB sparse, GPT) │
└─────────────────┘ └────────────┬────────────┘
│ dd
▼
┌──────────────────────┐
│ USB stick │
└──────────┬───────────┘
│ carry to ATM
▼
┌─────────────────┐ ┌─────────────────────┐
│ Alpine live USB │ boot Sintra │ Sintra (UP Board) │
│ (toolchain) │ ─────────────────────▶ │ eMMC: empty │
└────────┬────────┘ └──────────┬──────────┘
│ apk add + dd USB → eMMC + parted resize + reboot
▼
┌────────────────────────────────────────────────────────────────┐
│ Sintra booted from eMMC, bitspire.service in needs-prov state │
└──────────────────────────────┬─────────────────────────────────┘
│ provision-atm.sh from dev box
▼
┌────────────────────────────────────────────────────────────────┐
│ /var/lib/bitspire/.env populated, service restarted, kiosk │
│ connects to LNbits over nostr-transport, ready for transactions │
└────────────────────────────────────────────────────────────────┘
``` ```
## Deployment ## Why this pipeline
### Option A: AppImage (Recommended) Three properties matter for ATM software running unattended in retail locations:
The AppImage is a self-contained executable that works on any Linux distribution. 1. **Reproducible boot media.** Two ATMs flashed from the same `nixos.img` boot identical environments — same kernel, same systemd units, same Electron build, same nix-store closure. No "works on my machine" drift between fleet units.
2. **Auditable provenance.** Every file on the ATM traces back to a derivation in `/nix/store/`, and every derivation traces back to a commit in this repo. There's no `pip install` reaching out to PyPI, no `npm install` pulling un-pinned packages, no `apt update` mutating the system underfoot.
3. **Safe in-place updates.** `nixos-rebuild switch` against the `dev` branch atomically installs a new generation; if the new generation fails to boot or activate, the previous one stays bootable. Auto-upgrade at 04:00 daily means an operator can patch the fleet by pushing to `dev`.
1. Copy to Sintra: The disk-image approach (versus `nixos-install` from a live USB) is specifically to avoid the human-in-the-loop installation step. Every Sintra gets the same image; the only per-machine variation is the `.env` written at provisioning time.
```bash ## What's in the image
scp "apps/machine/release/bitSpire-0.1.0.AppImage" user@sintra:/opt/lamassu/
```
2. Make executable: Conceptually:
```bash | Layer | Source | Purpose |
ssh user@sintra |---|---|---|
chmod +x "/opt/lamassu/bitSpire-0.1.0.AppImage" | **Kernel + initrd** | `nixpkgs` 24.05 + `upboard.nix` initrd modules | Boot the Sintra hardware (eMMC via `sdhci-acpi`, validator/dispenser at `ttyJ5`/`ttyJ7`) |
``` | **NixOS base** | `nixpkgs` 24.05 | systemd, Xorg, openbox, the `lamassu` user, sshd for provisioning |
| **bitspire.service** | `deploy/nixos/bitspire-atm.nix` | systemd unit that launches the Electron kiosk |
| **The Electron app** | `apps/machine` built into a nix derivation | The actual ATM UI + state machine + Lightning client |
| **Hardware-specific config** | `deploy/nixos/hardware/upboard.nix` (or `douro.nix`, `batm3.nix`) | udev rules, kernel modules, panel calibration |
3. Test manually: ## What's NOT in the image
```bash The image is **identity-free** by design. After flashing, the ATM has no concept of:
export DISPLAY=:0 - Which LNbits server to talk to
"/opt/lamassu/bitSpire-0.1.0.AppImage" --no-sandbox - Which nostr relay to use
``` - Its own nostr identity (signing key)
- Which fiat currency to display (defaulted from build args, but overridable)
### Option B: Unpacked Directory All of these come from `/var/lib/bitspire/.env`, which is written by `provision-atm.sh` after first boot. This separation means a single image flavor can serve dev, staging, and prod just by changing the provisioning data.
For faster startup times, deploy the unpacked application: ## Pre-deployment checklist
1. Copy to Sintra: Before flashing a Sintra you'll want:
```bash - [ ] **An Alpine live USB** for use as the installer-OS on the Sintra (Alpine is small, has a working busybox toolchain, and apk is fast). The bitSpire image itself is *not* a live system — it expects to live on the eMMC.
scp -r apps/machine/release/linux-unpacked user@sintra:/opt/lamassu/ - [ ] **A second USB stick** to flash the bitSpire image onto (this is what you'll dd between on the Sintra).
``` - [ ] **A running LNbits instance** with the nostr-native-transport branch built in. The dev compose at `~/dev/local/docker/regtest` provides one; production deployments point at `lnbits.aiolabs.dev` or your operator's instance.
- [ ] **Network reachability between the Sintra and the LNbits host.** The ATM connects to the relay endpoint at `ws://<lnbits-host>:5001/nostrrelay/test` (no separate strfry container — LNbits ships its own `nostrrelay` extension).
- [ ] **The LNbits server pubkey** (`docker logs <lnbits-container> | grep 'Public key (share this)'`).
- [ ] **A freshly generated nostr private key** for the ATM (`openssl rand -hex 32`). Each ATM should have its own; never share keys between machines.
2. Test manually: ## After deployment
```bash Once the kiosk is up, useful things to know:
ssh user@sintra
export DISPLAY=:0
/opt/lamassu/linux-unpacked/lamassu-machine --no-sandbox
```
## Configuration - **Service status:** `ssh lamassu@<atm> 'sudo systemctl status bitspire'`
- **Live log tail:** `ssh lamassu@<atm> 'sudo journalctl -u bitspire -f'`
- **Re-provision (e.g., wrong relay URL):** rerun `provision-atm.sh` from the dev box with the new env vars
- **Push a code change without reflashing:** `nixos-rebuild switch --flake .#sintra-installed --target-host lamassu@<atm> --use-remote-sudo`
- **Inspect transaction history:** `ssh lamassu@<atm> 'sudo bash /etc/nixos/atm-transactions.sh'` (queries `/var/lib/bitspire/state.db`)
### Environment Variables ## Related documentation
Create a configuration file at `/opt/lamassu/.env`: - [deploy/nixos/README.md](../deploy/nixos/README.md) — the full step-by-step walkthrough and NixOS module reference
- [device-configuration.md](./device-configuration.md) — hardware-specific configuration (validator types, cassette layouts, fiat currency)
```bash - [adr/001-hal-architecture.md](./adr/001-hal-architecture.md) — why HAL is TypeScript-in-Node rather than Rust-via-Tauri
# Machine model (sintra, gaia, or custom) - [CLAUDE.md](../CLAUDE.md) — the dev-facing overview, including the hardware-specific gotchas we hard-learned on the first real Sintra flash
VITE_LAMASSU_MACHINE_MODEL=sintra
# Fiat currency code (ISO 4217)
VITE_LAMASSU_FIAT_CODE=USD
# Custom device paths (optional, uses preset defaults if not set)
# VITE_LAMASSU_VALIDATOR_DEVICE=/dev/ttyJ5
# VITE_LAMASSU_DISPENSER_DEVICE=/dev/ttyJ7
# Cassette configuration (optional)
# VITE_LAMASSU_CASSETTES='[{"denomination":20,"count":100}]'
```
See [Device Configuration](./device-configuration.md) for detailed configuration options.
### Lightning.Pub Connection
The ATM needs to connect to a Lightning.Pub instance. Configure via environment:
```bash
# Nostr relay WebSocket URL (required)
VITE_RELAY_URL=wss://your-relay.example.com
# Lightning.Pub's Nostr public key (required)
# Get from: docker logs lamassu-lightning-pub | grep pubkey
VITE_LIGHTNING_PUB_PUBKEY=4be8e203a3341bb2b74a4dcbf8774e061437f63ec21af7ec3144c8d0a68e2f39
# Lightning.Pub HTTP API URL (optional, for admin operations)
VITE_LIGHTNING_PUB_API_URL=https://lp.operator.com
# ATM's Nostr private key (recommended for persistent identity)
# Generate with: npx @lamassu/nostr-client generate-keypair
# If not set, a new ephemeral identity is generated on each restart
VITE_ATM_PRIVATE_KEY=0123456789abcdef...
```
**Important:** The `VITE_LIGHTNING_PUB_PUBKEY` is required. Without it, the ATM cannot communicate with Lightning.Pub.
## Auto-Start on Boot
### Systemd Service
Create `/etc/systemd/system/lamassu-kiosk.service`:
```ini
[Unit]
Description=bitSpire ATM Kiosk
After=graphical.target network-online.target
Wants=network-online.target
[Service]
Type=simple
User=lamassu
Environment=DISPLAY=:0
EnvironmentFile=/opt/lamassu/.env
WorkingDirectory=/opt/lamassu
ExecStart=/opt/lamassu/linux-unpacked/lamassu-machine --no-sandbox
Restart=always
RestartSec=5
[Install]
WantedBy=graphical.target
```
Enable and start:
```bash
sudo systemctl daemon-reload
sudo systemctl enable lamassu-kiosk
sudo systemctl start lamassu-kiosk
```
### Monitoring
```bash
# Check status
sudo systemctl status lamassu-kiosk
# View logs
sudo journalctl -u lamassu-kiosk -f
# Restart after configuration changes
sudo systemctl restart lamassu-kiosk
```
## Hardware Verification
### Serial Port Access
1. Verify devices exist:
```bash
ls -la /dev/ttyJ*
# Expected: /dev/ttyJ4 (printer), /dev/ttyJ5 (validator), /dev/ttyJ7 (dispenser)
```
2. Check user permissions:
```bash
groups
# Should include 'dialout'
```
3. If not in dialout group:
```bash
sudo usermod -a -G dialout $USER
# Logout and login for changes to take effect
```
### Display Configuration
The Sintra uses a 1080x1920 portrait display. Verify X11 is running:
```bash
echo $DISPLAY
# Should output :0 or similar
xdpyinfo | head -5
# Should show display information
```
## Troubleshooting
### Application Won't Start
1. **Missing display**: Ensure `DISPLAY=:0` is set
2. **Sandbox error**: Use `--no-sandbox` flag
3. **Permission denied**: Check file is executable (`chmod +x`)
### Hardware Not Responding
1. **Check serial ports exist**: `ls -la /dev/ttyJ*`
2. **Check permissions**: User must be in `dialout` group
3. **Check connections**: Ensure cables are properly seated
4. **Power cycle hardware**: Turn validator/dispenser off and on
### Network Issues
1. **Test relay connection**: `websocat wss://your-relay.example.com`
2. **Check DNS resolution**: `ping your-relay.example.com`
3. **Verify firewall**: Ensure outbound WebSocket connections allowed
### Logs
Application logs are written to:
- **Systemd**: `journalctl -u lamassu-kiosk`
- **Electron**: `~/.config/lamassu-machine/logs/`
## Updating
1. Build new version on development machine
2. Stop the service:
```bash
sudo systemctl stop lamassu-kiosk
```
3. Replace application files:
```bash
scp -r apps/machine/release/linux-unpacked/* user@sintra:/opt/lamassu/linux-unpacked/
```
4. Restart service:
```bash
sudo systemctl start lamassu-kiosk
```
## Security Notes
- The `--no-sandbox` flag is required for Electron on some Linux configurations. This is acceptable for a dedicated kiosk machine.
- Keep the ATM's nsec private key secure. It authorizes all transactions from this machine.
- Use a dedicated user account (`lamassu`) with minimal privileges.
- Consider firewall rules to restrict network access to only required services.