Skip to content

freezedry-protocol/freezedry-node

Repository files navigation

FreezeDry Node

A lightweight indexer and cache node for the FreezeDry Protocol — on-chain art storage on Solana.

Nodes scan the Solana blockchain for FREEZEDRY: pointer memos, fetch the associated chunk data, and serve reconstructed artwork blobs over HTTP. The chain is the source of truth; nodes are a discovery and caching layer.

Full app: freezedry.art — managed inscriptions, NFT minting, and fast hydration.

Prerequisites

  • Node.js v18+nodejs.org
  • Helius API key — Free at helius.dev (sign up → Dashboard → API Keys → copy)

Writer/marketplace nodes also need:

  • Solana CLI (optional) — sh -c "$(curl -sSfL https://release.anza.xyz/stable/install)" for wallet management
  • Public domain + HTTPS — for peer network participation (e.g. node.yourdomain.com with reverse proxy)

5-Minute Setup

Option A: Guided setup (recommended)

git clone https://github.com/freezedry-protocol/freezedry-node.git
cd freezedry-node
npm run setup
npm start

The setup wizard walks you through role selection, Helius key, wallet, and node identity. It generates .env with safe defaults, a secure WEBHOOK_SECRET, and peer network bootstrap.

Option B: Manual setup

git clone https://github.com/freezedry-protocol/freezedry-node.git
cd freezedry-node
npm install
cp .env.example .env
# Edit .env — at minimum set HELIUS_API_KEY and generate WEBHOOK_SECRET:
#   openssl rand -hex 32
npm start

Required configuration

Variable Required What it is Where to get it
HELIUS_API_KEY Yes Solana RPC access helius.dev (free tier works)
WEBHOOK_SECRET Yes Auth for write endpoints openssl rand -hex 32 (setup.sh generates this)
IDENTITY_KEYPAIR For peers Ed25519 identity key (JSON array) setup.sh generates one
HOT_WALLET_KEYPAIR For writer Solana keypair for TX signing setup.sh generates one
NODE_URL For peers Your node's public https URL Your domain + reverse proxy
NODE_ENDPOINT For peers IP:port for domain-free nodes Your public IP + port (e.g. 203.0.113.5:3100)

Legacy: WALLET_KEYPAIR still works — used for both identity and hot wallet if the separate keys aren't set.

Verify it's working

After npm start, you should see:

FreezeDry Node (my-freezedry-node) listening on :3100
Indexer: starting (poll every 120s, wallet: 6ao3hnvK...)
Indexer: seeded N artworks from registry

Check health:

curl http://localhost:3100/health
# {"status":"ok","indexed":{"artworks":19,"complete":19},"peers":2,"identityPubkey":"AbCd...","displayName":"Brave Tiger"}

The node seeds existing artworks from the registry on startup, then begins scanning the chain for new ones. Blobs are fetched from peers first (free, instant HTTP) before falling back to chain reads.

Choosing a Role

Role What it does Helius plan Wallet needed?
reader Index chain + serve artwork to peers Free works No
writer Accept inscription jobs, earn fees Developer+ Yes (funded)
both Reader + writer (default) Developer+ Yes (funded)

Reader-only is the simplest way to help the network. No wallet, no SOL, just a Helius key.

Writer economics: Your wallet needs SOL as working capital to send memo transactions. You get reimbursed from the job escrow: 5,000 lamports/chunk (covers TX costs) plus 40% of the margin as profit. Start with ~0.1 SOL for testing, ~1 SOL for production.

How It Works

Solana Chain                    Your Node                    Peers / CDN
    |                              |                           |
    |--- FREEZEDRY: pointer ------>| discover artwork           |
    |--- chunk memos ------------->| fetch & cache chunks       |
    |                              |                           |
    |                              |<-- GET /artwork/:hash ----| metadata
    |                              |<-- GET /blob/:hash -------| cached blob (peers only)
    |                              |<-- GET /verify/:hash -----| SHA-256 proof

Discovery: The indexer polls for the configured SERVER_WALLET's memo transactions, looking for FREEZEDRY: pointers. Each pointer contains a hash, chunk count, and blob size. Paginated — handles artworks with thousands of chunks.

Caching: Once a pointer is found, the node fetches all chunk transactions (paginated beyond API limits), strips memo headers, and stores the raw data in SQLite.

Peer Sync: Before reading from chain, the node tries peers first. Peer blob downloads are instant HTTP — no RPC credits needed. All peer-to-peer requests are authenticated with ed25519 signed messages.

Serving: Peers request blobs via HTTP. Only complete blobs are served — partial data is never sent.

API Endpoints

Public (no auth)

Endpoint Method Returns
/health GET Node status, indexed artwork count, peer count
/artwork/:hash GET Artwork metadata (dimensions, mode, chunk count, complete status)
/artworks?limit=50&offset=0 GET List indexed artworks
/verify/:hash GET SHA-256 verification of stored blob

Peer-gated (signed identity required)

Endpoint Method How to access
/blob/:hash GET Ed25519 signed identity headers (X-FD-Identity, X-FD-Signature, X-FD-Message)
/sync/list GET Same — lists available artworks for sync
/sync/chunks/:hash GET Same — base64 blob for peer sync
/nodes GET Same — list known peers (gossip discovery)

Protected (require WEBHOOK_SECRET)

Endpoint Method Description
/ingest POST Push artwork metadata (coordinator → node)
/webhook/helius POST Receive real-time Helius webhook pushes

Peer discovery (public, rate-limited + liveness-verified)

Endpoint Method Description
/sync/announce POST Register a peer node URL (must be https, public IP, reachable)

Peer Network

Nodes discover each other and sync blobs without using RPC credits. All peer communication is authenticated with ed25519 signed messages — no shared secrets.

Identity System (Two-Wallet)

Each node has two keypairs:

Key Purpose Needs SOL?
Identity key Peer authentication, reputation, display name No
Hot wallet Signs Solana memo TXs, pays fees, earns escrow Yes (writer only)

Separate keys = separate risk. If the hot wallet is compromised, identity and reputation are untouched.

Setup

# In .env — choose one connectivity method:

# Option A: Domain (requires HTTPS reverse proxy)
NODE_URL=https://node.yourdomain.com

# Option B: IP:port (no domain needed — simplest setup)
NODE_ENDPOINT=203.0.113.5:3100

# Optional: bootstrap peers (otherwise discovered via coordinator)
PEER_NODES=https://peer1.example.com,http://198.51.100.10:3100

How Peer Auth Works

Every peer-to-peer request includes three HTTP headers:

Header Contents
X-FD-Identity Node's public key (base58)
X-FD-Message FreezeDry:peer:{action}:{timestamp}:{nonce}
X-FD-Signature Ed25519 signature of the message

The receiving node verifies:

  1. Signature validity — only the private key holder could have signed this
  2. Timestamp freshness — must be within 5 minutes (prevents replay of old messages)
  3. Nonce uniqueness — random nonce prevents exact replay within the freshness window
  4. Known identity — the signing pubkey must be a registered peer

How Peer Sync Works

Your Node                          Peer Node
    |                                  |
    |--- POST /sync/announce --------->| signed identity + endpoint
    |         (peer verifies signature)
    |<-- POST /sync/announce ----------| signed identity + endpoint (bidirectional)
    |                                  |
    |--- GET /blob/:hash ------------->| complete blob (signed request)
    |    (peer sync — no RPC needed)   |
  1. Announce — Node sends its identity pubkey, endpoint, and signed auth headers. Receiving node verifies the ed25519 signature matches the claimed identity
  2. Bidirectional — Your node announces back automatically
  3. Parallel fill — When filling incomplete artworks, tries peers first (instant HTTP). Falls back to chain reads only if no peer has the data
  4. Gossip — Every ~20 minutes, nodes exchange peer lists to discover new nodes
  5. Coordinator — Nodes also register with freezedry.art for centralized discovery (optional, bootstrapping convenience)

Display Names

Each node gets a deterministic display name from its identity pubkey (SHA-256 hash → adjective + animal). Example: Brave Tiger, Silent Falcon. These are cosmetic — the pubkey is the real identity.

Security

  • Ed25519 identity auth: All peer requests require cryptographic proof of identity. No shared passwords.
  • Nonce replay protection: Each signed message includes a random nonce. Replayed messages are rejected.
  • SSRF protection: Private IPs (10.x, 192.168.x, 169.254.x, 127.x, ::1), .internal/.local hostnames, and IPv6 private ranges blocked
  • HTTP IP-only: Plain HTTP allowed only for raw public IPv4 addresses (prevents DNS rebinding)
  • Rate limiting: 10 announce requests/min per IP
  • Peer-gated data: Blob data requires signed identity from a known peer — no unauthenticated scraping
  • Complete blobs only: Partial/incomplete data is never served to peers
  • Minimal exposure: /health returns status + counts + identity pubkey. No memory, uptime, keys, or internal details

Helius Plan Auto-Detection

The node auto-detects your Helius plan on startup:

  • Free key: Uses standard RPC (getSignaturesForAddress + getTransaction). Works fine, slightly slower.
  • Paid key (Developer+): Uses Enhanced API. ~50x cheaper in credits, faster indexing.

Override with USE_ENHANCED_API=true|false in .env if needed.

Architecture

freezedry-node/
  src/
    server.js      — Fastify HTTP server + endpoints
    indexer.js     — Chain scanner + peer sync + gossip
    db.js          — SQLite storage (better-sqlite3, WAL mode)
    config.js      — Protocol constants
    wallet.js      — Two-wallet keypair loader (identity + hot wallet)
    crypto-auth.js — Ed25519 signing + verification for peer auth
    display-name.js — Deterministic display names from identity pubkey
  scripts/
    setup.sh       — Interactive setup wizard (two-wallet generation)
    register.js    — Manual PDA registration
  .env.example     — Configuration template

Database: SQLite via better-sqlite3 with WAL mode for concurrent reads. Created automatically on first run. This is a cache — delete it to re-index from chain.

Dependencies: 3 runtime deps: fastify, better-sqlite3, @solana/web3.js (optional — reader-only nodes work without it).

Production Deployment

Reverse proxy (nginx)

server {
    listen 443 ssl;
    server_name node.yourdomain.com;

    location / {
        proxy_pass http://127.0.0.1:3100;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        # Forward identity auth headers for peer-to-peer communication
        proxy_set_header X-FD-Identity $http_x_fd_identity;
        proxy_set_header X-FD-Signature $http_x_fd_signature;
        proxy_set_header X-FD-Message $http_x_fd_message;
    }
}

systemd service

[Unit]
Description=FreezeDry Node
After=network.target

[Service]
Type=simple
User=ubuntu
WorkingDirectory=/home/ubuntu/freezedry-node
ExecStart=/usr/bin/node src/server.js
Restart=on-failure
MemoryMax=512M
MemoryHigh=400M

[Install]
WantedBy=multi-user.target
sudo systemctl enable freezedry-node
sudo systemctl start freezedry-node

Docker

docker compose up -d
docker compose logs -f

Helius Webhook (real-time indexing)

Instead of polling every 2 minutes, configure a Helius webhook for instant indexing:

  1. Go to Helius Dashboard > Webhooks
  2. Create webhook watching the SERVER_WALLET address
  3. Set URL to https://node.yourdomain.com/webhook/helius
  4. Set auth header to your WEBHOOK_SECRET
  5. Select "Enhanced" format

Troubleshooting

Problem Fix
better-sqlite3 build fails Install build tools: apt install python3 make g++ (or use Docker)
Port 3100 already in use Change PORT in .env or stop the other process
0 artworks after startup Check HELIUS_API_KEY is valid. The node seeds from registry first, then scans chain.
Node can't find peers Ensure PEER_NODES is set (setup.sh adds defaults). Check network connectivity.
Peer auth fails Check IDENTITY_KEYPAIR is set. Verify identity pubkey matches what the peer expects.
Registration fails Ensure NODE_URL or NODE_ENDPOINT is publicly reachable. The coordinator verifies your signature.
No display name Set IDENTITY_KEYPAIR — display names derive from the identity pubkey.
High credit usage Lower POLL_INTERVAL (default 1hr is safe). Keep CHAIN_FILL=false. See docs/rpc-budget.md.

Related

License

MIT

About

Community node template for the Freeze Dry Protocol — index, cache, and inscribe

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors