Files
floonet-strfry/docker-compose.yml
2ro 6143fce8ac Add the name-transfer (marketplace) feature to the bundled authority, off by default
Ports the Goblin Name Transfer Protocol v1 into the colocated Floonet name
authority: a seller lodges a signed kind-3402 offer to reassign a name to a
buyer for a Grin price, and the buyer claims it with an on-chain payment proof.
Strictly non-custodial: the buyer pays the seller wallet to wallet, the
authority verifies two artifacts and swaps one names row. Keys never move.

New routes, mounted only when FLOONET_TRANSFERS is set (else they 404):
  POST   /api/v1/transfer/offer          NIP-98 (seller), body {offer}
  GET    /api/v1/transfer/offer/{id}      public, read-limited, CORS *
  DELETE /api/v1/transfer/offer/{id}      NIP-98 (seller)
  POST   /api/v1/transfer/claim           NIP-98 (buyer), body {offer_id, proof}

Claim verification runs in the spec's order: NIP-98 + replay set, offer exists
(consumed -> idempotent 200 or 409), revoked/expired grace + end_height gate,
buyer key == p tag, seller still owner, both proof signatures over the canonical
73-byte message (amount_BE || excess || sender), recipient address and exact
amount match the offer, kernel confirmed on chain >= min_conf, excess never used
(durable UNIQUE), buyer holds no name (exempt from the name-change cooldown).
The reassignment is one atomic SQLite transaction that also records the transfer,
consumes the offer, and arms the seller's cooldown. The lodge ambiguity rule
keys on proof_address alone among live offers (offer_ambiguous).

The transfer path is fully independent of the fork's GoblinPay paid-names
integration: zero calls into src/paid.rs, no GOBLINPAY_URL dependency, pure
in-process crypto plus the read-only Grin node foreign API. Transfers and paid
names toggle in any combination. Config follows the repo's FLOONET_ pattern
(from_env/validate/summary/for_test), fail-fast when transfers are on without
FLOONET_GRIN_NODE_URL, and is documented in .env.example, docker-compose.yml,
and the us-east env template.

Tests: 27 transfer integration tests (offer state machine, revoke, expiry,
late-claim grace and end_height rules, every claim failure mode with real
ed25519 proofs, successful reassign + idempotent retry + cooldown, plus a
transfers-with-pay-mode-active combination) adapted to tests/http.rs helpers,
plus proof, node and config unit tests. cargo test green (32 + 17 + 27).
2026-07-06 00:47:59 -04:00

124 lines
4.8 KiB
YAML

# A full, self-contained Floonet relay with automatic HTTPS.
#
# cp .env.example .env # edit FLOONET_DOMAIN etc.
# docker compose up -d
#
# gives you:
# - relay : stock strfry (built from source at a pinned ref) + the
# Floonet write policy plugin (default-deny kind whitelist,
# optional NIP-42 and paid-write gates)
# - authority : the bundled name authority (name@domain -> pubkey, with
# optional paid names / paid write access via GoblinPay)
# - caddy : auto-TLS reverse proxy terminating HTTPS for both
# - tor : OPTIONAL Tor onion in front of the relay
# (COMPOSE_PROFILES=tor), so wallets can reach it over Tor
# without a Tor exit hop. See deploy/tor/torrc.
#
# Set FLOONET_DOMAIN / FLOONET_BASE_URL / FLOONET_RELAYS in `.env` (copy
# .env.example) BEFORE bringing it up: Caddy obtains a certificate for
# FLOONET_DOMAIN, so DNS must already point at this host.
services:
relay:
build:
context: .
dockerfile: deploy/strfry/Dockerfile
image: floonet-strfry:latest
restart: unless-stopped
environment:
# Write policy plugin configuration (the plugin inherits strfry's
# environment). See plugin/floonet_writepolicy.py and .env.example.
FLOONET_ALLOWED_KINDS: ${FLOONET_ALLOWED_KINDS:-0,3,5,13,1059,10002,10050,27235}
FLOONET_REQUIRE_AUTH: ${FLOONET_REQUIRE_AUTH:-false}
FLOONET_PAY_MODE: ${FLOONET_PAY_MODE:-off}
FLOONET_AUTHORITY_URL: http://authority:8191
FLOONET_PAID_CACHE_SECS: ${FLOONET_PAID_CACHE_SECS:-60}
volumes:
- relay-data:/strfry-db
- ./deploy/strfry/strfry.conf:/app/strfry.conf:ro
expose:
- "7777"
# Bound the relay's footprint so an unauthenticated subscription/ingest
# flood can't starve the authority or proxy on the same host.
deploy:
resources:
limits:
memory: 512M
cpus: "1.0"
authority:
build: ./name-authority
image: floonet-name-authority:latest
restart: unless-stopped
environment:
# Identity. Override these in .env for your own deployment.
FLOONET_DOMAIN: ${FLOONET_DOMAIN:-floonet.example}
FLOONET_BASE_URL: ${FLOONET_BASE_URL:-https://floonet.example}
FLOONET_RELAYS: ${FLOONET_RELAYS:-wss://floonet.example}
# In-container paths (persisted on the named volume).
FLOONET_NAMES_BIND: 0.0.0.0:8191
FLOONET_NAMES_DB: /data/names.db
# Paid mode (all optional; free by default). See .env.example.
FLOONET_PAY_MODE: ${FLOONET_PAY_MODE:-off}
FLOONET_NAME_PRICE_GRIN: ${FLOONET_NAME_PRICE_GRIN:-0}
FLOONET_WRITE_PRICE_GRIN: ${FLOONET_WRITE_PRICE_GRIN:-0}
GOBLINPAY_URL: ${GOBLINPAY_URL:-}
GOBLINPAY_TOKEN: ${GOBLINPAY_TOKEN:-}
GOBLINPAY_WEBHOOK_SECRET: ${GOBLINPAY_WEBHOOK_SECRET:-}
# Name transfers (the name marketplace; off by default, strictly
# non-custodial, no GoblinPay involvement). Independent of the pay mode.
# When on, FLOONET_GRIN_NODE_URL is REQUIRED (get_kernel/get_tip). See
# .env.example and docs-notes/name-transfer-spec.md.
FLOONET_TRANSFERS: ${FLOONET_TRANSFERS:-false}
FLOONET_GRIN_NODE_URL: ${FLOONET_GRIN_NODE_URL:-}
FLOONET_TRANSFER_MIN_CONF: ${FLOONET_TRANSFER_MIN_CONF:-10}
FLOONET_TRANSFER_MAX_OFFER_TTL: ${FLOONET_TRANSFER_MAX_OFFER_TTL:-2592000}
FLOONET_TRANSFER_CLAIM_GRACE: ${FLOONET_TRANSFER_CLAIM_GRACE:-86400}
volumes:
- authority-data:/data
expose:
- "8191"
caddy:
image: caddy:2
restart: unless-stopped
depends_on:
- authority
- relay
environment:
FLOONET_DOMAIN: ${FLOONET_DOMAIN:-floonet.example}
ports:
- "80:80"
- "443:443"
volumes:
- ./deploy/Caddyfile:/etc/caddy/Caddyfile:ro
- ./deploy/landing:/srv/landing:ro
- caddy-data:/data
- caddy-config:/config
# Optional Tor onion service in front of the relay. Off unless the `tor`
# profile is active (set COMPOSE_PROFILES=tor in .env). It runs a plain
# system tor daemon whose hidden service forwards straight to the relay's
# websocket listener (no TLS on that hop: the onion transport is already
# encrypted and authenticated end to end). Wallets can then reach the relay
# over Tor without a Tor exit hop. The .onion address is derived from a key
# in the tor-data volume and printed to the tor logs on first start; back
# the volume up, since losing it rotates the address. See deploy/tor/torrc.
tor:
build: ./deploy/tor
image: floonet-tor:latest
restart: unless-stopped
profiles: ["tor"]
depends_on:
- relay
volumes:
- ./deploy/tor/torrc:/etc/tor/torrc:ro
- tor-data:/var/lib/tor
volumes:
relay-data:
authority-data:
caddy-data:
caddy-config:
tor-data: