1
0
Fork 0
forked from lthn/blockchain

Compare commits

...
Sign in to create a new pull request.

17 commits
dev ... dev

Author SHA1 Message Date
Claude
c6def43d69
feat: key bridge prototype — derive sidechain + document keys from Lethean spend key
One Lethean seed phrase derives keys for:
- Main chain (ed25519, native)
- HNS sidechain (secp256k1, derived via HKDF)
- Document signing (ed25519, for hash timestamping)
- Audit trail (ed25519, for revision verification)

Domain-separated HKDF ensures keys can't be reversed or collide.
Foundation for Web3 identity bridge and corporate document control.

Co-Authored-By: Charon <charon@lethean.io>
2026-04-03 15:03:07 +01:00
Claude
cbc49e93fe
docs: add TLD registrar model (Option B) to name registration plan
One constant change makes ALL names require authority signature.
Eliminates the 90K pre-registration race entirely. Lethean becomes
a proper TLD registrar for .lthn — curated namespace, revenue from
name sales, squatting impossible at protocol level.

Co-Authored-By: Charon <charon@lethean.io>
2026-04-03 14:55:19 +01:00
Claude
56d7607754
feat: mainnet name registration plan and batch tooling
90,041 names from HNS reserved list need pre-registering to prevent
squatting. Strategy: genesis seeds namereg wallets, staking funds the
rest, parallel wallets batch-register into multi-sig.

Co-Authored-By: Charon <charon@lethean.io>
2026-04-03 14:46:52 +01:00
Claude
a6773abaca
fix(pos): add --rpc-ignore-offline to all daemon configs
PoS mining requires this flag when the daemon has no peers. Without it,
the daemon's get_pos_mining_details RPC returns DISCONNECTED status and
the wallet refuses to mint.

First PoS blocks minted on testnet at height 12,382.

Co-Authored-By: Charon <charon@lethean.io>
2026-04-03 13:53:07 +01:00
Claude
1e565495bb
fix(docker): set empty ASSETS_WHITELIST_URL to prevent explorer crash on DNS failure
Co-Authored-By: Charon <charon@lethean.io>
2026-04-03 13:31:19 +01:00
Claude
3fd1af9824
feat(docker): single-container staking node for GUI integration
Dockerfile.staking-node — runs daemon + wallet with PoS staking in one
container using supervisord. No docker-compose needed.

For the CoreGUI desktop app to run on Windows/Mac/Linux:
  docker run -d -p 46941:36941 -p 46942:36942 -p 46944:36944 \
    -v lthn-chain:/data -v lthn-wallet:/wallet \
    -e WALLET_PASSWORD=pass \
    lthn/staking-node

Built-in helpers:
  docker exec <name> /status.sh         — chain + wallet status
  docker exec <name> /wallet-address.sh — get wallet address

Co-Authored-By: Charon <charon@lethean.io>
2026-04-03 13:20:40 +01:00
Claude
e1292a6445
feat(demos): expanded VHS demo library with live data
5 numbered demos using helper scripts against the live system:
01-install     — zero to running node in 30 seconds
02-chain-status — chain info, latest block, on-chain aliases
03-ecosystem-tour — full tour of all 8 services (teaser material)
04-mining      — pool stats, miner connection examples
05-exit-node   — VPN exit node setup and earning

New helpers: last-block.sh, pool-stats.sh, lns-status.sh, explorer-stats.sh
Removed old unnumbered demos.

Co-Authored-By: Charon <charon@lethean.io>
2026-04-03 13:11:47 +01:00
Claude
9e3c6c2223
fix(deploy): auto-generate JWT secret, include node/exit composes and helpers
Co-Authored-By: Charon <charon@lethean.io>
2026-04-03 12:37:57 +01:00
Claude
2ff53183c3
docs: add deployment options table and helper scripts to README
Co-Authored-By: Charon <charon@lethean.io>
2026-04-03 12:35:28 +01:00
Claude
96e79beef4
feat(demos): add ecosystem startup, wallet ops, LNS, and helper scripts
6 VHS terminal demos with reusable helper scripts:
- ecosystem-startup: full stack coming online
- wallet-basics: address, balance, chain info, aliases
- lns-dns: HTTP API and DNS resolution
- exit-node: VPN exit setup
- node-quickstart: 3-step home node
- health-check: all services green

Co-Authored-By: Charon <charon@lethean.io>
2026-04-03 12:34:00 +01:00
Claude
eebfb3f1f1
feat(demos): add VHS terminal demo GIFs for node, exit node, and health check
Rendered with charmbracelet/vhs. Tape scripts included for re-rendering.

Co-Authored-By: Charon <charon@lethean.io>
2026-04-03 12:25:51 +01:00
Claude
7af620e7f6
feat(docker): add home node and exit node compose files
docker-compose.node.yml — minimal chain node + wallet with PoS staking.
Home users run this to support the network and earn staking rewards.

docker-compose.exit.yml — full VPN exit node with WireGuard.
Home users run this to provide bandwidth via the Lethean dVPN and earn
LTHN. Includes chain node, wallet, WireGuard server, and a controller
that manages gateway peering and status reporting.

.env.example updated with exit node settings (public IP, name, max peers).

Co-Authored-By: Charon <charon@lethean.io>
2026-04-03 12:14:05 +01:00
Claude
1261d0d94e
fix(docker): correct LNS env vars in ecosystem compose (DAEMON_URL not DAEMON_RPC)
Co-Authored-By: Charon <charon@lethean.io>
2026-04-03 12:05:48 +01:00
Claude
87fbdbb08d
fix(docker): correct LNS env vars (DAEMON_URL not DAEMON_RPC), add HSD host access, fix trade-frontend API_URL for SSR
Co-Authored-By: Charon <charon@lethean.io>
2026-04-03 11:56:34 +01:00
Claude
21ebfa1d17
fix(docker): point pool explorer URLs to local Docker explorer
Co-Authored-By: Charon <charon@lethean.io>
2026-04-03 11:15:06 +01:00
Claude
91efcd41d0
feat(docker): full ecosystem Docker deployment
- docker-compose.pull.yml: production compose with pre-built images,
  health checks, env var config, service dependencies
- docker-compose.local.yml: override for mounting existing chain data
- docker-compose.ecosystem.yml: build-from-source compose with pool,
  LNS, Redis added
- Chain Dockerfile: add curl for health checks, swagger API resources
- Pool Dockerfile: Ubuntu 24.04, Boost 1.83, native ProgPoWZ compilation
- .env.example: configurable passwords, ports, hostname
- pool-config.json: Docker-networked pool config
- health.sh: one-command stack health check
- deploy.sh: remote server deployment script
- lethean-testnet.service: systemd auto-start unit
- README.md: quickstart, mining guide, backup, troubleshooting

All 8 services tested and running in Docker:
daemon, wallet, explorer, trade-api, trade-frontend, pool, LNS, docs

Co-Authored-By: Charon <charon@lethean.io>
2026-04-03 11:14:30 +01:00
b61d773a0f Merge pull request 'v6.0.1 — RandomX PoW, new genesis, multi-platform CI' (#1) from Charon/blockchain:dev into dev
Reviewed-on: lthn/blockchain#1
Reviewed-by: Snider <snider@noreply.forge.lthn.ai>
2026-02-14 20:19:13 +00:00
45 changed files with 3370 additions and 2 deletions

1
.gitignore vendored
View file

@ -15,3 +15,4 @@ Thumbs.db
.vs/*
CMakeUserPresets.json
ConanPresets.json
docker/dist/

209
PLAN-NAME-REGISTRATION.md Normal file
View file

@ -0,0 +1,209 @@
# Mainnet Name Registration Plan
> **Status:** Validating on testnet
> **Date:** 2026-04-03
> **Authors:** Snider, Charon
## Problem
The Lethean chain merges two blockchains — a main chain (aliases) and an HNS sidechain (DNS). The sidechain blocks Alexa top 100K domains and ICANN TLDs from being registered by random users. But the main chain has no such protection — anyone could register `@google` as an alias before the sidechain has a chance to refuse it.
This creates a window for name squatting that corrupts the namespace before real users arrive.
## Numbers
| Category | Count |
|----------|-------|
| Reserved (Alexa top 100K) | 90,016 |
| Locked (ICANN TLDs) | 11,554 |
| **Total unique names** | **90,041** |
| Long names (>=6 chars, public) | 73,977 |
| Short names (<6 chars, need authority key) | 16,062 |
**Source:** `blockchain-network-dcore/lib/covenants/names.json` and `lockup.json`
## Strategy
### Phase 1: Genesis + HF Warmup
1. Mainnet launches with HF0 — 10M LTHN premine in SWAP wallet
2. During HF warmup (HF0 → HF1), send **1,000+ LTHN** from genesis to dedicated namereg wallets
3. Multiple namereg wallets (2-4) for parallel registration
4. Destination for all reserved aliases: a **multi-sig wallet** controlled by the team
### Phase 2: Staking Funds the Protection
1. Network wallet stakes during warmup — earns ~720 LTHN/day from PoS
2. Legacy users haven't reclaimed tokens yet (SWAP happens later)
3. Staking rewards from the network's own balance fund all registrations
4. The network literally pays for its own name protection
### Phase 3: Batch Registration
When aliases activate (HF that enables alias registration):
1. Namereg wallets already have coins and mature UTXOs ready
2. Multiple wallets work in parallel, each tackling a chunk of the 90K list
3. Priority order:
- **Tier 1:** Top brands (google, facebook, amazon, etc) — ~120 names
- **Tier 2:** Long names from Alexa list (>=6 chars) — ~73,977 names
- **Tier 3:** Short names with authority key (<6 chars) ~16,062 names
4. Rate: ~1 alias per 3 seconds per wallet = ~28,800/day with 1 wallet, ~115,200/day with 4
### Phase 4: Ongoing Governance
1. All reserved names sit in a multi-sig wallet
2. Legitimate brand owners can request transfer (proof of domain ownership)
3. Community aliases (common words like "wallet", "exchange") remain locked
4. Infrastructure names (@vpn, @proxy, @exit, etc) transferred to service operators
## Cost Model
| Item | Cost |
|------|------|
| Alias registration fee | 1 LTHN each |
| Transaction fee | 0.01 LTHN each |
| **Total for 90K names** | **~90,900 LTHN** |
| PoS earnings (720/day) | Covered in ~126 days |
| With 1000 LTHN seed | First ~1000 names immediate |
The network earns back the registration cost through staking before legacy users even begin the SWAP process.
## Technical Requirements
### Testnet Validation (current)
- [x] PoS staking working (first PoS block at height 12,382)
- [x] Alias registration via wallet RPC confirmed
- [x] Batch registration script built and tested
- [x] Priority name list generated from HNS data (90,041 names)
- [ ] Multi-wallet parallel registration tested
- [ ] Authority key for short names (<6 chars) tested
- [ ] Full 90K registration completed on testnet
- [ ] Explorer confirms all names visible
- [ ] LNS resolves registered names
### Mainnet Requirements
- [ ] Multi-sig wallet created for reserved names
- [ ] Genesis transaction plan (SWAP wallet → namereg wallets)
- [ ] Authority key provisioned for short-name registration
- [ ] Ansible playbook for deploying namereg wallets on prod
- [ ] Monitoring: alias count dashboard, registration rate, failures
## Alias Comment Format
All reserved names use this comment format for identification:
```
v=lthn1;type=reserved;reason=hns-protected
```
This allows:
- LNS to identify reserved vs user-registered names
- Future tools to query reserved names
- Governance decisions (release to brand owners) to be automated
## Daemon Configuration
PoS staking requires `--rpc-ignore-offline` when the daemon has no peers:
```
lethean-chain-node --rpc-ignore-offline --do-pos-mining
```
Without this flag, the daemon refuses PoS mining requests when disconnected.
## Batch Registration Tool
```bash
# Generate priority list from HNS data
python3 tools/generate-name-list.py
# Register names (3s delay, checks balance every 10)
bash tools/register-reserved.sh /tmp/protected-names-long.txt http://127.0.0.1:46944/json_rpc 10 3
```
Tool location: `docker/tools/register-reserved.sh`
## Option B: TLD Registrar Model (Recommended)
Instead of racing to pre-register 90K names, change one constant to make
ALL alias registration require the authority key:
```c
// currency_config.h — one line change
#define ALIAS_MINIMUM_PUBLIC_SHORT_NAME_ALLOWED 64 // was 6
```
This makes Lethean a proper TLD registrar — every name under `.lthn`
requires authority signature. No squatting possible at any length.
### Why This Is Better
| Pre-reg (Option A) | Registrar (Option B) |
|---------------------|----------------------|
| Race against squatters | No race — authority controls all |
| 90K transactions needed | Zero pre-registration needed |
| ~90K LTHN cost | Zero cost |
| Only covers known names | Covers ALL names, forever |
| New brands unprotected | New brands protected by default |
### Registrar Portal Flow
1. User requests `@myname` via website or CLI
2. Portal checks: not taken, not reserved, not offensive
3. User pays registration fee (1 LTHN + portal fee if desired)
4. Authority key signs the registration transaction
5. Name goes live on chain within 1 block
### Revenue Potential
| Name Type | Price |
|-----------|-------|
| Standard (6+ chars) | 1 LTHN (chain fee only) |
| Short (3-5 chars) | 10-100 LTHN (premium) |
| Ultra-short (1-2 chars) | Auction or reserved |
### Implementation
- **Hardfork:** Change `ALIAS_MINIMUM_PUBLIC_SHORT_NAME_ALLOWED` to 64
- **Portal:** Web API that accepts requests, validates, signs with authority key
- **Key security:** Authority private key in HSM or multi-sig, never on a hot server
- **Delegation:** Portal signs, daemon validates — separation of concerns
### Why Lethean Should Be a Registrar
- Lethean owns `.lthn` on Handshake — it IS the TLD operator
- Names route real traffic (VPN, DNS, service discovery)
- A curated namespace is trustworthy; a squatted one is worthless
- Every TLD in the world works this way — `.com`, `.co.uk`, `.eth`
- Revenue stream from name sales funds network development
### Migration Path
1. Testnet: validate both approaches (pre-reg + registrar)
2. HF proposal: include the constant change in next hardfork
3. Build portal: simple web UI + authority signing service
4. Mainnet launch: registrar model active from HF activation
5. Pre-reg only needed for names registered BEFORE the HF
## Risk Mitigations
| Risk | Mitigation |
|------|-----------|
| Tx pool congestion | Multiple wallets, 3s delay, batch checkpoints |
| Insufficient funds | Staking covers ongoing costs, seed from genesis |
| Authority key compromise | Multi-sig for the key, not single holder |
| Name disputes post-launch | Governance process for brand transfers |
| Testnet/mainnet drift | Same tooling, same scripts, validated on testnet first |
## Timeline
| Phase | When | Duration |
|-------|------|----------|
| Testnet validation | Now (Apr 2026) | 1-2 weeks |
| Mainnet genesis | TBD (Darbs approval) | Day 0 |
| HF warmup | Day 0-7 | 1 week |
| Batch registration | Day 7+ | 1-4 days (with parallel wallets) |
| Full coverage | Day 14 | All 90K protected |

54
docker/.env Normal file
View file

@ -0,0 +1,54 @@
# Lethean Testnet Configuration
# Copy to .env and customise before running:
# cp .env.example .env
# docker compose -f docker-compose.pull.yml up -d
# Public hostname (used by explorer and trade frontend)
PUBLIC_HOST=localhost
# Wallet password (empty = no password, change for production)
WALLET_PASSWORD=
# Trade API JWT secret (CHANGE THIS for production)
JWT_SECRET=change-me-before-production
# Explorer database
EXPLORER_DB_USER=explorer
EXPLORER_DB_PASS=explorer
EXPLORER_DB_NAME=lethean_explorer
# Trade database
TRADE_DB_USER=trade
TRADE_DB_PASS=trade
TRADE_DB_NAME=lethean_trade
# Daemon log level (0=minimal, 1=normal, 2=detailed, 3=trace)
DAEMON_LOG_LEVEL=1
# Port overrides (defaults shown)
# DAEMON_RPC_PORT=46941
# DAEMON_P2P_PORT=46942
# WALLET_RPC_PORT=46944
# EXPLORER_PORT=3335
# TRADE_API_PORT=3336
# TRADE_FRONTEND_PORT=3338
# POOL_STRATUM_PORT=5555
# POOL_API_PORT=2117
# LNS_HTTP_PORT=5553
# LNS_DNS_PORT=5354
# === Exit Node Settings (docker-compose.exit.yml) ===
# Your public IP address (required for VPN exit node)
# EXIT_PUBLIC_IP=auto
# Exit node name (registered as on-chain alias)
# EXIT_NAME=my-exit-node
# Maximum VPN peers (WireGuard clients)
# EXIT_MAX_PEERS=25
# Timezone
# TZ=Europe/London
EXIT_PUBLIC_IP=203.0.113.50
EXIT_NAME=my-exit

52
docker/.env.example Normal file
View file

@ -0,0 +1,52 @@
# Lethean Testnet Configuration
# Copy to .env and customise before running:
# cp .env.example .env
# docker compose -f docker-compose.pull.yml up -d
# Public hostname (used by explorer and trade frontend)
PUBLIC_HOST=localhost
# Wallet password (empty = no password, change for production)
WALLET_PASSWORD=
# Trade API JWT secret (CHANGE THIS for production)
JWT_SECRET=change-me-before-production
# Explorer database
EXPLORER_DB_USER=explorer
EXPLORER_DB_PASS=explorer
EXPLORER_DB_NAME=lethean_explorer
# Trade database
TRADE_DB_USER=trade
TRADE_DB_PASS=trade
TRADE_DB_NAME=lethean_trade
# Daemon log level (0=minimal, 1=normal, 2=detailed, 3=trace)
DAEMON_LOG_LEVEL=1
# Port overrides (defaults shown)
# DAEMON_RPC_PORT=46941
# DAEMON_P2P_PORT=46942
# WALLET_RPC_PORT=46944
# EXPLORER_PORT=3335
# TRADE_API_PORT=3336
# TRADE_FRONTEND_PORT=3338
# POOL_STRATUM_PORT=5555
# POOL_API_PORT=2117
# LNS_HTTP_PORT=5553
# LNS_DNS_PORT=5354
# === Exit Node Settings (docker-compose.exit.yml) ===
# Your public IP address (required for VPN exit node)
# EXIT_PUBLIC_IP=auto
# Exit node name (registered as on-chain alias)
# EXIT_NAME=my-exit-node
# Maximum VPN peers (WireGuard clients)
# EXIT_MAX_PEERS=25
# Timezone
# TZ=Europe/London

View file

@ -0,0 +1,38 @@
# Cross-platform build for Lethean blockchain binaries
# Produces Linux x86_64 and ARM64 binaries
# macOS and Windows need native build environments or cross-compilation toolchains
#
# Usage:
# docker build -f Dockerfile.cross-build --target linux-x64 -o ./out .
# docker build -f Dockerfile.cross-build --target linux-arm64 -o ./out .
# === Base builder ===
FROM ubuntu:24.04 AS base-builder
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y \
build-essential cmake git python3 python3-pip \
libboost-all-dev libssl-dev pkg-config curl \
&& pip3 install conan --break-system-packages \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /build
COPY . /build/
# === Linux x86_64 ===
FROM base-builder AS linux-x64-build
ARG TESTNET=1
RUN make build CPU_CORES=$(nproc) TESTNET=${TESTNET} STATIC=1 || \
(cd build/release && cmake --build . --parallel $(nproc))
FROM scratch AS linux-x64
COPY --from=linux-x64-build /build/build/release/src/lethean-* /
# === Linux ARM64 (cross-compile) ===
FROM base-builder AS linux-arm64-build
RUN apt-get update && apt-get install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu
ARG TESTNET=1
# ARM64 cross-compilation requires CMAKE_TOOLCHAIN_FILE
# This is a placeholder — actual cross-compile needs Conan profile for aarch64
RUN echo "ARM64 cross-compilation requires additional setup. Use native ARM64 builder or QEMU."
FROM scratch AS linux-arm64
# Placeholder — use buildx with --platform=linux/arm64 for native ARM builds

View file

@ -0,0 +1,183 @@
# Lethean Staking Node — single container, no compose needed
# Runs daemon + wallet with PoS staking in one container.
#
# Build:
# docker build -f Dockerfile.staking-node -t lthn/staking-node .
#
# Run:
# docker run -d --name lthn-staker \
# -p 46941:36941 -p 46942:36942 -p 46944:36944 \
# -v lthn-chain:/data -v lthn-wallet:/wallet \
# lthn/staking-node
#
# Run with password:
# docker run -d --name lthn-staker \
# -e WALLET_PASSWORD=my-pass \
# -p 46941:36941 -p 46942:36942 -p 46944:36944 \
# -v lthn-chain:/data -v lthn-wallet:/wallet \
# lthn/staking-node
#
# Check status:
# docker exec lthn-staker /status.sh
#
# Get wallet address:
# docker exec lthn-staker /wallet-address.sh
#
# Ports:
# 36941 — Daemon RPC (chain queries)
# 36942 — P2P (open on router for full node)
# 36944 — Wallet RPC (balance, transfers)
FROM lthn/chain:testnet
RUN apt-get update && apt-get install -y --no-install-recommends \
curl supervisor bc \
&& rm -rf /var/lib/apt/lists/*
# Supervisor manages both daemon and wallet in one container
COPY <<'SUPERVISOR' /etc/supervisor/conf.d/lethean.conf
[supervisord]
nodaemon=true
logfile=/dev/stdout
logfile_maxbytes=0
loglevel=info
[program:daemon]
command=lethean-chain-node
--data-dir /data
--rpc-bind-ip 0.0.0.0
--rpc-bind-port 36941
--p2p-bind-port 36942
--rpc-enable-admin-api
--allow-local-ip
--log-level %(ENV_DAEMON_LOG_LEVEL)s
--disable-upnp
--rpc-ignore-offline
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:wallet]
command=/start-wallet.sh
autorestart=true
startsecs=30
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
SUPERVISOR
# Wallet startup script — waits for daemon, creates wallet if needed
COPY <<'WALLETSH' /start-wallet.sh
#!/bin/bash
set -e
WALLET_FILE="/wallet/staker.wallet"
WALLET_PASS="${WALLET_PASSWORD:-}"
DAEMON="127.0.0.1:36941"
# Wait for daemon RPC
echo "[wallet] Waiting for daemon..."
until curl -sf "http://$DAEMON/json_rpc" \
-d '{"jsonrpc":"2.0","id":"0","method":"getinfo"}' \
-H 'Content-Type: application/json' > /dev/null 2>&1; do
sleep 5
done
echo "[wallet] Daemon ready"
# Create wallet if it doesn't exist
if [ ! -f "$WALLET_FILE" ]; then
echo "[wallet] Creating new wallet..."
echo "$WALLET_PASS" | lethean-wallet-cli \
--generate-new-wallet "$WALLET_FILE" \
--password "$WALLET_PASS" \
--daemon-address "$DAEMON" \
--command exit
echo "[wallet] Wallet created"
fi
# Start wallet with PoS staking
exec lethean-wallet-cli \
--wallet-file "$WALLET_FILE" \
--password "$WALLET_PASS" \
--daemon-address "$DAEMON" \
--rpc-bind-port 36944 \
--rpc-bind-ip 0.0.0.0 \
--do-pos-mining
WALLETSH
# Status helper — shows chain + wallet in one command
COPY <<'STATUSSH' /status.sh
#!/bin/bash
echo "=== Lethean Staking Node ==="
echo ""
# Chain
INFO=$(curl -sf http://127.0.0.1:36941/json_rpc \
-d '{"jsonrpc":"2.0","id":"0","method":"getinfo"}' \
-H 'Content-Type: application/json' 2>/dev/null)
if [ -n "$INFO" ]; then
echo "$INFO" | python3 -c "
import sys,json
d=json.load(sys.stdin)['result']
hf=d.get('is_hardfok_active',[])
active=sum(1 for h in hf if h)
print(f'Chain:')
print(f' Height: {d[\"height\"]:,}')
print(f' Hardforks: HF0-{active-1}')
print(f' PoW diff: {d.get(\"pow_difficulty\",0):,}')
print(f' Aliases: {d.get(\"alias_count\",0)}')
print(f' Peers in: {d.get(\"incoming_connections_count\",0)}')
print(f' Peers out: {d.get(\"outgoing_connections_count\",0)}')
print(f' Status: {d[\"status\"]}')
" 2>/dev/null
else
echo "Chain: starting..."
fi
echo ""
# Wallet
BAL=$(curl -sf http://127.0.0.1:36944/json_rpc \
-d '{"jsonrpc":"2.0","id":"0","method":"getbalance"}' \
-H 'Content-Type: application/json' 2>/dev/null)
if [ -n "$BAL" ]; then
echo "$BAL" | python3 -c "
import sys,json
d=json.load(sys.stdin)['result']
print(f'Wallet:')
print(f' Balance: {d[\"balance\"]/1e12:.4f} LTHN')
print(f' Unlocked: {d[\"unlocked_balance\"]/1e12:.4f} LTHN')
print(f' Staking: yes (PoS mining active)')
" 2>/dev/null
else
echo "Wallet: syncing..."
fi
STATUSSH
# Wallet address helper
COPY <<'ADDRSH' /wallet-address.sh
#!/bin/bash
curl -sf http://127.0.0.1:36944/json_rpc \
-d '{"jsonrpc":"2.0","id":"0","method":"getaddress"}' \
-H 'Content-Type: application/json' | python3 -c "
import sys,json
d=json.load(sys.stdin)['result']
print(d['address'])
" 2>/dev/null || echo "Wallet not ready yet — daemon still syncing"
ADDRSH
RUN chmod +x /start-wallet.sh /status.sh /wallet-address.sh
ENV DAEMON_LOG_LEVEL=1
ENV WALLET_PASSWORD=
EXPOSE 36941 36942 36944
VOLUME ["/data", "/wallet"]
ENTRYPOINT ["supervisord", "-c", "/etc/supervisor/conf.d/lethean.conf"]

152
docker/README.md Normal file
View file

@ -0,0 +1,152 @@
# Lethean Testnet — Docker Deployment
Run the full Lethean ecosystem in Docker. No compilation needed.
## Deployment Options
| Compose File | For | Services |
|---|---|---|
| `docker-compose.node.yml` | Home node operator | Daemon + wallet (PoS staking) |
| `docker-compose.exit.yml` | VPN exit node operator | Daemon + wallet + WireGuard |
| `docker-compose.pull.yml` | Full ecosystem | All 8 services + databases |
## Helper Scripts
```bash
bash demos/helpers/chain-info.sh # Chain height, difficulty, status
bash demos/helpers/chain-aliases.sh # List on-chain aliases
bash demos/helpers/wallet-address.sh # Show your wallet address
bash demos/helpers/wallet-balance.sh # Check LTHN balance
bash health.sh # Full ecosystem health check
```
## Quick Start
```bash
# 1. Configure
cp .env.example .env
# Edit .env — at minimum change JWT_SECRET and WALLET_PASSWORD
# 2. Start
docker compose -f docker-compose.pull.yml up -d
# 3. Check
docker compose -f docker-compose.pull.yml ps
```
The daemon takes ~30 seconds to initialise the chain database on first run.
Other services wait for the daemon health check before starting.
## Services
| Service | URL | Description |
|---------|-----|-------------|
| Block Explorer | http://localhost:3335 | Browse blocks, transactions, aliases |
| Trade DEX | http://localhost:3338 | Decentralised exchange frontend |
| Trade API | http://localhost:3336 | REST API for trade operations |
| Mining Pool | http://localhost:2117 | Pool stats API |
| Pool Stratum | localhost:5555 | Connect miners here |
| LNS | http://localhost:5553 | Lethean Name Service HTTP API |
| LNS DNS | localhost:5354 | DNS resolver for .lthn names |
| Daemon RPC | http://localhost:46941 | Chain node JSON-RPC |
| Wallet RPC | http://localhost:46944 | Wallet JSON-RPC |
| Docs | http://localhost:8099 | Documentation site |
## Mining
Connect a ProgPoWZ miner to the pool stratum:
```
stratum+tcp://YOUR_IP:5555
```
Compatible miners:
- **progminer** (recommended) — `progminer -P stratum+tcp://YOUR_WALLET_ADDRESS@localhost:5555`
- **T-Rex** — ProgPoWZ algorithm
- **TeamRedMiner** — ProgPoWZ algorithm
## Wallet
The wallet auto-creates on first start. To check your balance:
```bash
curl -s http://localhost:46944/json_rpc \
-d '{"jsonrpc":"2.0","id":"0","method":"getbalance"}' \
-H 'Content-Type: application/json'
```
The wallet runs with PoS mining enabled (`--do-pos-mining`), so it will stake
any balance automatically.
## Chain Status
```bash
curl -s http://localhost:46941/json_rpc \
-d '{"jsonrpc":"2.0","id":"0","method":"getinfo"}' \
-H 'Content-Type: application/json' | python3 -m json.tool
```
## Volumes
| Volume | Contains | Backup? |
|--------|----------|---------|
| `chain-data` | Blockchain database (LMDB) | Optional — can resync |
| `wallet-data` | Wallet file + keys | **Critical — back up regularly** |
| `explorer-db` | Explorer PostgreSQL | Can recreate from chain |
| `trade-db` | Trade PostgreSQL | Important for trade history |
### Backup wallet
```bash
docker compose -f docker-compose.pull.yml stop wallet
docker cp lthn-wallet:/wallet ./wallet-backup-$(date +%Y%m%d)
docker compose -f docker-compose.pull.yml start wallet
```
## Ports
All ports can be changed via `.env`. See `.env.example` for the full list.
If you're running behind a firewall and want external access:
- **P2P:** Open port 46942 (TCP) for other nodes to connect
- **Stratum:** Open port 5555 (TCP) for external miners
- **Explorer:** Open port 3335 (TCP) for public block explorer
## Troubleshooting
**Daemon crashes on start:**
Check logs with `docker logs lthn-daemon`. Common causes:
- Port already in use — change ports in `.env`
- Corrupt chain data — remove volume: `docker volume rm docker_chain-data`
**Wallet shows 0 balance:**
The wallet needs to sync with the daemon first. Check sync status in daemon logs.
New wallets start empty — mine or receive LTHN to fund them.
**Pool shows "Daemon died":**
The pool needs both the daemon and wallet running. Check both are healthy:
```bash
docker compose -f docker-compose.pull.yml ps daemon wallet
```
**Explorer shows "offline":**
The explorer connects to the daemon internally. If the daemon is still syncing,
the explorer will show offline until sync completes.
## Stopping
```bash
docker compose -f docker-compose.pull.yml down # stop containers
docker compose -f docker-compose.pull.yml down -v # stop + delete data
```
## Building from Source
If you want to build the images yourself instead of pulling pre-built ones:
```bash
docker compose -f docker-compose.ecosystem.yml build
docker compose -f docker-compose.ecosystem.yml up -d
```
This requires the full source tree — see the main repository README.

77
docker/build-all-platforms.sh Executable file
View file

@ -0,0 +1,77 @@
#!/bin/bash
# Build Lethean binaries for all platforms
# Requires: Docker with buildx for multi-arch
#
# Usage:
# ./build-all-platforms.sh testnet # testnet binaries
# ./build-all-platforms.sh mainnet # mainnet binaries
#
# Outputs to: ../build/packages/
set -e
TARGET=${1:-testnet}
TESTNET=$( [ "$TARGET" = "testnet" ] && echo 1 || echo 0 )
VERSION=$(grep 'BUILD_VERSION:=' ../Makefile | cut -d= -f2)
OUTDIR="../build/packages"
echo "Building Lethean $TARGET v$VERSION for all platforms"
mkdir -p "$OUTDIR"
# === Linux x86_64 (Docker) ===
echo ""
echo "=== Linux x86_64 ==="
docker build -f Dockerfile.cross-build \
--build-arg TESTNET=$TESTNET \
--target linux-x64 \
-o "$OUTDIR/linux-x64" \
.. 2>&1 | tail -5
if ls "$OUTDIR/linux-x64/lethean-"* > /dev/null 2>&1; then
PKG="lethean-${TARGET}-linux-x86_64-v${VERSION}"
mkdir -p "$OUTDIR/$PKG"
cp "$OUTDIR/linux-x64/lethean-"* "$OUTDIR/$PKG/"
cd "$OUTDIR" && tar czf "${PKG}.tar.gz" "$PKG/" && cd -
echo " Package: $OUTDIR/${PKG}.tar.gz"
else
echo " Build failed — check Docker output"
fi
# === Linux ARM64 (needs Docker buildx with QEMU) ===
echo ""
echo "=== Linux ARM64 ==="
if docker buildx ls 2>/dev/null | grep -q "linux/arm64"; then
echo " ARM64 builder available — building..."
docker buildx build -f ../utils/docker/lthn-chain/Dockerfile \
--platform linux/arm64 \
--build-arg BUILD_TESTNET=$TESTNET \
--build-arg BUILD_THREADS=4 \
--target build-artifacts \
-o "$OUTDIR/linux-arm64" \
.. 2>&1 | tail -5
else
echo " SKIP: Docker buildx ARM64 emulation not configured"
echo " To enable: docker run --privileged --rm tonistiigi/binfmt --install arm64"
fi
# === macOS (needs native build or osxcross) ===
echo ""
echo "=== macOS ==="
echo " SKIP: macOS builds require native macOS environment or osxcross toolchain"
echo " Build on Cladius (M3 Ultra): cd blockchain && make $TARGET"
echo " Or use GitHub Actions with macos-latest runner"
# === Windows (needs MSVC or mingw-w64) ===
echo ""
echo "=== Windows ==="
echo " SKIP: Windows builds require MSVC or mingw-w64 cross-compiler"
echo " Recommended: GitHub Actions with windows-latest runner"
echo " Or: Docker with dockcross/windows-shared-x64 image"
# === Summary ===
echo ""
echo "=== Build Summary ==="
ls -lh "$OUTDIR"/*.tar.gz 2>/dev/null || echo "No packages built yet"
echo ""
echo "For macOS/Windows, use CI/CD:"
echo " .forgejo/workflows/build-release.yml (push a tag to trigger)"

BIN
docker/demos/01-install.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 232 KiB

View file

@ -0,0 +1,54 @@
# 01 — Install Lethean from scratch
# Shows a home user going from zero to running node
Output demos/01-install.gif
Set FontSize 13
Set Width 1000
Set Height 600
Set Theme "Dracula"
Set TypingSpeed 35ms
Set Padding 12
Set Shell bash
Type "# Install Lethean — from zero to running node"
Enter
Sleep 1s
Type "mkdir lethean && cd lethean"
Enter
Sleep 500ms
Type "# Download the compose files"
Enter
Type "curl -sO https://forge.lthn.ai/lthn/blockchain/raw/branch/dev/docker/docker-compose.node.yml"
Enter
Sleep 500ms
Type "curl -sO https://forge.lthn.ai/lthn/blockchain/raw/branch/dev/docker/.env.example"
Enter
Sleep 500ms
Type "cp .env.example .env"
Enter
Sleep 300ms
Type "# Set a wallet password"
Enter
Type "sed -i 's/WALLET_PASSWORD=/WALLET_PASSWORD=my-secure-pass/' .env"
Enter
Sleep 500ms
Type ""
Enter
Type "# Start the node"
Enter
Type "docker compose -f docker-compose.node.yml up -d"
Enter
Sleep 3s
Type ""
Enter
Type "# That's it. Your node is syncing and staking."
Enter
Sleep 2s

Binary file not shown.

After

Width:  |  Height:  |  Size: 443 KiB

View file

@ -0,0 +1,39 @@
# 02 — Chain Status (live data)
Output demos/02-chain-status.gif
Set FontSize 13
Set Width 1000
Set Height 600
Set Theme "Dracula"
Set TypingSpeed 35ms
Set Padding 12
Set Shell bash
Type "# Lethean Chain — Live Status"
Enter
Sleep 500ms
Type "bash demos/helpers/chain-info.sh"
Enter
Sleep 3s
Type ""
Enter
Type "bash demos/helpers/last-block.sh"
Enter
Sleep 3s
Type ""
Enter
Type "# On-chain service discovery via aliases"
Enter
Type "bash demos/helpers/chain-aliases.sh"
Enter
Sleep 4s
Type ""
Enter
Type "# Blocks arrive every ~2 minutes — PoW and PoS alternating"
Enter
Sleep 2s

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 MiB

View file

@ -0,0 +1,87 @@
# 03 — Ecosystem Tour (the teaser material)
# Every service, live data, no fluff
Output demos/03-ecosystem-tour.gif
Set FontSize 13
Set Width 1000
Set Height 700
Set Theme "Dracula"
Set TypingSpeed 30ms
Set Padding 12
Set Shell bash
Type "# Lethean Ecosystem — Full Tour"
Enter
Sleep 500ms
Type "bash health.sh"
Enter
Sleep 4s
Type ""
Enter
Type "# === Chain ==="
Enter
Type "bash demos/helpers/chain-info.sh"
Enter
Sleep 3s
Type ""
Enter
Type "# === Latest Block ==="
Enter
Type "bash demos/helpers/last-block.sh"
Enter
Sleep 3s
Type ""
Enter
Type "# === Wallet ==="
Enter
Type "bash demos/helpers/wallet-address.sh"
Enter
Sleep 2s
Type "bash demos/helpers/wallet-balance.sh"
Enter
Sleep 2s
Type ""
Enter
Type "# === Explorer ==="
Enter
Type "bash demos/helpers/explorer-stats.sh"
Enter
Sleep 3s
Type ""
Enter
Type "# === Mining Pool ==="
Enter
Type "bash demos/helpers/pool-stats.sh"
Enter
Sleep 3s
Type ""
Enter
Type "# === Name Service ==="
Enter
Type "bash demos/helpers/lns-status.sh"
Enter
Sleep 3s
Type ""
Enter
Type "# === Aliases — services advertise on-chain ==="
Enter
Type "bash demos/helpers/chain-aliases.sh"
Enter
Sleep 4s
Type ""
Enter
Type "# Lethean — privacy by default, earn by participating"
Enter
Type "# https://lethean.io"
Enter
Sleep 3s

BIN
docker/demos/04-mining.gif Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 957 KiB

View file

@ -0,0 +1,50 @@
# 04 — Mining
# Shows pool stats and how to connect a miner
Output demos/04-mining.gif
Set FontSize 13
Set Width 1000
Set Height 550
Set Theme "Dracula"
Set TypingSpeed 35ms
Set Padding 12
Set Shell bash
Type "# Lethean Mining — ProgPoWZ GPU Mining"
Enter
Sleep 500ms
Type "bash demos/helpers/pool-stats.sh"
Enter
Sleep 3s
Type ""
Enter
Type "# Get your wallet address for mining"
Enter
Type "bash demos/helpers/wallet-address.sh"
Enter
Sleep 2s
Type ""
Enter
Type "# Connect a GPU miner:"
Enter
Type "echo 'progminer -P stratum+tcp://YOUR_WALLET@localhost:5555'"
Enter
Sleep 1s
Type "echo 't-rex -a progpowz -o stratum+tcp://localhost:5555 -u YOUR_WALLET'"
Enter
Sleep 1s
Type "echo 'teamredminer -a progpow -o stratum+tcp://localhost:5555 -u YOUR_WALLET'"
Enter
Sleep 2s
Type ""
Enter
Type "# Pool dashboard: http://localhost:2117"
Enter
Type "# Block reward: 1 LTHN every ~2 minutes"
Enter
Sleep 2s

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

View file

@ -0,0 +1,68 @@
# 05 — Exit Node Setup
# Shows how to run a VPN exit node and earn LTHN
Output demos/05-exit-node.gif
Set FontSize 13
Set Width 1000
Set Height 600
Set Theme "Dracula"
Set TypingSpeed 35ms
Set Padding 12
Set Shell bash
Type "# Lethean VPN Exit Node — Earn LTHN"
Enter
Sleep 500ms
Type "# Configure your exit node"
Enter
Type "cp .env.example .env"
Enter
Sleep 300ms
Type "echo 'WALLET_PASSWORD=my-secure-pass' >> .env"
Enter
Type "echo 'EXIT_PUBLIC_IP=203.0.113.50' >> .env"
Enter
Type "echo 'EXIT_NAME=my-exit' >> .env"
Enter
Sleep 500ms
Type ""
Enter
Type "# Start the exit node stack"
Enter
Type "docker compose -f docker-compose.exit.yml up -d"
Enter
Sleep 3s
Type ""
Enter
Type "# Your exit node runs:"
Enter
Type "echo ' Chain daemon — syncs blockchain'"
Enter
Type "echo ' Wallet — PoS staking + receives payments'"
Enter
Type "echo ' WireGuard VPN — encrypted tunnel for gateway traffic'"
Enter
Type "echo ' Controller — manages peering and on-chain registration'"
Enter
Sleep 2s
Type ""
Enter
Type "# Gateways discover you via on-chain alias:"
Enter
Type "echo ' @my-exit v=lthn1;type=exit;cap=vpn,proxy;ip=203.0.113.50'"
Enter
Sleep 2s
Type ""
Enter
Type "# Open ports 46942/tcp + 51820/udp on your router"
Enter
Type "# Then sit back and earn LTHN"
Enter
Sleep 3s

View file

@ -0,0 +1,13 @@
#!/bin/bash
curl -s localhost:46941/json_rpc \
-d '{"jsonrpc":"2.0","id":"0","method":"get_all_alias_details"}' \
-H 'Content-Type: application/json' | python3 -c "
import sys,json
aliases=json.load(sys.stdin)['result']['aliases']
print(f'{len(aliases)} aliases registered on-chain:')
print()
for a in aliases:
name = a['alias']
comment = a.get('comment','')
print(f' @{name:12s} {comment[:50]}')
"

View file

@ -0,0 +1,17 @@
#!/bin/bash
curl -s localhost:46941/json_rpc \
-d '{"jsonrpc":"2.0","id":"0","method":"getinfo"}' \
-H 'Content-Type: application/json' | python3 -c "
import sys,json
d=json.load(sys.stdin)['result']
hf = d.get('is_hardfok_active',[])
active = sum(1 for h in hf if h)
print(f'Height: {d[\"height\"]:,}')
print(f'Hardforks: HF0-{active-1} active')
print(f'PoW diff: {d.get(\"pow_difficulty\",0):,}')
print(f'Aliases: {d.get(\"alias_count\",0)}')
print(f'TX count: {d.get(\"tx_count\",0):,}')
print(f'Peers in: {d.get(\"incoming_connections_count\",0)}')
print(f'Peers out: {d.get(\"outgoing_connections_count\",0)}')
print(f'Status: {d[\"status\"]}')
"

View file

@ -0,0 +1,15 @@
#!/bin/bash
curl -s localhost:3335/api/get_info | python3 -c "
import sys,json
try:
d=json.load(sys.stdin)
print('Block Explorer')
print(f' Height: {d.get(\"height\",0):,}')
print(f' PoW diff: {d.get(\"pow_difficulty\",0):,}')
print(f' Aliases: {d.get(\"alias_count\",0)}')
print(f' TX count: {d.get(\"tx_count\",0):,}')
print(f' Hashrate: {d.get(\"current_network_hashrate_350\",0):,} H/s')
print(f' URL: http://localhost:3335')
except:
print('Explorer: connecting...')
"

View file

@ -0,0 +1,13 @@
#!/bin/bash
curl -s localhost:46941/json_rpc \
-d '{"jsonrpc":"2.0","id":"0","method":"getlastblockheader"}' \
-H 'Content-Type: application/json' | python3 -c "
import sys,json
b=json.load(sys.stdin)['result']['block_header']
btype = 'PoW' if b.get('major_version',0)==3 else 'PoS'
print(f'Latest Block #{b[\"height\"]:,}')
print(f' Type: {btype}')
print(f' Reward: {b[\"reward\"]/1e12:.4f} LTHN')
print(f' Difficulty: {b[\"difficulty\"]:,}')
print(f' Hash: {b[\"hash\"][:20]}...')
"

View file

@ -0,0 +1,15 @@
#!/bin/bash
echo "Lethean Name Service (LNS)"
curl -s localhost:5553/health | python3 -c "
import sys,json
d=json.load(sys.stdin)
print(f' Mode: {d.get(\"mode\",\"?\")}')
print(f' Names: {d.get(\"names\",0)} cached')
print(f' Status: {d.get(\"status\",\"?\")}')
"
echo ""
echo "Resolving .lthn names:"
for name in charon gateway explorer trading; do
result=$(curl -s "localhost:5553/resolve?name=$name.lthn" | python3 -c "import sys,json; d=json.load(sys.stdin); print('found' if d.get('found') else 'not cached')" 2>/dev/null)
printf " %-16s %s\n" "$name.lthn" "$result"
done

View file

@ -0,0 +1,17 @@
#!/bin/bash
curl -s localhost:2117/stats | python3 -c "
import sys,json
d=json.load(sys.stdin)
pool = d.get('pool',{})
net = d.get('network',{})
cfg = d.get('config',{})
print(f'Lethean Mining Pool')
print(f' Coin: {cfg.get(\"coin\",\"?\")}')
print(f' Algorithm: ProgPoWZ')
print(f' Pool hash: {pool.get(\"hashrate\",0)} H/s')
print(f' Miners: {pool.get(\"miners\",0)}')
print(f' Blocks found: {pool.get(\"totalBlocks\",0)}')
print(f' Net height: {net.get(\"height\",\"?\")}')
print(f' Net diff: {net.get(\"difficulty\",\"?\")}')
print(f' Stratum: stratum+tcp://localhost:5555')
"

View file

@ -0,0 +1,8 @@
#!/bin/bash
curl -s localhost:46944/json_rpc \
-d '{"jsonrpc":"2.0","id":"0","method":"getaddress"}' \
-H 'Content-Type: application/json' | python3 -c "
import sys,json
d=json.load(sys.stdin)['result']
print('Address:', d['address'])
"

View file

@ -0,0 +1,9 @@
#!/bin/bash
curl -s localhost:46944/json_rpc \
-d '{"jsonrpc":"2.0","id":"0","method":"getbalance"}' \
-H 'Content-Type: application/json' | python3 -c "
import sys,json
d=json.load(sys.stdin)['result']
print(f'Balance: {d[\"balance\"]/1e12:.4f} LTHN')
print(f'Unlocked: {d[\"unlocked_balance\"]/1e12:.4f} LTHN')
"

78
docker/deploy.sh Executable file
View file

@ -0,0 +1,78 @@
#!/bin/bash
# Lethean Testnet — Production Deploy Script
# Transfers images and config to a remote server and starts the stack.
#
# Usage:
# bash deploy.sh user@server
# bash deploy.sh user@server /opt/lethean/docker
#
# Prerequisites on the remote server:
# - Docker + Docker Compose v2
# - SSH access
set -e
REMOTE="${1:?Usage: deploy.sh user@server [install_dir]}"
INSTALL_DIR="${2:-/opt/lethean/docker}"
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
IMAGES="$SCRIPT_DIR/dist/lethean-testnet-images.tar.gz"
if [ ! -f "$IMAGES" ]; then
echo "Building image bundle..."
mkdir -p "$SCRIPT_DIR/dist"
docker save \
lthn/chain:testnet \
lthn/explorer:testnet \
lthn/trade-api:testnet \
lthn/trade-frontend:testnet \
lthn/pool:testnet \
lthn/lns:testnet \
lthn/docs:testnet \
| gzip > "$IMAGES"
fi
echo "Image bundle: $(du -h "$IMAGES" | cut -f1)"
echo "Deploying to $REMOTE:$INSTALL_DIR"
echo ""
# Create remote directory
ssh "$REMOTE" "mkdir -p $INSTALL_DIR"
# Transfer files
echo "Transferring config files..."
scp "$SCRIPT_DIR/docker-compose.pull.yml" "$REMOTE:$INSTALL_DIR/"
scp "$SCRIPT_DIR/.env.example" "$REMOTE:$INSTALL_DIR/"
scp "$SCRIPT_DIR/pool-config.json" "$REMOTE:$INSTALL_DIR/"
scp "$SCRIPT_DIR/health.sh" "$REMOTE:$INSTALL_DIR/"
scp "$SCRIPT_DIR/lethean-testnet.service" "$REMOTE:$INSTALL_DIR/"
scp "$SCRIPT_DIR/docker-compose.node.yml" "$REMOTE:$INSTALL_DIR/"
scp "$SCRIPT_DIR/docker-compose.exit.yml" "$REMOTE:$INSTALL_DIR/"
scp -r "$SCRIPT_DIR/demos/helpers" "$REMOTE:$INSTALL_DIR/demos/"
# Create .env if it doesn't exist — generate unique JWT secret
ssh "$REMOTE" "
if [ ! -f $INSTALL_DIR/.env ]; then
cp $INSTALL_DIR/.env.example $INSTALL_DIR/.env
JWT=\$(openssl rand -hex 32 2>/dev/null || head -c 64 /dev/urandom | xxd -p | tr -d '\n' | head -c 64)
sed -i \"s/change-me-before-production/\$JWT/\" $INSTALL_DIR/.env
echo 'Generated unique JWT secret'
else
echo '.env already exists — skipping'
fi
"
echo "Transferring images (~$(du -h "$IMAGES" | cut -f1))... this may take a while"
scp "$IMAGES" "$REMOTE:/tmp/lethean-testnet-images.tar.gz"
echo "Loading images on remote..."
ssh "$REMOTE" "docker load < /tmp/lethean-testnet-images.tar.gz && rm /tmp/lethean-testnet-images.tar.gz"
echo "Starting stack..."
ssh "$REMOTE" "cd $INSTALL_DIR && docker compose -f docker-compose.pull.yml up -d"
echo ""
echo "Deploy complete. Run health check:"
echo " ssh $REMOTE 'cd $INSTALL_DIR && bash health.sh'"
echo ""
echo "Install systemd service (optional):"
echo " ssh $REMOTE 'sudo cp $INSTALL_DIR/lethean-testnet.service /etc/systemd/system/ && sudo systemctl daemon-reload && sudo systemctl enable lethean-testnet'"

View file

@ -0,0 +1,203 @@
# Lethean Full Ecosystem
# Chain node + wallet + explorer + trade + pool + LNS + docs
#
# Usage:
# docker compose -f docker-compose.ecosystem.yml up -d
#
# URLs after startup:
# Explorer: http://localhost:3335
# Trade: http://localhost:3337
# Trade API: http://localhost:3336
# Pool API: http://localhost:2117
# Pool Web: http://localhost:8888
# LNS HTTP: http://localhost:5553
# LNS DNS: localhost:5354
# Docs: http://localhost:8098
# Daemon RPC: http://localhost:46941
# Wallet RPC: http://localhost:46944
services:
# --- Chain ---
daemon:
build:
context: ..
dockerfile: utils/docker/lthn-chain/Dockerfile
target: chain-service
args:
BUILD_TESTNET: 1
BUILD_THREADS: 4
container_name: lthn-daemon
ports:
- "46941:36941"
- "46942:36942"
volumes:
- daemon-data:/data
command: >
lethean-chain-node
--data-dir /data
--rpc-bind-ip 0.0.0.0
--rpc-bind-port 36941
--p2p-bind-port 36942
--rpc-enable-admin-api
--allow-local-ip
--log-level 1
--disable-upnp
wallet:
build:
context: ..
dockerfile: utils/docker/lthn-chain/Dockerfile
target: chain-service
args:
BUILD_TESTNET: 1
BUILD_THREADS: 4
container_name: lthn-wallet
ports:
- "46944:36944"
volumes:
- wallet-data:/wallet
entrypoint: >
sh -c "
if [ ! -f /wallet/main.wallet ]; then
echo '' | lethean-wallet-cli --generate-new-wallet /wallet/main.wallet --password '' --daemon-address daemon:36941 --command exit;
fi;
lethean-wallet-cli
--wallet-file /wallet/main.wallet
--password ''
--daemon-address daemon:36941
--rpc-bind-port 36944
--rpc-bind-ip 0.0.0.0
--do-pos-mining
"
depends_on:
- daemon
# --- Explorer ---
explorer-db:
image: postgres:16-alpine
container_name: lthn-explorer-db
environment:
POSTGRES_USER: explorer
POSTGRES_PASSWORD: explorer
POSTGRES_DB: lethean_explorer
volumes:
- explorer-db:/var/lib/postgresql/data
explorer:
build:
context: ../../zano-upstream/zano-explorer-zarcanum
container_name: lthn-explorer
ports:
- "3335:3335"
environment:
API: http://daemon:36941
FRONTEND_API: http://localhost:3335
SERVER_PORT: "3335"
AUDITABLE_WALLET_API: http://daemon:36941
PGUSER: explorer
PGPASSWORD: explorer
PGDATABASE: lethean_explorer
PGHOST: explorer-db
PGPORT: "5432"
MEXC_API_URL: ""
depends_on:
- daemon
- explorer-db
# --- Trade ---
trade-db:
image: postgres:16-alpine
container_name: lthn-trade-db
environment:
POSTGRES_USER: trade
POSTGRES_PASSWORD: trade
POSTGRES_DB: lethean_trade
volumes:
- trade-db:/var/lib/postgresql/data
trade-api:
build:
context: ../../zano-upstream/zano_trade_backend
container_name: lthn-trade-api
ports:
- "3336:3336"
environment:
PORT: "3336"
PGUSER: trade
PGPASSWORD: trade
PGDATABASE: lethean_trade
PGHOST: trade-db
PGPORT: "5432"
JWT_SECRET: testnet-dev-secret
DAEMON_RPC_URL: http://daemon:36941/json_rpc
depends_on:
- daemon
- trade-db
trade-frontend:
build:
context: ../../zano-upstream/zano_trade_frontend
container_name: lthn-trade-frontend
ports:
- "3337:30289"
environment:
NEXT_PUBLIC_API_URL: http://trade-api:3336
depends_on:
- trade-api
# --- Mining Pool ---
pool-redis:
image: redis:7-alpine
container_name: lthn-pool-redis
restart: unless-stopped
pool:
build:
context: ../..
dockerfile: zano-upstream/zano-pool/Dockerfile
container_name: lthn-pool
ports:
- "5555:5555"
- "7777:7777"
- "2117:2117"
- "8888:8888"
volumes:
- ./pool-config.json:/pool/config.json:ro
depends_on:
- daemon
- wallet
- pool-redis
# --- LNS (Lethean Name Service) ---
lns:
build:
context: ../../lns
container_name: lthn-lns
ports:
- "5553:5553" # HTTP API
- "5354:5354/udp" # DNS
- "5354:5354/tcp" # DNS (TCP)
environment:
DAEMON_URL: http://daemon:36941
HSD_URL: http://host.docker.internal:14037
LNS_MODE: light
LNS_HTTP_PORT: "5553"
LNS_DNS_PORT: "5354"
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on:
- daemon
# --- Docs ---
docs:
build:
context: ../../zano-upstream/zano-docs
container_name: lthn-docs
ports:
- "8098:80"
volumes:
daemon-data:
wallet-data:
explorer-db:
trade-db:

View file

@ -0,0 +1,186 @@
# Lethean Exit Node
# Run a VPN exit node and earn LTHN by providing bandwidth to the network.
# Peers with Lethean gateways which route encrypted VPN traffic through your node.
#
# Usage:
# cp .env.example .env
# # Set WALLET_PASSWORD, EXIT_PUBLIC_IP, and EXIT_NAME
# docker compose -f docker-compose.exit.yml up -d
#
# What it does:
# - Runs a full chain node (syncs blockchain)
# - Runs a wallet with PoS staking
# - Runs a WireGuard VPN server
# - Registers as an exit node on-chain via alias
# - Peers with gateway nodes for traffic routing
# - Earns LTHN for bandwidth provided
#
# Requirements:
# - Public IP address (or port forwarding on router)
# - Open ports: 46942 (P2P), 51820/udp (WireGuard)
# - Linux with Docker (WireGuard kernel module)
#
# Ports:
# 46942 — P2P (chain peering)
# 51820 — WireGuard VPN (UDP)
# 8124 — Exit node management API (local only)
services:
# --- Chain Node ---
daemon:
image: lthn/chain:testnet
container_name: lthn-exit-daemon
restart: unless-stopped
ports:
- "${DAEMON_P2P_PORT:-46942}:36942"
volumes:
- chain-data:/data
entrypoint:
- lethean-chain-node
command:
- --data-dir
- /data
- --rpc-bind-ip
- "0.0.0.0"
- --rpc-bind-port
- "36941"
- --p2p-bind-port
- "36942"
- --rpc-enable-admin-api
- --allow-local-ip
- --log-level
- "${DAEMON_LOG_LEVEL:-1}"
- --disable-upnp
- --rpc-ignore-offline
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:36941/json_rpc -d '{\"jsonrpc\":\"2.0\",\"id\":\"0\",\"method\":\"getinfo\"}' -H 'Content-Type: application/json' | grep -q OK"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
networks:
exit-net:
# --- Wallet (PoS staking + payment receipt) ---
wallet:
image: lthn/chain:testnet
container_name: lthn-exit-wallet
restart: unless-stopped
volumes:
- wallet-data:/wallet
entrypoint:
- sh
- -c
command:
- |
if [ ! -f /wallet/exit.wallet ]; then
echo '${WALLET_PASSWORD:-}' | lethean-wallet-cli --generate-new-wallet /wallet/exit.wallet --password '${WALLET_PASSWORD:-}' --daemon-address daemon:36941 --command exit;
fi;
lethean-wallet-cli \
--wallet-file /wallet/exit.wallet \
--password '${WALLET_PASSWORD:-}' \
--daemon-address daemon:36941 \
--rpc-bind-port 36944 \
--rpc-bind-ip 0.0.0.0 \
--do-pos-mining
depends_on:
daemon:
condition: service_healthy
networks:
exit-net:
# --- WireGuard VPN Exit ---
wireguard:
image: lscr.io/linuxserver/wireguard:latest
container_name: lthn-exit-wireguard
restart: unless-stopped
cap_add:
- NET_ADMIN
- SYS_MODULE
environment:
PUID: 1000
PGID: 1000
TZ: ${TZ:-Europe/London}
SERVERURL: ${EXIT_PUBLIC_IP:-auto}
SERVERPORT: 51820
PEERS: ${EXIT_MAX_PEERS:-25}
PEERDNS: 1.1.1.1,1.0.0.1
INTERNAL_SUBNET: 10.13.13.0
ALLOWEDIPS: 0.0.0.0/0,::/0
LOG_CONFS: "false"
ports:
- "51820:51820/udp"
volumes:
- wireguard-config:/config
sysctls:
- net.ipv4.conf.all.src_valid_mark=1
- net.ipv4.ip_forward=1
networks:
exit-net:
# --- Exit Node Controller ---
# Manages gateway peering, alias registration, and payment verification
controller:
image: lthn/chain:testnet
container_name: lthn-exit-controller
restart: unless-stopped
volumes:
- controller-data:/data
entrypoint:
- sh
- -c
command:
- |
echo "Lethean Exit Node Controller"
echo "Waiting for daemon and wallet..."
# Wait for daemon
until curl -sf http://daemon:36941/json_rpc -d '{"jsonrpc":"2.0","id":"0","method":"getinfo"}' -H 'Content-Type: application/json' > /dev/null 2>&1; do
sleep 5
done
echo "Daemon ready"
# Wait for wallet
until curl -sf http://wallet:36944/json_rpc -d '{"jsonrpc":"2.0","id":"0","method":"getbalance"}' -H 'Content-Type: application/json' > /dev/null 2>&1; do
sleep 5
done
echo "Wallet ready"
# Get wallet address
ADDR=$$(curl -sf http://wallet:36944/json_rpc -d '{"jsonrpc":"2.0","id":"0","method":"getaddress"}' -H 'Content-Type: application/json' | grep -oP '"address":"[^"]+' | cut -d'"' -f4)
echo "Exit node wallet: $$ADDR"
# Get chain height
HEIGHT=$$(curl -sf http://daemon:36941/json_rpc -d '{"jsonrpc":"2.0","id":"0","method":"getinfo"}' -H 'Content-Type: application/json' | grep -oP '"height":[0-9]+' | cut -d: -f2)
echo "Chain height: $$HEIGHT"
# Register alias if EXIT_NAME is set and we have balance
if [ -n "${EXIT_NAME:-}" ]; then
echo "Exit node name: ${EXIT_NAME}"
echo "To register on-chain: send 1 LTHN to this wallet, then alias will auto-register"
echo "Alias comment: v=lthn1;type=exit;cap=vpn,proxy;ip=${EXIT_PUBLIC_IP:-auto}"
fi
# Status loop
while true; do
BALANCE=$$(curl -sf http://wallet:36944/json_rpc -d '{"jsonrpc":"2.0","id":"0","method":"getbalance"}' -H 'Content-Type: application/json' | grep -oP '"balance":[0-9]+' | cut -d: -f2)
HEIGHT=$$(curl -sf http://daemon:36941/json_rpc -d '{"jsonrpc":"2.0","id":"0","method":"getinfo"}' -H 'Content-Type: application/json' | grep -oP '"height":[0-9]+' | cut -d: -f2)
WG_PEERS=$$(curl -sf http://wireguard:51820 2>/dev/null | wc -l || echo 0)
echo "$$(date +%H:%M:%S) height=$$HEIGHT balance=$$(echo "scale=4; $$BALANCE/1000000000000" | bc 2>/dev/null || echo $$BALANCE) peers=$$WG_PEERS"
sleep 60
done
depends_on:
daemon:
condition: service_healthy
networks:
exit-net:
networks:
exit-net:
driver: bridge
volumes:
chain-data:
wallet-data:
wireguard-config:
controller-data:

View file

@ -0,0 +1,7 @@
# Local override — mounts existing testnet chain data
# Usage: docker compose -f docker-compose.pull.yml -f docker-compose.local.yml up -d
services:
daemon:
volumes:
- /opt/lethean/testnet-data:/data

View file

@ -0,0 +1,87 @@
# Lethean Home Node
# A self-contained chain node for home users who want to support the network.
# Syncs the chain, runs a wallet with PoS staking, and exposes P2P for peering.
#
# Usage:
# cp .env.example .env # edit WALLET_PASSWORD at minimum
# docker compose -f docker-compose.node.yml up -d
#
# What it does:
# - Syncs and validates the Lethean blockchain
# - Peers with other nodes (P2P port 46942)
# - Runs a wallet that stakes automatically (PoS mining)
# - Optionally mines with ProgPoWZ (connect external GPU miner)
#
# Earnings:
# - PoS staking rewards (proportional to balance)
# - PoW block rewards if mining
#
# Ports:
# 46941 — Daemon RPC (local tools)
# 46942 — P2P (open this on your router for full node)
# 46944 — Wallet RPC (local tools)
services:
daemon:
image: lthn/chain:testnet
container_name: lthn-node
restart: unless-stopped
ports:
- "${DAEMON_RPC_PORT:-46941}:36941"
- "${DAEMON_P2P_PORT:-46942}:36942"
volumes:
- chain-data:/data
entrypoint:
- lethean-chain-node
command:
- --data-dir
- /data
- --rpc-bind-ip
- "0.0.0.0"
- --rpc-bind-port
- "36941"
- --p2p-bind-port
- "36942"
- --rpc-enable-admin-api
- --allow-local-ip
- --log-level
- "${DAEMON_LOG_LEVEL:-1}"
- --disable-upnp
- --rpc-ignore-offline
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:36941/json_rpc -d '{\"jsonrpc\":\"2.0\",\"id\":\"0\",\"method\":\"getinfo\"}' -H 'Content-Type: application/json' | grep -q OK"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
wallet:
image: lthn/chain:testnet
container_name: lthn-wallet
restart: unless-stopped
ports:
- "${WALLET_RPC_PORT:-46944}:36944"
volumes:
- wallet-data:/wallet
entrypoint:
- sh
- -c
command:
- |
if [ ! -f /wallet/node.wallet ]; then
echo '${WALLET_PASSWORD:-}' | lethean-wallet-cli --generate-new-wallet /wallet/node.wallet --password '${WALLET_PASSWORD:-}' --daemon-address daemon:36941 --command exit;
fi;
lethean-wallet-cli \
--wallet-file /wallet/node.wallet \
--password '${WALLET_PASSWORD:-}' \
--daemon-address daemon:36941 \
--rpc-bind-port 36944 \
--rpc-bind-ip 0.0.0.0 \
--do-pos-mining
depends_on:
daemon:
condition: service_healthy
volumes:
chain-data:
wallet-data:

View file

@ -0,0 +1,114 @@
# Lethean Chain Nodes Only — minimal network simulation
# 3 nodes + miner, no explorer/trade (for testing consensus, HF activation, reorgs)
#
# Usage:
# docker compose -f docker-compose.nodes.yml up -d
# # Start mining on node1:
# docker exec lthn-node1 curl -X POST http://127.0.0.1:36941/start_mining \
# -H 'Content-Type: application/json' \
# -d '{"miner_address":"YOUR_iTHN_ADDRESS","threads_count":2}'
# # Check sync status:
# for n in 1 2 3; do
# echo -n "node$n: "; docker exec lthn-node$n curl -s http://127.0.0.1:36941/json_rpc \
# -H 'Content-Type: application/json' \
# -d '{"jsonrpc":"2.0","id":"0","method":"getinfo"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['result']['height'])"
# done
services:
node1:
build:
context: ..
dockerfile: utils/docker/lthn-chain/Dockerfile
target: chain-service
args:
BUILD_TESTNET: 1
BUILD_THREADS: 4
container_name: lthn-node1
ports:
- "46941:36941"
- "46942:36942"
volumes:
- node1-data:/data
command: >
lethean-chain-node
--data-dir /data
--rpc-bind-ip 0.0.0.0
--rpc-bind-port 36941
--p2p-bind-port 36942
--rpc-enable-admin-api
--allow-local-ip
--log-level 1
--disable-upnp
networks:
lthn-net:
ipv4_address: 172.29.0.10
node2:
build:
context: ..
dockerfile: utils/docker/lthn-chain/Dockerfile
target: chain-service
args:
BUILD_TESTNET: 1
BUILD_THREADS: 4
container_name: lthn-node2
ports:
- "46951:36941"
volumes:
- node2-data:/data
command: >
lethean-chain-node
--data-dir /data
--rpc-bind-ip 0.0.0.0
--rpc-bind-port 36941
--p2p-bind-port 36942
--add-exclusive-node 172.29.0.10:36942
--allow-local-ip
--log-level 1
--disable-upnp
depends_on:
- node1
networks:
lthn-net:
ipv4_address: 172.29.0.11
node3:
build:
context: ..
dockerfile: utils/docker/lthn-chain/Dockerfile
target: chain-service
args:
BUILD_TESTNET: 1
BUILD_THREADS: 4
container_name: lthn-node3
ports:
- "46961:36941"
volumes:
- node3-data:/data
command: >
lethean-chain-node
--data-dir /data
--rpc-bind-ip 0.0.0.0
--rpc-bind-port 36941
--p2p-bind-port 36942
--add-exclusive-node 172.29.0.10:36942
--allow-local-ip
--log-level 1
--disable-upnp
depends_on:
- node1
networks:
lthn-net:
ipv4_address: 172.29.0.12
networks:
lthn-net:
driver: bridge
ipam:
config:
- subnet: 172.29.0.0/16
volumes:
node1-data:
node2-data:
node3-data:

View file

@ -0,0 +1,235 @@
# Lethean Testnet Ecosystem
# Full node + wallet + explorer + trade + pool + LNS
#
# Quick start:
# cp .env.example .env # edit passwords/hostname
# docker compose -f docker-compose.pull.yml up -d
# docker compose -f docker-compose.pull.yml logs -f daemon
#
# Services:
# Explorer: http://${PUBLIC_HOST:-localhost}:3335
# Trade: http://${PUBLIC_HOST:-localhost}:3338
# Trade API: http://${PUBLIC_HOST:-localhost}:3336
# Pool API: http://${PUBLIC_HOST:-localhost}:2117
# LNS HTTP: http://${PUBLIC_HOST:-localhost}:5553
# Docs: http://${PUBLIC_HOST:-localhost}:8099
# Daemon RPC: http://${PUBLIC_HOST:-localhost}:46941
# Wallet RPC: http://${PUBLIC_HOST:-localhost}:46944
services:
# --- Chain ---
daemon:
image: lthn/chain:testnet
container_name: lthn-daemon
restart: unless-stopped
ports:
- "${DAEMON_RPC_PORT:-46941}:36941"
- "${DAEMON_P2P_PORT:-46942}:36942"
volumes:
- chain-data:/data
entrypoint:
- lethean-chain-node
command:
- --data-dir
- /data
- --rpc-bind-ip
- "0.0.0.0"
- --rpc-bind-port
- "36941"
- --p2p-bind-port
- "36942"
- --rpc-enable-admin-api
- --allow-local-ip
- --log-level
- "${DAEMON_LOG_LEVEL:-1}"
- --disable-upnp
- --rpc-ignore-offline
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:36941/json_rpc -d '{\"jsonrpc\":\"2.0\",\"id\":\"0\",\"method\":\"getinfo\"}' -H 'Content-Type: application/json' | grep -q OK"]
interval: 30s
timeout: 10s
retries: 5
start_period: 30s
wallet:
image: lthn/chain:testnet
container_name: lthn-wallet
restart: unless-stopped
ports:
- "${WALLET_RPC_PORT:-46944}:36944"
volumes:
- wallet-data:/wallet
entrypoint:
- sh
- -c
command:
- |
if [ ! -f /wallet/main.wallet ]; then
echo '${WALLET_PASSWORD:-}' | lethean-wallet-cli --generate-new-wallet /wallet/main.wallet --password '${WALLET_PASSWORD:-}' --daemon-address daemon:36941 --command exit;
fi;
lethean-wallet-cli \
--wallet-file /wallet/main.wallet \
--password '${WALLET_PASSWORD:-}' \
--daemon-address daemon:36941 \
--rpc-bind-port 36944 \
--rpc-bind-ip 0.0.0.0 \
--do-pos-mining
depends_on:
daemon:
condition: service_healthy
# --- Explorer ---
explorer-db:
image: postgres:16-alpine
container_name: lthn-explorer-db
restart: unless-stopped
environment:
POSTGRES_USER: ${EXPLORER_DB_USER:-explorer}
POSTGRES_PASSWORD: ${EXPLORER_DB_PASS:-explorer}
POSTGRES_DB: ${EXPLORER_DB_NAME:-lethean_explorer}
volumes:
- explorer-db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${EXPLORER_DB_USER:-explorer}"]
interval: 10s
timeout: 5s
retries: 5
explorer:
image: lthn/explorer:testnet
container_name: lthn-explorer
restart: unless-stopped
ports:
- "${EXPLORER_PORT:-3335}:3335"
environment:
API: http://daemon:36941
FRONTEND_API: http://${PUBLIC_HOST:-localhost}:${EXPLORER_PORT:-3335}
SERVER_PORT: "3335"
AUDITABLE_WALLET_API: http://daemon:36941
PGUSER: ${EXPLORER_DB_USER:-explorer}
PGPASSWORD: ${EXPLORER_DB_PASS:-explorer}
PGDATABASE: ${EXPLORER_DB_NAME:-lethean_explorer}
PGHOST: explorer-db
PGPORT: "5432"
MEXC_API_URL: ""
ASSETS_WHITELIST_URL: ""
depends_on:
daemon:
condition: service_healthy
explorer-db:
condition: service_healthy
# --- Trade ---
trade-db:
image: postgres:16-alpine
container_name: lthn-trade-db
restart: unless-stopped
environment:
POSTGRES_USER: ${TRADE_DB_USER:-trade}
POSTGRES_PASSWORD: ${TRADE_DB_PASS:-trade}
POSTGRES_DB: ${TRADE_DB_NAME:-lethean_trade}
volumes:
- trade-db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${TRADE_DB_USER:-trade}"]
interval: 10s
timeout: 5s
retries: 5
trade-api:
image: lthn/trade-api:testnet
container_name: lthn-trade-api
restart: unless-stopped
ports:
- "${TRADE_API_PORT:-3336}:3336"
environment:
PORT: "3336"
PGUSER: ${TRADE_DB_USER:-trade}
PGPASSWORD: ${TRADE_DB_PASS:-trade}
PGDATABASE: ${TRADE_DB_NAME:-lethean_trade}
PGHOST: trade-db
PGPORT: "5432"
JWT_SECRET: ${JWT_SECRET:-change-me-before-production}
DAEMON_RPC_URL: http://daemon:36941/json_rpc
depends_on:
daemon:
condition: service_healthy
trade-db:
condition: service_healthy
trade-frontend:
image: lthn/trade-frontend:testnet
container_name: lthn-trade-frontend
restart: unless-stopped
ports:
- "${TRADE_FRONTEND_PORT:-3338}:30289"
environment:
NEXT_PUBLIC_API_URL: http://${PUBLIC_HOST:-localhost}:${TRADE_API_PORT:-3336}
API_URL: http://trade-api:3336
depends_on:
- trade-api
# --- Mining Pool ---
pool-redis:
image: redis:7-alpine
container_name: lthn-pool-redis
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
pool:
image: lthn/pool:testnet
container_name: lthn-pool
restart: unless-stopped
ports:
- "${POOL_STRATUM_PORT:-5555}:5555"
- "${POOL_API_PORT:-2117}:2117"
- "7777:7777"
- "8888:8888"
volumes:
- ./pool-config.json:/pool/config.json:ro
depends_on:
daemon:
condition: service_healthy
wallet:
condition: service_started
pool-redis:
condition: service_healthy
# --- LNS (Lethean Name Service) ---
lns:
image: lthn/lns:testnet
container_name: lthn-lns
restart: unless-stopped
ports:
- "${LNS_HTTP_PORT:-5553}:5553"
- "${LNS_DNS_PORT:-5354}:5354/udp"
- "${LNS_DNS_PORT:-5354}:5354/tcp"
environment:
DAEMON_URL: http://daemon:36941
HSD_URL: http://host.docker.internal:14037
LNS_MODE: light
LNS_HTTP_PORT: "5553"
LNS_DNS_PORT: "5354"
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on:
daemon:
condition: service_healthy
# --- Docs ---
docs:
image: lthn/docs:testnet
container_name: lthn-docs
restart: unless-stopped
ports:
- "${DOCS_PORT:-8099}:80"
volumes:
chain-data:
wallet-data:
explorer-db:
trade-db:

View file

@ -0,0 +1,221 @@
# Lethean Testnet Network Simulation
# Spins up a 3-node testnet with explorer, wallet RPC, and trade services
#
# Usage:
# docker compose -f docker-compose.testnet.yml up -d
# docker compose -f docker-compose.testnet.yml logs -f node1
# docker compose -f docker-compose.testnet.yml down -v
#
# Nodes discover each other via exclusive-node flags.
# Node1 is the seed node, Node2 and Node3 connect to it.
services:
# === Blockchain Nodes ===
node1:
build:
context: ..
dockerfile: utils/docker/lthn-chain/Dockerfile
target: chain-service
args:
BUILD_TESTNET: 1
BUILD_THREADS: 4
container_name: lthn-node1
ports:
- "46941:36941" # RPC
- "46942:36942" # P2P
volumes:
- node1-data:/data
command: >
lethean-chain-node
--data-dir /data
--rpc-bind-ip 0.0.0.0
--rpc-bind-port 36941
--p2p-bind-port 36942
--rpc-enable-admin-api
--allow-local-ip
--log-level 1
--disable-upnp
networks:
lthn-testnet:
ipv4_address: 172.28.0.10
node2:
build:
context: ..
dockerfile: utils/docker/lthn-chain/Dockerfile
target: chain-service
args:
BUILD_TESTNET: 1
BUILD_THREADS: 4
container_name: lthn-node2
volumes:
- node2-data:/data
command: >
lethean-chain-node
--data-dir /data
--rpc-bind-ip 0.0.0.0
--rpc-bind-port 36941
--p2p-bind-port 36942
--add-exclusive-node 172.28.0.10:36942
--allow-local-ip
--log-level 1
--disable-upnp
depends_on:
- node1
networks:
lthn-testnet:
ipv4_address: 172.28.0.11
node3:
build:
context: ..
dockerfile: utils/docker/lthn-chain/Dockerfile
target: chain-service
args:
BUILD_TESTNET: 1
BUILD_THREADS: 4
container_name: lthn-node3
volumes:
- node3-data:/data
command: >
lethean-chain-node
--data-dir /data
--rpc-bind-ip 0.0.0.0
--rpc-bind-port 36941
--p2p-bind-port 36942
--add-exclusive-node 172.28.0.10:36942
--allow-local-ip
--log-level 1
--disable-upnp
depends_on:
- node1
networks:
lthn-testnet:
ipv4_address: 172.28.0.12
# === Wallet RPC ===
wallet:
build:
context: ..
dockerfile: utils/docker/lthn-chain/Dockerfile
target: chain-service
args:
BUILD_TESTNET: 1
BUILD_THREADS: 4
container_name: lthn-wallet
ports:
- "46944:36944" # Wallet RPC
volumes:
- wallet-data:/wallet
entrypoint: >
sh -c "
if [ ! -f /wallet/testnet.wallet ]; then
echo '' | lethean-wallet-cli --generate-new-wallet /wallet/testnet.wallet --password '' --daemon-address node1:36941 --command exit;
fi;
lethean-wallet-cli
--wallet-file /wallet/testnet.wallet
--password ''
--daemon-address node1:36941
--rpc-bind-port 36944
--rpc-bind-ip 0.0.0.0
--do-pos-mining
"
depends_on:
- node1
networks:
lthn-testnet:
ipv4_address: 172.28.0.20
# === Explorer ===
explorer-db:
image: postgres:16-alpine
container_name: lthn-explorer-db
environment:
POSTGRES_USER: explorer
POSTGRES_PASSWORD: explorer
POSTGRES_DB: lethean_explorer
volumes:
- explorer-db-data:/var/lib/postgresql/data
networks:
lthn-testnet:
ipv4_address: 172.28.0.30
explorer:
build:
context: ../../lthn/zano-upstream/zano-explorer-zarcanum
dockerfile: Dockerfile
container_name: lthn-explorer
ports:
- "3335:3335"
environment:
API: http://node1:36941
FRONTEND_API: http://127.0.0.1:3335
SERVER_PORT: "3335"
AUDITABLE_WALLET_API: http://node1:36941
PGUSER: explorer
PGPASSWORD: explorer
PGDATABASE: lethean_explorer
PGHOST: explorer-db
PGPORT: "5432"
MEXC_API_URL: ""
depends_on:
- node1
- explorer-db
networks:
lthn-testnet:
ipv4_address: 172.28.0.31
# === Trade Backend ===
trade-db:
image: postgres:16-alpine
container_name: lthn-trade-db
environment:
POSTGRES_USER: trade
POSTGRES_PASSWORD: trade
POSTGRES_DB: lethean_trade
volumes:
- trade-db-data:/var/lib/postgresql/data
networks:
lthn-testnet:
ipv4_address: 172.28.0.40
trade-api:
build:
context: ../../lthn/zano-upstream/zano_trade_backend
container_name: lthn-trade-api
ports:
- "3336:3336"
environment:
PORT: "3336"
PGUSER: trade
PGPASSWORD: trade
PGDATABASE: lethean_trade
PGHOST: trade-db
PGPORT: "5432"
JWT_SECRET: testnet-dev-secret
DAEMON_RPC_URL: http://node1:36941/json_rpc
depends_on:
- node1
- trade-db
networks:
lthn-testnet:
ipv4_address: 172.28.0.41
networks:
lthn-testnet:
driver: bridge
ipam:
config:
- subnet: 172.28.0.0/16
volumes:
node1-data:
node2-data:
node3-data:
wallet-data:
explorer-db-data:
trade-db-data:

View file

@ -0,0 +1,126 @@
# Lethean VPN Stack — sandboxed legacy Python + WireGuard
# Chain node + wallet + VPN dispatcher + WireGuard gateway
#
# Usage:
# docker compose -f docker-compose.vpn.yml up -d
#
# This sandboxes the legacy Python VPN code inside containers
# until the CoreGO replacement is ready.
services:
# Chain daemon (testnet)
daemon:
build:
context: ..
dockerfile: utils/docker/lthn-chain/Dockerfile
target: chain-service
args:
BUILD_TESTNET: 1
BUILD_THREADS: 4
container_name: lthn-vpn-daemon
volumes:
- daemon-data:/data
command: >
lethean-chain-node
--data-dir /data
--rpc-bind-ip 0.0.0.0
--rpc-bind-port 36941
--p2p-bind-port 36942
--rpc-enable-admin-api
--allow-local-ip
--log-level 1
--disable-upnp
networks:
vpn-net:
ipv4_address: 172.31.0.10
# Wallet RPC (for payment processing)
wallet:
build:
context: ..
dockerfile: utils/docker/lthn-chain/Dockerfile
target: chain-service
args:
BUILD_TESTNET: 1
BUILD_THREADS: 4
container_name: lthn-vpn-wallet
volumes:
- wallet-data:/wallet
entrypoint: >
sh -c "
if [ ! -f /wallet/vpn.wallet ]; then
echo '' | lethean-wallet-cli --generate-new-wallet /wallet/vpn.wallet --password '' --daemon-address daemon:36941 --command exit;
fi;
lethean-wallet-cli
--wallet-file /wallet/vpn.wallet
--password ''
--daemon-address daemon:36941
--rpc-bind-port 36944
--rpc-bind-ip 0.0.0.0
"
depends_on:
- daemon
networks:
vpn-net:
ipv4_address: 172.31.0.20
# VPN Dispatcher (legacy Python, sandboxed)
dispatcher:
build:
context: ../../lthn/lthn-app-vpn
container_name: lthn-vpn-dispatcher
cap_add:
- NET_ADMIN
environment:
DAEMON_HOST: daemon
DAEMON_RPC_PORT: "36941"
MODE: server
ports:
- "8124:8124" # Server management API
depends_on:
- daemon
- wallet
networks:
vpn-net:
ipv4_address: 172.31.0.30
# WireGuard Gateway
wireguard:
image: lscr.io/linuxserver/wireguard:latest
container_name: lthn-vpn-wireguard
cap_add:
- NET_ADMIN
- SYS_MODULE
environment:
PUID: 1000
PGID: 1000
TZ: Europe/London
SERVERURL: auto
SERVERPORT: 51820
PEERS: 10
PEERDNS: 1.1.1.1
INTERNAL_SUBNET: 10.13.13.0
ALLOWEDIPS: 0.0.0.0/0,::/0
LOG_CONFS: "false"
ports:
- "51820:51820/udp"
volumes:
- wireguard-config:/config
sysctls:
- net.ipv4.conf.all.src_valid_mark=1
- net.ipv4.ip_forward=1
networks:
vpn-net:
ipv4_address: 172.31.0.40
networks:
vpn-net:
driver: bridge
ipam:
config:
- subnet: 172.31.0.0/24
volumes:
daemon-data:
wallet-data:
wireguard-config:

58
docker/health.sh Executable file
View file

@ -0,0 +1,58 @@
#!/bin/bash
# Lethean Testnet Health Check
# Run: bash health.sh
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
check() {
local name="$1"
local url="$2"
local code
code=$(curl -sf -o /dev/null -w "%{http_code}" "$url" 2>/dev/null)
if [ "$code" = "200" ] || [ "$code" = "307" ] || [ "$code" = "404" ]; then
printf " ${GREEN}%-16s${NC} %s ${YELLOW}(HTTP %s)${NC}\n" "$name" "$url" "$code"
else
printf " ${RED}%-16s${NC} %s ${RED}(DOWN)${NC}\n" "$name" "$url"
fi
}
rpc() {
local name="$1"
local url="$2"
local method="$3"
local result
result=$(curl -sf "$url" -d "{\"jsonrpc\":\"2.0\",\"id\":\"0\",\"method\":\"$method\"}" -H 'Content-Type: application/json' 2>/dev/null)
if [ -n "$result" ]; then
printf " ${GREEN}%-16s${NC} %s\n" "$name" "$(echo "$result" | python3 -c "
import sys,json
d=json.load(sys.stdin).get('result',{})
if 'height' in d:
hf = d.get('is_hardfok_active',[])
active = sum(1 for h in hf if h)
print(f'height={d[\"height\"]}, HF0-{active-1} active, status={d.get(\"status\",\"?\")}')
elif 'balance' in d:
print(f'{d[\"balance\"]/1e12:.4f} LTHN ({d[\"unlocked_balance\"]/1e12:.4f} unlocked)')
else:
print(json.dumps(d)[:80])
" 2>/dev/null)"
else
printf " ${RED}%-16s${NC} %s ${RED}(DOWN)${NC}\n" "$name" "$url"
fi
}
echo ""
echo " Lethean Testnet Health Check"
echo " $(date)"
echo " ---"
rpc "Daemon" "http://localhost:${DAEMON_RPC_PORT:-46941}/json_rpc" "getinfo"
rpc "Wallet" "http://localhost:${WALLET_RPC_PORT:-46944}/json_rpc" "getbalance"
check "Explorer" "http://localhost:${EXPLORER_PORT:-3335}/"
check "Trade API" "http://localhost:${TRADE_API_PORT:-3336}/"
check "Trade Web" "http://localhost:${TRADE_FRONTEND_PORT:-3338}/"
check "Pool API" "http://localhost:${POOL_API_PORT:-2117}/stats"
check "LNS" "http://localhost:${LNS_HTTP_PORT:-5553}/"
check "Docs" "http://localhost:${DOCS_PORT:-8099}/"
echo ""

View file

@ -0,0 +1,15 @@
[Unit]
Description=Lethean Testnet Ecosystem
Requires=docker.service
After=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/lethean/docker
ExecStart=/usr/bin/docker compose -f docker-compose.pull.yml up -d
ExecStop=/usr/bin/docker compose -f docker-compose.pull.yml down
TimeoutStartSec=120
[Install]
WantedBy=multi-user.target

52
docker/lethean/.env Normal file
View file

@ -0,0 +1,52 @@
# Lethean Testnet Configuration
# Copy to .env and customise before running:
# cp .env.example .env
# docker compose -f docker-compose.pull.yml up -d
# Public hostname (used by explorer and trade frontend)
PUBLIC_HOST=localhost
# Wallet password (empty = no password, change for production)
WALLET_PASSWORD=my-secure-pass
# Trade API JWT secret (CHANGE THIS for production)
JWT_SECRET=change-me-before-production
# Explorer database
EXPLORER_DB_USER=explorer
EXPLORER_DB_PASS=explorer
EXPLORER_DB_NAME=lethean_explorer
# Trade database
TRADE_DB_USER=trade
TRADE_DB_PASS=trade
TRADE_DB_NAME=lethean_trade
# Daemon log level (0=minimal, 1=normal, 2=detailed, 3=trace)
DAEMON_LOG_LEVEL=1
# Port overrides (defaults shown)
# DAEMON_RPC_PORT=46941
# DAEMON_P2P_PORT=46942
# WALLET_RPC_PORT=46944
# EXPLORER_PORT=3335
# TRADE_API_PORT=3336
# TRADE_FRONTEND_PORT=3338
# POOL_STRATUM_PORT=5555
# POOL_API_PORT=2117
# LNS_HTTP_PORT=5553
# LNS_DNS_PORT=5354
# === Exit Node Settings (docker-compose.exit.yml) ===
# Your public IP address (required for VPN exit node)
# EXIT_PUBLIC_IP=auto
# Exit node name (registered as on-chain alias)
# EXIT_NAME=my-exit-node
# Maximum VPN peers (WireGuard clients)
# EXIT_MAX_PEERS=25
# Timezone
# TZ=Europe/London

View file

@ -0,0 +1,52 @@
# Lethean Testnet Configuration
# Copy to .env and customise before running:
# cp .env.example .env
# docker compose -f docker-compose.pull.yml up -d
# Public hostname (used by explorer and trade frontend)
PUBLIC_HOST=localhost
# Wallet password (empty = no password, change for production)
WALLET_PASSWORD=
# Trade API JWT secret (CHANGE THIS for production)
JWT_SECRET=change-me-before-production
# Explorer database
EXPLORER_DB_USER=explorer
EXPLORER_DB_PASS=explorer
EXPLORER_DB_NAME=lethean_explorer
# Trade database
TRADE_DB_USER=trade
TRADE_DB_PASS=trade
TRADE_DB_NAME=lethean_trade
# Daemon log level (0=minimal, 1=normal, 2=detailed, 3=trace)
DAEMON_LOG_LEVEL=1
# Port overrides (defaults shown)
# DAEMON_RPC_PORT=46941
# DAEMON_P2P_PORT=46942
# WALLET_RPC_PORT=46944
# EXPLORER_PORT=3335
# TRADE_API_PORT=3336
# TRADE_FRONTEND_PORT=3338
# POOL_STRATUM_PORT=5555
# POOL_API_PORT=2117
# LNS_HTTP_PORT=5553
# LNS_DNS_PORT=5354
# === Exit Node Settings (docker-compose.exit.yml) ===
# Your public IP address (required for VPN exit node)
# EXIT_PUBLIC_IP=auto
# Exit node name (registered as on-chain alias)
# EXIT_NAME=my-exit-node
# Maximum VPN peers (WireGuard clients)
# EXIT_MAX_PEERS=25
# Timezone
# TZ=Europe/London

View file

@ -0,0 +1,86 @@
# Lethean Home Node
# A self-contained chain node for home users who want to support the network.
# Syncs the chain, runs a wallet with PoS staking, and exposes P2P for peering.
#
# Usage:
# cp .env.example .env # edit WALLET_PASSWORD at minimum
# docker compose -f docker-compose.node.yml up -d
#
# What it does:
# - Syncs and validates the Lethean blockchain
# - Peers with other nodes (P2P port 46942)
# - Runs a wallet that stakes automatically (PoS mining)
# - Optionally mines with ProgPoWZ (connect external GPU miner)
#
# Earnings:
# - PoS staking rewards (proportional to balance)
# - PoW block rewards if mining
#
# Ports:
# 46941 — Daemon RPC (local tools)
# 46942 — P2P (open this on your router for full node)
# 46944 — Wallet RPC (local tools)
services:
daemon:
image: lthn/chain:testnet
container_name: lthn-node
restart: unless-stopped
ports:
- "${DAEMON_RPC_PORT:-46941}:36941"
- "${DAEMON_P2P_PORT:-46942}:36942"
volumes:
- chain-data:/data
entrypoint:
- lethean-chain-node
command:
- --data-dir
- /data
- --rpc-bind-ip
- "0.0.0.0"
- --rpc-bind-port
- "36941"
- --p2p-bind-port
- "36942"
- --rpc-enable-admin-api
- --allow-local-ip
- --log-level
- "${DAEMON_LOG_LEVEL:-1}"
- --disable-upnp
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:36941/json_rpc -d '{\"jsonrpc\":\"2.0\",\"id\":\"0\",\"method\":\"getinfo\"}' -H 'Content-Type: application/json' | grep -q OK"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
wallet:
image: lthn/chain:testnet
container_name: lthn-wallet
restart: unless-stopped
ports:
- "${WALLET_RPC_PORT:-46944}:36944"
volumes:
- wallet-data:/wallet
entrypoint:
- sh
- -c
command:
- |
if [ ! -f /wallet/node.wallet ]; then
echo '${WALLET_PASSWORD:-}' | lethean-wallet-cli --generate-new-wallet /wallet/node.wallet --password '${WALLET_PASSWORD:-}' --daemon-address daemon:36941 --command exit;
fi;
lethean-wallet-cli \
--wallet-file /wallet/node.wallet \
--password '${WALLET_PASSWORD:-}' \
--daemon-address daemon:36941 \
--rpc-bind-port 36944 \
--rpc-bind-ip 0.0.0.0 \
--do-pos-mining
depends_on:
daemon:
condition: service_healthy
volumes:
chain-data:
wallet-data:

301
docker/pool-config.json Normal file
View file

@ -0,0 +1,301 @@
{
"poolHost": "lethean.somewhere.com",
"coin": "Lethean",
"symbol": "LTHN",
"coinUnits": 1000000000000,
"coinDecimalPlaces": 12,
"coinDifficultyTarget": 120,
"blockchainExplorer": "http://localhost:3335/block/{id}",
"transactionExplorer": "http://localhost:3335/transaction/{id}",
"daemonType": "default",
"cnAlgorithm": "progpowz",
"cnVariant": 2,
"cnBlobType": 0,
"isRandomX": false,
"includeHeight": false,
"previousOffset": 7,
"offset": 2,
"isCryptonight": false,
"reward": 1000000000000,
"logging": {
"files": {
"level": "info",
"directory": "logs",
"flushInterval": 5,
"prefix": "Lethean"
},
"console": {
"level": "info",
"colors": true
}
},
"childPools": [],
"poolServer": {
"enabled": true,
"mergedMining": false,
"clusterForks": 3,
"poolAddress": "iTHNUNiuu3VP1yy8xH2y5iQaABKXurdjqZmzFiBiyR4dKG3j6534e9jMriY6SM7PH8NibVwVWW1DWJfQEWnSjS8n3Wgx86pQpY",
"intAddressPrefix": null,
"blockRefreshInterval": 1000,
"minerTimeout": 900,
"sslCert": "cert.pem",
"sslKey": "privkey.pem",
"sslCA": "chain.pem",
"ports": [
{
"port": 5555,
"difficulty": 50000,
"desc": "Low end hardware"
},
{
"port": 7777,
"difficulty": 500000,
"desc": "Mid/high end hardware"
},
{
"port": 8888,
"difficulty": 5000000,
"desc": "Nicehash, MRR"
}
],
"varDiff": {
"minDiff": 10000,
"maxDiff": 5000000,
"targetTime": 45,
"retargetTime": 60,
"variancePercent": 30,
"maxJump": 100
},
"paymentId": {
"addressSeparator": "+"
},
"fixedDiff": {
"enabled": true,
"addressSeparator": "."
},
"shareTrust": {
"enabled": true,
"min": 10,
"stepDown": 3,
"threshold": 10,
"penalty": 30
},
"banning": {
"enabled": true,
"time": 600,
"invalidPercent": 25,
"checkThreshold": 30
},
"slushMining": {
"enabled": false,
"weight": 300,
"blockTime": 60,
"lastBlockCheckRate": 1
}
},
"payments": {
"enabled": true,
"interval": 900,
"maxAddresses": 5,
"mixin": 10,
"priority": 0,
"transferFee": 10000000000,
"dynamicTransferFee": true,
"minerPayFee": true,
"minPayment": 1000000000000,
"maxPayment": 100000000000000,
"maxTransactionAmount": 100000000000000,
"denomination": 1000000000000
},
"blockUnlocker": {
"enabled": true,
"interval": 60,
"depth": 10,
"poolFee": 0.2,
"soloFee": 0.2,
"devDonation": 0.5,
"networkFee": 0.0
},
"api": {
"enabled": true,
"hashrateWindow": 600,
"updateInterval": 15,
"bindIp": "0.0.0.0",
"port": 2117,
"blocks": 30,
"payments": 30,
"password": "password",
"ssl": false,
"sslPort": 2119,
"sslCert": "cert.pem",
"sslKey": "privkey.pem",
"sslCA": "chain.pem",
"trustProxyIP": true
},
"zmq": {
"enabled": false,
"host": "127.0.0.1",
"port": 39995
},
"daemon": {
"host": "daemon",
"port": 36941
},
"wallet": {
"host": "wallet",
"port": 36944
},
"redis": {
"host": "pool-redis",
"port": 6379,
"db": 11,
"cleanupInterval": 15
},
"notifications": {
"emailTemplate": "email_templates/default.txt",
"emailSubject": {
"emailAdded": "Your email was registered",
"workerConnected": "Worker %WORKER_NAME% connected",
"workerTimeout": "Worker %WORKER_NAME% stopped hashing",
"workerBanned": "Worker %WORKER_NAME% banned",
"blockFound": "Block %HEIGHT% found !",
"blockUnlocked": "Block %HEIGHT% unlocked !",
"blockOrphaned": "Block %HEIGHT% orphaned !",
"payment": "We sent you a payment !"
},
"emailMessage": {
"emailAdded": "Your email has been registered to receive pool notifications.",
"workerConnected": "Your worker %WORKER_NAME% for address %MINER% is now connected from ip %IP%.",
"workerTimeout": "Your worker %WORKER_NAME% for address %MINER% has stopped submitting hashes on %LAST_HASH%.",
"workerBanned": "Your worker %WORKER_NAME% for address %MINER% has been banned.",
"blockFound": "Block found at height %HEIGHT% by miner %MINER% on %TIME%. Waiting maturity.",
"blockUnlocked": "Block mined at height %HEIGHT% with %REWARD% and %EFFORT% effort on %TIME%.",
"blockOrphaned": "Block orphaned at height %HEIGHT% :(",
"payment": "A payment of %AMOUNT% has been sent to %ADDRESS% wallet."
},
"telegramMessage": {
"workerConnected": "Your worker _%WORKER_NAME%_ for address _%MINER%_ is now connected from ip _%IP%_.",
"workerTimeout": "Your worker _%WORKER_NAME%_ for address _%MINER%_ has stopped submitting hashes on _%LAST_HASH%_.",
"workerBanned": "Your worker _%WORKER_NAME%_ for address _%MINER%_ has been banned.",
"blockFound": "*Block found at height* _%HEIGHT%_ *by miner* _%MINER%_*! Waiting maturity.*",
"blockUnlocked": "*Block mined at height* _%HEIGHT%_ *with* _%REWARD%_ *and* _%EFFORT%_ *effort on* _%TIME%_*.*",
"blockOrphaned": "*Block orphaned at height* _%HEIGHT%_ *:(*",
"payment": "A payment of _%AMOUNT%_ has been sent."
}
},
"email": {
"enabled": false,
"fromAddress": "your@email.com",
"transport": "sendmail",
"sendmail": {
"path": "/usr/sbin/sendmail"
},
"smtp": {
"host": "smtp.example.com",
"port": 587,
"secure": false,
"auth": {
"user": "username",
"pass": "password"
},
"tls": {
"rejectUnauthorized": false
}
},
"mailgun": {
"key": "your-private-key",
"domain": "mg.yourdomain"
}
},
"telegram": {
"enabled": false,
"botName": "",
"token": "",
"channel": "",
"channelStats": {
"enabled": false,
"interval": 30
},
"botCommands": {
"stats": "/stats",
"report": "/report",
"notify": "/notify",
"blocks": "/blocks"
}
},
"monitoring": {
"daemon": {
"checkInterval": 60,
"rpcMethod": "getblockcount"
},
"wallet": {
"checkInterval": 60,
"rpcMethod": "getbalance"
}
},
"prices": {
"source": "tradeogre",
"currency": "USD"
},
"charts": {
"pool": {
"hashrate": {
"enabled": true,
"updateInterval": 60,
"stepInterval": 1800,
"maximumPeriod": 86400
},
"miners": {
"enabled": true,
"updateInterval": 60,
"stepInterval": 1800,
"maximumPeriod": 86400
},
"workers": {
"enabled": true,
"updateInterval": 60,
"stepInterval": 1800,
"maximumPeriod": 86400
},
"difficulty": {
"enabled": true,
"updateInterval": 1800,
"stepInterval": 10800,
"maximumPeriod": 604800
},
"price": {
"enabled": true,
"updateInterval": 1800,
"stepInterval": 10800,
"maximumPeriod": 604800
},
"profit": {
"enabled": true,
"updateInterval": 1800,
"stepInterval": 10800,
"maximumPeriod": 604800
}
},
"user": {
"hashrate": {
"enabled": true,
"updateInterval": 180,
"stepInterval": 1800,
"maximumPeriod": 86400
},
"worker_hashrate": {
"enabled": true,
"updateInterval": 60,
"stepInterval": 60,
"maximumPeriod": 86400
},
"payments": {
"enabled": true
}
},
"blocks": {
"enabled": true,
"days": 30
}
}
}

90
docker/test-network.sh Executable file
View file

@ -0,0 +1,90 @@
#!/bin/bash
# Lethean testnet network validation script
# Tests: node sync, mining, wallet, block propagation
#
# Usage: ./test-network.sh [compose-file]
# Default: docker-compose.nodes.yml
set -e
COMPOSE=${1:-docker-compose.nodes.yml}
echo "=== Lethean Network Test ==="
echo "Using: $COMPOSE"
echo ""
# Check all nodes are running
echo "1. Checking node health..."
for n in 1 2 3; do
port=$((46940 + (n-1)*10 + 1))
height=$(curl -s -X POST http://127.0.0.1:$port/json_rpc \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":"0","method":"getinfo"}' 2>/dev/null | \
python3 -c "import sys,json; print(json.load(sys.stdin)['result']['height'])" 2>/dev/null)
echo " node$n (:$port): height=$height"
done
echo ""
echo "2. Checking P2P connectivity..."
peers=$(curl -s -X POST http://127.0.0.1:46941/json_rpc \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":"0","method":"getinfo"}' 2>/dev/null | \
python3 -c "import sys,json; d=json.load(sys.stdin)['result']; print(f'out={d[\"outgoing_connections_count\"]} in={d[\"incoming_connections_count\"]}')" 2>/dev/null)
echo " node1 peers: $peers"
echo ""
echo "3. Testing getblocktemplate..."
template=$(curl -s -X POST http://127.0.0.1:46941/json_rpc \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":"0","method":"getblocktemplate","params":{"wallet_address":"iTHNUNiuu3VP1yy8xH2y5iQaABKXurdjqZmzFiBiyR4dKG3j6534e9jMriY6SM7PH8NibVwVWW1DWJfQEWnSjS8n3Wgx86pQpY"}}' 2>/dev/null | \
python3 -c "import sys,json; r=json.load(sys.stdin)['result']; print(f'height={r[\"height\"]} diff={r[\"difficulty\"]}')" 2>/dev/null)
echo " template: $template"
echo ""
echo "4. Starting mining on node1..."
curl -s -X POST http://127.0.0.1:46941/start_mining \
-H 'Content-Type: application/json' \
-d '{"miner_address":"iTHNUNiuu3VP1yy8xH2y5iQaABKXurdjqZmzFiBiyR4dKG3j6534e9jMriY6SM7PH8NibVwVWW1DWJfQEWnSjS8n3Wgx86pQpY","threads_count":4}' 2>/dev/null | python3 -m json.tool 2>/dev/null
echo ""
echo "5. Waiting 60s for blocks..."
sleep 60
echo ""
echo "6. Checking sync across nodes..."
heights=""
for n in 1 2 3; do
port=$((46940 + (n-1)*10 + 1))
h=$(curl -s -X POST http://127.0.0.1:$port/json_rpc \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":"0","method":"getinfo"}' 2>/dev/null | \
python3 -c "import sys,json; print(json.load(sys.stdin)['result']['height'])" 2>/dev/null)
echo " node$n: height=$h"
heights="$heights $h"
done
# Check all nodes at same height
unique=$(echo $heights | tr ' ' '\n' | sort -u | wc -l)
if [ "$unique" -eq 1 ]; then
echo ""
echo " PASS: All nodes synced at same height"
else
echo ""
echo " WARN: Nodes at different heights (propagation delay normal)"
fi
echo ""
echo "7. Checking HF status..."
curl -s -X POST http://127.0.0.1:46941/json_rpc \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":"0","method":"getinfo"}' 2>/dev/null | \
python3 -c "
import sys,json
d=json.load(sys.stdin)['result']
hf=[i for i,v in enumerate(d['is_hardfok_active']) if v]
print(f' Active HFs: {hf}')
print(f' PoS allowed: {d[\"pos_allowed\"]}')
print(f' Aliases: {d[\"alias_count\"]}')
"
echo ""
echo "=== Test Complete ==="

View file

@ -0,0 +1,153 @@
#!/usr/bin/env python3
"""
Lethean Key Bridge Prototype
Derives an HNS sidechain secp256k1 keypair from a Lethean ed25519 spend key.
One seed phrase both chains.
Lethean seed phrase
ed25519 spend key (32 bytes)
Main chain wallet (native CryptoNote)
HKDF("lethean-hns-bridge", spend_key)
secp256k1 private key (32 bytes)
Sidechain wallet (derived)
Usage:
python3 key-bridge-prototype.py <hex_spend_key>
python3 key-bridge-prototype.py --from-wallet http://127.0.0.1:46944/json_rpc
Dependencies:
pip install cryptography secp256k1 (or use hashlib for prototype)
"""
import hashlib
import hmac
import sys
import json
import urllib.request
def derive_sidechain_key(spend_key_hex: str) -> dict:
"""
Derive a secp256k1 private key from a Lethean ed25519 spend key.
Uses HKDF-like construction:
prk = HMAC-SHA256(key="lethean-hns-bridge", msg=spend_key)
secp256k1_key = HMAC-SHA256(key=prk, msg="hns-sidechain-v1" || 0x01)
The domain separation ensures:
- Different derivation paths produce different keys
- The ed25519 key cannot be recovered from the secp256k1 key
- Future derivation paths (v2, v3) won't collide
"""
spend_key = bytes.fromhex(spend_key_hex)
assert len(spend_key) == 32, f"Spend key must be 32 bytes, got {len(spend_key)}"
# Step 1: Extract — domain-separated PRK
salt = b"lethean-hns-bridge"
prk = hmac.new(salt, spend_key, hashlib.sha256).digest()
# Step 2: Expand — derive the secp256k1 key
info = b"hns-sidechain-v1\x01"
secp256k1_privkey = hmac.new(prk, info, hashlib.sha256).digest()
# secp256k1 requires the key to be in range [1, n-1]
# n = FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
n = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141
key_int = int.from_bytes(secp256k1_privkey, 'big')
if key_int == 0 or key_int >= n:
# Extremely unlikely but handle it — rehash
secp256k1_privkey = hmac.new(prk, b"hns-sidechain-v1\x02", hashlib.sha256).digest()
key_int = int.from_bytes(secp256k1_privkey, 'big') % (n - 1) + 1
secp256k1_privkey = key_int.to_bytes(32, 'big')
# Derive additional keys for future use
document_signing_key = hmac.new(prk, b"document-signing-v1\x01", hashlib.sha256).digest()
audit_key = hmac.new(prk, b"audit-trail-v1\x01", hashlib.sha256).digest()
return {
"source": {
"type": "ed25519",
"chain": "lethean-mainchain",
"spend_key": spend_key_hex,
},
"derived": {
"hns_sidechain": {
"type": "secp256k1",
"chain": "lethean-hns-sidechain",
"private_key": secp256k1_privkey.hex(),
"derivation": "HKDF(salt=lethean-hns-bridge, ikm=spend_key, info=hns-sidechain-v1)",
},
"document_signing": {
"type": "ed25519",
"purpose": "document hash timestamping",
"private_key": document_signing_key.hex(),
"derivation": "HKDF(salt=lethean-hns-bridge, ikm=spend_key, info=document-signing-v1)",
},
"audit": {
"type": "ed25519",
"purpose": "audit trail verification",
"private_key": audit_key.hex(),
"derivation": "HKDF(salt=lethean-hns-bridge, ikm=spend_key, info=audit-trail-v1)",
},
},
"derivation_version": "1.0",
"note": "All keys derived deterministically from the Lethean spend key. One seed, all chains and purposes.",
}
def get_spend_key_from_wallet(rpc_url: str) -> str:
"""Fetch the spend key from a running wallet RPC."""
payload = json.dumps({
"jsonrpc": "2.0",
"id": "0",
"method": "get_wallet_info",
"params": {}
}).encode()
req = urllib.request.Request(
rpc_url,
data=payload,
headers={"Content-Type": "application/json"}
)
try:
with urllib.request.urlopen(req, timeout=5) as resp:
data = json.loads(resp.read())
# The spend key isn't directly in get_wallet_info
# We'd need a custom RPC or the seed to derive it
# For prototype, use the address as a stand-in
print("Note: Full implementation needs 'get_spend_key' RPC or seed phrase input")
print(f"Wallet address: {data.get('result', {}).get('wi', {}).get('address', '?')}")
return None
except Exception as e:
print(f"Error connecting to wallet: {e}")
return None
def main():
if len(sys.argv) < 2:
print(__doc__)
print("\nExample with test key:")
test_key = "a" * 64 # 32 bytes of 0xAA
result = derive_sidechain_key(test_key)
print(json.dumps(result, indent=2))
return
if sys.argv[1] == "--from-wallet":
rpc_url = sys.argv[2] if len(sys.argv) > 2 else "http://127.0.0.1:46944/json_rpc"
spend_key = get_spend_key_from_wallet(rpc_url)
if not spend_key:
print("\nUsing test key for demonstration:")
spend_key = "7b9f1e2a3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7"
else:
spend_key = sys.argv[1]
result = derive_sidechain_key(spend_key)
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()

130
docker/tools/register-reserved.sh Executable file
View file

@ -0,0 +1,130 @@
#!/bin/bash
# Lethean Reserved Name Registrar
# Batch-registers protected aliases on the main chain to prevent squatting.
#
# Usage:
# bash register-reserved.sh [names-file] [wallet-rpc] [batch-size] [delay]
#
# Defaults:
# names-file: /tmp/protected-names-priority.txt
# wallet-rpc: http://127.0.0.1:46944/json_rpc
# batch-size: 10 (aliases per batch before checking balance)
# delay: 2 (seconds between registrations)
NAMES_FILE="${1:-/tmp/protected-names-priority.txt}"
WALLET_RPC="${2:-http://127.0.0.1:46944/json_rpc}"
BATCH_SIZE="${3:-10}"
DELAY="${4:-2}"
COMMENT="v=lthn1;type=reserved;reason=hns-protected"
LOG_FILE="/tmp/register-reserved.log"
DONE_FILE="/tmp/registered-names.txt"
touch "$DONE_FILE"
get_balance() {
curl -sf "$WALLET_RPC" -d '{"jsonrpc":"2.0","id":"0","method":"getbalance"}' \
-H 'Content-Type: application/json' | python3 -c "
import sys,json
d=json.load(sys.stdin)['result']
print(d['unlocked_balance'])
" 2>/dev/null
}
get_address() {
curl -sf "$WALLET_RPC" -d '{"jsonrpc":"2.0","id":"0","method":"getaddress"}' \
-H 'Content-Type: application/json' | python3 -c "
import sys,json
print(json.load(sys.stdin)['result']['address'])
" 2>/dev/null
}
register_alias() {
local name="$1"
local address="$2"
result=$(curl -sf "$WALLET_RPC" -d "{
\"jsonrpc\":\"2.0\",\"id\":\"0\",\"method\":\"register_alias\",
\"params\":{
\"alias\":\"$name\",
\"address\":\"$address\",
\"comment\":\"$COMMENT\"
}
}" -H 'Content-Type: application/json' 2>/dev/null)
if echo "$result" | grep -q '"result"'; then
tx=$(echo "$result" | python3 -c "import sys,json; print(json.load(sys.stdin)['result'].get('tx_hash','ok'))" 2>/dev/null)
echo "$name" >> "$DONE_FILE"
echo "$(date +%H:%M:%S) OK @$name ($tx)" | tee -a "$LOG_FILE"
return 0
else
error=$(echo "$result" | python3 -c "import sys,json; print(json.load(sys.stdin).get('error',{}).get('message','unknown')[:80])" 2>/dev/null)
echo "$(date +%H:%M:%S) ERR @$name$error" | tee -a "$LOG_FILE"
return 1
fi
}
# Get wallet address
ADDRESS=$(get_address)
if [ -z "$ADDRESS" ]; then
echo "ERROR: Can't connect to wallet at $WALLET_RPC"
exit 1
fi
echo "Wallet: $ADDRESS"
echo "Names file: $NAMES_FILE ($(wc -l < "$NAMES_FILE") names)"
echo "Already registered: $(wc -l < "$DONE_FILE") names"
echo "Comment: $COMMENT"
echo "Batch: $BATCH_SIZE, Delay: ${DELAY}s"
echo "Log: $LOG_FILE"
echo "---"
BALANCE=$(get_balance)
BALANCE_LTHN=$(echo "scale=2; $BALANCE / 1000000000000" | bc 2>/dev/null)
echo "Balance: $BALANCE_LTHN LTHN"
echo ""
COUNT=0
REGISTERED=0
FAILED=0
SKIPPED=0
while IFS= read -r name; do
# Skip empty lines and comments
[[ -z "$name" || "$name" == \#* ]] && continue
# Skip already registered
if grep -qxF "$name" "$DONE_FILE" 2>/dev/null; then
((SKIPPED++))
continue
fi
# Check balance every batch
if (( COUNT % BATCH_SIZE == 0 && COUNT > 0 )); then
BALANCE=$(get_balance)
BALANCE_LTHN=$(echo "scale=2; $BALANCE / 1000000000000" | bc 2>/dev/null)
echo "--- Batch checkpoint: $BALANCE_LTHN LTHN, $REGISTERED registered, $FAILED failed ---"
# Stop if balance too low (need 1 LTHN + fee)
if (( BALANCE < 1100000000000 )); then
echo "Balance too low ($BALANCE_LTHN LTHN). Stopping."
break
fi
fi
register_alias "$name" "$ADDRESS"
if [ $? -eq 0 ]; then
((REGISTERED++))
else
((FAILED++))
fi
((COUNT++))
sleep "$DELAY"
done < "$NAMES_FILE"
echo ""
echo "=== Done ==="
echo "Registered: $REGISTERED"
echo "Failed: $FAILED"
echo "Skipped: $SKIPPED"
echo "Total processed: $COUNT"

View file

@ -58,12 +58,15 @@ RUN if [ "$BUILD_TESTNET" = "1" ]; then \
# use --target=build-artifacts to return the binaries
FROM scratch AS build-artifacts
COPY --from=build /code/build/release/src/lethean-* /
COPY --from=build /code/build/release/src/lethean-* /bin/
COPY --from=build /code/build/release/share/ /share/
# use --target=chain-service to return a working chain node
FROM ubuntu:24.04 AS chain-service
COPY --from=build-artifacts --chmod=+x / /bin
RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
COPY --from=build-artifacts --chmod=+x /bin/ /bin/
COPY --from=build-artifacts /share/ /share/
EXPOSE 36941
EXPOSE 36942