1
0
forked from GRIN/grim
2ro 528d088e54 fix(nostr): bound payment-request store + isolate handler panics
F1 (disk-exhaustion DoS): incoming payment-request storage grew unbounded —
an attacker could stream valid Invoice1 requests from fresh ephemeral keys
(bypassing the per-sender limit), each persisting ~30KB of slatepack armor
until the device disk filled. Expiry only flipped status to Expired, never
freeing the row.

- expire_stale now DELETES terminal (Expired/Cancelled/Declined) request rows
  via NostrStore::prune_terminal_requests, actually reclaiming disk.
- New NostrStore::save_incoming_request enforces a REQUEST_CAP (2000): when at
  the cap it evicts terminal rows oldest-first to make room, and only if none
  can be freed does it REFUSE the new request (backpressure) rather than ever
  evict a live PENDING/Approved one. A refused request is not marked processed,
  so a later catch-up re-surfaces it once capacity frees. Ingest path routes
  through it; save_request stays unbounded for backup-restore/status-updates.
- Pure plan_incoming_admission policy + assert tests prove terminal-first
  eviction and that no pending row is ever dropped (unit + full-cap store test).

F2 (payment-service brick): handle_* were awaited inline in the notification
select! loop; a panic killed the service thread before started.store(false),
leaving started stuck true so restart() hung forever, and next-launch catch-up
re-ran the same event and re-panicked (self-reinforcing brick).

- Per-event dispatch is wrapped in futures catch_unwind so a panic in one event
  is logged and skipped, keeping the loop alive; the offending event is marked
  processed so it is not retried forever. Happy path unchanged (same inline,
  in-order execution).
- A drop guard on the service thread resets started/connected on ANY exit
  (normal or unwinding panic) so restart() can always recover. (Android APK is
  panic=abort; there the drop guard is the remaining safeguard.)
2026-07-10 15:36:29 -04:00
2026-05-03 10:05:03 +03:00
2024-04-14 14:04:34 +03:00

Goblin

Goblin

Goblin is a private, pay-by-username wallet for GRIN ツ - confidential digital cash on Mimblewimble, with no amounts or addresses on the chain.

Instead of passing slatepack files back and forth, you pay a username (or an npub) and the payment is delivered for you as an end-to-end encrypted message over nostr, routed through Tor. Relays only ever see ciphertext - never the amount, the sender, or the recipient. Tor hides your IP from the relay; the relay and encryption hide the rest - content, sender, timing.

Goblin is a fork of the Grim egui GRIN wallet: it keeps Grim's full GRIN node/wallet engine and layers a Nostr-native, mobile-first payments experience on top.

What it does

  • Send to people - pay a username or npub; the GRIN slatepack travels as a NIP-17 gift-wrapped DM (kind 1059) over Tor and is applied automatically by the recipient's wallet. No files to swap, no need to both be online at once.
  • Manual slatepacks too - when you need to pay or get paid without a handle, Settings → Wallet → Slatepacks exposes the classic by-hand flow: create a slatepack to send, or paste one to receive, finalize, or pay.
  • Open-to-pay links - a goblin: or nostr: pay link, or a scanned checkout QR, opens the wallet straight to a prefilled review screen (recipient, amount and note filled in, ready to hold-to-send) on desktop, macOS and Android.
  • Proofs on request - payments can include a native Grin payment proof when the payment request asks for one, off by default, shown on the review screen. An ordinary person-to-person send carries none.
  • In-app identity - a nostr payment key that is deliberately not part of your seed, so you can rotate it any time to stay unlinkable without touching your funds. An optional human-readable name comes from the goblin.st identity service.
  • Private by construction - GRIN's address-less, confidential chain; your payments and identity (nostr relays, NIP-05 lookups, price) are routed through Tor, so who-pays-whom never touches the clear net. The GRIN node connection - block sync and broadcasting your transaction - is direct: public chain data, the same for everyone, and not tied to your identity. Keys, names and history stay on your device.
  • Configurable amount pairing - show balances against a world currency, Bitcoin, or sats (rates fetched over Tor), or turn the preview off.
  • News on Home - the latest post from the official Goblin news key (a kind 30023 long-form article) appears on the Home screen in your wallet's language, falling back to English; it stays hidden when there is nothing to show, and only ever shows the newest article.
  • Cross-platform - Linux, macOS, Windows, Android, built in pure Rust on egui.

How a payment travels

   you ──slatepack──▶ NIP-17 gift wrap (kind 1059, NIP-44 encrypted)
                          │
                         Tor
                          │
            ┌─────────────┴─────────────┐
        your relays              recipient's DM relays (kind 10050)
            └─────────────┬─────────────┘
                          ▼
   recipient ◀──unwrap, verify seal author, apply slatepack

The wrap is NIP-44-encrypted, and delivery uses the recipient's DM relay list (kind 10050). Tor hides your IP from the relay; the relay and the encryption above hide the rest - content, sender, timing.

Both parties only need one relay in common. The default set is the Floonet relay (relay.floonet.dev) plus Tor-friendly public relays (relay.0xchat.com, offchain.pub) that accept connections from Tor exits, and the set is editable in Settings → Relays.

Build

Desktop (Linux / macOS / Windows)

Goblin links Tor in-process via arti - the wallet is a single self-contained binary, no sidecar, nothing separate to install:

git submodule update --init --recursive
cargo build --release
./target/release/goblin

Goblin's identity and payment traffic (nostr relays, NIP-05 lookups and price fetches) rides Tor: every relay, the money-path relay included, is reached over a Tor exit to its ordinary clearnet host. The GRIN node connection (block sync and transaction broadcast) is not routed through Tor: it connects directly, as it carries only public chain data that isn't linked to your wallet.

Android

Install the Android SDK / NDK, then from the repo root:

./scripts/android.sh build|release v7|v8|x86

v7/v8/x86 is the device CPU architecture for build; for release pass a version in major.minor.patch form.

Identity service (goblin-nip05d)

The optional name service lives in goblin-nip05d/ (axum + SQLite) and is deployed at goblin.st. It implements NIP-05 resolution, NIP-98-authenticated registration and release (names are never transferred - on a key rotation you release the old name and re-register, or import your existing identity). The wallet is fully usable - and fully anonymous - without it. Avatars aren't stored or served - clients render them from the pubkey (an npub gradient with the username's first letter, else the Grin mark).

License

Apache License v2.0.

Credits

🤖 Built with AI pair-programming assistance (Claude)

The underlying cross-platform GRIN wallet engine is the upstream Grim project.

Simplified and Traditional Chinese translations by jasperli2026 (https://github.com/jasperli2026).

S
Description
No description provided
Readme Apache-2.0 61 MiB
Languages
Rust 96.6%
Java 2.3%
Shell 1%