Compare commits

...

19 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
3ca12b2ec0 style(dashboard): remove card header icons from dashboard templates 2026-02-13 11:18:55 +01:00
48354b7041 Update .gitignore 2026-02-13 11:14:09 +01:00
10ad8dc680 feat(dashboard): add Electrum active servers page and optimize ElectrumX card/perf 2026-02-13 11:12:29 +01:00
4f528f8a1e Auto-detect public IP for ElectrumX peer announcement 2026-02-13 09:33:58 +01:00
f631edfc4d Add daemon binaries setup documentation 2026-02-13 08:56:06 +01:00
2ed5c746a9 Add Palladium seed peers and enable peer discovery 2026-02-13 08:49:31 +01:00
9cb7922ac1 Add server donation address 2026-02-13 08:36:18 +01:00
3ce18c9860 Fix PEER_DEFAULT_PORTS to match actual ElectrumX service ports 2026-02-13 00:56:17 +01:00
5cc3dcacc8 Use coins_plm.py as single source of truth for ElectrumX patch
- Remove inline Palladium coin definition from Dockerfile.electrumx
  and COPY coins_plm.py instead, reducing duplication
- Add TX_COUNT/TX_COUNT_HEIGHT/TX_PER_BLOCK to coins_plm.py with
  real chain data (457478 txs at height 382404)
- Update entrypoint.sh to fetch live TX stats from palladiumd at
  container startup, keeping sync estimates accurate automatically
2026-02-13 00:50:41 +01:00
22 changed files with 1149 additions and 212 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

19
.gitignore vendored
View File

@@ -1,3 +1,9 @@
# SSL certificates (auto-generated or user-provided)
/certs/
# Environment file
.env
# ElectrumX server data
/electrumx-data/
@@ -26,4 +32,15 @@ Thumbs.db
# Temporary files
*.tmp
*.temp
*.temp
# 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,116 +1,32 @@
FROM lukechilds/electrumx
RUN python3 - <<'PY'
import pathlib
p = pathlib.Path('/usr/local/lib/python3.13/dist-packages/electrumx/lib/coins.py')
s = p.read_text(encoding='utf-8')
# Install curl (needed by entrypoint for RPC calls and IP detection)
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/*
# Define Palladium coin classes directly in coins.py to avoid circular imports
palladium_classes = '''
# Copy Palladium coin definition and patch ElectrumX
COPY electrumx-patch/coins_plm.py /tmp/coins_plm.py
# Palladium (PLM) - Bitcoin-based cryptocurrency
class Palladium(Bitcoin):
NAME = "Palladium"
SHORTNAME = "PLM"
NET = "mainnet"
RUN python3 - <<'PATCH'
import pathlib, re
# Address prefixes (same as Bitcoin mainnet)
P2PKH_VERBYTE = bytes([0x00])
P2SH_VERBYTE = bytes([0x05])
WIF_BYTE = bytes([0x80])
patch = pathlib.Path('/tmp/coins_plm.py').read_text()
# Extract only class definitions (skip import lines)
classes = re.split(r'^(?=class )', patch, flags=re.MULTILINE)
classes = [c for c in classes if c.strip().startswith('class ')]
body = '\n' + '\n'.join(classes)
# Bech32 prefix
HRP = "plm"
for target in [
'/usr/local/lib/python3.13/dist-packages/electrumx/lib/coins.py',
'/electrumx/src/electrumx/lib/coins.py',
]:
p = pathlib.Path(target)
if p.exists():
s = p.read_text()
if 'class Palladium(Bitcoin):' not in s:
p.write_text(s + body)
# Genesis hash (Bitcoin mainnet)
GENESIS_HASH = "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f"
# Clear inherited checkpoints
CHECKPOINTS = []
# Network statistics (required by ElectrumX)
TX_COUNT = 1000
TX_COUNT_HEIGHT = 1
TX_PER_BLOCK = 4
# Default ports
RPC_PORT = 2332
PEER_DEFAULT_PORTS = {'t': '2333', 's': '52333'}
# Deserializer
DESERIALIZER = lib_tx.DeserializerSegWit
class PalladiumTestnet(Palladium):
NAME = "Palladium"
SHORTNAME = "tPLM"
NET = "testnet"
# Testnet address prefixes
P2PKH_VERBYTE = bytes([0x7f]) # 127 decimal - addresses start with 't'
P2SH_VERBYTE = bytes([0x73]) # 115 decimal
WIF_BYTE = bytes([0xff]) # 255 decimal
# Bech32 prefix for testnet
HRP = "tplm"
# Genesis hash (Bitcoin testnet)
GENESIS_HASH = "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"
# Network statistics (required by ElectrumX)
TX_COUNT = 500
TX_COUNT_HEIGHT = 1
TX_PER_BLOCK = 2
# Testnet ports
RPC_PORT = 12332
PEER_DEFAULT_PORTS = {'t': '12333', 's': '62333'}
'''
# Add Palladium classes if not already present
if 'class Palladium(Bitcoin):' not in s:
s += palladium_classes
p.write_text(s, encoding='utf-8')
# Also patch the source file used by the container
p_src = pathlib.Path('/electrumx/src/electrumx/lib/coins.py')
if p_src.exists():
s_src = p_src.read_text(encoding='utf-8')
if 'class Palladium(Bitcoin):' not in s_src:
s_src += palladium_classes
p_src.write_text(s_src, encoding='utf-8')
print('>> Patched ElectrumX with Palladium and PalladiumTestnet coins')
PY
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
print('>> Patched ElectrumX with Palladium coin classes')
PATCH
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/)
---

67
daemon/README.md Normal file
View File

@@ -0,0 +1,67 @@
# Palladium Core Binaries
This directory must contain the pre-compiled Palladium Core binaries used by the Docker node container.
## Download
Download from the official release repository: **https://github.com/palladium-coin/palladiumcore/releases/latest**
## Architecture
The binaries **must match the host architecture** where Docker is running.
Choose the correct archive for your platform:
| Host Architecture | Archive to download | Common hardware |
|-------------------|-------------------------|-------------------------------------|
| `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:
```bash
uname -m
```
## Required binaries
Extract the following files from the release archive and place them in this directory:
```
daemon/
palladiumd # Full node daemon (required)
palladium-cli # RPC command-line client (required)
palladium-tx # Transaction utility
palladium-wallet # Wallet utility
```
## Quick setup
### Example for x86_64 (VPS/PC)
```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:
```bash
docker compose build palladiumd
```

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
@@ -57,11 +55,14 @@ services:
SSL_KEYFILE: "/certs/server.key"
DB_DIRECTORY: "/data"
PEER_DISCOVERY: "off"
PEER_ANNOUNCE: "false"
PEER_DISCOVERY: "on"
PEER_ANNOUNCE: "true"
# REPORT_SERVICES is auto-detected at startup. Set manually only to override.
# REPORT_SERVICES: "tcp://your.public.ip:50001,ssl://your.public.ip:50002"
INITIAL_CONCURRENT: "2"
COST_SOFT_LIMIT: "0"
COST_HARD_LIMIT: "0"
DONATION_ADDRESS: "plm1qdq3gu2zvg9lyr8gxd6yln4wavc5tlp8prmvfay"
ulimits:
nofile:
@@ -71,6 +72,7 @@ services:
volumes:
- ./electrumx-data:/data
- ./.palladium/palladium.conf:/palladium-config/palladium.conf:ro
- ./certs:/certs
dashboard:
build:
@@ -90,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

@@ -21,9 +21,22 @@ class Palladium(Bitcoin):
# Since we share Genesis with Bitcoin, we must clear inherited checkpoints
CHECKPOINTS = []
# === Network statistics (required by ElectrumX) ===
TX_COUNT = 457478
TX_COUNT_HEIGHT = 382404
TX_PER_BLOCK = 2
# === Default ports ===
RPC_PORT = 2332
PEER_DEFAULT_PORTS = {'t': '2333', 's': '52333'}
PEER_DEFAULT_PORTS = {'t': '50001', 's': '50002'}
# === Seed peers for discovery ===
PEERS = [
'66.94.115.80 t',
'89.117.149.130 t',
'173.212.224.67 t',
'82.165.218.152 t',
]
# === Deserializer ===
DESERIALIZER = lib_tx.DeserializerSegWit
@@ -45,7 +58,12 @@ class PalladiumTestnet(Palladium):
# === Genesis hash (Bitcoin testnet) ===
GENESIS_HASH = "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943"
# === Network statistics (required by ElectrumX) ===
TX_COUNT = 500
TX_COUNT_HEIGHT = 1
TX_PER_BLOCK = 2
# === Testnet ports ===
RPC_PORT = 12332
PEER_DEFAULT_PORTS = {'t': '12333', 's': '62333'}
PEER_DEFAULT_PORTS = {'t': '60001', 's': '60002'}

View File

@@ -43,6 +43,22 @@ fi
# Build DAEMON_URL with extracted credentials
export DAEMON_URL="http://${RPC_USER}:${RPC_PASSWORD}@palladiumd:${RPC_PORT}/"
# Auto-detect public IP and set REPORT_SERVICES for peer discovery
if [ -z "$REPORT_SERVICES" ]; then
echo "REPORT_SERVICES not set, detecting public IP..."
for url in https://icanhazip.com https://ifconfig.me https://api.ipify.org; do
PUBLIC_IP=$(curl -sf --max-time 5 "$url" 2>/dev/null | tr -d '[:space:]')
if [ -n "$PUBLIC_IP" ]; then
export REPORT_SERVICES="tcp://${PUBLIC_IP}:50001,ssl://${PUBLIC_IP}:50002"
echo ">> Auto-detected REPORT_SERVICES: ${REPORT_SERVICES}"
break
fi
done
if [ -z "$REPORT_SERVICES" ]; then
echo ">> WARNING: Could not detect public IP. Peer discovery will not announce this server."
fi
fi
echo "=========================================="
echo "ElectrumX Configuration"
echo "=========================================="
@@ -51,7 +67,99 @@ echo "Network: ${NET}"
echo "RPC User: ${RPC_USER}"
echo "RPC Port: ${RPC_PORT}"
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}" \
--data-binary '{"jsonrpc":"1.0","method":"getchaintxstats","params":[]}' \
-H 'Content-Type: text/plain;' \
"http://palladiumd:${RPC_PORT}/" 2>/dev/null || true)
if [ -n "$TX_STATS" ]; then
TX_COUNT=$(echo "$TX_STATS" | python3 -c "import sys,json; print(json.load(sys.stdin)['result']['txcount'])" 2>/dev/null || true)
TX_HEIGHT=$(echo "$TX_STATS" | python3 -c "import sys,json; print(json.load(sys.stdin)['result']['window_final_block_height'])" 2>/dev/null || true)
if [ -n "$TX_COUNT" ] && [ -n "$TX_HEIGHT" ]; then
echo "Patching coins.py: TX_COUNT=${TX_COUNT}, TX_COUNT_HEIGHT=${TX_HEIGHT}"
python3 - "$TX_COUNT" "$TX_HEIGHT" <<'TXPATCH'
import sys, pathlib, re
tx_count, tx_height = sys.argv[1], sys.argv[2]
for target in [
'/usr/local/lib/python3.13/dist-packages/electrumx/lib/coins.py',
'/electrumx/src/electrumx/lib/coins.py',
]:
p = pathlib.Path(target)
if not p.exists():
continue
s = p.read_text()
s = re.sub(r'(class Palladium\(Bitcoin\):.*?TX_COUNT\s*=\s*)\d+', rf'\g<1>{tx_count}', s, count=1, flags=re.DOTALL)
s = re.sub(r'(class Palladium\(Bitcoin\):.*?TX_COUNT_HEIGHT\s*=\s*)\d+', rf'\g<1>{tx_height}', s, count=1, flags=re.DOTALL)
p.write_text(s)
TXPATCH
echo ">> TX stats updated from live node"
else
echo ">> Could not parse tx stats, using defaults from coins_plm.py"
fi
else
echo ">> Node not reachable yet, using defaults from coins_plm.py"
fi
# Execute the original electrumx command
exec /usr/local/bin/electrumx_server

View File

@@ -9,6 +9,12 @@ import requests
import json
import os
import time
import copy
import threading
import ssl
import ipaddress
import base64
import hmac
from datetime import datetime
import psutil
import socket
@@ -16,11 +22,369 @@ 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'))
ELECTRUMX_RPC_HOST = os.getenv('ELECTRUMX_RPC_HOST', 'electrumx')
ELECTRUMX_RPC_PORT = int(os.getenv('ELECTRUMX_RPC_PORT', '8000'))
ELECTRUMX_STATS_TTL = int(os.getenv('ELECTRUMX_STATS_TTL', '60'))
ELECTRUMX_SERVERS_TTL = int(os.getenv('ELECTRUMX_SERVERS_TTL', '120'))
ELECTRUMX_EMPTY_SERVERS_TTL = int(os.getenv('ELECTRUMX_EMPTY_SERVERS_TTL', '15'))
# In-memory caches for fast card stats and heavier server probing stats
_electrumx_stats_cache = {'timestamp': 0.0, 'stats': None}
_electrumx_servers_cache = {'timestamp': 0.0, 'stats': None}
def warm_electrumx_caches_async():
"""Pre-warm caches in background to reduce first-load latency."""
def _worker():
try:
get_electrumx_stats_cached(force_refresh=True, include_addnode_probes=False)
get_electrumx_stats_cached(force_refresh=True, include_addnode_probes=True)
except Exception as e:
print(f"ElectrumX cache warmup error: {e}")
threading.Thread(target=_worker, daemon=True).start()
def parse_addnode_hosts(conf_path='/palladium-config/palladium.conf'):
"""Extract addnode hosts from palladium.conf"""
hosts = []
try:
with open(conf_path, 'r') as f:
for raw_line in f:
line = raw_line.strip()
if not line or line.startswith('#') or not line.startswith('addnode='):
continue
value = line.split('=', 1)[1].strip()
if not value:
continue
host = value.rsplit(':', 1)[0] if ':' in value else value
if host and host not in hosts:
hosts.append(host)
except Exception as e:
print(f"Error parsing addnode hosts: {e}")
return hosts
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)
sock.settimeout(timeout)
sock.connect((host, port))
request = {
"jsonrpc": "2.0",
"id": 100,
"method": "server.version",
"params": ["palladium-dashboard", "1.4"]
}
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()
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, tcp_port))
request = {
"jsonrpc": "2.0",
"id": 999,
"method": "server.version",
"params": ["palladium-health", "1.4"]
}
sock.send((json.dumps(request) + '\n').encode())
response = sock.recv(4096).decode()
sock.close()
data = json.loads(response)
return 'result' in data
except Exception:
return False
def is_electrumx_reachable_retry():
"""Retry-aware liveness check to avoid transient false negatives."""
if is_electrumx_reachable(timeout=1.2):
return True
time.sleep(0.15)
return is_electrumx_reachable(timeout=2.0)
def get_electrumx_stats_cached(force_refresh=False, include_addnode_probes=False):
"""Return cached ElectrumX stats unless cache is stale."""
cache = _electrumx_servers_cache if include_addnode_probes else _electrumx_stats_cache
ttl = ELECTRUMX_SERVERS_TTL if include_addnode_probes else ELECTRUMX_STATS_TTL
now = time.time()
cached = cache.get('stats')
cached_ts = cache.get('timestamp', 0.0)
if (
include_addnode_probes
and cached is not None
and (cached.get('active_servers_count', 0) == 0)
):
ttl = ELECTRUMX_EMPTY_SERVERS_TTL
if not force_refresh and cached is not None and (now - cached_ts) < ttl:
return copy.deepcopy(cached)
fresh = get_electrumx_stats(include_addnode_probes=include_addnode_probes)
if fresh is not None:
cache['timestamp'] = now
cache['stats'] = fresh
return copy.deepcopy(fresh)
# Fallback to stale cache if fresh fetch fails
if cached is not None:
return copy.deepcopy(cached)
return None
# Read RPC credentials from palladium.conf
def get_rpc_credentials():
@@ -78,7 +442,7 @@ def palladium_rpc_call(method, params=None):
print(f"RPC call error ({method}): {e}")
return None
def get_electrumx_stats():
def get_electrumx_stats(include_addnode_probes=False):
"""Get ElectrumX statistics via Electrum protocol and system info"""
try:
import socket
@@ -86,27 +450,34 @@ def get_electrumx_stats():
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,
'peer_discovery': 'unknown',
'peer_announce': 'unknown',
'active_servers': [],
'active_servers_count': 0,
'requests': 0,
'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'
}
# Get server IP address
try:
# Try to get public IP from external service
response = requests.get('https://api.ipify.org?format=json', timeout=3)
response = requests.get('https://api.ipify.org?format=json', timeout=0.4)
if response.status_code == 200:
stats['server_ip'] = response.json().get('ip', 'Unknown')
else:
@@ -125,9 +496,11 @@ def get_electrumx_stats():
# 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",
@@ -143,15 +516,220 @@ def get_electrumx_stats():
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, local_tcp_port))
request = {
"jsonrpc": "2.0",
"id": 2,
"method": "server.peers.subscribe",
"params": []
}
sock.send((json.dumps(request) + '\n').encode())
response = sock.recv(65535).decode()
sock.close()
data = json.loads(response)
if 'result' in data and isinstance(data['result'], list):
peers = []
for peer in data['result']:
if not isinstance(peer, list) or len(peer) < 3:
continue
host = peer[1]
features = peer[2] if isinstance(peer[2], list) else []
tcp_port = None
ssl_port = None
for feat in features:
if isinstance(feat, str) and feat.startswith('t') and feat[1:].isdigit():
tcp_port = feat[1:]
if isinstance(feat, str) and feat.startswith('s') and feat[1:].isdigit():
ssl_port = feat[1:]
if host:
peers.append({
'host': host,
'tcp_port': tcp_port,
'ssl_port': ssl_port,
'tcp_reachable': None,
'ssl_reachable': None
})
stats['active_servers'] = peers
stats['active_servers_count'] = len(peers)
except Exception as e:
print(f"ElectrumX peers error: {e}")
# Keep peers list without self for dashboard card count
try:
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')) 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
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:
print(f"Electrum peers normalization error: {e}")
# Optional full probing for dedicated servers page
if include_addnode_probes:
try:
addnode_hosts = parse_addnode_hosts()
extra_servers = []
for host in addnode_hosts:
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': 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_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')) 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
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
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)
except Exception as e:
print(f"Supplemental Electrum discovery error: {e}")
# Read peer discovery/announce settings from electrumx container env
try:
result = subprocess.run(
['docker', 'exec', 'electrumx-server', 'sh', '-c',
'printf "%s|%s" "${PEER_DISCOVERY:-unknown}" "${PEER_ANNOUNCE:-unknown}"'],
capture_output=True,
text=True,
timeout=2
)
if result.returncode == 0:
values = (result.stdout or '').strip().split('|', 1)
if len(values) == 2:
stats['peer_discovery'] = values[0] or 'unknown'
stats['peer_announce'] = values[1] or 'unknown'
except Exception as e:
print(f"Peer discovery env read error: {e}")
# Try to get container stats via Docker
try:
# Get container uptime
@@ -187,9 +765,11 @@ def get_electrumx_stats():
# 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
@@ -215,6 +795,11 @@ def peers():
"""Serve peers page"""
return render_template('peers.html')
@app.route('/electrum-servers')
def electrum_servers():
"""Serve Electrum active servers page"""
return render_template('electrum_servers.html')
@app.route('/api/palladium/info')
def palladium_info():
"""Get Palladium node blockchain info"""
@@ -299,8 +884,15 @@ def recent_blocks():
def electrumx_stats():
"""Get ElectrumX server statistics"""
try:
stats = get_electrumx_stats()
stats = get_electrumx_stats_cached(include_addnode_probes=False)
if stats:
# 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:
# Try to get container stats
@@ -315,7 +907,6 @@ def electrumx_stats():
if result.returncode == 0:
# Process is running, add placeholder stats
stats['status'] = 'running'
stats['sessions'] = 0 # Will show 0 for now
stats['requests'] = 0
stats['subs'] = 0
except:
@@ -329,6 +920,23 @@ def electrumx_stats():
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/electrumx/servers')
def electrumx_servers():
"""Get active Electrum servers discovered by this node"""
try:
stats = get_electrumx_stats_cached(include_addnode_probes=True)
if not stats:
return jsonify({'error': 'Cannot connect to ElectrumX'}), 500
servers = stats.get('active_servers') or []
return jsonify({
'servers': servers,
'total': len(servers),
'timestamp': datetime.now().isoformat()
})
except Exception as e:
return jsonify({'error': str(e)}), 500
@app.route('/api/system/resources')
def system_resources():
"""Get system resource usage"""
@@ -363,7 +971,10 @@ def system_resources():
def health():
"""Health check endpoint"""
palladium_ok = palladium_rpc_call('getblockchaininfo') is not None
electrumx_ok = get_electrumx_stats() is not None
stats = get_electrumx_stats_cached(include_addnode_probes=False)
if not stats or stats.get('server_version') in (None, '', 'Unknown'):
stats = get_electrumx_stats_cached(force_refresh=True, include_addnode_probes=False)
electrumx_ok = bool(stats and (stats.get('server_version') not in (None, '', 'Unknown')))
return jsonify({
'status': 'healthy' if (palladium_ok and electrumx_ok) else 'degraded',
@@ -375,4 +986,5 @@ def health():
})
if __name__ == '__main__':
warm_electrumx_caches_async()
app.run(host='0.0.0.0', port=8080, debug=False)

View File

@@ -205,10 +205,6 @@ async function updateElectrumXStats() {
}
document.getElementById('serverVersion').textContent = serverVersion;
// Active sessions
const sessions = typeof data.stats.sessions === 'number' ? data.stats.sessions : '--';
document.getElementById('activeSessions').textContent = sessions;
// Database size
const dbSize = data.stats.db_size > 0 ? formatBytes(data.stats.db_size) : '--';
document.getElementById('dbSize').textContent = dbSize;
@@ -221,10 +217,14 @@ 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 : [];
document.getElementById('activeServersCount').textContent = data.stats.active_servers_count ?? activeServers.length;
}
} catch (error) {

View File

@@ -0,0 +1,56 @@
// Electrum Active Servers Page JavaScript
function updateLastUpdateTime() {
const now = new Date().toLocaleString();
document.getElementById('lastUpdate').textContent = now;
}
async function updateElectrumServers() {
try {
const response = await fetch('/api/electrumx/servers');
const data = await response.json();
if (data.error) {
console.error('Electrum servers error:', data.error);
return;
}
const servers = Array.isArray(data.servers) ? data.servers : [];
const tbody = document.getElementById('electrumServersTable');
tbody.innerHTML = '';
if (servers.length === 0) {
tbody.innerHTML = '<tr><td colspan="5" class="loading">No active servers found</td></tr>';
document.getElementById('totalServers').textContent = '0';
return;
}
servers.forEach(server => {
const row = document.createElement('tr');
row.innerHTML = `
<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);
} catch (error) {
console.error('Error fetching Electrum servers:', error);
document.getElementById('electrumServersTable').innerHTML =
'<tr><td colspan="5" class="loading">Error loading servers</td></tr>';
}
}
async function updateAll() {
updateLastUpdateTime();
await updateElectrumServers();
}
document.addEventListener('DOMContentLoaded', async () => {
await updateAll();
setInterval(updateAll, 10000);
});

View File

@@ -242,6 +242,12 @@ body {
word-break: break-word;
}
#activeServers {
font-size: 14px;
font-weight: 500;
line-height: 1.5;
}
/* Resource Monitoring */
.resource-item {
display: grid;

View File

@@ -0,0 +1,73 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Electrum Active Servers - Palladium Dashboard</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}?v=11">
</head>
<body>
<div class="container">
<header class="header">
<div class="header-content">
<h1>
<svg class="logo-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
<path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/>
</svg>
Electrum Active Servers
</h1>
<a href="/" class="back-button">
<span>← Back to Dashboard</span>
</a>
</div>
</header>
<div class="dashboard-grid">
<div class="card">
<div class="card-header">
<h2>Discovery Summary</h2>
</div>
<div class="card-content">
<div class="stat-grid">
<div class="stat-item">
<div class="stat-label">Total Active Servers</div>
<div class="stat-value" id="totalServers">--</div>
</div>
</div>
</div>
</div>
</div>
<div class="card full-width">
<div class="card-header">
<h2>Other Active Servers</h2>
</div>
<div class="card-content">
<div class="table-container">
<table class="blocks-table peers-table">
<thead>
<tr>
<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="5" class="loading">Loading servers...</td></tr>
</tbody>
</table>
</div>
</div>
</div>
<footer class="footer">
<p>Last updated: <span id="lastUpdate">--</span></p>
<p>Auto-refresh every 10 seconds</p>
</footer>
</div>
<script src="{{ url_for('static', filename='electrum_servers.js') }}?v=3"></script>
</body>
</html>

View File

@@ -4,7 +4,7 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Palladium & ElectrumX Dashboard</title>
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}?v=10">
<link rel="stylesheet" href="{{ url_for('static', filename='style.css') }}?v=11">
</head>
<body>
<div class="container">
@@ -30,7 +30,6 @@
<div class="card">
<div class="card-header">
<h2>System Resources</h2>
<span class="card-icon">💻</span>
</div>
<div class="card-content">
<div class="resource-item">
@@ -61,7 +60,6 @@
<div class="card">
<div class="card-header">
<h2>Palladium Node</h2>
<span class="card-icon">⛏️</span>
</div>
<div class="card-content">
<div class="stat-grid">
@@ -97,7 +95,6 @@
<div class="card">
<div class="card-header">
<h2>ElectrumX Server</h2>
<span class="card-icon"></span>
</div>
<div class="card-content">
<div class="electrumx-grid">
@@ -105,10 +102,6 @@
<div class="stat-label">Server Version</div>
<div class="stat-value" id="serverVersion">--</div>
</div>
<div class="stat-item">
<div class="stat-label">Active Sessions</div>
<div class="stat-value" id="activeSessions">--</div>
</div>
<div class="stat-item">
<div class="stat-label">Database Size</div>
<div class="stat-value" id="dbSize">--</div>
@@ -117,6 +110,10 @@
<div class="stat-label">Uptime</div>
<div class="stat-value" id="uptime">--</div>
</div>
<a href="/electrum-servers" class="stat-item stat-link">
<div class="stat-label">Active Servers</div>
<div class="stat-value" id="activeServersCount">--</div>
</a>
<div class="stat-item full-width">
<div class="stat-label">Server IP</div>
<div class="stat-value" id="serverIP">--</div>
@@ -137,7 +134,6 @@
<div class="card">
<div class="card-header">
<h2>Mempool</h2>
<span class="card-icon">📝</span>
</div>
<div class="card-content">
<div class="stat-grid">
@@ -165,7 +161,6 @@
<div class="card full-width">
<div class="card-header">
<h2>Recent Blocks</h2>
<span class="card-icon">🔗</span>
</div>
<div class="card-content">
<div class="table-container">
@@ -195,6 +190,6 @@
</footer>
</div>
<script src="{{ url_for('static', filename='dashboard.js') }}?v=10"></script>
<script src="{{ url_for('static', filename='dashboard.js') }}?v=11"></script>
</body>
</html>

View File

@@ -28,7 +28,6 @@
<div class="card">
<div class="card-header">
<h2>Connection Statistics</h2>
<span class="card-icon">📊</span>
</div>
<div class="card-content">
<div class="stat-grid">
@@ -57,7 +56,6 @@
<div class="card full-width">
<div class="card-header">
<h2>Connected Peers</h2>
<span class="card-icon">🌐</span>
</div>
<div class="card-content">
<div class="table-container">