Compare commits

...

10 Commits

Author SHA1 Message Date
48785992d2 update documentation and .gitignore 2026-02-18 09:29:50 +01:00
6c75fe55d0 feat(ssl): move certificate generation to runtime with persistent volume
Self-signed SSL certificates are now generated at first startup instead
of being baked into the Docker image. Certificates persist in ./certs/
and are reused on subsequent runs. Users can provide their own certs
2026-02-17 08:58:44 +01:00
742b0662a7 feat(dashboard): require basic auth for external clients only
- Allow direct access from localhost and private RFC1918 networks
- Enforce HTTP Basic Auth for non-private/external source IPs
- Read dashboard credentials from compose env vars
- Add .env.example entries for DASHBOARD_AUTH_USERNAME/PASSWORD
- Update README and DASHBOARD docs
2026-02-16 09:41:44 +01:00
ae91163168 harden: restrict RPC/ZMQ exposure and remove unsafe RPC override
- Bind RPC to localhost only (127.0.0.1:2332)
- Bind ZMQ 28332/28334/28335 to localhost for host-only debugging
- Remove -rpcallowip=0.0.0.0/0 from palladiumd
- Remove redundant -rpcbind flag from compose (handled in palladium.conf)
2026-02-16 09:23:14 +01:00
525d1bc9e0 chore(gitignore): ignore Qt GUI binary 2026-02-15 16:36:04 +01:00
26b69c6b55 feat(dashboard): filter ElectrumX peers by genesis hash and disable API caching
Ensure only peers belonging to the same network (matching genesis_hash)
are shown in the dashboard. Add no-cache headers to all /api/ responses
to prevent stale data from browser or proxy caches
2026-02-15 01:13:42 +01:00
8e0aaecaa9 Improve ElectrumX discovery consistency and reachability checks
Unify dashboard and servers page data source to keep active server counts synchronized.

Remove hardcoded port fallbacks and derive TCP/SSL ports from advertised services.

Harden TCP/SSL probing (self-signed SSL support, better timeouts, peer-port-aware checks).

Simplify Discovery Summary UI to show only total active servers.
2026-02-13 14:22:07 +01:00
ed5a34438a update README.md and .gitignore 2026-02-13 13:41:58 +01:00
39dc6c5d26 chore(gitignore): ignore daemon binaries 2026-02-13 13:30:53 +01:00
cc2f493eb2 Update default palladium.conf
- replace specific docker subnets with universal private ranges

- allow RPC from 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16

- improve portability across different Docker/network setups
2026-02-13 13:30:53 +01:00
18 changed files with 616 additions and 176 deletions

3
.env.example Normal file
View File

@@ -0,0 +1,3 @@
# Dashboard auth for external clients (non-RFC1918 source IPs)
DASHBOARD_AUTH_USERNAME=admin
DASHBOARD_AUTH_PASSWORD=change-me-now

13
.gitignore vendored
View File

@@ -1,3 +1,9 @@
# SSL certificates (auto-generated or user-provided)
/certs/
# Environment file
.env
# ElectrumX server data
/electrumx-data/
@@ -31,3 +37,10 @@ Thumbs.db
# Python cache files
__pycache__/
*.py[cod]
# Daemon binaries
/daemon/palladiumd
/daemon/palladium-cli
/daemon/palladium-tx
/daemon/palladium-wallet
/daemon/palladium-qt

View File

@@ -17,8 +17,9 @@ port=2333
rpcport=2332
rpcbind=0.0.0.0
rpcallowip=172.17.0.0/16
rpcallowip=172.18.0.0/16
rpcallowip=10.0.0.0/8
rpcallowip=172.16.0.0/12
rpcallowip=192.168.0.0/16
maxconnections=50
fallbackfee=0.0001

View File

@@ -154,8 +154,14 @@ curl http://localhost:8080/api/health | jq
## Security Note
The dashboard is exposed on `0.0.0.0:8080` making it accessible from your network. If you're running this on a public server, consider:
The dashboard is exposed on `0.0.0.0:8080`.
Requests from localhost/LAN private ranges are allowed directly.
Requests from public/external IPs require HTTP Basic Auth.
1. Using a reverse proxy (nginx) with authentication
2. Restricting access with firewall rules
3. Using HTTPS with SSL certificates
Set credentials with:
```bash
# .env (copy from .env.example)
DASHBOARD_AUTH_USERNAME=admin
DASHBOARD_AUTH_PASSWORD=change-me-now
```

View File

@@ -1,7 +1,7 @@
FROM lukechilds/electrumx
# Install curl (needed by entrypoint for RPC calls and IP detection)
RUN apk add --no-cache curl || apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
RUN apk add --no-cache curl openssl || apt-get update && apt-get install -y --no-install-recommends curl openssl && rm -rf /var/lib/apt/lists/*
# Copy Palladium coin definition and patch ElectrumX
COPY electrumx-patch/coins_plm.py /tmp/coins_plm.py
@@ -28,34 +28,6 @@ for target in [
print('>> Patched ElectrumX with Palladium coin classes')
PATCH
RUN mkdir -p /certs && \
cat >/certs/openssl.cnf <<'EOF' && \
openssl req -x509 -nodes -newkey rsa:4096 -days 3650 \
-keyout /certs/server.key -out /certs/server.crt \
-config /certs/openssl.cnf && \
chmod 600 /certs/server.key && chmod 644 /certs/server.crt
[req]
distinguished_name = dn
x509_extensions = v3_req
prompt = no
[dn]
C = IT
ST = -
L = -
O = ElectrumX
CN = plm.local
[v3_req]
keyUsage = keyEncipherment, dataEncipherment, digitalSignature
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
DNS.1 = plm.local
IP.1 = 127.0.0.1
EOF
ENV SSL_CERTFILE=/certs/server.crt
ENV SSL_KEYFILE=/certs/server.key

175
README.md
View File

@@ -8,12 +8,13 @@ Everything runs in Docker containers - no need to install dependencies on your h
## What You Get
- **Palladium Full Node** (palladiumd) - Runs in Docker with full blockchain sync
- **Palladium Full Node** (palladiumd) - Runs in Docker with full blockchain sync ([binary setup](daemon/README.md))
- **ElectrumX Server** - Pre-configured for Palladium network with automatic indexing
- **Web Dashboard** - Professional monitoring interface with real-time statistics, charts, and peer management
- **Web Dashboard** - Professional monitoring interface with real-time statistics, peer views, and Electrum server discovery ([quick start](DASHBOARD.md) | [technical docs](web-dashboard/README.md))
- **Automatic RPC Configuration** - ElectrumX reads credentials directly from palladium.conf
- **Self-Signed SSL Certificates** - Secure connections ready out-of-the-box
- **Production Ready** - Includes restart policies and health checks
- **Self-Signed SSL Certificates** - Auto-generated on first startup, persisted in `./certs/`
- **Public IP Auto-Detection** - Automatically configures REPORT_SERVICES and SSL certificate SAN
- **Production Ready** - Includes restart policies, health endpoint, and Basic Auth for external dashboard access
---
@@ -35,29 +36,40 @@ Everything runs in Docker containers - no need to install dependencies on your h
## Project Structure
```
plm-electrumx/
palladium-stack/
├── daemon/ # Palladium binaries (YOU must add these)
│ ├── palladiumd # Node daemon (required)
│ ├── palladium-cli # CLI tool (required)
│ ├── palladium-tx # Transaction tool (optional)
── palladium-wallet # Wallet tool (optional)
── palladium-wallet # Wallet tool (optional)
│ └── README.md # Binary download instructions
├── .palladium/
│ ├── palladium.conf # Node configuration (edit this!)
│ ├── blocks/ # Blockchain blocks (auto-generated)
│ ├── chainstate/ # Blockchain state (auto-generated)
│ └── ... # Other runtime data (auto-generated)
├── certs/ # SSL certificates (auto-generated on first run)
│ ├── server.crt # Self-signed certificate
│ └── server.key # Private key
├── electrumx-data/ # ElectrumX database (auto-generated)
├── electrumx-patch/
│ └── coins_plm.py # Palladium coin definition for ElectrumX
├── web-dashboard/ # Web monitoring dashboard
│ ├── app.py # Flask backend API
│ ├── templates/ # HTML templates
── static/ # CSS and JavaScript
── static/ # CSS and JavaScript
│ └── README.md # Dashboard technical docs
├── Dockerfile.palladium-node # Builds Palladium node container
├── Dockerfile.electrumx # Builds ElectrumX server container
├── Dockerfile.dashboard # Builds web dashboard container
── docker-compose.yml # Main orchestration file
── docker-compose.yml # Main orchestration file
├── entrypoint.sh # ElectrumX startup (auto-config, SSL, IP detection)
├── test-server.py # ElectrumX protocol test client
├── .env.example # Environment variables template
└── DASHBOARD.md # Dashboard quick start guide
```
🔗 **Palladium Full Node:** [palladium-coin/palladiumcore](https://github.com/palladium-coin/palladiumcore)
**Palladium Full Node:** [palladium-coin/palladiumcore](https://github.com/palladium-coin/palladiumcore)
---
@@ -118,45 +130,23 @@ Browser◄───────────────────────
### Step 1: Clone the Repository
```bash
git clone https://github.com/palladium-coin/plm-electrumx.git
cd plm-electrumx
git clone <your-repository-url>
cd palladium-stack
```
---
### Step 2: Get Palladium Binaries
**IMPORTANT:** Download binaries matching your system architecture.
**IMPORTANT:** Download binaries matching your system architecture in `daemon/`.
#### Option A: Download from Official Release
1. Go to: [palladium-coin/palladiumcore/releases](https://github.com/palladium-coin/palladiumcore/releases)
2. Download the correct version:
- **Linux x64**: `palladium-x.x.x-x86_64-linux-gnu.tar.gz`
- **Linux ARM64**: `palladium-x.x.x-aarch64-linux-gnu.tar.gz`
3. Extract and copy binaries:
```bash
tar -xzf palladium-*.tar.gz
mkdir -p daemon
cp palladium-*/bin/palladiumd daemon/
cp palladium-*/bin/palladium-cli daemon/
chmod +x daemon/*
```
#### Verify Installation
```bash
ls -lh daemon/
# Should show: palladiumd, palladium-cli (both executable)
```
See [daemon/README.md](daemon/README.md) for detailed instructions.
---
### Step 3: Configure Network and Router
#### A. Configure RPC Credentials
#### 3.1 Configure RPC Credentials
Open the configuration file:
@@ -166,13 +156,13 @@ nano .palladium/palladium.conf
**Change these credentials:**
```conf
rpcuser=your_secure_username # ← Change this
rpcpassword=your_secure_password # ← Use a strong password!
rpcuser=your_username # ← Change this
rpcpassword=your_password # ← Use a strong password!
```
Save and close (`Ctrl+X`, then `Y`, then `Enter`).
#### B. Router Port Forwarding (Required for Public Access)
#### 3.2 Router Port Forwarding (Required for Public Access)
For your ElectrumX server to be accessible from the internet, you **must** configure port forwarding on your router.
@@ -242,11 +232,31 @@ For your ElectrumX server to be accessible from the internet, you **must** confi
**Security Notes:**
- Only forward port **8080** if you want the dashboard accessible from internet (not recommended without authentication)
- Consider using a VPN for dashboard access instead
- External dashboard clients (public IPs) require Basic Auth. Configure `DASHBOARD_AUTH_USERNAME` and `DASHBOARD_AUTH_PASSWORD` in `.env` (see `.env.example`).
- Ports **50001** and **50002** need to be public for Electrum wallets to connect
- Port **2333** is required for the node to sync with the Palladium network
---
#### 3.3: (Optional) Configure Dashboard Authentication
If you plan to expose the dashboard to the internet (port 8080), configure Basic Auth credentials:
```bash
cp .env.example .env
nano .env
```
Set strong credentials:
```bash
DASHBOARD_AUTH_USERNAME=admin
DASHBOARD_AUTH_PASSWORD=a-strong-random-password
```
LAN clients (private IPs) can access the dashboard without authentication. External clients (public IPs) will be prompted for these credentials automatically.
---
### Step 4: (Optional) Copy Existing Blockchain Data
If you have a synced Palladium blockchain, copy it to speed up initial sync:
@@ -270,7 +280,7 @@ docker compose up -d
**What happens:**
1. Builds three Docker images: `palladium-node`, `electrumx-server`, and `palladium-dashboard`
2. Starts Palladium node first
3. Starts ElectrumX (waits for node to be ready)
3. Starts ElectrumX (waits for node to be ready, auto-generates SSL certificates in `./certs/` if not present)
4. Starts Web Dashboard (connects to both services)
**First build takes 5-10 minutes.**
@@ -318,15 +328,18 @@ http://<your-public-ip>:8080
The dashboard shows:
- System resources (CPU, RAM, Disk)
- Palladium node status (height, difficulty, connections, sync progress)
- ElectrumX server stats (version, sessions, DB size, uptime, **server IP**, ports)
- ElectrumX server stats (version, active servers, DB size, uptime, **server IP**, ports)
- Mempool information (transactions, size, usage)
- Recent blocks table
- Network peers (click "Connections" to view detailed peer list)
- Electrum active servers page (click "Active Servers")
---
## Web Dashboard Features
See also: [DASHBOARD.md](DASHBOARD.md) for quick start | [web-dashboard/README.md](web-dashboard/README.md) for technical details
### Main Dashboard (http://localhost:8080)
**System Monitoring:**
@@ -343,9 +356,9 @@ The dashboard shows:
**ElectrumX Server:**
- Server Version
- Active Sessions (concurrent connections)
- Database Size
- Uptime
- Active Servers (clickable to dedicated server list)
- **Server IP** (for client configuration)
- TCP Port (50001)
- SSL Port (50002)
@@ -378,6 +391,23 @@ The dashboard shows:
**Auto-refresh:** Every 10 seconds
### Electrum Active Servers Page (http://localhost:8080/electrum-servers)
**Summary:**
- Total Active Servers
- TCP 50001 Reachable
**Detailed Server List:**
- Host
- TCP Port
- SSL Port
- TCP Reachable (Yes/No)
- SSL Reachable (Yes/No)
Servers are filtered by genesis hash to show only peers on the same network (mainnet or testnet).
**Auto-refresh:** Every 10 seconds
---
## Verify Installation
@@ -394,8 +424,8 @@ Should show all three containers "Up".
```bash
docker exec palladium-node palladium-cli \
-rpcuser=your_username \
-rpcpassword=your_password \
-rpcuser=<your_username> \
-rpcpassword=<your_password> \
getblockchaininfo
```
@@ -416,6 +446,25 @@ curl http://<your-public-ip>:8080
python test-server.py <your-public-ip>:50002
```
### REST API Endpoints
The dashboard exposes a REST API for programmatic access:
| Endpoint | Description |
|----------|-------------|
| `GET /api/health` | Service health check (palladium + electrumx status) |
| `GET /api/system/resources` | CPU, memory, and disk usage |
| `GET /api/palladium/info` | Node info (blockchain, network, mining, mempool) |
| `GET /api/palladium/peers` | Detailed peer list with traffic stats |
| `GET /api/palladium/blocks/recent` | Last 10 blocks |
| `GET /api/electrumx/stats` | ElectrumX version, uptime, DB size, active servers |
| `GET /api/electrumx/servers` | Discovered ElectrumX peers with reachability |
```bash
# Example
curl http://localhost:8080/api/health | jq
```
---
## Configuration Details
@@ -426,15 +475,17 @@ Key settings in `.palladium/palladium.conf`:
| Setting | Value | Purpose |
|---------|-------|---------|
| `rpcuser` | `your_username` | RPC authentication |
| `rpcpassword` | `your_password` | RPC authentication |
| `rpcuser` | `<your_username>` | RPC authentication |
| `rpcpassword` | `<your_password>` | RPC authentication |
| `server=1` | Required | Enable RPC server |
| `txindex=1` | Required | Index all transactions (ElectrumX needs this) |
| `addressindex=1` | Recommended | Index addresses for fast queries |
| `timestampindex=1` | Recommended | Index timestamps |
| `spentindex=1` | Recommended | Index spent outputs |
| `rpcbind=0.0.0.0` | Required | Allow Docker connections |
| `rpcallowip=172.17.0.0/16` | Required | Allow Docker network |
| `rpcallowip=10.0.0.0/8` | Recommended | Allow private RFC1918 networks |
| `rpcallowip=172.16.0.0/12` | Recommended | Allow private RFC1918 networks |
| `rpcallowip=192.168.0.0/16` | Recommended | Allow private RFC1918 networks |
| `port=2333` | Default | P2P network port (mainnet) |
| `rpcport=2332` | Default | RPC port (mainnet) |
@@ -456,10 +507,12 @@ environment:
# RPC credentials automatically read from palladium.conf
```
**Automatic Configuration:**
- ElectrumX reads RPC credentials from mounted `palladium.conf`
- No need to manually configure `DAEMON_URL`
- Single source of truth for credentials
**Automatic Configuration (via `entrypoint.sh`):**
- **RPC credentials**: Read automatically from mounted `palladium.conf` — no need to configure `DAEMON_URL`
- **Public IP detection**: Discovers your public IP and sets `REPORT_SERVICES` for peer announcement
- **SSL certificates**: Auto-generated on first startup in `./certs/` with SAN including localhost and public IP (see [Security > SSL Certificates](#production-deployment))
- **TX stats patching**: Queries the live node for `TX_COUNT` / `TX_COUNT_HEIGHT` and patches the ElectrumX coin definition at startup
- Single source of truth for credentials across all services
---
@@ -661,13 +714,19 @@ docker compose build --no-cache
```
3. **SSL Certificates:**
- Default uses self-signed certificates
- For production, use valid SSL certificates (Let's Encrypt)
- Self-signed certificates are auto-generated on first startup in `./certs/`
- The certificate includes localhost and the auto-detected public IP in its SAN
- To use your own certificates (e.g. Let's Encrypt), place `server.crt` and `server.key` in `./certs/` before starting
4. **Dashboard Access:**
- Consider adding authentication
- Use VPN for remote access
- Or restrict to local network only
- LAN clients (RFC1918 private IPs) can access without authentication
- External clients (public IPs) require HTTP Basic Auth automatically
- Configure credentials in `.env` (copy from `.env.example`):
```bash
DASHBOARD_AUTH_USERNAME=admin
DASHBOARD_AUTH_PASSWORD=a-strong-random-password
```
- Consider using a VPN instead of exposing port 8080 publicly
5. **Regular Updates:**
```bash
@@ -725,7 +784,7 @@ environment:
## Notes
* **Data Persistence:** All data stored in `./.palladium/` and `./electrumx-data/`
* **Data Persistence:** All data stored in `./.palladium/`, `./electrumx-data/`, and `./certs/`
* **Backup:** Regularly backup `.palladium/wallet.dat` if you store funds
* **Network Switch:** Always clear ElectrumX database when switching networks
* **Updates:** Check for Palladium Core updates regularly
@@ -740,8 +799,6 @@ Distributed under the **MIT** license. See `LICENSE` file for details.
## Support
- **Issues:** [GitHub Issues](https://github.com/palladium-coin/plm-electrumx/issues)
- **Palladium Community:** [Palladium Coin](https://github.com/palladium-coin)
- **ElectrumX Documentation:** [Official Docs](https://electrumx.readthedocs.io/)
---

View File

@@ -4,9 +4,7 @@ This directory must contain the pre-compiled Palladium Core binaries used by the
## Download
Download the latest release from the official repository:
**https://github.com/palladium-coin/palladiumcore/releases**
Download from the official release repository: **https://github.com/palladium-coin/palladiumcore/releases/latest**
## Architecture
@@ -15,8 +13,8 @@ Choose the correct archive for your platform:
| Host Architecture | Archive to download | Common hardware |
|-------------------|-------------------------|-------------------------------------|
| `x86_64` | `x86_64-linux-gnu.tar.gz` | Standard PCs, most VPS/cloud servers |
| `aarch64` | `aarch64-linux-gnu.tar.gz` | Single-board computers (Raspberry Pi 4/5, Orange Pi, etc.) |
| `x86_64` | `palladium-linux-x86_64.tar.gz` | Standard PCs, most VPS/cloud servers |
| `aarch64` | `palladium-linux-aarch64.tar.gz` | Single-board computers (Raspberry Pi 4/5, Orange Pi, etc.) |
To check your host architecture:
@@ -38,14 +36,28 @@ daemon/
## Quick setup
```bash
# Example for aarch64 (Raspberry Pi)
tar xzf palladiumcore-*-aarch64-linux-gnu.tar.gz
cp palladiumcore-*/bin/palladium{d,-cli,-tx,-wallet} daemon/
### Example for x86_64 (VPS/PC)
# Example for x86_64 (VPS/PC)
tar xzf palladiumcore-*-x86_64-linux-gnu.tar.gz
cp palladiumcore-*/bin/palladium{d,-cli,-tx,-wallet} daemon/
```bash
cd daemon
wget https://github.com/palladium-coin/palladiumcore/releases/latest/download/palladium-linux-x86_64.tar.gz
tar -xzf palladium-linux-x86_64.tar.gz
cd linux-x86_64
mv palladium* ..
cd ..
rm -rf linux-x86_64/ && rm palladium-linux-x86_64.tar.gz
```
### Example for aarch64 (Raspberry Pi)
```bash
cd daemon
wget https://github.com/palladium-coin/palladiumcore/releases/latest/download/palladium-linux-aarch64.tar.gz
tar -xzf palladium-linux-aarch64.tar.gz
cd linux-aarch64
mv palladium* ..
cd ..
rm -rf linux-aarch64/ && rm palladium-linux-aarch64.tar.gz
```
After placing the binaries, rebuild the node image:

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -7,11 +7,11 @@ services:
container_name: palladium-node
restart: unless-stopped
ports:
- "0.0.0.0:2332:2332" # RPC port mainnet (accessible from network)
- "127.0.0.1:2332:2332" # RPC port mainnet (host-local only)
- "0.0.0.0:2333:2333" # P2P port mainnet (accessible from network)
- "0.0.0.0:28332:28332" # ZMQ hashblock (accessible from network)
- "0.0.0.0:28334:28334" # ZMQ rawblock (accessible from network)
- "0.0.0.0:28335:28335" # ZMQ rawtx (accessible from network)
- "127.0.0.1:28332:28332" # ZMQ hashblock (host-local debug only)
- "127.0.0.1:28334:28334" # ZMQ rawblock (host-local debug only)
- "127.0.0.1:28335:28335" # ZMQ rawtx (host-local debug only)
# For testnet, expose: 12332 (RPC) and 12333 (P2P)
volumes:
@@ -22,8 +22,6 @@ services:
palladiumd
-conf=/root/.palladium/palladium.conf
-datadir=/root/.palladium
-rpcbind=0.0.0.0
-rpcallowip=0.0.0.0/0
-daemon=0
-printtoconsole=1
@@ -74,6 +72,7 @@ services:
volumes:
- ./electrumx-data:/data
- ./.palladium/palladium.conf:/palladium-config/palladium.conf:ro
- ./certs:/certs
dashboard:
build:
@@ -93,7 +92,9 @@ services:
PALLADIUM_RPC_PORT: "2332"
ELECTRUMX_RPC_HOST: "electrumx"
ELECTRUMX_RPC_PORT: "8000"
DASHBOARD_AUTH_USERNAME: "${DASHBOARD_AUTH_USERNAME:-admin}"
DASHBOARD_AUTH_PASSWORD: "${DASHBOARD_AUTH_PASSWORD:-change-me-now}"
volumes:
- ./.palladium/palladium.conf:/palladium-config/palladium.conf:ro
- /var/run/docker.sock:/var/run/docker.sock:ro
- /var/run/docker.sock:/var/run/docker.sock:ro

View File

@@ -70,6 +70,61 @@ echo "DAEMON_URL: http://${RPC_USER}:***@palladiumd:${RPC_PORT}/"
echo "REPORT_SERVICES: ${REPORT_SERVICES:-not set}"
echo "=========================================="
# ── SSL certificate generation (skip if certs already exist) ──
if [ ! -f /certs/server.crt ] || [ ! -f /certs/server.key ]; then
echo "SSL certificates not found, generating self-signed certificate..."
# Collect SAN entries
SAN="DNS.1 = localhost"
SAN_IDX=1
IP_IDX=1
SAN="${SAN}\nIP.${IP_IDX} = 127.0.0.1"
# Try to detect public IP for SAN
for url in https://icanhazip.com https://ifconfig.me https://api.ipify.org; do
DETECTED_IP=$(curl -sf --max-time 5 "$url" 2>/dev/null | tr -d '[:space:]')
if [ -n "$DETECTED_IP" ]; then
IP_IDX=$((IP_IDX + 1))
SAN="${SAN}\nIP.${IP_IDX} = ${DETECTED_IP}"
echo ">> Including public IP in certificate SAN: ${DETECTED_IP}"
break
fi
done
cat >/tmp/openssl.cnf <<SSLEOF
[req]
distinguished_name = dn
x509_extensions = v3_req
prompt = no
[dn]
C = IT
ST = -
L = -
O = PalladiumStack
CN = localhost
[v3_req]
keyUsage = keyEncipherment, dataEncipherment, digitalSignature
extendedKeyUsage = serverAuth
subjectAltName = @alt_names
[alt_names]
$(echo -e "$SAN")
SSLEOF
openssl req -x509 -nodes -newkey rsa:4096 -days 3650 \
-keyout /certs/server.key -out /certs/server.crt \
-config /tmp/openssl.cnf 2>/dev/null
chmod 600 /certs/server.key
chmod 644 /certs/server.crt
rm -f /tmp/openssl.cnf
echo ">> SSL certificate generated successfully"
else
echo ">> Using existing SSL certificates from /certs/"
fi
# Update TX_COUNT / TX_COUNT_HEIGHT in coins.py from the live node
echo "Fetching chain tx stats from palladiumd..."
TX_STATS=$(curl -sf --user "${RPC_USER}:${RPC_PASSWORD}" \

View File

@@ -11,6 +11,10 @@ import os
import time
import copy
import threading
import ssl
import ipaddress
import base64
import hmac
from datetime import datetime
import psutil
import socket
@@ -18,6 +22,76 @@ import socket
app = Flask(__name__)
CORS(app)
TRUSTED_CLIENT_NETWORKS = (
ipaddress.ip_network('127.0.0.0/8'),
ipaddress.ip_network('10.0.0.0/8'),
ipaddress.ip_network('172.16.0.0/12'),
ipaddress.ip_network('192.168.0.0/16'),
ipaddress.ip_network('::1/128'),
)
def is_trusted_client_ip(ip_text):
"""Allow direct access for localhost and private RFC1918 LAN clients."""
if not ip_text:
return False
try:
ip_obj = ipaddress.ip_address(ip_text.strip())
if getattr(ip_obj, 'ipv4_mapped', None):
ip_obj = ip_obj.ipv4_mapped
return any(ip_obj in network for network in TRUSTED_CLIENT_NETWORKS)
except ValueError:
return False
def unauthorized_response():
response = jsonify({'error': 'Authentication required'})
response.status_code = 401
response.headers['WWW-Authenticate'] = 'Basic realm="Palladium Dashboard"'
return response
@app.before_request
def enforce_external_auth():
"""
Localhost and LAN clients can access directly.
External clients must authenticate via Basic Auth credentials from env vars.
"""
client_ip = request.remote_addr or ''
if is_trusted_client_ip(client_ip):
return None
expected_user = os.getenv('DASHBOARD_AUTH_USERNAME', '').strip()
expected_pass = os.getenv('DASHBOARD_AUTH_PASSWORD', '').strip()
if not expected_user or not expected_pass:
return jsonify({'error': 'Dashboard auth is not configured'}), 503
auth_header = request.headers.get('Authorization', '')
if not auth_header.startswith('Basic '):
return unauthorized_response()
try:
decoded = base64.b64decode(auth_header.split(' ', 1)[1]).decode('utf-8')
username, password = decoded.split(':', 1)
except Exception:
return unauthorized_response()
user_ok = hmac.compare_digest(username, expected_user)
pass_ok = hmac.compare_digest(password, expected_pass)
if not (user_ok and pass_ok):
return unauthorized_response()
return None
@app.after_request
def disable_api_cache(response):
"""Prevent stale API payloads from browser/proxy caches."""
if request.path.startswith('/api/'):
response.headers['Cache-Control'] = 'no-store, no-cache, must-revalidate, max-age=0'
response.headers['Pragma'] = 'no-cache'
response.headers['Expires'] = '0'
return response
# Configuration
PALLADIUM_RPC_HOST = os.getenv('PALLADIUM_RPC_HOST', 'palladiumd')
PALLADIUM_RPC_PORT = int(os.getenv('PALLADIUM_RPC_PORT', '2332'))
@@ -64,7 +138,49 @@ def parse_addnode_hosts(conf_path='/palladium-config/palladium.conf'):
return hosts
def probe_electrum_server(host, port=50001, timeout=1.2):
def parse_services_ports(services):
"""Extract TCP/SSL ports from SERVICES string (e.g. tcp://0.0.0.0:50001,ssl://0.0.0.0:50002)."""
tcp_port = None
ssl_port = None
for item in services.split(','):
item = item.strip()
if item.startswith('tcp://') and ':' in item:
try:
tcp_port = int(item.rsplit(':', 1)[1])
except ValueError:
pass
if item.startswith('ssl://') and ':' in item:
try:
ssl_port = int(item.rsplit(':', 1)[1])
except ValueError:
pass
return tcp_port, ssl_port
def get_electrumx_service_ports():
"""
Resolve ElectrumX service ports dynamically.
Priority:
1) SERVICES env from electrumx container
2) local SERVICES env (if provided)
"""
try:
import subprocess
result = subprocess.run(
['docker', 'exec', 'electrumx-server', 'sh', '-c', 'printf "%s" "${SERVICES:-}"'],
capture_output=True,
text=True,
timeout=2
)
if result.returncode == 0 and result.stdout.strip():
return parse_services_ports(result.stdout.strip())
except Exception:
pass
return parse_services_ports(os.getenv('SERVICES', ''))
def probe_electrum_server(host, port, timeout=2.0):
"""Check if an Electrum server is reachable and speaking protocol on host:port"""
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
@@ -76,23 +192,148 @@ def probe_electrum_server(host, port=50001, timeout=1.2):
"method": "server.version",
"params": ["palladium-dashboard", "1.4"]
}
sock.send((json.dumps(request) + '\n').encode())
response = sock.recv(4096).decode()
sock.sendall((json.dumps(request) + '\n').encode())
data = b""
for _ in range(6):
chunk = sock.recv(4096)
if not chunk:
break
data += chunk
candidate = data.decode(errors='ignore')
candidate = candidate.split("\n", 1)[0].strip()
if candidate:
try:
payload = json.loads(candidate)
sock.close()
return 'result' in payload
except Exception:
pass
sock.close()
data = json.loads(response)
if 'result' in data:
line = data.split(b"\n", 1)[0].decode(errors='ignore').strip() if data else ""
payload = json.loads(line) if line else {}
if 'result' in payload:
return True
except Exception:
return False
return False
def probe_electrum_server_ssl(host, port, timeout=2.0):
"""Check if an Electrum SSL server is reachable on host:port (self-signed allowed)."""
try:
raw_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
raw_sock.settimeout(timeout)
raw_sock.connect((host, port))
context = ssl.create_default_context()
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
ssl_sock = context.wrap_socket(raw_sock, server_hostname=host)
ssl_sock.settimeout(timeout)
request = {
"jsonrpc": "2.0",
"id": 101,
"method": "server.version",
"params": ["palladium-dashboard", "1.4"]
}
ssl_sock.sendall((json.dumps(request) + '\n').encode())
data = b""
for _ in range(6):
chunk = ssl_sock.recv(4096)
if not chunk:
break
data += chunk
candidate = data.decode(errors='ignore')
candidate = candidate.split("\n", 1)[0].strip()
if candidate:
try:
payload = json.loads(candidate)
ssl_sock.close()
return 'result' in payload
except Exception:
pass
ssl_sock.close()
line = data.split(b"\n", 1)[0].decode(errors='ignore').strip() if data else ""
payload = json.loads(line) if line else {}
if 'result' in payload:
return True
except Exception:
return False
return False
def get_electrum_server_genesis(host, tcp_port=None, ssl_port=None, timeout=2.0):
"""Return peer genesis_hash via server.features, trying TCP first then SSL."""
request = {
"jsonrpc": "2.0",
"id": 102,
"method": "server.features",
"params": []
}
if tcp_port:
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
sock.connect((host, tcp_port))
sock.sendall((json.dumps(request) + '\n').encode())
data = b""
for _ in range(6):
chunk = sock.recv(4096)
if not chunk:
break
data += chunk
candidate = data.decode(errors='ignore').split("\n", 1)[0].strip()
if candidate:
payload = json.loads(candidate)
genesis = (payload.get('result') or {}).get('genesis_hash')
sock.close()
return genesis
sock.close()
except Exception:
pass
if ssl_port:
try:
raw_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
raw_sock.settimeout(timeout)
raw_sock.connect((host, ssl_port))
context = ssl.create_default_context()
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
ssl_sock = context.wrap_socket(raw_sock, server_hostname=host)
ssl_sock.settimeout(timeout)
ssl_sock.sendall((json.dumps(request) + '\n').encode())
data = b""
for _ in range(6):
chunk = ssl_sock.recv(4096)
if not chunk:
break
data += chunk
candidate = data.decode(errors='ignore').split("\n", 1)[0].strip()
if candidate:
payload = json.loads(candidate)
genesis = (payload.get('result') or {}).get('genesis_hash')
ssl_sock.close()
return genesis
ssl_sock.close()
except Exception:
pass
return None
def is_electrumx_reachable(timeout=1.0):
"""Fast ElectrumX liveness check used by /api/health"""
tcp_port, _ = get_electrumx_service_ports()
if not tcp_port:
return False
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
sock.connect((ELECTRUMX_RPC_HOST, 50001))
sock.connect((ELECTRUMX_RPC_HOST, tcp_port))
request = {
"jsonrpc": "2.0",
"id": 999,
@@ -209,11 +450,14 @@ def get_electrumx_stats(include_addnode_probes=False):
import subprocess
from datetime import datetime
local_tcp_port, local_ssl_port = get_electrumx_service_ports()
stats = {
'server_version': 'Unknown',
'protocol_min': '',
'protocol_max': '',
'genesis_hash': '',
'genesis_hash_full': '',
'hash_function': '',
'pruning': None,
'sessions': 0,
@@ -225,8 +469,8 @@ def get_electrumx_stats(include_addnode_probes=False):
'subs': 0,
'uptime': 0,
'db_size': 0,
'tcp_port': 50001,
'ssl_port': 50002,
'tcp_port': str(local_tcp_port) if local_tcp_port else None,
'ssl_port': str(local_ssl_port) if local_ssl_port else None,
'server_ip': 'Unknown'
}
@@ -252,9 +496,11 @@ def get_electrumx_stats(include_addnode_probes=False):
# Get server features via Electrum protocol
try:
if not local_tcp_port:
raise RuntimeError("SERVICES tcp port not configured")
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5)
sock.connect((ELECTRUMX_RPC_HOST, 50001))
sock.connect((ELECTRUMX_RPC_HOST, local_tcp_port))
request = {
"jsonrpc": "2.0",
@@ -270,20 +516,35 @@ def get_electrumx_stats(include_addnode_probes=False):
data = json.loads(response)
if 'result' in data:
result = data['result']
full_genesis = result.get('genesis_hash', '')
stats['server_version'] = result.get('server_version', 'Unknown')
stats['protocol_min'] = result.get('protocol_min', '')
stats['protocol_max'] = result.get('protocol_max', '')
stats['genesis_hash'] = result.get('genesis_hash', '')[:16] + '...'
stats['genesis_hash_full'] = full_genesis
stats['genesis_hash'] = (full_genesis[:16] + '...') if full_genesis else ''
stats['hash_function'] = result.get('hash_function', '')
stats['pruning'] = result.get('pruning')
except Exception as e:
print(f"ElectrumX protocol error: {e}")
# Fallback: derive expected network genesis from local Palladium node
# if Electrum server.features is temporarily unavailable.
if not stats.get('genesis_hash_full'):
try:
local_genesis = palladium_rpc_call('getblockhash', [0])
if isinstance(local_genesis, str) and local_genesis:
stats['genesis_hash_full'] = local_genesis
stats['genesis_hash'] = local_genesis[:16] + '...'
except Exception as e:
print(f"Local genesis fallback error: {e}")
# Get peers discovered by ElectrumX
try:
if not local_tcp_port:
raise RuntimeError("SERVICES tcp port not configured")
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(5)
sock.connect((ELECTRUMX_RPC_HOST, 50001))
sock.connect((ELECTRUMX_RPC_HOST, local_tcp_port))
request = {
"jsonrpc": "2.0",
@@ -315,7 +576,9 @@ def get_electrumx_stats(include_addnode_probes=False):
peers.append({
'host': host,
'tcp_port': tcp_port,
'ssl_port': ssl_port
'ssl_port': ssl_port,
'tcp_reachable': None,
'ssl_reachable': None
})
stats['active_servers'] = peers
@@ -325,25 +588,35 @@ def get_electrumx_stats(include_addnode_probes=False):
# Keep peers list without self for dashboard card count
try:
merged = []
seen = set()
merged_by_host = {}
self_host = (stats.get('server_ip') or '').strip()
for peer in (stats.get('active_servers') or []):
host = (peer.get('host') or '').strip()
tcp_port = str(peer.get('tcp_port') or '50001')
tcp_port = str(peer.get('tcp_port')) if peer.get('tcp_port') else None
ssl_port = str(peer.get('ssl_port')) if peer.get('ssl_port') else None
if not host:
continue
if self_host and host == self_host:
continue
key = f"{host}:{tcp_port}"
if key in seen:
continue
seen.add(key)
merged.append({
'host': host,
'tcp_port': tcp_port,
'ssl_port': peer.get('ssl_port')
})
existing = merged_by_host.get(host)
if not existing:
merged_by_host[host] = {
'host': host,
'tcp_port': tcp_port,
'ssl_port': ssl_port,
'tcp_reachable': peer.get('tcp_reachable'),
'ssl_reachable': peer.get('ssl_reachable')
}
else:
if not existing.get('tcp_port') and tcp_port:
existing['tcp_port'] = tcp_port
if not existing.get('ssl_port') and ssl_port:
existing['ssl_port'] = ssl_port
if existing.get('tcp_reachable') is None and peer.get('tcp_reachable') is not None:
existing['tcp_reachable'] = peer.get('tcp_reachable')
if existing.get('ssl_reachable') is None and peer.get('ssl_reachable') is not None:
existing['ssl_reachable'] = peer.get('ssl_reachable')
merged = list(merged_by_host.values())
stats['active_servers'] = merged
stats['active_servers_count'] = len(merged)
except Exception as e:
@@ -355,32 +628,85 @@ def get_electrumx_stats(include_addnode_probes=False):
addnode_hosts = parse_addnode_hosts()
extra_servers = []
for host in addnode_hosts:
if probe_electrum_server(host, 50001, timeout=0.5):
tcp_ok = probe_electrum_server(host, local_tcp_port, timeout=2.0) if local_tcp_port else False
ssl_ok = probe_electrum_server_ssl(host, local_ssl_port, timeout=2.0) if local_ssl_port else False
if tcp_ok or ssl_ok:
extra_servers.append({
'host': host,
'tcp_port': '50001',
'ssl_port': None
'tcp_port': str(local_tcp_port) if tcp_ok and local_tcp_port else None,
'ssl_port': str(local_ssl_port) if ssl_ok and local_ssl_port else None,
'tcp_reachable': tcp_ok,
'ssl_reachable': ssl_ok
})
merged = []
seen = set()
merged_by_host = {}
self_host = (stats.get('server_ip') or '').strip()
for peer in (stats.get('active_servers') or []) + extra_servers:
host = (peer.get('host') or '').strip()
tcp_port = str(peer.get('tcp_port') or '50001')
tcp_port = str(peer.get('tcp_port')) if peer.get('tcp_port') else None
ssl_port = str(peer.get('ssl_port')) if peer.get('ssl_port') else None
if not host:
continue
if self_host and host == self_host:
continue
key = f"{host}:{tcp_port}"
if key in seen:
existing = merged_by_host.get(host)
if not existing:
merged_by_host[host] = {
'host': host,
'tcp_port': tcp_port,
'ssl_port': ssl_port,
'tcp_reachable': peer.get('tcp_reachable'),
'ssl_reachable': peer.get('ssl_reachable')
}
else:
if not existing.get('tcp_port') and tcp_port:
existing['tcp_port'] = tcp_port
if not existing.get('ssl_port') and ssl_port:
existing['ssl_port'] = ssl_port
if existing.get('tcp_reachable') is None and peer.get('tcp_reachable') is not None:
existing['tcp_reachable'] = peer.get('tcp_reachable')
if existing.get('ssl_reachable') is None and peer.get('ssl_reachable') is not None:
existing['ssl_reachable'] = peer.get('ssl_reachable')
merged = list(merged_by_host.values())
# Probe merged list so summary can report both TCP and SSL reachability
for peer in merged:
host = peer.get('host')
if not host:
continue
seen.add(key)
merged.append({
'host': host,
'tcp_port': tcp_port,
'ssl_port': peer.get('ssl_port')
})
peer_tcp_port = int(peer.get('tcp_port')) if str(peer.get('tcp_port', '')).isdigit() else local_tcp_port
peer_ssl_port = int(peer.get('ssl_port')) if str(peer.get('ssl_port', '')).isdigit() else local_ssl_port
if peer.get('tcp_reachable') is None and peer_tcp_port:
peer['tcp_reachable'] = probe_electrum_server(host, peer_tcp_port, timeout=2.0)
if peer.get('ssl_reachable') is None and peer_ssl_port:
peer['ssl_reachable'] = probe_electrum_server_ssl(host, peer_ssl_port, timeout=2.0)
if peer.get('tcp_reachable') is True and not peer.get('tcp_port') and peer_tcp_port:
peer['tcp_port'] = str(peer_tcp_port)
if peer.get('ssl_reachable') is True and not peer.get('ssl_port') and peer_ssl_port:
peer['ssl_port'] = str(peer_ssl_port)
# Keep only peers matching local Electrum network (same genesis hash).
expected_genesis = (stats.get('genesis_hash_full') or '').strip().lower()
if expected_genesis:
filtered = []
for peer in merged:
host = peer.get('host')
if not host:
continue
if peer.get('tcp_reachable') is not True and peer.get('ssl_reachable') is not True:
continue
peer_tcp_port = int(peer.get('tcp_port')) if str(peer.get('tcp_port', '')).isdigit() else None
peer_ssl_port = int(peer.get('ssl_port')) if str(peer.get('ssl_port', '')).isdigit() else None
peer_genesis = get_electrum_server_genesis(
host,
tcp_port=peer_tcp_port,
ssl_port=peer_ssl_port,
timeout=2.0
)
if peer_genesis and peer_genesis.strip().lower() == expected_genesis:
filtered.append(peer)
merged = filtered
stats['active_servers'] = merged
stats['active_servers_count'] = len(merged)
@@ -439,9 +765,11 @@ def get_electrumx_stats(include_addnode_probes=False):
# Count active connections (TCP sessions)
try:
if not local_tcp_port:
raise RuntimeError("SERVICES tcp port not configured")
result = subprocess.run(
['docker', 'exec', 'electrumx-server', 'sh', '-c',
'netstat -an 2>/dev/null | grep ":50001.*ESTABLISHED" | wc -l'],
f'netstat -an 2>/dev/null | grep ":{local_tcp_port}.*ESTABLISHED" | wc -l'],
capture_output=True,
text=True,
timeout=2
@@ -558,12 +886,12 @@ def electrumx_stats():
try:
stats = get_electrumx_stats_cached(include_addnode_probes=False)
if stats:
# If fast path reports no servers, reuse full servers cache if available.
if (stats.get('active_servers_count') or 0) == 0:
heavy_stats = get_electrumx_stats_cached(include_addnode_probes=True)
if heavy_stats and (heavy_stats.get('active_servers_count') or 0) > 0:
stats['active_servers'] = heavy_stats.get('active_servers', [])
stats['active_servers_count'] = heavy_stats.get('active_servers_count', 0)
# Keep dashboard card aligned with /api/electrumx/servers:
# always prefer the full discovery view for active servers.
heavy_stats = get_electrumx_stats_cached(include_addnode_probes=True)
if heavy_stats:
stats['active_servers'] = heavy_stats.get('active_servers', [])
stats['active_servers_count'] = heavy_stats.get('active_servers_count', 0)
# Get additional info from logs if available
try:
@@ -601,11 +929,6 @@ def electrumx_servers():
return jsonify({'error': 'Cannot connect to ElectrumX'}), 500
servers = stats.get('active_servers') or []
if len(servers) == 0:
# Fallback to fast discovery results if full probing is temporarily empty.
fast_stats = get_electrumx_stats_cached(include_addnode_probes=False)
if fast_stats:
servers = fast_stats.get('active_servers') or []
return jsonify({
'servers': servers,
'total': len(servers),

View File

@@ -217,10 +217,10 @@ async function updateElectrumXStats() {
document.getElementById('serverIP').textContent = data.stats.server_ip || '--';
// TCP Port
document.getElementById('tcpPort').textContent = data.stats.tcp_port || 50001;
document.getElementById('tcpPort').textContent = data.stats.tcp_port || '--';
// SSL Port
document.getElementById('sslPort').textContent = data.stats.ssl_port || 50002;
document.getElementById('sslPort').textContent = data.stats.ssl_port || '--';
// Active servers from peer discovery
const activeServers = Array.isArray(data.stats.active_servers) ? data.stats.active_servers : [];

View File

@@ -20,9 +20,8 @@ async function updateElectrumServers() {
tbody.innerHTML = '';
if (servers.length === 0) {
tbody.innerHTML = '<tr><td colspan="3" class="loading">No active servers found</td></tr>';
tbody.innerHTML = '<tr><td colspan="5" class="loading">No active servers found</td></tr>';
document.getElementById('totalServers').textContent = '0';
document.getElementById('tcpReachable').textContent = '0';
return;
}
@@ -32,17 +31,17 @@ async function updateElectrumServers() {
<td class="peer-addr">${server.host || '--'}</td>
<td>${server.tcp_port || '--'}</td>
<td>${server.ssl_port || '--'}</td>
<td>${server.tcp_reachable === true ? 'Yes' : 'No'}</td>
<td>${server.ssl_reachable === true ? 'Yes' : 'No'}</td>
`;
tbody.appendChild(row);
});
document.getElementById('totalServers').textContent = String(servers.length);
const tcpCount = servers.filter(s => !!s.tcp_port).length;
document.getElementById('tcpReachable').textContent = String(tcpCount);
} catch (error) {
console.error('Error fetching Electrum servers:', error);
document.getElementById('electrumServersTable').innerHTML =
'<tr><td colspan="3" class="loading">Error loading servers</td></tr>';
'<tr><td colspan="5" class="loading">Error loading servers</td></tr>';
}
}

View File

@@ -33,10 +33,6 @@
<div class="stat-label">Total Active Servers</div>
<div class="stat-value" id="totalServers">--</div>
</div>
<div class="stat-item">
<div class="stat-label">TCP 50001 Reachable</div>
<div class="stat-value" id="tcpReachable">--</div>
</div>
</div>
</div>
</div>
@@ -54,10 +50,12 @@
<th>Host</th>
<th>TCP Port</th>
<th>SSL Port</th>
<th>TCP Reachable</th>
<th>SSL Reachable</th>
</tr>
</thead>
<tbody id="electrumServersTable">
<tr><td colspan="3" class="loading">Loading servers...</td></tr>
<tr><td colspan="5" class="loading">Loading servers...</td></tr>
</tbody>
</table>
</div>
@@ -70,6 +68,6 @@
</footer>
</div>
<script src="{{ url_for('static', filename='electrum_servers.js') }}?v=1"></script>
<script src="{{ url_for('static', filename='electrum_servers.js') }}?v=3"></script>
</body>
</html>