From 937646cc45ca6a9de074d5cce207ae44fc6b057e Mon Sep 17 00:00:00 2001 From: ncantu Date: Wed, 4 Feb 2026 03:07:57 +0100 Subject: [PATCH] Daily backup to git cron, backup/restore scripts, docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **Motivations:** - Export Signet and mining wallet backups to git with only 2 versions kept - Document and add backup/restore scripts for signet and mining wallet **Correctifs:** - Backup-to-git uses SSH URL for passwordless cron; copy timestamped files only; prune to 2 versions; remove *-latest from backup repo **Evolutions:** - data/backup-to-git-cron.sh: daily export to git.4nkweb.com/4nk/backup - save-signet-datadir-backup.sh, restore-signet-from-backup.sh, export-mining-wallet.sh, import-mining-wallet.sh - features/backup-to-git-daily-cron.md, docs/MAINTENANCE.md backup section - .gitignore: data/backup-to-git.log **Pages affectées:** - .gitignore, data/backup-to-git-cron.sh, docs/MAINTENANCE.md, features/backup-to-git-daily-cron.md - save-signet-datadir-backup.sh, restore-signet-from-backup.sh, export-mining-wallet.sh, import-mining-wallet.sh - Plus autres fichiers modifiés ou non suivis déjà présents dans le working tree --- .gitignore | 4 +- api-anchorage/anchorage-api.service | 10 +- api-anchorage/src/routes/health.js | 13 + api-anchorage/src/server.js | 31 ++- api-clamav/clamav-api.service | 4 +- api-clamav/src/server.js | 28 ++ api-faucet/.env.example | 4 +- api-faucet/faucet-api.service | 9 +- api-faucet/src/server.js | 28 ++ api-filigrane/filigrane-api.service | 4 +- api-filigrane/src/server.js | 28 ++ data/backup-to-git-cron.sh | 130 ++++++++++ data/restart-services-cron.sh | 3 +- data/restart-services.log | 223 ++++++++-------- data/start-docker-services.sh | 3 +- data/sync-utxos.log | 160 ++++++------ docs/DASHBOARD.md | 7 +- docs/DOMAINS_AND_PORTS.md | 15 +- docs/ENVIRONMENT.md | 41 ++- docs/INSTALLATION_NEW_NODE.md | 6 +- docs/INTERFACES.md | 5 +- docs/MAINTENANCE.md | 97 ++++++- docs/MEMPOOL.md | 28 +- docs/README.md | 21 ++ docs/SIGNET-CUSTOM-CONFIG.md | 40 +++ docs/TROUBLESHOOTING_MINING.md | 1 + docs/USERWALLET_KEY_DERIVATION.md | 60 +++++ env.example | 2 +- export-backup.sh | 5 +- export-mining-wallet.sh | 86 +++++++ features/backup-to-git-daily-cron.md | 73 ++++++ features/services-ecoute-ipv4-proxy.md | 60 +++++ fix-dashboard-anchor-chain.sh | 91 +++++++ .../api-anchorage-health-503-remediation.md | 82 ++++++ ...anchor-wrong-chain-insufficient-balance.md | 124 +++++++++ fixKnowledge/mempool-api-healthcheck-fix.md | 2 +- fixKnowledge/mempool-websocket-offline-fix.md | 4 +- .../signet-chain-lost-volume-persistent.md | 77 ++++++ import-mining-wallet.sh | 99 ++++++++ mine.sh | 4 +- miner | 11 +- miner_imports/docs/README.md | 76 ++++++ restore-signet-from-backup.sh | 65 +++++ save-signet-datadir-backup.sh | 59 +++++ signet-dashboard/.env.example | 4 +- signet-dashboard/public/api-docs.html | 28 ++ signet-dashboard/public/app.js | 82 +++++- signet-dashboard/public/hash-list.html | 19 +- signet-dashboard/public/index.html | 5 + signet-dashboard/public/join-signet.html | 239 ------------------ signet-dashboard/public/learn.html | 36 +++ signet-dashboard/public/styles.css | 10 + signet-dashboard/signet-dashboard.service | 7 +- signet-dashboard/src/server.js | 71 +++++- test-mempool-rpc-config.sh | 72 ++++++ update-signet.sh | 33 ++- userwallet/docs/ports.md | 2 +- userwallet/src/components/GlobalActionBar.tsx | 2 +- userwallet/src/components/HomeScreen.tsx | 194 +++++++++++++- .../src/components/PairingDisplayScreen.tsx | 19 ++ .../src/components/PairingSetupBlock.tsx | 76 ++++-- userwallet/src/components/WordInputGrid.tsx | 155 ++++++------ userwallet/src/index.css | 3 +- userwallet/src/types/identity.ts | 8 +- userwallet/src/utils/bip32.ts | 8 + userwallet/src/utils/crypto.ts | 65 +++++ userwallet/src/utils/pairing.ts | 85 +++++++ userwallet/vite.config.ts | 2 +- verify-chain-alignment.sh | 98 +++++++ verify-dashboard-signet.sh | 117 +++++++++ website-skeleton/cryptographie.html | 49 +++- 71 files changed, 2802 insertions(+), 610 deletions(-) create mode 100755 data/backup-to-git-cron.sh create mode 100644 docs/SIGNET-CUSTOM-CONFIG.md create mode 100644 docs/USERWALLET_KEY_DERIVATION.md create mode 100755 export-mining-wallet.sh create mode 100644 features/backup-to-git-daily-cron.md create mode 100644 features/services-ecoute-ipv4-proxy.md create mode 100755 fix-dashboard-anchor-chain.sh create mode 100644 fixKnowledge/api-anchorage-health-503-remediation.md create mode 100644 fixKnowledge/dashboard-anchor-wrong-chain-insufficient-balance.md create mode 100644 fixKnowledge/signet-chain-lost-volume-persistent.md create mode 100755 import-mining-wallet.sh create mode 100644 miner_imports/docs/README.md create mode 100755 restore-signet-from-backup.sh create mode 100755 save-signet-datadir-backup.sh create mode 100755 test-mempool-rpc-config.sh create mode 100755 verify-chain-alignment.sh create mode 100755 verify-dashboard-signet.sh diff --git a/.gitignore b/.gitignore index 4e208b7..bbf6cf5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ .env -backups/ \ No newline at end of file +backups/ +data/backup-to-git.log +.verify-chain-dashboard-*.json \ No newline at end of file diff --git a/api-anchorage/anchorage-api.service b/api-anchorage/anchorage-api.service index 7233dd5..701c0c5 100644 --- a/api-anchorage/anchorage-api.service +++ b/api-anchorage/anchorage-api.service @@ -8,7 +8,15 @@ User=ncantu WorkingDirectory=/home/ncantu/Bureau/code/bitcoin/api-anchorage Environment=NODE_ENV=production Environment=API_PORT=3010 -Environment=API_HOST=0.0.0.0 +# Bind IPv4 only: machine bitcoin (192.168.1.105). Accept only from proxy 192.168.1.100. +Environment=API_HOST=192.168.1.105 +Environment=ALLOWED_SOURCE_IP=192.168.1.100 +# Same node as Mempool: 127.0.0.1:38332 = host.docker.internal:38332 = bitcoin-signet-instance +Environment=BITCOIN_RPC_HOST=127.0.0.1 +Environment=BITCOIN_RPC_PORT=38332 +Environment=BITCOIN_RPC_USER=bitcoin +Environment=BITCOIN_RPC_PASSWORD=bitcoin +Environment=BITCOIN_RPC_WALLET=custom_signet ExecStart=/usr/bin/node /home/ncantu/Bureau/code/bitcoin/api-anchorage/src/server.js Restart=always RestartSec=10 diff --git a/api-anchorage/src/routes/health.js b/api-anchorage/src/routes/health.js index 3b598cf..4f47ee8 100644 --- a/api-anchorage/src/routes/health.js +++ b/api-anchorage/src/routes/health.js @@ -9,6 +9,19 @@ import { getDatabase } from '../database.js'; export const healthRouter = express.Router(); +/** + * GET /health/live + * Liveness probe: returns 200 while the process is running. + * Use for monitoring/load-balancer to distinguish "process down" from "not ready". + */ +healthRouter.get('/live', (req, res) => { + res.status(200).json({ + ok: true, + service: 'anchor-api', + timestamp: new Date().toISOString(), + }); +}); + /** * GET /health * Vérifie l'état de l'API et de la connexion Bitcoin diff --git a/api-anchorage/src/server.js b/api-anchorage/src/server.js index f768c3b..2990158 100644 --- a/api-anchorage/src/server.js +++ b/api-anchorage/src/server.js @@ -34,6 +34,34 @@ dotenv.config({ path: envPath }); const app = express(); const PORT = process.env.API_PORT || 3010; const HOST = process.env.API_HOST || '0.0.0.0'; +const ALLOWED_SOURCE_IP = process.env.ALLOWED_SOURCE_IP ?? ''; + +/** + * Normalize remote address: IPv6-mapped IPv4 (::ffff:192.168.1.100) -> 192.168.1.100 + * @param {string} addr - req.socket.remoteAddress + * @returns {string} + */ +function normalizeRemoteAddress(addr) { + if (!addr) return ''; + if (addr.startsWith('::ffff:')) return addr.slice(7); + return addr; +} + +// Middleware: accept only requests from proxy when ALLOWED_SOURCE_IP is set (IPv4 only) +app.use((req, res, next) => { + if (!ALLOWED_SOURCE_IP) return next(); + const remote = normalizeRemoteAddress(req.socket.remoteAddress ?? ''); + if (remote !== ALLOWED_SOURCE_IP) { + logger.warn('Request rejected: source not allowed', { + remoteAddress: req.socket.remoteAddress, + allowedSourceIp: ALLOWED_SOURCE_IP, + path: req.path, + }); + res.status(403).json({ error: 'Forbidden', message: 'Source not allowed' }); + return; + } + next(); +}); // Middleware app.use(cors()); @@ -52,7 +80,7 @@ app.use((req, res, next) => { // Middleware d'authentification API Key app.use((req, res, next) => { // Exclure /health, /health/detailed et /api/anchor/locked-utxos de l'authentification - if (req.path === '/health' || req.path === '/health/detailed' || req.path === '/' || req.path.startsWith('/api/anchor/locked-utxos')) { + if (req.path === '/health' || req.path === '/health/live' || req.path === '/health/detailed' || req.path === '/' || req.path.startsWith('/api/anchor/locked-utxos')) { return next(); } @@ -83,6 +111,7 @@ app.get('/', (req, res) => { version: '1.0.0', endpoints: { health: '/health', + healthLive: '/health/live', healthDetailed: '/health/detailed', anchor: '/api/anchor/document', verify: '/api/anchor/verify', diff --git a/api-clamav/clamav-api.service b/api-clamav/clamav-api.service index 55be248..b251354 100644 --- a/api-clamav/clamav-api.service +++ b/api-clamav/clamav-api.service @@ -8,7 +8,9 @@ User=ncantu WorkingDirectory=/home/ncantu/Bureau/code/bitcoin/api-clamav Environment=NODE_ENV=production Environment=CLAMAV_API_PORT=3023 -Environment=CLAMAV_API_HOST=0.0.0.0 +# Bind IPv4 only: machine prod (192.168.1.103). Accept only from proxy 192.168.1.100. +Environment=CLAMAV_API_HOST=192.168.1.103 +Environment=ALLOWED_SOURCE_IP=192.168.1.100 Environment=CLAMAV_HOST=localhost Environment=CLAMAV_PORT=3310 ExecStart=/usr/bin/node /home/ncantu/Bureau/code/bitcoin/api-clamav/src/server.js diff --git a/api-clamav/src/server.js b/api-clamav/src/server.js index 8d60060..7dffa06 100644 --- a/api-clamav/src/server.js +++ b/api-clamav/src/server.js @@ -31,6 +31,34 @@ const app = express(); // Port fixe : 3023 const PORT = 3023; const HOST = process.env.CLAMAV_API_HOST || '0.0.0.0'; +const ALLOWED_SOURCE_IP = process.env.ALLOWED_SOURCE_IP ?? ''; + +/** + * Normalize remote address: IPv6-mapped IPv4 (::ffff:192.168.1.100) -> 192.168.1.100 + * @param {string} addr - req.socket.remoteAddress + * @returns {string} + */ +function normalizeRemoteAddress(addr) { + if (!addr) return ''; + if (addr.startsWith('::ffff:')) return addr.slice(7); + return addr; +} + +// Middleware: accept only requests from proxy when ALLOWED_SOURCE_IP is set (IPv4 only) +app.use((req, res, next) => { + if (!ALLOWED_SOURCE_IP) return next(); + const remote = normalizeRemoteAddress(req.socket.remoteAddress ?? ''); + if (remote !== ALLOWED_SOURCE_IP) { + logger.warn('Request rejected: source not allowed', { + remoteAddress: req.socket.remoteAddress, + allowedSourceIp: ALLOWED_SOURCE_IP, + path: req.path, + }); + res.status(403).json({ error: 'Forbidden', message: 'Source not allowed' }); + return; + } + next(); +}); // Middleware app.use(cors()); diff --git a/api-faucet/.env.example b/api-faucet/.env.example index 4e7c635..d494154 100644 --- a/api-faucet/.env.example +++ b/api-faucet/.env.example @@ -1,5 +1,5 @@ -# Bitcoin RPC Configuration -BITCOIN_RPC_HOST=localhost +# Bitcoin RPC Configuration (same node as Mempool: 127.0.0.1:38332 = bitcoin-signet-instance) +BITCOIN_RPC_HOST=127.0.0.1 BITCOIN_RPC_PORT=38332 BITCOIN_RPC_USER=bitcoin BITCOIN_RPC_PASSWORD=bitcoin diff --git a/api-faucet/faucet-api.service b/api-faucet/faucet-api.service index f20642f..31e3e34 100644 --- a/api-faucet/faucet-api.service +++ b/api-faucet/faucet-api.service @@ -8,7 +8,14 @@ User=ncantu WorkingDirectory=/home/ncantu/Bureau/code/bitcoin/api-faucet Environment=NODE_ENV=production Environment=FAUCET_API_PORT=3021 -Environment=FAUCET_API_HOST=0.0.0.0 +# Bind IPv4 only: machine bitcoin (192.168.1.105). Accept only from proxy 192.168.1.100. +Environment=FAUCET_API_HOST=192.168.1.105 +Environment=ALLOWED_SOURCE_IP=192.168.1.100 +# Same node as Mempool: 127.0.0.1:38332 = bitcoin-signet-instance (same machine) +Environment=BITCOIN_RPC_HOST=127.0.0.1 +Environment=BITCOIN_RPC_PORT=38332 +Environment=BITCOIN_RPC_USER=bitcoin +Environment=BITCOIN_RPC_PASSWORD=bitcoin ExecStart=/usr/bin/node /home/ncantu/Bureau/code/bitcoin/api-faucet/src/server.js Restart=always RestartSec=10 diff --git a/api-faucet/src/server.js b/api-faucet/src/server.js index 5d689fb..03367c9 100644 --- a/api-faucet/src/server.js +++ b/api-faucet/src/server.js @@ -23,6 +23,34 @@ dotenv.config(); const app = express(); const PORT = process.env.FAUCET_API_PORT || 3021; const HOST = process.env.FAUCET_API_HOST || '0.0.0.0'; +const ALLOWED_SOURCE_IP = process.env.ALLOWED_SOURCE_IP ?? ''; + +/** + * Normalize remote address: IPv6-mapped IPv4 (::ffff:192.168.1.100) -> 192.168.1.100 + * @param {string} addr - req.socket.remoteAddress + * @returns {string} + */ +function normalizeRemoteAddress(addr) { + if (!addr) return ''; + if (addr.startsWith('::ffff:')) return addr.slice(7); + return addr; +} + +// Middleware: accept only requests from proxy when ALLOWED_SOURCE_IP is set (IPv4 only) +app.use((req, res, next) => { + if (!ALLOWED_SOURCE_IP) return next(); + const remote = normalizeRemoteAddress(req.socket.remoteAddress ?? ''); + if (remote !== ALLOWED_SOURCE_IP) { + logger.warn('Request rejected: source not allowed', { + remoteAddress: req.socket.remoteAddress, + allowedSourceIp: ALLOWED_SOURCE_IP, + path: req.path, + }); + res.status(403).json({ error: 'Forbidden', message: 'Source not allowed' }); + return; + } + next(); +}); // Middleware app.use(cors()); diff --git a/api-filigrane/filigrane-api.service b/api-filigrane/filigrane-api.service index 76f6a75..a473792 100644 --- a/api-filigrane/filigrane-api.service +++ b/api-filigrane/filigrane-api.service @@ -8,7 +8,9 @@ User=ncantu WorkingDirectory=/home/ncantu/Bureau/code/bitcoin/api-filigrane Environment=NODE_ENV=production Environment=WATERMARK_API_PORT=3022 -Environment=WATERMARK_API_HOST=0.0.0.0 +# Bind IPv4 only: machine prod (192.168.1.103). Accept only from proxy 192.168.1.100. +Environment=WATERMARK_API_HOST=192.168.1.103 +Environment=ALLOWED_SOURCE_IP=192.168.1.100 ExecStart=/usr/bin/node /home/ncantu/Bureau/code/bitcoin/api-filigrane/src/server.js Restart=always RestartSec=10 diff --git a/api-filigrane/src/server.js b/api-filigrane/src/server.js index 1f14767..a8fca14 100644 --- a/api-filigrane/src/server.js +++ b/api-filigrane/src/server.js @@ -29,6 +29,34 @@ dotenv.config({ path: envPath }); const app = express(); const PORT = process.env.WATERMARK_API_PORT || 3022; const HOST = process.env.WATERMARK_API_HOST || '0.0.0.0'; +const ALLOWED_SOURCE_IP = process.env.ALLOWED_SOURCE_IP ?? ''; + +/** + * Normalize remote address: IPv6-mapped IPv4 (::ffff:192.168.1.100) -> 192.168.1.100 + * @param {string} addr - req.socket.remoteAddress + * @returns {string} + */ +function normalizeRemoteAddress(addr) { + if (!addr) return ''; + if (addr.startsWith('::ffff:')) return addr.slice(7); + return addr; +} + +// Middleware: accept only requests from proxy when ALLOWED_SOURCE_IP is set (IPv4 only) +app.use((req, res, next) => { + if (!ALLOWED_SOURCE_IP) return next(); + const remote = normalizeRemoteAddress(req.socket.remoteAddress ?? ''); + if (remote !== ALLOWED_SOURCE_IP) { + logger.warn('Request rejected: source not allowed', { + remoteAddress: req.socket.remoteAddress, + allowedSourceIp: ALLOWED_SOURCE_IP, + path: req.path, + }); + res.status(403).json({ error: 'Forbidden', message: 'Source not allowed' }); + return; + } + next(); +}); // Middleware app.use(cors()); diff --git a/data/backup-to-git-cron.sh b/data/backup-to-git-cron.sh new file mode 100755 index 0000000..7dc33b1 --- /dev/null +++ b/data/backup-to-git-cron.sh @@ -0,0 +1,130 @@ +#!/bin/bash +# +# Daily backup of Signet datadir and mining wallet to git (https://git.4nkweb.com/4nk/backup). +# Runs save-signet-datadir-backup.sh and export-mining-wallet.sh, then pushes to the +# backup repo. Keeps only 2 versions of the full chain (signet-datadir) and 2 of the +# mining wallet export. +# +# Run via cron. Requires: docker (for backup scripts), git, passwordless push to backup repo. +# Log: data/backup-to-git.log +# +# Author: 4NK Team +# Date: 2026-02-04 + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" +BACKUP_DIR="${PROJECT_DIR}/backups" +LOG_FILE="${SCRIPT_DIR}/backup-to-git.log" +BACKUP_REPO_URL="git@git.4nkweb.com:4nk/backup.git" +BACKUP_GIT_WORKSPACE="${BACKUP_GIT_WORKSPACE:-$HOME/.4nk-backup-git}" +KEEP_VERSIONS=2 + +SIGNET_SUBDIR="signet-datadir" +WALLET_SUBDIR="mining-wallet" + +log() { echo "$(date -Iseconds) $*" | tee -a "$LOG_FILE"; } + +cd "$PROJECT_DIR" || exit 1 + +log "=== Backup to git cron ===" + +# 1. Run signet datadir backup +if [ ! -x "${PROJECT_DIR}/save-signet-datadir-backup.sh" ]; then + log "ERROR: save-signet-datadir-backup.sh not executable" + exit 1 +fi +log "Running save-signet-datadir-backup.sh..." +if ! ./save-signet-datadir-backup.sh >> "$LOG_FILE" 2>&1; then + log "ERROR: save-signet-datadir-backup.sh failed" + exit 1 +fi +log " signet datadir backup OK" + +# 2. Run mining wallet export +if [ ! -x "${PROJECT_DIR}/export-mining-wallet.sh" ]; then + log "ERROR: export-mining-wallet.sh not executable" + exit 1 +fi +log "Running export-mining-wallet.sh..." +if ! ./export-mining-wallet.sh >> "$LOG_FILE" 2>&1; then + log "ERROR: export-mining-wallet.sh failed" + exit 1 +fi +log " mining wallet export OK" + +# 3. Clone or pull backup repo +if [ -d "$BACKUP_GIT_WORKSPACE/.git" ]; then + log "Pulling backup repo..." + (cd "$BACKUP_GIT_WORKSPACE" && git pull) >> "$LOG_FILE" 2>&1 || { + log "WARN: git pull failed, continuing with local state" + } +else + log "Cloning backup repo..." + mkdir -p "$(dirname "$BACKUP_GIT_WORKSPACE")" + if ! git clone "$BACKUP_REPO_URL" "$BACKUP_GIT_WORKSPACE" >> "$LOG_FILE" 2>&1; then + log "ERROR: git clone failed. Create repo at $BACKUP_REPO_URL first." + exit 1 + fi +fi + +mkdir -p "${BACKUP_GIT_WORKSPACE}/${SIGNET_SUBDIR}" +mkdir -p "${BACKUP_GIT_WORKSPACE}/${WALLET_SUBDIR}" + +# 4. Copy latest signet-datadir backup (use real file, not symlink, for timestamped filename) +LATEST_SIGNET=$(readlink -f "$BACKUP_DIR/signet-datadir-latest.tar.gz" 2>/dev/null) +if [ -z "$LATEST_SIGNET" ] || [ ! -f "$LATEST_SIGNET" ]; then + log "ERROR: no signet-datadir backup found in $BACKUP_DIR" + exit 1 +fi +cp "$LATEST_SIGNET" "${BACKUP_GIT_WORKSPACE}/${SIGNET_SUBDIR}/$(basename "$LATEST_SIGNET")" +log " Copied $(basename "$LATEST_SIGNET") to ${SIGNET_SUBDIR}/" + +# 5. Copy latest mining wallet export (use real file, not symlink, for timestamped filename) +LATEST_WALLET=$(readlink -f "$BACKUP_DIR/mining-wallet-export-latest.json" 2>/dev/null) +if [ -z "$LATEST_WALLET" ] || [ ! -f "$LATEST_WALLET" ]; then + log "ERROR: no mining-wallet export found in $BACKUP_DIR" + exit 1 +fi +cp "$LATEST_WALLET" "${BACKUP_GIT_WORKSPACE}/${WALLET_SUBDIR}/$(basename "$LATEST_WALLET")" +log " Copied $(basename "$LATEST_WALLET") to ${WALLET_SUBDIR}/" + +# 6. Remove any *-latest* files from backup repo (only timestamped versions belong) +rm -f "${BACKUP_GIT_WORKSPACE}/${SIGNET_SUBDIR}/signet-datadir-latest.tar.gz" +rm -f "${BACKUP_GIT_WORKSPACE}/${WALLET_SUBDIR}/mining-wallet-export-latest.json" + +# 7. Prune signet-datadir: keep only KEEP_VERSIONS most recent (by filename timestamp) +cd "${BACKUP_GIT_WORKSPACE}/${SIGNET_SUBDIR}" +while [ "$(ls -1 signet-datadir-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-*.tar.gz 2>/dev/null | wc -l)" -gt "$KEEP_VERSIONS" ]; do + OLDEST=$(ls signet-datadir-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-*.tar.gz 2>/dev/null | sort | head -1) + rm -f "$OLDEST" + log " Pruned signet: $OLDEST" +done + +# 8. Prune mining-wallet: keep only KEEP_VERSIONS most recent (by filename timestamp) +cd "${BACKUP_GIT_WORKSPACE}/${WALLET_SUBDIR}" +while [ "$(ls -1 mining-wallet-export-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-*.json 2>/dev/null | wc -l)" -gt "$KEEP_VERSIONS" ]; do + OLDEST=$(ls mining-wallet-export-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-*.json 2>/dev/null | sort | head -1) + rm -f "$OLDEST" + log " Pruned wallet: $OLDEST" +done + +# 9. Commit and push +cd "$BACKUP_GIT_WORKSPACE" +if git status --porcelain | grep -q .; then + git add "${SIGNET_SUBDIR}/" "${WALLET_SUBDIR}/" + git commit -m "backup: signet datadir and mining wallet $(date -Iseconds)" >> "$LOG_FILE" 2>&1 + log "Pushing to $BACKUP_REPO_URL..." + if git push >> "$LOG_FILE" 2>&1; then + log " Push OK" + else + log "ERROR: git push failed" + exit 1 + fi +else + log "No changes to commit (backups identical)" +fi + +log "=== Done ===" +tail -n 150 "$LOG_FILE" > "$LOG_FILE.tmp" && mv "$LOG_FILE.tmp" "$LOG_FILE" diff --git a/data/restart-services-cron.sh b/data/restart-services-cron.sh index 8b92df4..8d469fa 100755 --- a/data/restart-services-cron.sh +++ b/data/restart-services-cron.sh @@ -85,7 +85,8 @@ if docker ps -a -q -f "name=^${BITCOIND_CONTAINER}$" 2>/dev/null | grep -q .; th max_wait=60 wait_count=0 while [ $wait_count -lt $max_wait ]; do - if docker exec "$BITCOIND_CONTAINER" bitcoin-cli -datadir=/root/.bitcoin getblockchaininfo &>/dev/null; then + BITCOIN_DATADIR=$(docker exec "$BITCOIND_CONTAINER" printenv BITCOIN_DIR 2>/dev/null || echo "/root/.bitcoin") + if docker exec "$BITCOIND_CONTAINER" bitcoin-cli -datadir="$BITCOIN_DATADIR" getblockchaininfo &>/dev/null; then log " $BITCOIND_CONTAINER RPC ready" break fi diff --git a/data/restart-services.log b/data/restart-services.log index e7c3529..68cea97 100644 --- a/data/restart-services.log +++ b/data/restart-services.log @@ -1,100 +1,123 @@ - config Manage Docker configs - container Manage containers - context Manage contexts - image Manage images - manifest Manage Docker image manifests and manifest lists - network Manage networks - node Manage Swarm nodes - plugin Manage plugins - secret Manage Docker secrets - service Manage services - stack Manage Docker stacks - swarm Manage Swarm - system Manage Docker - trust Manage trust on Docker images - volume Manage volumes - -Commands: - attach Attach local standard input, output, and error streams to a running container - build Build an image from a Dockerfile - commit Create a new image from a container's changes - cp Copy files/folders between a container and the local filesystem - create Create a new container - diff Inspect changes to files or directories on a container's filesystem - events Get real time events from the server - exec Run a command in a running container - export Export a container's filesystem as a tar archive - history Show the history of an image - images List images - import Import the contents from a tarball to create a filesystem image - info Display system-wide information - inspect Return low-level information on Docker objects - kill Kill one or more running containers - load Load an image from a tar archive or STDIN - login Log in to a Docker registry - logout Log out from a Docker registry - logs Fetch the logs of a container - pause Pause all processes within one or more containers - port List port mappings or a specific mapping for the container - ps List containers - pull Pull an image or a repository from a registry - push Push an image or a repository to a registry - rename Rename a container - restart Restart one or more containers - rm Remove one or more containers - rmi Remove one or more images - run Run a command in a new container - save Save one or more images to a tar archive (streamed to STDOUT by default) - search Search the Docker Hub for images - start Start one or more stopped containers - stats Display a live stream of container(s) resource usage statistics - stop Stop one or more running containers - tag Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE - top Display the running processes of a container - unpause Unpause all processes within one or more containers - update Update configuration of one or more containers - version Show the Docker version information - wait Block until one or more containers stop, then print their exit codes - -Run 'docker COMMAND --help' for more information on a command. - -To get more help with docker, check out our guides at https://docs.docker.com/go/guides/ - -Restarting mempool_api_1 ... -Restarting mempool_electrs_1 ... -Restarting mempool_web_1 ... -Restarting mempool_db_1 ... -Restarting mempool_electrs_1 ... done -Restarting mempool_db_1 ... done -Restarting mempool_api_1 ... done -Restarting mempool_web_1 ... done -2026-01-28T00:43:59+01:00 mempool OK -2026-01-28T00:43:59+01:00 Restarting bitcoin-signet-instance... -2026-01-28T00:44:06+01:00 bitcoin-signet-instance OK -2026-01-28T00:44:06+01:00 === Done === -2026-01-28T00:45:24+01:00 === Restart services cron (local only, no SSH) === -2026-01-28T00:45:24+01:00 WARN: bitcoin-signet is not enabled (will not start at boot) -2026-01-28T00:45:24+01:00 WARN: bitcoin is not enabled (will not start at boot) -2026-01-28T00:45:24+01:00 SKIP: bitcoin-signet not active (not installed or not running on this machine) -2026-01-28T00:45:24+01:00 SKIP: bitcoin not active (not installed or not running on this machine) -2026-01-28T00:45:24+01:00 Restarting anchorage-api... -2026-01-28T00:45:24+01:00 anchorage-api OK -2026-01-28T00:45:24+01:00 Restarting api-relay... -2026-01-28T00:45:24+01:00 api-relay OK -2026-01-28T00:45:24+01:00 Restarting clamav-api... -2026-01-28T00:45:24+01:00 clamav-api OK -2026-01-28T00:45:24+01:00 Restarting faucet-api... -2026-01-28T00:45:24+01:00 faucet-api OK -2026-01-28T00:45:24+01:00 Restarting filigrane-api... -2026-01-28T00:45:24+01:00 filigrane-api OK -2026-01-28T00:45:24+01:00 Restarting signet-dashboard... -2026-01-28T00:45:25+01:00 signet-dashboard OK -2026-01-28T00:45:25+01:00 Restarting userwallet... -2026-01-28T00:45:25+01:00 userwallet OK -2026-01-28T00:45:25+01:00 Restarting website-skeleton... -2026-01-28T00:45:25+01:00 website-skeleton OK -2026-01-28T00:45:25+01:00 Restarting mempool (docker)... -2026-01-28T00:46:26+01:00 mempool OK -2026-01-28T00:46:26+01:00 Restarting bitcoin-signet-instance... -2026-01-28T00:46:33+01:00 bitcoin-signet-instance OK -2026-01-28T00:46:33+01:00 === Done === +2026-02-02T11:35:06+01:00 api-relay OK +2026-02-02T11:35:06+01:00 Restarting clamav-api... +2026-02-02T11:35:06+01:00 clamav-api OK +2026-02-02T11:35:06+01:00 Restarting faucet-api... +2026-02-02T11:35:06+01:00 faucet-api OK +2026-02-02T11:35:06+01:00 Restarting filigrane-api... +2026-02-02T11:35:06+01:00 filigrane-api OK +2026-02-02T11:35:06+01:00 Restarting signet-dashboard... +2026-02-02T11:35:06+01:00 signet-dashboard OK +2026-02-02T11:35:06+01:00 Restarting userwallet... +2026-02-02T11:35:06+01:00 userwallet OK +2026-02-02T11:35:06+01:00 Restarting website-skeleton... +2026-02-02T11:35:06+01:00 website-skeleton OK +2026-02-02T11:35:06+01:00 Starting/restarting bitcoin-signet-instance... +2026-02-02T11:35:13+01:00 bitcoin-signet-instance started, waiting for RPC to be ready... +2026-02-02T11:35:14+01:00 bitcoin-signet-instance RPC ready +2026-02-02T11:35:14+01:00 Starting/restarting mempool (docker)... +2026-02-02T11:35:15+01:00 mempool started +2026-02-02T11:35:20+01:00 === Done === +2026-02-02T11:43:03+01:00 === Restart services cron (local only, no SSH) === +2026-02-02T11:43:03+01:00 WARN: bitcoin-signet is not enabled (will not start at boot) +2026-02-02T11:43:03+01:00 WARN: bitcoin is not enabled (will not start at boot) +2026-02-02T11:43:03+01:00 SKIP: bitcoin-signet not active (not installed or not running on this machine) +2026-02-02T11:43:03+01:00 SKIP: bitcoin not active (not installed or not running on this machine) +2026-02-02T11:43:03+01:00 Restarting anchorage-api... +2026-02-02T11:43:03+01:00 anchorage-api OK +2026-02-02T11:43:03+01:00 Restarting api-relay... +2026-02-02T11:43:04+01:00 api-relay OK +2026-02-02T11:43:04+01:00 Restarting clamav-api... +2026-02-02T11:43:04+01:00 clamav-api OK +2026-02-02T11:43:04+01:00 Restarting faucet-api... +2026-02-02T11:43:04+01:00 faucet-api OK +2026-02-02T11:43:04+01:00 Restarting filigrane-api... +2026-02-02T11:43:04+01:00 filigrane-api OK +2026-02-02T11:43:04+01:00 Restarting signet-dashboard... +2026-02-02T11:43:04+01:00 signet-dashboard OK +2026-02-02T11:43:04+01:00 Restarting userwallet... +2026-02-02T11:43:04+01:00 userwallet OK +2026-02-02T11:43:04+01:00 Restarting website-skeleton... +2026-02-02T11:43:04+01:00 website-skeleton OK +2026-02-02T11:43:04+01:00 Starting/restarting bitcoin-signet-instance... +2026-02-02T11:43:10+01:00 bitcoin-signet-instance started, waiting for RPC to be ready... +2026-02-02T11:43:12+01:00 bitcoin-signet-instance RPC ready +2026-02-02T11:43:12+01:00 Starting/restarting mempool (docker)... +2026-02-02T11:43:12+01:00 mempool started +2026-02-02T11:43:17+01:00 === Done === +2026-02-02T11:50:48+01:00 === Restart services cron (local only, no SSH) === +2026-02-02T11:50:48+01:00 WARN: bitcoin-signet is not enabled (will not start at boot) +2026-02-02T11:50:48+01:00 WARN: bitcoin is not enabled (will not start at boot) +2026-02-02T11:50:48+01:00 SKIP: bitcoin-signet not active (not installed or not running on this machine) +2026-02-02T11:50:48+01:00 SKIP: bitcoin not active (not installed or not running on this machine) +2026-02-02T11:50:48+01:00 Restarting anchorage-api... +2026-02-02T11:50:48+01:00 anchorage-api OK +2026-02-02T11:50:48+01:00 Restarting api-relay... +2026-02-02T11:50:48+01:00 api-relay OK +2026-02-02T11:50:48+01:00 Restarting clamav-api... +2026-02-02T11:50:48+01:00 clamav-api OK +2026-02-02T11:50:48+01:00 Restarting faucet-api... +2026-02-02T11:50:48+01:00 faucet-api OK +2026-02-02T11:50:48+01:00 Restarting filigrane-api... +2026-02-02T11:50:48+01:00 filigrane-api OK +2026-02-02T11:50:48+01:00 Restarting signet-dashboard... +2026-02-02T11:50:48+01:00 signet-dashboard OK +2026-02-02T11:50:48+01:00 Restarting userwallet... +2026-02-02T11:50:49+01:00 userwallet OK +2026-02-02T11:50:49+01:00 Restarting website-skeleton... +2026-02-02T11:50:49+01:00 website-skeleton OK +2026-02-02T11:50:49+01:00 Starting/restarting bitcoin-signet-instance... +2026-02-02T11:50:55+01:00 bitcoin-signet-instance started, waiting for RPC to be ready... +2026-02-02T11:50:56+01:00 bitcoin-signet-instance RPC ready +2026-02-02T11:50:56+01:00 Starting/restarting mempool (docker)... +2026-02-02T11:50:57+01:00 mempool started +2026-02-02T11:51:02+01:00 === Done === +2026-02-02T11:57:11+01:00 === Restart services cron (local only, no SSH) === +2026-02-02T11:57:11+01:00 WARN: bitcoin-signet is not enabled (will not start at boot) +2026-02-02T11:57:11+01:00 WARN: bitcoin is not enabled (will not start at boot) +2026-02-02T11:57:11+01:00 SKIP: bitcoin-signet not active (not installed or not running on this machine) +2026-02-02T11:57:11+01:00 SKIP: bitcoin not active (not installed or not running on this machine) +2026-02-02T11:57:11+01:00 Restarting anchorage-api... +2026-02-02T11:57:11+01:00 anchorage-api OK +2026-02-02T11:57:11+01:00 Restarting api-relay... +2026-02-02T11:57:11+01:00 api-relay OK +2026-02-02T11:57:11+01:00 Restarting clamav-api... +2026-02-02T11:57:11+01:00 clamav-api OK +2026-02-02T11:57:11+01:00 Restarting faucet-api... +2026-02-02T11:57:12+01:00 faucet-api OK +2026-02-02T11:57:12+01:00 Restarting filigrane-api... +2026-02-02T11:57:12+01:00 filigrane-api OK +2026-02-02T11:57:12+01:00 Restarting signet-dashboard... +2026-02-02T11:57:12+01:00 signet-dashboard OK +2026-02-02T11:57:12+01:00 Restarting userwallet... +2026-02-02T11:57:12+01:00 userwallet OK +2026-02-02T11:57:12+01:00 Restarting website-skeleton... +2026-02-02T11:57:12+01:00 website-skeleton OK +2026-02-02T11:57:12+01:00 Starting/restarting bitcoin-signet-instance... +2026-02-02T11:57:18+01:00 bitcoin-signet-instance started, waiting for RPC to be ready... +2026-02-02T11:57:49+01:00 bitcoin-signet-instance RPC ready +2026-02-02T11:57:49+01:00 Starting/restarting mempool (docker)... +2026-02-02T11:57:49+01:00 mempool started +2026-02-02T11:57:54+01:00 === Done === +2026-02-03T00:29:32+01:00 === Restart services cron (local only, no SSH) === +2026-02-03T00:29:32+01:00 WARN: bitcoin-signet is not enabled (will not start at boot) +2026-02-03T00:29:32+01:00 WARN: bitcoin is not enabled (will not start at boot) +2026-02-03T00:29:32+01:00 SKIP: bitcoin-signet not active (not installed or not running on this machine) +2026-02-03T00:29:32+01:00 SKIP: bitcoin not active (not installed or not running on this machine) +2026-02-03T00:29:32+01:00 Restarting anchorage-api... +2026-02-03T00:29:32+01:00 anchorage-api OK +2026-02-03T00:29:32+01:00 Restarting api-relay... +2026-02-03T00:29:32+01:00 api-relay OK +2026-02-03T00:29:32+01:00 Restarting clamav-api... +2026-02-03T00:29:32+01:00 clamav-api OK +2026-02-03T00:29:32+01:00 Restarting faucet-api... +2026-02-03T00:29:32+01:00 faucet-api OK +2026-02-03T00:29:32+01:00 Restarting filigrane-api... +2026-02-03T00:29:32+01:00 filigrane-api OK +2026-02-03T00:29:32+01:00 Restarting signet-dashboard... +2026-02-03T00:29:33+01:00 signet-dashboard OK +2026-02-03T00:29:33+01:00 Restarting userwallet... +2026-02-03T00:29:33+01:00 userwallet OK +2026-02-03T00:29:33+01:00 Restarting website-skeleton... +2026-02-03T00:29:33+01:00 website-skeleton OK +2026-02-03T00:29:33+01:00 Starting/restarting bitcoin-signet-instance... +2026-02-03T00:29:40+01:00 bitcoin-signet-instance started, waiting for RPC to be ready... diff --git a/data/start-docker-services.sh b/data/start-docker-services.sh index e3a2463..27940b6 100755 --- a/data/start-docker-services.sh +++ b/data/start-docker-services.sh @@ -36,7 +36,8 @@ if docker ps -a -q -f "name=^${BITCOIND_CONTAINER}$" 2>/dev/null | grep -q .; th max_wait=120 wait_count=0 while [ $wait_count -lt $max_wait ]; do - if docker exec "$BITCOIND_CONTAINER" bitcoin-cli -datadir=/root/.bitcoin getblockchaininfo &>/dev/null; then + BITCOIN_DATADIR=$(docker exec "$BITCOIND_CONTAINER" printenv BITCOIN_DIR 2>/dev/null || echo "/root/.bitcoin") + if docker exec "$BITCOIND_CONTAINER" bitcoin-cli -datadir="$BITCOIN_DATADIR" getblockchaininfo &>/dev/null; then log " $BITCOIND_CONTAINER RPC ready after ${wait_count}s" break fi diff --git a/data/sync-utxos.log b/data/sync-utxos.log index 051064e..bf6add7 100644 --- a/data/sync-utxos.log +++ b/data/sync-utxos.log @@ -1,100 +1,100 @@ -📈 Statistiques finales: - - Total UTXOs: 283165 - - Dépensés: 124307 - - Non dépensés: 158858 - -✅ Synchronisation terminée -🔍 Démarrage de la synchronisation des UTXOs dépensés... - -📊 UTXOs à vérifier: 141938 -📡 Récupération des UTXOs depuis Bitcoin... -📊 UTXOs disponibles dans Bitcoin: 272415 -💾 Création de la table temporaire... -💾 Insertion des UTXOs disponibles par batch... - ⏳ Traitement: 10000/272415 UTXOs insérés... - ⏳ Traitement: 20000/272415 UTXOs insérés... - ⏳ Traitement: 30000/272415 UTXOs insérés... - ⏳ Traitement: 40000/272415 UTXOs insérés... - ⏳ Traitement: 50000/272415 UTXOs insérés... - ⏳ Traitement: 60000/272415 UTXOs insérés... - ⏳ Traitement: 70000/272415 UTXOs insérés... - ⏳ Traitement: 80000/272415 UTXOs insérés... - ⏳ Traitement: 90000/272415 UTXOs insérés... - ⏳ Traitement: 100000/272415 UTXOs insérés... - ⏳ Traitement: 110000/272415 UTXOs insérés... - ⏳ Traitement: 120000/272415 UTXOs insérés... - ⏳ Traitement: 130000/272415 UTXOs insérés... - ⏳ Traitement: 140000/272415 UTXOs insérés... - ⏳ Traitement: 150000/272415 UTXOs insérés... - ⏳ Traitement: 160000/272415 UTXOs insérés... - ⏳ Traitement: 170000/272415 UTXOs insérés... - ⏳ Traitement: 180000/272415 UTXOs insérés... - ⏳ Traitement: 190000/272415 UTXOs insérés... - ⏳ Traitement: 200000/272415 UTXOs insérés... - ⏳ Traitement: 210000/272415 UTXOs insérés... - ⏳ Traitement: 220000/272415 UTXOs insérés... - ⏳ Traitement: 230000/272415 UTXOs insérés... - ⏳ Traitement: 240000/272415 UTXOs insérés... - ⏳ Traitement: 250000/272415 UTXOs insérés... - ⏳ Traitement: 260000/272415 UTXOs insérés... - ⏳ Traitement: 270000/272415 UTXOs insérés... -💾 Mise à jour des UTXOs dépensés... - -📊 Résumé: - - UTXOs vérifiés: 141938 - - UTXOs toujours disponibles: 141938 - UTXOs dépensés détectés: 0 📈 Statistiques finales: - Total UTXOs: 283165 - - Dépensés: 141287 - - Non dépensés: 141878 + - Dépensés: 202127 + - Non dépensés: 81038 ✅ Synchronisation terminée 🔍 Démarrage de la synchronisation des UTXOs dépensés... -📊 UTXOs à vérifier: 124298 +📊 UTXOs à vérifier: 80978 📡 Récupération des UTXOs depuis Bitcoin... -📊 UTXOs disponibles dans Bitcoin: 270669 +📊 UTXOs disponibles dans Bitcoin: 267500 💾 Création de la table temporaire... 💾 Insertion des UTXOs disponibles par batch... - ⏳ Traitement: 10000/270669 UTXOs insérés... - ⏳ Traitement: 20000/270669 UTXOs insérés... - ⏳ Traitement: 30000/270669 UTXOs insérés... - ⏳ Traitement: 40000/270669 UTXOs insérés... - ⏳ Traitement: 50000/270669 UTXOs insérés... - ⏳ Traitement: 60000/270669 UTXOs insérés... - ⏳ Traitement: 70000/270669 UTXOs insérés... - ⏳ Traitement: 80000/270669 UTXOs insérés... - ⏳ Traitement: 90000/270669 UTXOs insérés... - ⏳ Traitement: 100000/270669 UTXOs insérés... - ⏳ Traitement: 110000/270669 UTXOs insérés... - ⏳ Traitement: 120000/270669 UTXOs insérés... - ⏳ Traitement: 130000/270669 UTXOs insérés... - ⏳ Traitement: 140000/270669 UTXOs insérés... - ⏳ Traitement: 150000/270669 UTXOs insérés... - ⏳ Traitement: 160000/270669 UTXOs insérés... - ⏳ Traitement: 170000/270669 UTXOs insérés... - ⏳ Traitement: 180000/270669 UTXOs insérés... - ⏳ Traitement: 190000/270669 UTXOs insérés... - ⏳ Traitement: 200000/270669 UTXOs insérés... - ⏳ Traitement: 210000/270669 UTXOs insérés... - ⏳ Traitement: 220000/270669 UTXOs insérés... - ⏳ Traitement: 230000/270669 UTXOs insérés... - ⏳ Traitement: 240000/270669 UTXOs insérés... - ⏳ Traitement: 250000/270669 UTXOs insérés... - ⏳ Traitement: 260000/270669 UTXOs insérés... - ⏳ Traitement: 270000/270669 UTXOs insérés... + ⏳ Traitement: 10000/267500 UTXOs insérés... + ⏳ Traitement: 20000/267500 UTXOs insérés... + ⏳ Traitement: 30000/267500 UTXOs insérés... + ⏳ Traitement: 40000/267500 UTXOs insérés... + ⏳ Traitement: 50000/267500 UTXOs insérés... + ⏳ Traitement: 60000/267500 UTXOs insérés... + ⏳ Traitement: 70000/267500 UTXOs insérés... + ⏳ Traitement: 80000/267500 UTXOs insérés... + ⏳ Traitement: 90000/267500 UTXOs insérés... + ⏳ Traitement: 100000/267500 UTXOs insérés... + ⏳ Traitement: 110000/267500 UTXOs insérés... + ⏳ Traitement: 120000/267500 UTXOs insérés... + ⏳ Traitement: 130000/267500 UTXOs insérés... + ⏳ Traitement: 140000/267500 UTXOs insérés... + ⏳ Traitement: 150000/267500 UTXOs insérés... + ⏳ Traitement: 160000/267500 UTXOs insérés... + ⏳ Traitement: 170000/267500 UTXOs insérés... + ⏳ Traitement: 180000/267500 UTXOs insérés... + ⏳ Traitement: 190000/267500 UTXOs insérés... + ⏳ Traitement: 200000/267500 UTXOs insérés... + ⏳ Traitement: 210000/267500 UTXOs insérés... + ⏳ Traitement: 220000/267500 UTXOs insérés... + ⏳ Traitement: 230000/267500 UTXOs insérés... + ⏳ Traitement: 240000/267500 UTXOs insérés... + ⏳ Traitement: 250000/267500 UTXOs insérés... + ⏳ Traitement: 260000/267500 UTXOs insérés... 💾 Mise à jour des UTXOs dépensés... 📊 Résumé: - - UTXOs vérifiés: 124298 - - UTXOs toujours disponibles: 124298 + - UTXOs vérifiés: 80978 + - UTXOs toujours disponibles: 80978 - UTXOs dépensés détectés: 0 📈 Statistiques finales: - Total UTXOs: 283165 - - Dépensés: 158927 - - Non dépensés: 124238 + - Dépensés: 202187 + - Non dépensés: 80978 + +✅ Synchronisation terminée +🔍 Démarrage de la synchronisation des UTXOs dépensés... + +📊 UTXOs à vérifier: 80978 +📡 Récupération des UTXOs depuis Bitcoin... +📊 UTXOs disponibles dans Bitcoin: 267516 +💾 Création de la table temporaire... +💾 Insertion des UTXOs disponibles par batch... + ⏳ Traitement: 10000/267516 UTXOs insérés... + ⏳ Traitement: 20000/267516 UTXOs insérés... + ⏳ Traitement: 30000/267516 UTXOs insérés... + ⏳ Traitement: 40000/267516 UTXOs insérés... + ⏳ Traitement: 50000/267516 UTXOs insérés... + ⏳ Traitement: 60000/267516 UTXOs insérés... + ⏳ Traitement: 70000/267516 UTXOs insérés... + ⏳ Traitement: 80000/267516 UTXOs insérés... + ⏳ Traitement: 90000/267516 UTXOs insérés... + ⏳ Traitement: 100000/267516 UTXOs insérés... + ⏳ Traitement: 110000/267516 UTXOs insérés... + ⏳ Traitement: 120000/267516 UTXOs insérés... + ⏳ Traitement: 130000/267516 UTXOs insérés... + ⏳ Traitement: 140000/267516 UTXOs insérés... + ⏳ Traitement: 150000/267516 UTXOs insérés... + ⏳ Traitement: 160000/267516 UTXOs insérés... + ⏳ Traitement: 170000/267516 UTXOs insérés... + ⏳ Traitement: 180000/267516 UTXOs insérés... + ⏳ Traitement: 190000/267516 UTXOs insérés... + ⏳ Traitement: 200000/267516 UTXOs insérés... + ⏳ Traitement: 210000/267516 UTXOs insérés... + ⏳ Traitement: 220000/267516 UTXOs insérés... + ⏳ Traitement: 230000/267516 UTXOs insérés... + ⏳ Traitement: 240000/267516 UTXOs insérés... + ⏳ Traitement: 250000/267516 UTXOs insérés... + ⏳ Traitement: 260000/267516 UTXOs insérés... +💾 Mise à jour des UTXOs dépensés... + +📊 Résumé: + - UTXOs vérifiés: 80978 + - UTXOs toujours disponibles: 80978 + - UTXOs dépensés détectés: 0 + +📈 Statistiques finales: + - Total UTXOs: 283165 + - Dépensés: 202187 + - Non dépensés: 80978 ✅ Synchronisation terminée diff --git a/docs/DASHBOARD.md b/docs/DASHBOARD.md index 16315e7..1213b65 100644 --- a/docs/DASHBOARD.md +++ b/docs/DASHBOARD.md @@ -145,7 +145,7 @@ Le Dashboard Bitcoin Signet est une interface web de supervision et de test acce - Documentation complète de toutes les APIs - Endpoints documentés : - API d'Ancrage (`/api/anchor/document`, `/api/anchor/verify`), Faucet, Filigrane, ClamAV - - **API Dashboard** : `/api/utxo/count`, `/api/utxo/list` (pagination, catégories), `/api/utxo/fees`, `POST /api/utxo/fees/update`, `/api/utxo/small-info`, `POST /api/utxo/consolidate`, `/api/hash/list` (pagination), `POST /api/hash/generate`, `/api/mining/difficulty`, `/api/mining/avg-block-time`, `/api/transactions/avg-fee`, `/api/transactions/avg-amount`, `/api/anchor/example`, etc. + - **API Dashboard** : `/api/utxo/count`, `/api/utxo/list` (pagination, catégories), `/api/utxo/fees`, `POST /api/utxo/fees/update`, `/api/utxo/small-info`, `POST /api/utxo/consolidate`, `/api/hash/list` (pagination), `POST /api/hash/generate`, `/api/mining/difficulty`, `/api/mining/status`, `/api/mining/avg-block-time`, `/api/transactions/avg-fee`, `/api/transactions/avg-amount`, `/api/anchor/example`, etc. - Exemples de requêtes curl, paramètres (query/body), réponses - Codes de statut HTTP, authentification (APIs externes), format des réponses @@ -245,6 +245,7 @@ Le dashboard utilise les endpoints suivants. Tous les endpoints internes sont se - `GET /api/hash/list` : Liste des hash ancrés (pagination : `page`, `limit`) - `POST /api/hash/generate` : Génère un hash SHA256 (body : `text` ou `fileContent`, optionnel `isBase64`) - `GET /api/mining/difficulty` : Difficulté de minage +- `GET /api/mining/status` : État du miner (actif / inactif, inféré depuis l’âge du dernier bloc) - `GET /api/mining/avg-block-time` : Temps moyen entre blocs (Mempool) - `GET /api/transactions/avg-fee` : Frais moyen ancrages (1200 sats) - `GET /api/transactions/avg-amount` : Montant moyen ancrages (1000 sats) @@ -283,6 +284,10 @@ Le Dashboard n'expose pas de route `/health`. Pour vérifier qu'il répond, util ## Maintenance +### Alignement avec la chaîne Signet (~11535 blocs) + +Le Dashboard lit la hauteur de la chaîne via RPC vers le nœud Bitcoin Signet (127.0.0.1:38332). Il affiche donc la même chaîne que le miner et Mempool (même nœud). La hauteur attendue est d’environ **11535 blocs**. Pour vérifier l’alignement Dashboard / Miner / Signet, voir [MAINTENANCE.md - Vérification de l’alignement](./MAINTENANCE.md#vérification-de-lalignement-dashboard--miner--signet-chaîne-11535). + ### Vérifier que le dashboard fonctionne ```bash diff --git a/docs/DOMAINS_AND_PORTS.md b/docs/DOMAINS_AND_PORTS.md index ef42e88..c54413f 100644 --- a/docs/DOMAINS_AND_PORTS.md +++ b/docs/DOMAINS_AND_PORTS.md @@ -20,10 +20,19 @@ Ce document liste tous les domaines, ports et services de l'infrastructure Certi | `antivir.certificator.4nkweb.com` | API ClamAV | 3023 | API REST pour scanner les fichiers (antivirus) | | `dashboard.certificator.4nkweb.com` | Dashboard | 3020 | Interface web de supervision | | `faucet.certificator.4nkweb.com` | API Faucet | 3021 | API REST pour distribuer des sats | -| `mempool.4nkweb.com` | Mempool | 3015 | Explorateur de blockchain Bitcoin | +| `mempool.4nkweb.com` | Mempool | 3015 | Explorateur de blockchain Bitcoin (machine bitcoin 192.168.1.105) | | `skeleton.certificator.4nkweb.com` | Website skeleton | 3024 | Site squelette iframe UserWallet | | `data.certificator.4nkweb.com` | Website data | 3025 | Iframe data (non clés), site ↔ data ↔ userwallet | +### Écoute des services : IPv4 uniquement, flux reçus du proxy (192.168.1.100) + +Les services backend écoutent en **IPv4 uniquement** (pas d’écoute sur `[::]`) et n’acceptent les flux que **reçus de la machine proxy (192.168.1.100)** : + +- **Bind** : chaque service écoute sur l’adresse IPv4 de sa machine (`DASHBOARD_HOST=192.168.1.105` sur bitcoin, `FAUCET_API_HOST=192.168.1.103` sur prod, etc.). +- **Restriction à la source** : si `ALLOWED_SOURCE_IP=192.168.1.100` est défini, toute requête dont la source n’est pas le proxy est rejetée (403). + +Voir `features/services-ecoute-ipv4-proxy.md` et `docs/ENVIRONMENT.md`. + ### Configuration Nginx Tous les domaines sont routés via le proxy Nginx sur le serveur `192.168.1.100` (proxy). @@ -116,10 +125,10 @@ Internet │ ├─→ certificator.4nkweb.com → 192.168.1.103:3004 (API LeCoffre Anchor) │ ├─→ watermark.certificator.4nkweb.com → 192.168.1.103:3022 (API Filigrane) │ ├─→ antivir.certificator.4nkweb.com → 192.168.1.103:3023 (API ClamAV) - │ ├─→ dashboard.certificator.4nkweb.com → 192.168.1.103:3020 (Dashboard) + │ ├─→ dashboard.certificator.4nkweb.com → 192.168.1.105:3020 (Dashboard, machine bitcoin) │ ├─→ skeleton.certificator.4nkweb.com → 192.168.1.105:3024 (Website skeleton) │ ├─→ faucet.certificator.4nkweb.com → 192.168.1.103:3021 (API Faucet) - │ └─→ mempool.4nkweb.com → 192.168.1.104:3015 (Mempool) + │ └─→ mempool.4nkweb.com → 192.168.1.105:3015 (Mempool, machine bitcoin) ``` ## Vérification des Ports diff --git a/docs/ENVIRONMENT.md b/docs/ENVIRONMENT.md index e74010a..49a55ef 100644 --- a/docs/ENVIRONMENT.md +++ b/docs/ENVIRONMENT.md @@ -78,7 +78,7 @@ LOG_LEVEL=info # error, warn, info, debug NODE_ENV=production ``` -**Note :** Le port est fixe (3010) et défini aussi dans le service systemd. +**Note :** Le port est fixe (3010) et défini aussi dans le service systemd. En production, `API_HOST=192.168.1.105` (machine bitcoin) et `ALLOWED_SOURCE_IP=192.168.1.100` pour n’accepter que le proxy en IPv4. ### 3. Fichier `.env` de l'API Faucet @@ -110,7 +110,7 @@ LOG_LEVEL=info NODE_ENV=production ``` -**Note :** Le port est fixe (3021) et défini aussi dans le service systemd. +**Note :** Le port est fixe (3021) et défini aussi dans le service systemd. En production, `FAUCET_API_HOST=192.168.1.103` (machine prod) et `ALLOWED_SOURCE_IP=192.168.1.100` pour n’accepter que le proxy en IPv4. ### 4. Fichier `.env` de l'API Filigrane @@ -143,7 +143,7 @@ LOG_LEVEL=info NODE_ENV=production ``` -**Note :** Le port est fixe (3022) et défini aussi dans le service systemd. +**Note :** Le port est fixe (3022) et défini aussi dans le service systemd. En production, `WATERMARK_API_HOST=192.168.1.103` (machine prod) et `ALLOWED_SOURCE_IP=192.168.1.100` pour n’accepter que le proxy en IPv4. ### 5. Fichier `.env` de l'API ClamAV @@ -166,7 +166,7 @@ LOG_LEVEL=info NODE_ENV=production ``` -**Note :** Le port est fixe (3023) et défini directement dans le code (`src/server.js`). +**Note :** Le port est fixe (3023) et défini directement dans le code (`src/server.js`). En production, `CLAMAV_API_HOST=192.168.1.103` (machine prod) et `ALLOWED_SOURCE_IP=192.168.1.100` pour n’accepter que le proxy en IPv4. ### 6. Fichier `.env` du Dashboard @@ -179,8 +179,8 @@ NODE_ENV=production DASHBOARD_PORT=3020 # Port fixe (défini aussi dans systemd) DASHBOARD_HOST=0.0.0.0 -# Bitcoin RPC Configuration -BITCOIN_RPC_HOST=localhost +# Bitcoin RPC Configuration (sur la machine bitcoin : 127.0.0.1 = même nœud que Mempool) +BITCOIN_RPC_HOST=127.0.0.1 BITCOIN_RPC_PORT=38332 BITCOIN_RPC_USER=bitcoin BITCOIN_RPC_PASSWORD=bitcoin @@ -207,7 +207,7 @@ LOG_LEVEL=info NODE_ENV=production ``` -**Note :** Le port est fixe (3020) et défini aussi dans le service systemd. +**Note :** Le port est fixe (3020) et défini aussi dans le service systemd. En production, `DASHBOARD_HOST=192.168.1.105` (machine bitcoin) et `ALLOWED_SOURCE_IP=192.168.1.100` pour n’accepter que le proxy en IPv4. ## Variables d'Environnement dans les Services Systemd @@ -217,28 +217,32 @@ Les services systemd définissent aussi des variables d'environnement pour garan ```ini Environment=API_PORT=3010 -Environment=API_HOST=0.0.0.0 +Environment=API_HOST=192.168.1.105 +Environment=ALLOWED_SOURCE_IP=192.168.1.100 ``` ### API Faucet (`api-faucet/faucet-api.service`) ```ini Environment=FAUCET_API_PORT=3021 -Environment=FAUCET_API_HOST=0.0.0.0 +Environment=FAUCET_API_HOST=192.168.1.103 +Environment=ALLOWED_SOURCE_IP=192.168.1.100 ``` ### API Filigrane (`api-filigrane/filigrane-api.service`) ```ini Environment=WATERMARK_API_PORT=3022 -Environment=WATERMARK_API_HOST=0.0.0.0 +Environment=WATERMARK_API_HOST=192.168.1.103 +Environment=ALLOWED_SOURCE_IP=192.168.1.100 ``` ### API ClamAV (`api-clamav/clamav-api.service`) ```ini Environment=CLAMAV_API_PORT=3023 -Environment=CLAMAV_API_HOST=0.0.0.0 +Environment=CLAMAV_API_HOST=192.168.1.103 +Environment=ALLOWED_SOURCE_IP=192.168.1.100 Environment=CLAMAV_HOST=localhost Environment=CLAMAV_PORT=3310 ``` @@ -247,9 +251,22 @@ Environment=CLAMAV_PORT=3310 ```ini Environment=DASHBOARD_PORT=3020 -Environment=DASHBOARD_HOST=0.0.0.0 +Environment=DASHBOARD_HOST=192.168.1.105 +Environment=ALLOWED_SOURCE_IP=192.168.1.100 ``` +## Écoute réseau : IPv4 uniquement, flux reçus du proxy (192.168.1.100) + +Les services backend doivent : + +1. **Écouter en IPv4 uniquement** : pas d’écoute sur `[::]`. En fixant `*_HOST` à l’adresse IPv4 de la machine (ex. `192.168.1.105` pour la machine bitcoin), le service n’écoute que sur cette interface (IPv4). +2. **Accepter les flux reçus du proxy uniquement** : si `ALLOWED_SOURCE_IP=192.168.1.100` est défini, le service rejette (403) toute requête dont la source (après normalisation IPv6-mapped → IPv4) n’est pas 192.168.1.100. + +- **Machine bitcoin (192.168.1.105)** : `DASHBOARD_HOST=192.168.1.105`, `API_HOST=192.168.1.105` (anchorage), `ALLOWED_SOURCE_IP=192.168.1.100`. +- **Machine prod (192.168.1.103)** : `FAUCET_API_HOST=192.168.1.103`, `WATERMARK_API_HOST=192.168.1.103`, `CLAMAV_API_HOST=192.168.1.103`, `ALLOWED_SOURCE_IP=192.168.1.100`. + +Voir `features/services-ecoute-ipv4-proxy.md` pour le détail. + ## Ordre de Priorité Les variables d'environnement sont chargées dans cet ordre : diff --git a/docs/INSTALLATION_NEW_NODE.md b/docs/INSTALLATION_NEW_NODE.md index 42eb1a7..b094c06 100644 --- a/docs/INSTALLATION_NEW_NODE.md +++ b/docs/INSTALLATION_NEW_NODE.md @@ -218,6 +218,7 @@ sudo docker exec bitcoin-signet-instance bash -c "echo 60 > /root/.bitcoin/BLOCK ```bash sudo docker run --env-file .env -d \ --name bitcoin-signet-instance \ + -v signet-bitcoin-data:/root/.bitcoin \ -p 38332:38332 \ -p 38333:38333 \ -p 28332:28332 \ @@ -226,6 +227,8 @@ sudo docker run --env-file .env -d \ bitcoin-signet ``` +Le volume `signet-bitcoin-data` conserve la chaîne ; sans lui, une recréation du conteneur repart d’une nouvelle chaîne. + ### Vérifier les Logs ```bash @@ -252,11 +255,12 @@ sudo docker stop bitcoin-signet-instance && sudo docker rm bitcoin-signet-instan # Redémarrer le conteneur sudo docker restart bitcoin-signet-instance -# Ou recréer le conteneur +# Ou recréer le conteneur (volume persistant pour conserver la chaîne) sudo docker stop bitcoin-signet-instance sudo docker rm bitcoin-signet-instance sudo docker run --env-file .env -d \ --name bitcoin-signet-instance \ + -v signet-bitcoin-data:/root/.bitcoin \ -p 38332:38332 -p 38333:38333 -p 28332:28332 -p 28333:28333 -p 28334:28334 \ bitcoin-signet ``` diff --git a/docs/INTERFACES.md b/docs/INTERFACES.md index 7c96f47..b67aa4e 100644 --- a/docs/INTERFACES.md +++ b/docs/INTERFACES.md @@ -377,9 +377,10 @@ Commandes Docker pour gérer le conteneur Bitcoin Signet. #### Gestion du Conteneur ```bash -# Démarrer +# Démarrer (volume persistant pour conserver la chaîne) sudo docker run --env-file .env -d \ --name bitcoin-signet-instance \ + -v signet-bitcoin-data:/root/.bitcoin \ -p 38332:38332 -p 38333:38333 \ -p 28332:28332 -p 28333:28333 -p 28334:28334 \ bitcoin-signet @@ -502,7 +503,7 @@ pm2 logs anchor-api Interface web et API REST pour explorer la blockchain Bitcoin Signet. ### Accès -- **URL Web** : `http://localhost:3015` (local) ou `https://mempool1.4nkweb.com` (production) +- **URL Web** : `http://localhost:3015` (local) ou `https://mempool.4nkweb.com` (production, machine bitcoin) - **API Backend** : `http://localhost:8999/api/v1/` - **Protocole** : HTTP/HTTPS - **Format** : HTML (web) / JSON (API) diff --git a/docs/MAINTENANCE.md b/docs/MAINTENANCE.md index c5570f2..5bd07af 100644 --- a/docs/MAINTENANCE.md +++ b/docs/MAINTENANCE.md @@ -85,7 +85,7 @@ bitcoin/ - `/root/.bitcoin/` : Répertoire de données Bitcoin - `signet/` : Données de la chaîne signet - - `bitcoin.conf` : Configuration du nœud + - `bitcoin.conf` : Configuration du nœud (générée par `gen-bitcoind-conf.sh` ; référence : [SIGNET-CUSTOM-CONFIG.md](./SIGNET-CUSTOM-CONFIG.md)) - `PRIVKEY.txt` : Clé privée du signer - `SIGNETCHALLENGE.txt` : Challenge du signet - `MAGIC.txt` : Magic number du réseau @@ -222,12 +222,27 @@ sudo docker exec bitcoin-signet-instance bitcoin-cli -datadir=/root/.bitcoin dernière archive (prête à télécharger) +``` + +Le symlink `backups/signet-datadir-latest.tar.gz` pointe toujours vers la dernière archive créée ; utile pour téléchargement ou restauration rapide : `./restore-signet-from-backup.sh backups/signet-datadir-latest.tar.gz`. + +**Export wallet de minage (réimportable, pour retrouver les fonds sur cette chaîne) :** + +```bash +./export-mining-wallet.sh +# Crée backups/mining-wallet-export-YYYYMMDD-HHMMSS.json et mining-wallet-export-latest.json +# Contient : PRIVKEY, SIGNETCHALLENGE, wallet_name, descriptors avec clés privées, infos chaîne + +# Réimporter sur un nœud avec la même chaîne : +./import-mining-wallet.sh backups/mining-wallet-export-latest.json +``` + +**Export quotidien vers git (2 versions conservées) :** + +```bash +# Tâche cron quotidienne : exporte signet-datadir et mining-wallet vers +# https://git.4nkweb.com/4nk/backup (2 versions max de chaque) +./data/backup-to-git-cron.sh +# Voir features/backup-to-git-daily-cron.md pour configuration cron +``` + +**Sauvegarde manuelle :** + ```bash # Sauvegarder les données Bitcoin sudo docker exec bitcoin-signet-instance tar czf /tmp/bitcoin-backup.tar.gz /root/.bitcoin/ @@ -912,8 +964,9 @@ cp .env .env.backup-$(date +%Y%m%d) sudo docker stop bitcoin-signet-instance sudo docker rm bitcoin-signet-instance -# Créer un nouveau conteneur +# Créer un nouveau conteneur (volume persistant pour que les données restaurées soient conservées) sudo docker run --env-file .env -d --name bitcoin-signet-instance \ + -v signet-bitcoin-data:/root/.bitcoin \ -p 38332:38332 -p 38333:38333 -p 28332:28332 -p 28333:28333 -p 28334:28334 \ bitcoin-signet @@ -941,6 +994,36 @@ Ces clés sont essentielles pour maintenir la cohérence du signet. ## Commandes Utiles +### Vérification de l'alignement Dashboard / Miner / Signet (chaîne ~11535) + +Dashboard, miner et signet utilisent **une seule source de vérité** : le nœud Bitcoin Signet dans le conteneur `bitcoin-signet-instance` (RPC port 38332). + +| Composant | Source de la hauteur | +|-----------|----------------------| +| **Signet (nœud)** | `bitcoind` dans le conteneur, datadir `/root/.bitcoin` | +| **Dashboard** | RPC `getblockchaininfo` vers `127.0.0.1:38332` (même nœud) | +| **Miner** | S'exécute dans le conteneur, appelle `bitcoin-cli -datadir=/root/.bitcoin` (même nœud) | +| **Mempool** | Backend connecté au même nœud (CORE_RPC_PORT=38332) | + +La chaîne attendue est d’environ **11535 blocs**. Pour vérifier que tout est aligné : + +1. **Hauteur depuis le nœud** (référence) : + ```bash + sudo docker exec bitcoin-signet-instance bitcoin-cli -datadir=/root/.bitcoin getblockchaininfo | grep -E '"chain"|"blocks"' + ``` + Attendu : `"chain": "signet"`, `"blocks":` proche de 11535. + +2. **Hauteur depuis le Dashboard** (doit être identique) : + ```bash + curl -s http://localhost:3020/api/blockchain/info | grep -o '"blocks":[0-9]*' + ``` + +3. **Miner** : les logs du conteneur affichent la hauteur à chaque bloc miné (`Mined ... at height N`). Cette hauteur est celle du nœud. + +Si le Dashboard affiche une hauteur très différente (ex. 2) ou si l’API d’ancrage retourne « Insufficient Balance » (0 BTC), exécuter sur la machine bitcoin : `./fix-dashboard-anchor-chain.sh` (ou avec chemin de backup pour restaurer la chaîne). Voir [fixKnowledge/dashboard-anchor-wrong-chain-insufficient-balance.md](../fixKnowledge/dashboard-anchor-wrong-chain-insufficient-balance.md) et [fixKnowledge/signet-chain-lost-volume-persistent.md](../fixKnowledge/signet-chain-lost-volume-persistent.md). + +**Script de test RPC (même nœud que Mempool) :** `./test-mempool-rpc-config.sh [HOST] [PORT]` (défaut 127.0.0.1 38332). **Script de vérification dashboard signet :** `./verify-dashboard-signet.sh` (machine bitcoin, pour que https://dashboard.certificator.4nkweb.com/ affiche le signet custom). **Script de vérification :** exécuter `./verify-chain-alignment.sh` à la racine du projet pour comparer la hauteur du nœud et du Dashboard et vérifier qu’elle est dans la plage attendue (~11535). + ### Script de Vérification Rapide ```bash @@ -980,4 +1063,4 @@ watch -n 5 'sudo docker exec bitcoin-signet-instance bitcoin-cli -datadir=/root/ --- -**Dernière mise à jour** : 2026-01-24 +**Dernière mise à jour** : 2026-02-02 diff --git a/docs/MEMPOOL.md b/docs/MEMPOOL.md index e4d9b82..e2fa264 100644 --- a/docs/MEMPOOL.md +++ b/docs/MEMPOOL.md @@ -6,7 +6,9 @@ ## Vue d'Ensemble -Mempool est un explorateur de blockchain Bitcoin qui permet de visualiser et d'analyser la blockchain et la mempool du signet custom. Il fournit une interface web moderne pour explorer les blocs, transactions, adresses et statistiques du réseau. +Mempool est un explorateur de blockchain Bitcoin qui permet de visualiser et d'analyser la blockchain et la mempool du signet custom. + +**Emplacement dans le projet :** le chemin de base du projet est `/home/ncantu/Bureau/code/bitcoin/` ; Mempool se trouve dans le sous-répertoire `mempool/` (soit `.../bitcoin/mempool/`). Il fournit une interface web moderne pour explorer les blocs, transactions, adresses et statistiques du réseau. ## Caractéristiques @@ -26,19 +28,23 @@ Mempool est un explorateur de blockchain Bitcoin qui permet de visualiser et d'a ### Installation Rapide +Depuis la racine du projet (`/home/ncantu/Bureau/code/bitcoin/`) : + ```bash -cd /home/ncantu/Bureau/code/bitcoin/mempool +cd mempool ./start.sh ``` Le script : -1. Charge les variables d'environnement depuis `../.env` +1. Charge les variables d'environnement depuis `../.env` (racine du projet) 2. Vérifie la connexion au nœud Bitcoin 3. Crée les répertoires nécessaires 4. Lance les services Docker (frontend, backend, base de données) ### Installation Manuelle +Depuis la racine du projet : + ```bash cd mempool docker-compose -f docker-compose.signet.yml up -d @@ -121,7 +127,7 @@ Mempool utilise quatre services Docker : ### Interface Web - **URL locale** : http://localhost:3015 -- **URL production** : https://mempool1.4nkweb.com (via nginx proxy) +- **URL production** : https://mempool.4nkweb.com (via nginx proxy, machine bitcoin 192.168.1.105) ### API Backend @@ -136,6 +142,8 @@ Mempool utilise quatre services Docker : ### Démarrage +Depuis la racine du projet (`/home/ncantu/Bureau/code/bitcoin/`) : + ```bash cd mempool ./start.sh @@ -189,6 +197,8 @@ sudo docker-compose -f docker-compose.signet.yml down -v ### Mise à Jour +Depuis la racine du projet : + ```bash cd mempool # Récupérer les dernières images @@ -318,19 +328,19 @@ sudo docker-compose -f docker-compose.signet.yml logs -f db ### Configuration Nginx (sur proxy 192.168.1.100) -Pour exposer Mempool via le proxy nginx (mempool1.4nkweb.com), ajouter une configuration similaire aux autres services : +Pour exposer Mempool via le proxy nginx (mempool.4nkweb.com), ajouter une configuration similaire aux autres services. Mempool est hébergé sur la machine bitcoin (192.168.1.105) uniquement : ```nginx # Mempool Bitcoin Signet Explorer server { listen 80; - server_name mempool1.4nkweb.com; + server_name mempool.4nkweb.com; - access_log /var/log/nginx/mempool1.4nkweb.com.access.log; - error_log /var/log/nginx/mempool1.4nkweb.com.error.log; + access_log /var/log/nginx/mempool.4nkweb.com.access.log; + error_log /var/log/nginx/mempool.4nkweb.com.error.log; location / { - proxy_pass http://192.168.1.XXX:3015; + proxy_pass http://192.168.1.105:3015; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection 'upgrade'; diff --git a/docs/README.md b/docs/README.md index 4183e47..a6fc9a1 100644 --- a/docs/README.md +++ b/docs/README.md @@ -2,6 +2,8 @@ Ce dossier contient toute la documentation nécessaire pour la maintenance et l'utilisation du Bitcoin Signet custom. +**Structure du projet :** chemin de base `/home/ncantu/Bureau/code/bitcoin/` ; Mempool dans le sous-répertoire `mempool/`. + ## Fichiers de Documentation - **[MAINTENANCE.md](./MAINTENANCE.md)** : Documentation complète de maintenance @@ -81,16 +83,35 @@ Ce dossier contient toute la documentation nécessaire pour la maintenance et l' cd /home/ncantu/Bureau/code/bitcoin sudo docker build -t bitcoin-signet . sudo docker run --env-file .env -d --name bitcoin-signet-instance \ + -v signet-bitcoin-data:/root/.bitcoin \ -p 38332:38332 -p 38333:38333 -p 28332:28332 -p 28333:28333 -p 28334:28334 \ bitcoin-signet ``` +Le volume `signet-bitcoin-data` conserve la chaîne Bitcoin ; sans lui, une recréation du conteneur repart d’une nouvelle chaîne (hauteur 0). + ### Vérification ```bash +# Alignement Dashboard / Miner / Signet (chaîne ~11535) +./verify-chain-alignment.sh + +# État du nœud sudo docker exec bitcoin-signet-instance bitcoin-cli -datadir=/root/.bitcoin getblockchaininfo ``` +### Correction Dashboard mauvaise chaîne / API ancrage Insufficient Balance + +Sur la machine bitcoin, à la racine du projet : + +```bash +# Vérifier, redémarrer Dashboard et API d’ancrage +./fix-dashboard-anchor-chain.sh + +# Avec restauration de la chaîne depuis une sauvegarde +./fix-dashboard-anchor-chain.sh backups/signet-datadir-YYYYMMDD-HHMMSS.tar.gz +``` + ### Logs ```bash diff --git a/docs/SIGNET-CUSTOM-CONFIG.md b/docs/SIGNET-CUSTOM-CONFIG.md new file mode 100644 index 0000000..d8e2a6e --- /dev/null +++ b/docs/SIGNET-CUSTOM-CONFIG.md @@ -0,0 +1,40 @@ +# Configuration de référence – Signet custom + +**Auteur** : Équipe 4NK +**Date** : 2026-02-02 + +Configuration de référence du nœud Bitcoin Signet custom. Elle est générée par `gen-bitcoind-conf.sh` à partir du fichier `.env` à la racine du projet. + +## bitcoin.conf (équivalent) + +```ini +signet=1 +txindex=1 +blockfilterindex=1 +peerblockfilters=1 +coinstatsindex=1 +dnsseed=0 +persistmempool=1 +uacomment=CustomSignet + +[signet] +daemon=1 +listen=1 +server=1 +discover=1 +signetchallenge=5121028b8d4cea1b3d8582babc8405bc618fbbb281c0f64e6561aa85968251931cd0a651ae +rpcbind=0.0.0.0:38332 +rpcallowip=0.0.0.0/0 +whitelist=0.0.0.0/0 +fallbackfee=0.0002 +``` + +Pas de `addnode` nécessaire pour cette machine (nœud unique sur 192.168.1.105). Le nœud et les services (Dashboard, Anchorage, Faucet, Mempool) tournent sur cette machine. + +## Variables .env correspondantes + +- `SIGNETCHALLENGE`, `RPCUSER`, `RPCPASSWORD`, `UACOMMENT` +- `RPCBIND=0.0.0.0:38332`, `RPCALLOWIP=0.0.0.0/0`, `WHITELIST=0.0.0.0/0` +- ZMQ : `ZMQPUBRAWBLOCK`, `ZMQPUBRAWTX`, `ZMQPUBHASHBLOCK` + +Voir `env.example` et `gen-bitcoind-conf.sh`. diff --git a/docs/TROUBLESHOOTING_MINING.md b/docs/TROUBLESHOOTING_MINING.md index 6aa04ae..27921c0 100644 --- a/docs/TROUBLESHOOTING_MINING.md +++ b/docs/TROUBLESHOOTING_MINING.md @@ -77,6 +77,7 @@ sudo docker rm bitcoin-signet-instance sudo docker build -t bitcoin-signet . sudo docker run --env-file .env -d \ --name bitcoin-signet-instance \ + -v signet-bitcoin-data:/root/.bitcoin \ -p 38332:38332 -p 38333:38333 \ -p 28332:28332 -p 28333:28333 -p 28334:28334 \ bitcoin-signet diff --git a/docs/USERWALLET_KEY_DERIVATION.md b/docs/USERWALLET_KEY_DERIVATION.md new file mode 100644 index 0000000..db99913 --- /dev/null +++ b/docs/USERWALLET_KEY_DERIVATION.md @@ -0,0 +1,60 @@ +# UserWallet — Dérivation de clés et clés multiples par pair + +**Author:** Équipe 4NK +**Date:** 2026-02-02 +**Version:** 1.0 + +## Objectif + +Permettre à un pair (identité locale ou appareil distant) d’avoir **plusieurs clés publiques** et de **dériver** de nouvelles paires de clés de façon déterministe à partir d’une clé privée. Vérifier rapidement si une clé publique donnée « appartient » à une clé privée (clé principale ou l’une des clés dérivées). + +## Modèle de données + +### PairConfig + +- **`publicKey?: string`** — Clé publique principale (hex 66 caractères). Utilisée pour ECDH, pairing, etc. +- **`publicKeys?: string[]`** — Liste optionnelle de clés publiques supplémentaires. Un pair « possède » toute clé dans `{ publicKey } ∪ publicKeys`. + +Un pair peut donc avoir un nombre important de clés publiques différentes (principale + dérivées ou ajoutées). + +## Dérivation déterministe (crypto) + +Une seule clé privée permet d’obtenir plusieurs paires (clé privée enfant, clé publique enfant) sans stocker plusieurs secrets. La dérivation est **déterministe** : même index ⇒ même paire. + +### Algorithme + +- **Entrée** : clé privée parente (64 hex), index ≥ 0. +- **Procédé** : HMAC-SHA256(clé_privée_parente, `"userwallet-derive-v1-"` + index) → 32 octets ; réduction modulo (ordre de la courbe secp256k1 − 1) + 1 pour obtenir un scalaire valide ; clé publique = multiplication du point de base par ce scalaire (format compressé). +- **Sortie** : paire (clé privée enfant, clé publique enfant). + +Courbe : secp256k1 (même que Bitcoin). La clé principale (index « aucun ») est la clé publique dérivée directement de la clé privée de l’identité. + +### API (userwallet) + +| Fonction | Rôle | +|----------|------| +| `deriveChildKeyPair(parentPrivateKeyHex, index)` | Retourne la paire (privée, publique) pour l’index donné. | +| `getDerivedPublicKeys(parentPrivateKeyHex, count)` | Retourne [clé principale, dérivée(0), …, dérivée(count−1)]. Longueur = 1 + count. | +| `publicKeyBelongsToIdentity(identityPrivateKeyHex, publicKeyHex, maxDerived?)` | Vrai si la clé publique est la clé principale ou l’une des dérivées d’indice 0..maxDerived−1. Par défaut maxDerived = 0 (seule la clé principale est testée). | + +**Performance** : la vérification « cette clé appartient-elle à mon identité ? » est en O(1) pour la clé principale (une dérivation + comparaison). Pour les dérivées, au plus `maxDerived` dérivations + comparaisons ; en pratique on borne `maxDerived` pour rester rapide. + +## Pairing (pairs et multi-clés) + +| Fonction | Rôle | +|----------|------| +| `getPairPublicKeys(pair, identityPrivateKeyHex?, derivedCount?)` | Liste toutes les clés publiques du pair. Pour le pair local + clé privée fournie : clé principale + dérivées 0..derivedCount−1. Pour un pair distant : `publicKey` + `publicKeys`. | +| `pairContainsPublicKey(pair, publicKeyHex, identityPrivateKeyHex?, maxDerived?)` | Vrai si le pair possède cette clé (locale : dérivation bornée ; distant : test d’appartenance à `publicKey` / `publicKeys`). | +| `addPairPublicKey(pairUuid, publicKeyHex)` | Ajoute une clé à `publicKeys` du pair (sans doublon avec `publicKey`). | + +## Fichiers concernés + +- `userwallet/src/utils/crypto.ts` : `deriveChildKeyPair`, `getDerivedPublicKeys`, `publicKeyBelongsToIdentity` +- `userwallet/src/utils/pairing.ts` : `getPairPublicKeys`, `pairContainsPublicKey`, `addPairPublicKey` +- `userwallet/src/types/identity.ts` : `PairConfig.publicKeys` + +## Usage typique + +- **Obtenir N clés dérivées** : `getDerivedPublicKeys(identity.privateKey, N)`. +- **Vérifier qu’une clé appartient à l’identité** (avec au plus 100 dérivées) : `publicKeyBelongsToIdentity(identity.privateKey, somePubKey, 100)`. +- **Vérifier qu’un pair possède une clé** : `pairContainsPublicKey(pair, somePubKey, identity?.privateKey, 100)`. diff --git a/env.example b/env.example index d1d0845..9dabc23 100644 --- a/env.example +++ b/env.example @@ -23,5 +23,5 @@ ZMQPUBHASHBLOCK=tcp://0.0.0.0:28334 RPCBIND=0.0.0.0:38332 RPCALLOWIP=0.0.0.0/0 WHITELIST=0.0.0.0/0 -ADDNODE= +ADDNODE= EXTERNAL_IP= diff --git a/export-backup.sh b/export-backup.sh index 7267a5e..ed5a0b3 100755 --- a/export-backup.sh +++ b/export-backup.sh @@ -7,7 +7,8 @@ set -e CONTAINER_NAME="bitcoin-signet-instance" -DATADIR="/root/.bitcoin" +# Use BITCOIN_DIR from container so we talk to the same datadir as bitcoind +DATADIR="" WALLET_NAME="custom_signet" BACKUP_DIR="./backups" TIMESTAMP=$(date +%Y%m%d_%H%M%S) @@ -27,6 +28,8 @@ if ! sudo docker ps --format "{{.Names}}" | grep -q "^${CONTAINER_NAME}$"; then exit 1 fi +DATADIR=$(sudo docker exec "$CONTAINER_NAME" printenv BITCOIN_DIR 2>/dev/null || echo "/root/.bitcoin") + # Start backup file with header cat > "${BACKUP_FILE}" << EOF # Bitcoin Signet Backup diff --git a/export-mining-wallet.sh b/export-mining-wallet.sh new file mode 100755 index 0000000..0522a7a --- /dev/null +++ b/export-mining-wallet.sh @@ -0,0 +1,86 @@ +#!/bin/bash +# +# Export the mining wallet (custom_signet) in a re-importable format. +# Contains: PRIVKEY, SIGNETCHALLENGE, wallet name, descriptors with private keys, +# and chain info — everything needed to recover funds on this Signet chain. +# +# Output: backups/mining-wallet-export-YYYYMMDD-HHMMSS.json +# Symlink: backups/mining-wallet-export-latest.json (ready to download) +# +# Re-import: ./import-mining-wallet.sh backups/mining-wallet-export-latest.json +# +# Author: 4NK Team +# Date: 2026-02-02 + +set -e + +CONTAINER_NAME="bitcoin-signet-instance" +WALLET_NAME="custom_signet" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" +BACKUP_DIR="${1:-$SCRIPT_DIR/backups}" +mkdir -p "$BACKUP_DIR" +TIMESTAMP=$(date +%Y%m%d-%H%M%S) +OUTPUT_FILE="$BACKUP_DIR/mining-wallet-export-$TIMESTAMP.json" + +if ! docker ps --format "{{.Names}}" | grep -q "^${CONTAINER_NAME}$"; then + echo "Error: Container ${CONTAINER_NAME} is not running" + exit 1 +fi + +DATADIR=$(docker exec "$CONTAINER_NAME" printenv BITCOIN_DIR 2>/dev/null || echo "/root/.bitcoin") + +echo "=== Export mining wallet (re-importable) ===" +echo "Container: $CONTAINER_NAME" +echo "Wallet: $WALLET_NAME" +echo "Output: $OUTPUT_FILE" +echo "" + +PRIVKEY=$(sudo docker exec "$CONTAINER_NAME" cat "$DATADIR/PRIVKEY.txt" 2>/dev/null || echo "") +SIGNETCHALLENGE=$(sudo docker exec "$CONTAINER_NAME" cat "$DATADIR/SIGNETCHALLENGE.txt" 2>/dev/null || echo "") +BLOCKCHAIN_INFO=$(sudo docker exec "$CONTAINER_NAME" bitcoin-cli -datadir="$DATADIR" getblockchaininfo 2>/dev/null || echo "{}") +DESCRIPTORS_RAW=$(sudo docker exec "$CONTAINER_NAME" bitcoin-cli -datadir="$DATADIR" -rpcwallet="$WALLET_NAME" listdescriptors true 2>/dev/null || echo "{\"descriptors\":[]}") + +# Build importdescriptors-compatible array: each item {desc, timestamp, internal} +DESCRIPTORS_IMPORT=$(echo "$DESCRIPTORS_RAW" | jq -c ' + .descriptors + | map(select(.desc != null)) + | map({ + desc: .desc, + timestamp: (if .timestamp != null then .timestamp else 0 end), + internal: (if .internal == true then true elif .internal == false then false else false end) + }) +') + +# Single JSON document for re-import and recovery on this chain +jq -n \ + --arg chain "signet" \ + --arg signet_challenge "${SIGNETCHALLENGE}" \ + --arg privkey "${PRIVKEY}" \ + --arg wallet_name "${WALLET_NAME}" \ + --argjson descriptors "${DESCRIPTORS_IMPORT}" \ + --argjson blockchain_info "${BLOCKCHAIN_INFO}" \ + '{ + export_version: "1.0", + export_date: (now | strftime("%Y-%m-%dT%H:%M:%SZ")), + chain: $chain, + signet_challenge: $signet_challenge, + privkey: $privkey, + wallet_name: $wallet_name, + descriptors: $descriptors, + blockchain_at_export: $blockchain_info, + reimport: "Run: ./import-mining-wallet.sh on a node with the same SIGNETCHALLENGE" + }' > "$OUTPUT_FILE" + +ln -sf "$(basename "$OUTPUT_FILE")" "$BACKUP_DIR/mining-wallet-export-latest.json" + +echo "Export saved: $OUTPUT_FILE" +echo "Latest symlink: $BACKUP_DIR/mining-wallet-export-latest.json" +echo "Size: $(du -h "$OUTPUT_FILE" | cut -f1)" +echo "" +echo "To re-import and recover funds on this chain:" +echo " ./import-mining-wallet.sh $OUTPUT_FILE" +echo " # or: ./import-mining-wallet.sh $BACKUP_DIR/mining-wallet-export-latest.json" +echo "" +echo "WARNING: This file contains private keys. Keep it secure and encrypted." +echo "" diff --git a/features/backup-to-git-daily-cron.md b/features/backup-to-git-daily-cron.md new file mode 100644 index 0000000..9b8f5fe --- /dev/null +++ b/features/backup-to-git-daily-cron.md @@ -0,0 +1,73 @@ +# Cron quotidien : export backups Signet et mining wallet vers git + +**Auteur** : Équipe 4NK +**Date** : 2026-02-04 + +## Objectif + +Exporter quotidiennement les sauvegardes du Signet (chaîne complète) et du wallet de minage vers le dépôt git https://git.4nkweb.com/4nk/backup, en ne conservant que 2 versions de chaque type. + +## Impacts + +- Sauvegardes Signet et mining wallet disponibles sur git pour récupération à distance. +- Rotation automatique : au plus 2 versions de la chaîne complète (signet-datadir) et 2 versions du wallet. +- Log : `data/backup-to-git.log`. + +## Solution implémentée + +### Script `data/backup-to-git-cron.sh` + +1. Exécute `save-signet-datadir-backup.sh` → crée `backups/signet-datadir-YYYYMMDD-HHMMSS.tar.gz` +2. Exécute `export-mining-wallet.sh` → crée `backups/mining-wallet-export-YYYYMMDD-HHMMSS.json` +3. Clone ou pull le dépôt https://git.4nkweb.com/4nk/backup dans `$HOME/.4nk-backup-git` (ou `$BACKUP_GIT_WORKSPACE`) +4. Copie les derniers fichiers dans `signet-datadir/` et `mining-wallet/` +5. Garde seulement 2 versions de signet-datadir et 2 de mining-wallet (supprime les plus anciennes) +6. Commit et push vers le dépôt + +### Structure dans le dépôt backup + +``` +4nk/backup/ +├── signet-datadir/ +│ ├── signet-datadir-YYYYMMDD-HHMMSS.tar.gz (max 2) +│ └── signet-datadir-YYYYMMDD-HHMMSS.tar.gz +└── mining-wallet/ + ├── mining-wallet-export-YYYYMMDD-HHMMSS.json (max 2) + └── mining-wallet-export-YYYYMMDD-HHMMSS.json +``` + +### Prérequis + +- Docker (conteneur `bitcoin-signet-instance` en cours d’exécution pour les scripts de backup) +- Git configuré avec accès push sans mot de passe (clés SSH vers git@git.4nkweb.com:4nk/backup.git) +- Dépôt backup créé au préalable sur https://git.4nkweb.com/4nk/backup (peut être vide) +- **Sécurité** : le dépôt backup contient des clés privées (mining wallet). Le rendre privé sur git.4nkweb.com. + +## Modifications + +**Fichiers créés :** + +- `data/backup-to-git-cron.sh` : script d’export quotidien +- `features/backup-to-git-daily-cron.md` : cette documentation + +## Modalités de déploiement + +1. Créer le dépôt `4nk/backup` sur https://git.4nkweb.com/4nk/backup si nécessaire. +2. Configurer l’accès git (credential helper, clé, token) pour push sans interaction. +3. Rendre le script exécutable : `chmod +x data/backup-to-git-cron.sh` +4. Ajouter une entrée cron quotidienne (ex. 5h00, après restart-services si applicable) : + ```text + 0 5 * * * /home/ncantu/Bureau/code/bitcoin/data/backup-to-git-cron.sh + ``` +5. Tester manuellement : `./data/backup-to-git-cron.sh` + +### Variables d'environnement (optionnel) + +- `BACKUP_GIT_WORKSPACE` : chemin du clone local du dépôt backup (défaut : `$HOME/.4nk-backup-git`) + +## Modalités d’analyse + +- Consulter `data/backup-to-git.log` pour les exécutions et erreurs. +- Vérifier le dépôt https://git.4nkweb.com/4nk/backup pour les commits. +- En cas d’erreur `git clone` : créer le dépôt sur git.4nkweb.com. +- En cas d’erreur `git push` : vérifier credential helper / accès réseau. diff --git a/features/services-ecoute-ipv4-proxy.md b/features/services-ecoute-ipv4-proxy.md new file mode 100644 index 0000000..226f883 --- /dev/null +++ b/features/services-ecoute-ipv4-proxy.md @@ -0,0 +1,60 @@ +# Écoute des services : IPv4 uniquement, flux reçus du proxy 192.168.1.100 + +**Auteur** : Équipe 4NK +**Date** : 2026-02-02 +**Version** : 1.0 + +## Objectif + +Les services backend (APIs, dashboard, etc.) doivent : + +1. **Écouter en IPv4 uniquement** : aucune écoute sur IPv6 (`[::]`). +2. **Accepter les flux reçus de la machine proxy uniquement** : les connexions entrantes doivent provenir du proxy (192.168.1.100). + +## Impacts + +- **Sécurité** : réduction de la surface d’exposition (pas d’IPv6, pas d’accès direct depuis d’autres machines que le proxy). +- **Cohérence** : tous les accès passent par le proxy (HTTPS, certificats, routage). +- **Environnements concernés** : machine bitcoin (192.168.1.105), machine prod (192.168.1.103), et tout backend recevant du trafic du proxy. + +## Règles d’écoute + +### 1. Bind sur l’adresse IPv4 de la machine + +Chaque service écoute sur l’adresse LAN IPv4 de la machine où il tourne, et non sur `0.0.0.0` : + +- **Machine bitcoin (192.168.1.105)** : `DASHBOARD_HOST=192.168.1.105`, `API_HOST=192.168.1.105` (dashboard, api-anchorage, etc.). +- **Machine prod (192.168.1.103)** : `FAUCET_API_HOST=192.168.1.103`, `WATERMARK_API_HOST=192.168.1.103`, `CLAMAV_API_HOST=192.168.1.103`, etc. + +Cela garantit une écoute IPv4 uniquement (pas d’écoute sur `[::]`). + +### 2. Restriction à la source proxy (192.168.1.100) + +Si la variable d’environnement `ALLOWED_SOURCE_IP=192.168.1.100` est définie, le service rejette toute requête dont l’adresse source (après normalisation IPv6-mapped → IPv4) n’est pas 192.168.1.100. + +- **Middleware** : chaque service applicatif (Node/Express) peut utiliser un middleware qui lit `req.socket.remoteAddress`, normalise (ex. `::ffff:192.168.1.100` → `192.168.1.100`) et compare à `ALLOWED_SOURCE_IP`. Si différent, réponse 403. +- **Alternative** : firewall sur chaque machine (autoriser uniquement 192.168.1.100 sur les ports des services). À documenter côté infra. + +## Modifications + +- **Fichiers de service systemd** : `DASHBOARD_HOST`, `API_HOST`, `FAUCET_API_HOST`, `WATERMARK_API_HOST`, `CLAMAV_API_HOST` définis à l’IP LAN de la machine ; `ALLOWED_SOURCE_IP=192.168.1.100` ajouté. +- **Code** : middleware optionnel (activé si `ALLOWED_SOURCE_IP` est défini) dans signet-dashboard, api-anchorage, api-faucet, api-filigrane, api-clamav pour rejeter les requêtes dont la source n’est pas le proxy. +- **Documentation** : `docs/ENVIRONMENT.md`, `docs/DOMAINS_AND_PORTS.md` mis à jour pour décrire cette règle et les variables. + +## Nginx / Mempool + +- **Mempool** : si un Nginx local expose le frontend (ex. port 3015), les directives `listen` doivent être en IPv4 uniquement (ex. `listen 192.168.1.105:3015;` ou `listen 0.0.0.0:3015;` sans `listen [::]:3015`). +- **Proxy (192.168.1.100)** : inchangé ; c’est lui qui envoie les flux vers les backends. + +## Modalités de déploiement + +1. Mettre à jour les fichiers `.service` avec les bonnes valeurs `*_HOST` et `ALLOWED_SOURCE_IP=192.168.1.100`. +2. Redémarrer les services après déploiement des unités systemd. +3. Vérifier que le proxy peut joindre les backends (192.168.1.105 et 192.168.1.103) sur les ports concernés. +4. Ne pas définir `ALLOWED_SOURCE_IP` en dev local si les requêtes ne viennent pas du proxy. + +## Modalités d’analyse + +- Vérifier qu’aucun service n’écoute sur `[::]` : `ss -tlnp` / `netstat -tlnp` sur chaque machine. +- Vérifier que les services écoutent sur l’IP LAN attendue : `ss -tlnp | grep `. +- Tester depuis le proxy : `curl http://192.168.1.105:3020/...` et depuis une autre machine : doit être refusé si firewall ou middleware est actif. diff --git a/fix-dashboard-anchor-chain.sh b/fix-dashboard-anchor-chain.sh new file mode 100755 index 0000000..ac283e5 --- /dev/null +++ b/fix-dashboard-anchor-chain.sh @@ -0,0 +1,91 @@ +#!/bin/bash +# +# Fix dashboard wrong chain and anchor API "Insufficient Balance". +# Run on the bitcoin machine (192.168.1.105) from the project root. +# +# Usage: +# ./fix-dashboard-anchor-chain.sh +# Verify, restart Dashboard and Anchor API, run alignment check. +# ./fix-dashboard-anchor-chain.sh backups/signet-datadir-YYYYMMDD-HHMMSS.tar.gz +# Restore chain from backup, then verify and restart services. +# +# Author: 4NK Team +# Date: 2026-02-02 + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +if [[ ! -f .env ]]; then + echo "Error: .env not found. Run from project root: $SCRIPT_DIR" + exit 1 +fi + +BACKUP_PATH="${1:-}" + +echo "=== Fix Dashboard / Anchor API chain and balance ===" +echo "Project root: $SCRIPT_DIR" +echo "" + +echo ">>> Testing RPC config (same node as Mempool: 127.0.0.1:38332)..." +if ! ./test-mempool-rpc-config.sh 127.0.0.1 38332; then + echo "Error: Node at 127.0.0.1:38332 does not have ~11535 blocks." + echo "Run this script on the machine where Mempool runs (bitcoin 192.168.1.105)." + exit 1 +fi +echo "" + +if [[ -n "$BACKUP_PATH" ]]; then + if [[ ! -f "$BACKUP_PATH" ]]; then + echo "Error: Backup file not found: $BACKUP_PATH" + exit 1 + fi + echo ">>> Restoring chain from backup: $BACKUP_PATH" + ./restore-signet-from-backup.sh "$BACKUP_PATH" + echo "" +fi + +echo ">>> Checking Bitcoin Signet container..." +CONTAINER_NAME="bitcoin-signet-instance" +if ! docker ps --format "{{.Names}}" | grep -q "^${CONTAINER_NAME}$"; then + echo "Error: Container $CONTAINER_NAME is not running." + echo "Start it with the persistent volume (see docs/MAINTENANCE.md)." + exit 1 +fi +echo "Container is running." +echo "" + +echo ">>> Restarting signet-dashboard and anchorage-api..." +if systemctl is-active --quiet signet-dashboard 2>/dev/null; then + sudo systemctl restart signet-dashboard + echo "signet-dashboard restarted." +else + echo "signet-dashboard not managed by systemctl (skip restart)." +fi +if systemctl is-active --quiet anchorage-api 2>/dev/null; then + sudo systemctl restart anchorage-api + echo "anchorage-api restarted." +else + echo "anchorage-api not managed by systemctl (skip restart)." +fi +echo "" + +echo ">>> Waiting 5s for services to start..." +sleep 5 +echo "" + +echo ">>> Running chain alignment check..." +./verify-chain-alignment.sh +echo "" + +echo ">>> Node wallet info (check balance):" +BITCOIN_DATADIR=$(docker exec "$CONTAINER_NAME" printenv BITCOIN_DIR 2>/dev/null || echo "/root/.bitcoin") +sudo docker exec "$CONTAINER_NAME" bitcoin-cli -datadir="$BITCOIN_DATADIR" getwalletinfo 2>/dev/null | grep -E '"walletname"|"balance"|"immature_balance"' || echo "(could not get walletinfo)" +echo "" + +echo "Done. If the dashboard still shows wrong chain or anchor API returns Insufficient Balance:" +echo " - Ensure Dashboard and Anchor API run on the bitcoin machine (192.168.1.105)." +echo " - If chain was lost, restore from backup: ./fix-dashboard-anchor-chain.sh backups/signet-datadir-YYYYMMDD-HHMMSS.tar.gz" +echo " - See fixKnowledge/dashboard-anchor-wrong-chain-insufficient-balance.md" +echo "" diff --git a/fixKnowledge/api-anchorage-health-503-remediation.md b/fixKnowledge/api-anchorage-health-503-remediation.md new file mode 100644 index 0000000..aeb2f9c --- /dev/null +++ b/fixKnowledge/api-anchorage-health-503-remediation.md @@ -0,0 +1,82 @@ +# Correction: Health check anchor-api HTTP 503 – causes et remédiation + +**Date:** 2026-01-28 +**Auteur:** Équipe 4NK + +## Problème + +Le health check de l’API d’ancrage renvoie HTTP 503, avec des messages du type « Health check a échoué (HTTP 503) » ou « L’API d’ancrage n’est pas accessible », sans indiquer la cause précise. + +**Machine concernée :** 192.168.1.105 (bitcoin) +**Domaine externe :** https://anchorage.certificator.4nkweb.com +**Service :** anchorage-api (port 3010) + +### Impact + +- Monitoring ou dashboard marquent l’API comme indisponible sans distinguer « processus arrêté » et « pas prêt » (Bitcoin déconnecté, UTXOs verrouillés). +- L’utilisateur ne voit pas la raison du 503 (Bitcoin, mutex, UTXOs stale) pour agir. + +## Root cause + +1. **Causes du 503 côté api-anchorage** (`src/routes/health.js`) : + - **GET /health** : 503 si Bitcoin RPC non connecté ou si `checkConnection()` lève. + - **GET /health/detailed** : 503 si Bitcoin non connecté, ou UTXOs verrouillés depuis > 10 min (`stale_locks > 0`), ou plus de 10 UTXOs verrouillés. + +2. **Causes côté consommateur** : + - Le dashboard proxy renvoyait 200 avec un body réduit au lieu de transmettre le 503 et le body complet → le client ne voyait pas la raison. + - Aucun endpoint de liveness (processus vivant) → impossible de distinguer « service down » et « service up mais not ready ». + +## Correctifs + +### 1. api-anchorage : endpoint de liveness + +- **GET /health/live** : retourne toujours 200 tant que le processus tourne (sans vérifier Bitcoin ni mutex). +- Usage : monitoring / load-balancer pour ne pas considérer le service comme mort quand il renvoie 503 (readiness). +- Exclusion de l’auth API Key pour `/health/live` dans `server.js`. + +### 2. signet-dashboard : propagation du 503 et du body + +- Option **preserveStatus** dans `makeHttpRequest` : retourne `{ statusCode, body }` pour préserver le code HTTP et le body JSON. +- Route **GET /api/anchor/health/detailed** : appelle l’API d’ancrage avec `preserveStatus: true`, puis répond avec `res.status(result.statusCode).json(result.body)`. +- Effet : en cas de 503, le client reçoit 503 et le détail (bitcoin.connected, mutex, utxos, stale_locks). + +### 3. signet-dashboard (hash-list.html) : affichage de la raison + +- Si la réponse est 503 et que le body n’a pas la structure health (mutex, utxos, bitcoin), affichage d’un message d’erreur incluant `health.error` ou `health.message` et le code 503. +- Si la réponse est 503 mais que le body contient le health complet, affichage du panneau health normal avec mention « 503 - voir détails ci-dessous » pour l’état général. +- En cas d’exception (réseau, parse), affichage de « L’API d’ancrage n’est pas accessible » avec le message d’erreur. + +## Modifications + +**Motivations :** +- Rendre le 503 explicable (Bitcoin, mutex, UTXOs) et permettre une remédiation ciblée. +- Séparer liveness (processus up) et readiness (prêt à ancrer). + +**Root causes :** +- 503 défini par l’API sans toujours être propagé avec son body ; pas de liveness. + +**Correctifs :** +- GET /health/live dans api-anchorage ; propagation 503 + body dans le dashboard ; affichage de la raison dans hash-list. + +**Pages affectées :** +- `api-anchorage/src/routes/health.js` (GET /health/live) +- `api-anchorage/src/server.js` (exclusion auth /health/live) +- `signet-dashboard/src/server.js` (preserveStatus, route health/detailed) +- `signet-dashboard/public/hash-list.html` (affichage erreur 503 et raison) + +## Modalités de remédiation en production + +Quand le health check renvoie 503 : + +1. **Vérifier liveness** : `curl -s https://anchorage.certificator.4nkweb.com/health/live` → 200 si le processus tourne. +2. **Lire la cause** : `curl -s https://anchorage.certificator.4nkweb.com/health/detailed` (ou via le dashboard) et regarder : + - `bitcoin.connected === false` → vérifier Bitcoin Core (RPC) sur 192.168.1.105. + - `utxos.stale_locks > 0` ou `utxos.locked > 10` → déverrouiller les UTXOs (voir ci-dessous). +3. **Service arrêté** : sur la machine 192.168.1.105, `systemctl status anchorage-api` puis `sudo systemctl restart anchorage-api` si besoin. +4. **UTXOs verrouillés** : sur la machine hébergeant api-anchorage, exécuter `node unlock-utxos.mjs` dans le répertoire api-anchorage, ou utiliser le bouton « Déverrouiller les UTXOs » depuis le dashboard (hash-list) si l’API est de nouveau joignable. + +## Modalités d’analyse + +- Consulter les logs du service : `journalctl -u anchorage-api -n 100`. +- Vérifier le RPC Bitcoin : `bitcoin-cli -rpcconnect=... getblockchaininfo`. +- Appeler GET /health/detailed et interpréter `ok`, `bitcoin.connected`, `utxos.locked`, `utxos.stale_locks`. diff --git a/fixKnowledge/dashboard-anchor-wrong-chain-insufficient-balance.md b/fixKnowledge/dashboard-anchor-wrong-chain-insufficient-balance.md new file mode 100644 index 0000000..171c3d9 --- /dev/null +++ b/fixKnowledge/dashboard-anchor-wrong-chain-insufficient-balance.md @@ -0,0 +1,124 @@ +# Dashboard mauvaise chaîne / API d'ancrage "Insufficient Balance" + +**Auteur** : Équipe 4NK +**Date** : 2026-02-02 +**Version** : 1.0 + +## Symptômes + +- **Dashboard** (https://dashboard.certificator.4nkweb.com/) : affiche une mauvaise chaîne (hauteur 0, "-", ou valeurs incohérentes) au lieu d’environ 11535 blocs. +- **Clients de l’API d’ancrage** : reçoivent `{"error":"Insufficient Balance","message":"Insufficient balance. Required: 0.00001 BTC, Available: 0 BTC"}` et l’API d’ancrage ne fonctionne pas correctement. + +## Impacts + +- Les utilisateurs ne voient pas l’état réel de la blockchain. +- L’ancrage de documents échoue (solde 0 côté nœud utilisé par l’API). + +## Cause + +Une seule cause racine couvre les deux symptômes : **le nœud Bitcoin Signet auquel se connectent le Dashboard et l’API d’ancrage n’a pas la bonne chaîne ou n’a pas de solde**. + +Causes possibles : + +1. **Chaîne perdue** : le conteneur `bitcoin-signet-instance` a été recréé **sans volume persistant** (`-v signet-bitcoin-data:/root/.bitcoin`). Le nœud repart sur une nouvelle chaîne (hauteur 0 ou très basse), sans historique de mining → solde 0. Voir [signet-chain-lost-volume-persistent.md](./signet-chain-lost-volume-persistent.md). +2. **Mauvais déploiement** : le Dashboard et/ou l’API d’ancrage tournent sur une **autre machine** (ex. prod 192.168.1.103). Avec `BITCOIN_RPC_HOST=127.0.0.1`, ils appellent alors le RPC de cette machine, où il n’y a pas de nœud Signet (ou un nœud vide) → hauteur 0 ou erreur, solde 0. +3. **Wallet par défaut** : le nœud a la bonne chaîne mais le **wallet par défaut** utilisé par l’API n’est pas celui qui reçoit les récompenses de minage (`custom_signet`) → `getBalance()` retourne 0. + +## Correctifs + +### 0. Script de correction (machine bitcoin) + +Sur la machine bitcoin (192.168.1.105), à la racine du projet : + +```bash +cd /home/ncantu/Bureau/code/bitcoin + +# Vérifier, redémarrer Dashboard et API d’ancrage, lancer la vérification d’alignement +./fix-dashboard-anchor-chain.sh + +# Si la chaîne a été perdue, restaurer depuis une sauvegarde puis redémarrer +./fix-dashboard-anchor-chain.sh backups/signet-datadir-YYYYMMDD-HHMMSS.tar.gz +``` + +Le script teste d'abord la config RPC (même nœud que Mempool) avec `./test-mempool-rpc-config.sh 127.0.0.1 38332`, puis redémarre `signet-dashboard` et `anchorage-api`, lance `verify-chain-alignment.sh`, et affiche le wallet du nœud (solde). + +**Configuration unique (une seule chaîne pour Mempool, dashboard, APIs, miner) :** Un seul nœud : **bitcoin-signet-instance** sur **38332** (Mempool = host.docker.internal:38332, dashboard/APIs/miner = 127.0.0.1:38332). **Volume par défaut (chaîne complète) :** `update-signet.sh` utilise par défaut le volume contenant la chaîne Signet complète (~11530 blocs) s'il existe : volume Docker d'ID **4b5dca4d940b9f6e5db67b460f40f230a5ef1195a3769e5f91fa02be6edde649** (`SIGNET_VOLUME_FULL_CHAIN` dans le script). Sinon, volume nommé **signet-bitcoin-data**. **Sauvegarde prête à télécharger :** `backups/signet-datadir-latest.tar.gz` (symlink vers la dernière archive créée par `./save-signet-datadir-backup.sh`). **Alignement :** Même machine : `BITCOIN_RPC_HOST=127.0.0.1`, `BITCOIN_RPC_PORT=38332`. Le seul processus sur 38332 doit être le conteneur **bitcoin-signet-instance** (Mempool utilise `host.docker.internal:38332` = ce même conteneur). Vérifier qu’il ne s’agit pas d’un autre Docker : `ss -tlnp | grep 38332` et `docker ps --format '{{.Names}}' | grep bitcoin-signet-instance`. Le miner utilise `BITCOIN_RPC_HOST` / `BITCOIN_RPC_PORT` en env (défaut 127.0.0.1:38332). Tester : `./test-mempool-rpc-config.sh 127.0.0.1 38332`. Vérifier le dashboard : `./verify-dashboard-signet.sh`. + +### 1. Vérifier où tournent le Dashboard et l’API d’ancrage + +- **Dashboard** : doit être sur la **machine bitcoin (192.168.1.105)**. Vérifier le service `signet-dashboard` sur cette machine. +- **API d’ancrage** (`anchorage.certificator.4nkweb.com`) : doit être sur la **machine bitcoin (192.168.1.105)**. Vérifier le service `anchorage-api` sur cette machine. + +Les deux doivent utiliser `BITCOIN_RPC_HOST=127.0.0.1` et `BITCOIN_RPC_PORT=38332` pour parler au nœud local (conteneur sur la même machine). + +### 2. Source de vérité : Mempool et utilisateur ncantu + +**Mempool** (machine bitcoin 192.168.1.105, `/srv/4NK/mempool.4nkweb.com`) se connecte au nœud Signet du même hôte (`host.docker.internal:38332`). Si Mempool affiche la bonne chaîne (~11535 blocs), le nœud utilisé par Mempool sur cette machine a encore la chaîne complète. + +**Où chercher la chaîne / les sauvegardes (utilisateur ncantu, machine bitcoin 105) :** + +- **Sauvegarde prête à télécharger** : `backups/signet-datadir-latest.tar.gz` (dernière archive datadir, ~11530 blocs). Créée par `./save-signet-datadir-backup.sh` ; le script met à jour le symlink à chaque sauvegarde. +- **Sauvegardes horodatées** : `backups/signet-datadir-YYYYMMDD-HHMMSS.tar.gz`. +- **Volume Docker « chaîne complète »** : volume d'ID `4b5dca4d940b9f6e5db67b460f40f230a5ef1195a3769e5f91fa02be6edde649` ; `update-signet.sh` l'utilise par défaut s'il existe (voir docs/MAINTENANCE.md). +- **Volume nommé** : après restauration via `restore-signet-from-backup.sh`, le conteneur utilise `signet-bitcoin-data`. Vérifier avec `docker inspect bitcoin-signet-instance` (Mounts). +- **Datadir dans le conteneur** : si le conteneur sur 105 n’a jamais été recréé sans volume, les blocs sont dans le conteneur ; faire une sauvegarde avec `save-signet-datadir-backup.sh` ou `docker exec bitcoin-signet-instance tar czf /tmp/bitcoin-backup.tar.gz /root/.bitcoin/` puis `docker cp` vers l’hôte. + +Pour corriger la machine dont le nœud n’a que quelques blocs (ex. 6) : soit **restaurer** depuis une archive issue de 105 ou de `backups/` sous ncantu, soit **pointer** le Dashboard / l’API d’ancrage vers le RPC du nœud sur 105 (ex. `BITCOIN_RPC_HOST=192.168.1.105`) si l’architecture le permet. + +### 3. Restaurer la chaîne si elle a été perdue + +Si le nœud a une hauteur très basse (ex. 0, 6) ou pas de volume persistant : + +1. Sur la machine qui a encore la chaîne ~11535 (ex. machine bitcoin 105, ou là où Mempool affiche la bonne chaîne) : exécuter `./save-signet-datadir-backup.sh`, ou récupérer une archive depuis `/home/ncantu/Bureau/code/bitcoin/backups/` (utilisateur ncantu). +2. Copier l’archive sur la machine à corriger si besoin. +3. Sur la machine à corriger : exécuter `./restore-signet-from-backup.sh backups/signet-datadir-YYYYMMDD-HHMMSS.tar.gz`. +4. Redémarrer le conteneur si nécessaire et vérifier : `sudo docker exec bitcoin-signet-instance bitcoin-cli -datadir=$(docker exec bitcoin-signet-instance printenv BITCOIN_DIR 2>/dev/null || echo /root/.bitcoin) getblockchaininfo`. + +Voir [signet-chain-lost-volume-persistent.md](./signet-chain-lost-volume-persistent.md) et [MAINTENANCE.md](../docs/MAINTENANCE.md). + +### 4. Vérifier l’alignement chaîne / solde + +Sur la machine bitcoin : + +```bash +./verify-chain-alignment.sh +sudo docker exec bitcoin-signet-instance bitcoin-cli -datadir=/root/.bitcoin getblockchaininfo | grep -E '"chain"|"blocks"' +sudo docker exec bitcoin-signet-instance bitcoin-cli -datadir=/root/.bitcoin getwalletinfo +``` + +- `chain` doit être `"signet"`, `blocks` proche de 11535. +- Le wallet utilisé par défaut (ex. `custom_signet`) doit avoir un solde > 0 après mining. + +### 5. Si la chaîne est bonne mais le solde reste 0 + +Vérifier que le wallet contenant les récompenses de minage est bien celui utilisé par l’API : + +- Le miner utilise en général le wallet `custom_signet`. +- L’API d’ancrage appelle `getBalance()` sans nom de wallet → utilise le **wallet par défaut** du nœud. +- Si le nœud a plusieurs wallets, s’assurer que le wallet par défaut est celui qui a du solde (ou charger `custom_signet` au démarrage du nœud comme wallet par défaut). + +## Modalités de déploiement + +- Sur la machine bitcoin : exécuter `./fix-dashboard-anchor-chain.sh` (avec chemin de backup si la chaîne a été perdue). Le script redémarre `signet-dashboard` et `anchorage-api`. +- Recréer le conteneur Bitcoin via `./update-signet.sh` : le script utilise par défaut le volume chaîne complète (ID `4b5dca4d940b9f6e5db67b460f40f230a5ef1195a3769e5f91fa02be6edde649`) s'il existe, sinon `signet-bitcoin-data`. Ne pas recréer manuellement sans volume persistant. +- Sauvegarde prête à télécharger : `backups/signet-datadir-latest.tar.gz` (créée par `./save-signet-datadir-backup.sh`). + +## Modalités d’analyse + +- Consulter les logs du Dashboard : `sudo journalctl -u signet-dashboard -f` (erreurs RPC). +- Consulter les logs de l’API d’ancrage : `sudo journalctl -u anchorage-api -f` (erreurs "Insufficient balance", connexion RPC). +- Vérifier la hauteur et le wallet sur le nœud : commandes ci-dessus. + +## Pages affectées + +- fixKnowledge/dashboard-anchor-wrong-chain-insufficient-balance.md (ce fichier) +- fixKnowledge/signet-chain-lost-volume-persistent.md (volume chaîne complète, sauvegarde latest) +- update-signet.sh (SIGNET_VOLUME_FULL_CHAIN, utilisation par défaut du volume chaîne complète) +- save-signet-datadir-backup.sh (symlink signet-datadir-latest.tar.gz, tolérance tar exit 1) +- backups/README.md (sauvegarde prête à télécharger, volume par défaut) +- docs/MAINTENANCE.md (volume chaîne complète, sauvegarde latest) +- test-mempool-rpc-config.sh (test de la config RPC utilisée par Mempool) +- verify-dashboard-signet.sh (vérification que le dashboard affiche le signet custom) +- fix-dashboard-anchor-chain.sh (script de correction sur la machine bitcoin) +- signet-dashboard.service, anchorage-api.service, faucet-api.service (alignement RPC sur le même nœud que Mempool) +- signet-dashboard/public/app.js (vérification response.ok et alerte chaîne anormale) diff --git a/fixKnowledge/mempool-api-healthcheck-fix.md b/fixKnowledge/mempool-api-healthcheck-fix.md index b04422f..86249c4 100644 --- a/fixKnowledge/mempool-api-healthcheck-fix.md +++ b/fixKnowledge/mempool-api-healthcheck-fix.md @@ -56,7 +56,7 @@ healthcheck: 1. Modifier le fichier `docker-compose.signet.yml` 2. Recréer le conteneur pour appliquer la nouvelle configuration: ```bash - cd /srv/4NK/mempool1.4nkweb.com + cd /srv/4NK/mempool.4nkweb.com docker-compose -f docker-compose.signet.yml up -d --force-recreate api ``` 3. Vérifier que le healthcheck passe à "healthy" après le délai de démarrage (40s) diff --git a/fixKnowledge/mempool-websocket-offline-fix.md b/fixKnowledge/mempool-websocket-offline-fix.md index 452981e..4d327aa 100644 --- a/fixKnowledge/mempool-websocket-offline-fix.md +++ b/fixKnowledge/mempool-websocket-offline-fix.md @@ -26,7 +26,7 @@ ## Evolutions - Création d'un script de diagnostic (`mempool/diagnose-mempool.sh`) pour vérifier l'état des services localement -- Création d'un script de diagnostic et correction (`mempool/fix-mempool-websocket.sh`) pour diagnostiquer et corriger les problèmes sur le serveur services +- Création d'un script de diagnostic et correction (`mempool/fix-mempool-websocket.sh`) pour diagnostiquer et corriger les problèmes sur la machine bitcoin - Amélioration de la configuration nginx pour le WebSocket (ajout des headers nécessaires dans `mempool/nginx-mempool.conf`) ## Pages affectées @@ -37,7 +37,7 @@ ## Modalités de déploiement -1. **Diagnostic local** : +1. **Diagnostic local** (depuis la racine du projet `/home/ncantu/Bureau/code/bitcoin/`) : ```bash cd mempool ./diagnose-mempool.sh diff --git a/fixKnowledge/signet-chain-lost-volume-persistent.md b/fixKnowledge/signet-chain-lost-volume-persistent.md new file mode 100644 index 0000000..8503d07 --- /dev/null +++ b/fixKnowledge/signet-chain-lost-volume-persistent.md @@ -0,0 +1,77 @@ +# Chaîne perdue après recréation du conteneur (hauteur 2 au lieu de ~11535) + +**Date** : 2026-02-02 +**Auteur** : Équipe 4NK + +## Problème + +- Le dashboard affiche une **hauteur de bloc 2** alors que la chaîne attendue est autour de **11535**. +- La chaîne visible n’est pas la bonne : le nœud est sur une **nouvelle chaîne** (genèse récente), pas sur la chaîne signet existante. + +## Cause racine + +- Le conteneur Docker `bitcoin-signet-instance` a été **recréé** (`docker stop` + `docker rm` + `docker run`) **sans montage persistant** du datadir Bitcoin (`/root/.bitcoin`). +- Le Dockerfile déclare `VOLUME $BITCOIN_DIR` ; en l’absence de `-v` dans `docker run`, Docker utilise un **volume anonyme** lié au conteneur. +- À la suppression du conteneur (`docker rm`), ce volume anonyme peut être supprimé (ou n’est plus rattaché), donc les **données de la chaîne (blocs, wallet, etc.) sont perdues**. +- Le nouveau conteneur repart avec un datadir vide : install/signet crée une **nouvelle chaîne** (hauteur 0), puis le miner produit quelques blocs (ex. hauteur 2). + +## Correctifs + +1. **Documentation** : Toutes les commandes `docker run` du projet ont été mises à jour pour utiliser un **volume nommé** : + ```bash + -v signet-bitcoin-data:/root/.bitcoin + ``` + Ainsi, à chaque recréation du conteneur, les données restent dans le volume `signet-bitcoin-data`. + +2. **Persistance** : Utiliser **toujours** soit : + - le volume « chaîne complète » (s'il existe) : `update-signet.sh` utilise par défaut le volume d'ID **4b5dca4d940b9f6e5db67b460f40f230a5ef1195a3769e5f91fa02be6edde649** (~11530 blocs), soit + - un volume nommé : `-v signet-bitcoin-data:/root/.bitcoin`, soit + - un montage host : `-v /chemin/hote/signet-data:/root/.bitcoin` + pour tout démarrage ou recréation du conteneur Bitcoin Signet. Voir docs/MAINTENANCE.md. + +## Récupérer la chaîne ~11535 (reprendre sur la chaîne précédente) + +1. **Obtenir une sauvegarde complète du datadir** (blocs + chainstate + config) : + - **Sauvegarde prête à télécharger** : `backups/signet-datadir-latest.tar.gz` (symlink vers la dernière archive créée par `./save-signet-datadir-backup.sh`). + - **Sur la machine qui a encore la chaîne** : exécuter `./save-signet-datadir-backup.sh` pour créer `backups/signet-datadir-YYYYMMDD-HHMMSS.tar.gz` et mettre à jour le symlink `signet-datadir-latest.tar.gz`. + - Ou utiliser une archive existante (ex. `bitcoin-backup-*.tar.gz` créée avec `docker exec ... tar czf /tmp/bitcoin-backup.tar.gz /root/.bitcoin`). + +2. **Sur la machine où reprendre la chaîne** : placer l’archive dans le projet (ex. `backups/`) puis lancer : + ```bash + ./restore-signet-from-backup.sh backups/signet-datadir-YYYYMMDD-HHMMSS.tar.gz + ``` + Le script arrête le conteneur actuel, remplit le volume nommé `signet-bitcoin-data` avec les données restaurées, puis redémarre le conteneur avec ce volume. + +3. **Si aucune sauvegarde n’existe** : la chaîne à 11535 n’est plus récupérable sur ce nœud. Il faut qu’un autre nœud (ex. machine bitcoin) ait encore cette chaîne et qu’on en tire une sauvegarde avec `save-signet-datadir-backup.sh`, puis qu’on restaure avec `restore-signet-from-backup.sh`. + +## Modifications + +- **docs/MAINTENANCE.md** : section « Persistance du datadir », et ajout de `-v signet-bitcoin-data:/root/.bitcoin` à toutes les commandes `docker run`. +- **docs/README.md** : idem + note sur le volume. +- **docs/INTERFACES.md** : idem. +- **docs/INSTALLATION_NEW_NODE.md** : idem (premier démarrage et recréation). +- **docs/TROUBLESHOOTING_MINING.md** : idem. + +## Modalités d’analyse + +- Vérifier la hauteur et la chaîne : + `sudo docker exec bitcoin-signet-instance bitcoin-cli -datadir=/root/.bitcoin getblockchaininfo` +- Vérifier les volumes Docker : + `docker volume ls` (présence de `signet-bitcoin-data` si déjà utilisé). +- En cas de hauteur très basse (0, 1, 2…) après une recréation récente du conteneur : confirmer si un volume persistant était utilisé. + +## Scripts ajoutés + +- **save-signet-datadir-backup.sh** : crée une archive complète du datadir depuis le conteneur en cours. À exécuter sur la machine qui a encore la chaîne (ex. machine bitcoin). +- **restore-signet-from-backup.sh** : restaure une archive datadir dans le volume `signet-bitcoin-data` et redémarre le conteneur. Usage : `./restore-signet-from-backup.sh `. + +## Pages affectées + +- docs/MAINTENANCE.md +- docs/README.md +- docs/INTERFACES.md +- docs/INSTALLATION_NEW_NODE.md +- docs/TROUBLESHOOTING_MINING.md +- save-signet-datadir-backup.sh (nouveau) +- restore-signet-from-backup.sh (nouveau) +- fixKnowledge/signet-chain-lost-volume-persistent.md (ce fichier) diff --git a/import-mining-wallet.sh b/import-mining-wallet.sh new file mode 100755 index 0000000..7ddd679 --- /dev/null +++ b/import-mining-wallet.sh @@ -0,0 +1,99 @@ +#!/bin/bash +# +# Re-import a mining wallet export (created by export-mining-wallet.sh). +# Use on a node that has the same Signet chain (same SIGNETCHALLENGE) to recover funds. +# +# Usage: ./import-mining-wallet.sh [container_name] +# Example: ./import-mining-wallet.sh backups/mining-wallet-export-latest.json +# +# Prerequisite: bitcoin-signet-instance (or given container) running with the same chain. +# +# Author: 4NK Team +# Date: 2026-02-02 + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" +EXPORT_PATH="${1:?Usage: $0 [container_name]}" +CONTAINER_NAME="${2:-bitcoin-signet-instance}" + +if [[ ! -f "$EXPORT_PATH" ]]; then + echo "Error: Export file not found: $EXPORT_PATH" + exit 1 +fi + +if ! docker ps --format "{{.Names}}" | grep -q "^${CONTAINER_NAME}$"; then + echo "Error: Container ${CONTAINER_NAME} is not running" + exit 1 +fi + +DATADIR=$(docker exec "$CONTAINER_NAME" printenv BITCOIN_DIR 2>/dev/null || echo "/root/.bitcoin") + +echo "=== Import mining wallet ===" +echo "Export file: $EXPORT_PATH" +echo "Container: $CONTAINER_NAME" +echo "" + +SIGNET_EXPORT=$(jq -r '.signet_challenge' "$EXPORT_PATH") +WALLET_NAME=$(jq -r '.wallet_name' "$EXPORT_PATH") +PRIVKEY_EXPORT=$(jq -r '.privkey' "$EXPORT_PATH") + +if [[ -z "$WALLET_NAME" ]] || [[ "$WALLET_NAME" == "null" ]]; then + echo "Error: Invalid export file (missing wallet_name)" + exit 1 +fi + +# Verify we are on the same chain (SIGNETCHALLENGE must match) +SIGNET_NODE=$(sudo docker exec "$CONTAINER_NAME" cat "$DATADIR/SIGNETCHALLENGE.txt" 2>/dev/null || echo "") +if [[ -n "$SIGNET_NODE" ]] && [[ -n "$SIGNET_EXPORT" ]] && [[ "$SIGNET_EXPORT" != "null" ]]; then + if [[ "$SIGNET_NODE" != "$SIGNET_EXPORT" ]]; then + echo "Error: SIGNETCHALLENGE mismatch. Export is for another chain." + echo " Node: ${SIGNET_NODE:0:20}..." + echo " Export: ${SIGNET_EXPORT:0:20}..." + exit 1 + fi + echo "SIGNETCHALLENGE matches (same chain)" +fi + +# Create wallet if it does not exist +if ! sudo docker exec "$CONTAINER_NAME" bitcoin-cli -datadir="$DATADIR" listwallets 2>/dev/null | grep -q "\"$WALLET_NAME\""; then + echo "Creating wallet: $WALLET_NAME" + sudo docker exec "$CONTAINER_NAME" bitcoin-cli -datadir="$DATADIR" -named createwallet \ + wallet_name="$WALLET_NAME" load_on_startup=true descriptors=true 2>/dev/null || true +fi + +# Load wallet if not loaded +if ! sudo docker exec "$CONTAINER_NAME" bitcoin-cli -datadir="$DATADIR" listwallets 2>/dev/null | grep -q "\"$WALLET_NAME\""; then + echo "Loading wallet: $WALLET_NAME" + sudo docker exec "$CONTAINER_NAME" bitcoin-cli -datadir="$DATADIR" loadwallet "$WALLET_NAME" 2>/dev/null || true +fi + +# Import descriptors (with private keys). Pass JSON via temp file to avoid shell escaping issues. +DESCRIPTORS_JSON=$(jq -c '.descriptors' "$EXPORT_PATH") +if [[ -z "$DESCRIPTORS_JSON" ]] || [[ "$DESCRIPTORS_JSON" == "null" ]] || [[ "$DESCRIPTORS_JSON" == "[]" ]]; then + echo "Warning: No descriptors in export. Importing PRIVKEY as pk() descriptor if present." + if [[ -n "$PRIVKEY_EXPORT" ]] && [[ "$PRIVKEY_EXPORT" != "null" ]]; then + DESCRIPTOR_INFO=$(sudo docker exec "$CONTAINER_NAME" bitcoin-cli -datadir="$DATADIR" -rpcwallet="$WALLET_NAME" getdescriptorinfo "pk($PRIVKEY_EXPORT)" 2>/dev/null || echo "{}") + CHECKSUM=$(echo "$DESCRIPTOR_INFO" | jq -r '.checksum') + if [[ -n "$CHECKSUM" ]] && [[ "$CHECKSUM" != "null" ]]; then + sudo docker exec "$CONTAINER_NAME" bitcoin-cli -datadir="$DATADIR" -rpcwallet="$WALLET_NAME" importdescriptors \ + "[{\"desc\":\"pk($PRIVKEY_EXPORT)#$CHECKSUM\",\"timestamp\":0,\"internal\":false}]" 2>/dev/null || true + echo "PRIVKEY imported as pk() descriptor" + fi + fi +else + echo "Importing descriptors..." + TMP_JSON=$(mktemp) + echo "$DESCRIPTORS_JSON" > "$TMP_JSON" + sudo docker cp "$TMP_JSON" "$CONTAINER_NAME:/tmp/import-descriptors.json" + sudo docker exec "$CONTAINER_NAME" sh -c "bitcoin-cli -datadir=$DATADIR -rpcwallet=$WALLET_NAME importdescriptors \"\$(cat /tmp/import-descriptors.json)\"" 2>/dev/null || true + sudo docker exec "$CONTAINER_NAME" rm -f /tmp/import-descriptors.json 2>/dev/null || true + rm -f "$TMP_JSON" + echo "Descriptors imported" +fi + +echo "" +echo "Wallet $WALLET_NAME imported. Check balance:" +sudo docker exec "$CONTAINER_NAME" bitcoin-cli -datadir="$DATADIR" -rpcwallet="$WALLET_NAME" getwalletinfo 2>/dev/null | jq '{walletname, balance, txcount}' || true +echo "" diff --git a/mine.sh b/mine.sh index 93efcfb..586aeb5 100644 --- a/mine.sh +++ b/mine.sh @@ -21,7 +21,7 @@ while true; do # Export PRIVKEY to ensure it's available to the miner process export PRIVKEY=${PRIVKEY:-$(cat ~/.bitcoin/PRIVKEY.txt 2>/dev/null || echo "")} # Get block template and pipe it to miner - # Use bitcoin-cli with -datadir but without -rpcwallet for miner (descriptorprocesspsbt is node RPC, not wallet RPC) + # Use bitcoin-cli with -datadir from BITCOIN_DIR (container env) but without -rpcwallet for miner (descriptorprocesspsbt is node RPC, not wallet RPC) bitcoin-cli -rpcwallet=$WALLET getblocktemplate '{"rules": ["segwit", "signet"]}' | \ - miner --cli="bitcoin-cli -datadir=/root/.bitcoin" generate --grind-cmd="bitcoin-util grind" --address=$ADDR --nbits=$NBITS --set-block-time=$(date +%s) + miner --cli="bitcoin-cli -datadir=${BITCOIN_DIR:-/root/.bitcoin}" generate --grind-cmd="bitcoin-util grind" --address=$ADDR --nbits=$NBITS --set-block-time=$(date +%s) done \ No newline at end of file diff --git a/miner b/miner index d4bc797..715c475 100644 --- a/miner +++ b/miner @@ -516,8 +516,10 @@ def do_generate(args): # Use JSON-RPC HTTP directly to avoid command line length limit import urllib.request import urllib.parse - # Extract RPC credentials from bitcoin-cli config or use defaults - rpc_url = "http://127.0.0.1:38332/" # Default signet RPC port + # RPC = same node as Mempool (BITCOIN_RPC_HOST:BITCOIN_RPC_PORT, default 127.0.0.1:38332) + rpc_host = os.environ.get("BITCOIN_RPC_HOST", "127.0.0.1") + rpc_port = os.environ.get("BITCOIN_RPC_PORT", "38332") + rpc_url = "http://%s:%s/" % (rpc_host, rpc_port) rpc_user = "bitcoin" rpc_pass = "bitcoin" # Try to get RPC credentials from environment or config @@ -637,7 +639,10 @@ def do_calibrate(args): return 0 def bitcoin_cli(basecmd, args, **kwargs): - cmd = basecmd + ["-signet"] + args + # When --cli includes -datadir=, the datadir config already selects signet; adding -signet can cause connection failure + if not any("-datadir=" in x for x in basecmd): + basecmd = basecmd + ["-signet"] + cmd = basecmd + args logging.debug("Calling bitcoin-cli: %r", cmd) out = subprocess.run(cmd, stdout=subprocess.PIPE, **kwargs, check=True).stdout if isinstance(out, bytes): diff --git a/miner_imports/docs/README.md b/miner_imports/docs/README.md new file mode 100644 index 0000000..0e50e58 --- /dev/null +++ b/miner_imports/docs/README.md @@ -0,0 +1,76 @@ +# Miner Bitcoin Signet – Documentation + +**Auteur** : Équipe 4NK +**Date** : 2026-02-02 +**Version** : 1.0 + +## Vue d’ensemble + +Le **miner** est un script Python dérivé de `contrib/signet/miner.py` de Bitcoin Core. Il permet de miner des blocs sur une chaîne Bitcoin Signet custom en produisant des blocs signés avec la clé du signet (PRIVKEY) et en respectant le SIGNETCHALLENGE. + +**Emplacement dans le projet :** + +- Racine du projet : `/home/ncantu/Bureau/code/bitcoin/` +- Script principal : `miner` (à la racine du projet) +- Imports et framework de test : `miner_imports/` (ce répertoire et son contenu) +- Script shell de boucle de minage : `mine.sh` (à la racine) +- Point d’entrée conteneur : `run.sh` (démarre bitcoind, importe la clé, lance `mine.sh` si `MINERENABLED=1`) + +## Rôle de `miner_imports/` + +Le répertoire `miner_imports/` contient le **test_framework** utilisé par le script `miner` : + +- **test_framework/** : modules Python (messages, blocktools, script, etc.) issus du framework de tests fonctionnels de Bitcoin Core, nécessaires pour construire et sérialiser blocs, transactions et PSBT signet. +- Le script `miner` ajoute `miner_imports` au `sys.path` et importe depuis `test_framework` (blocktools, messages, script, etc.). + +Aucun exécutable de minage n’est dans `miner_imports/` : l’exécutable est le fichier `miner` à la racine du projet. + +## Invocation du miner + +Dans le conteneur Docker (voir `mine.sh` et `run.sh`) : + +```bash +bitcoin-cli -rpcwallet=custom_signet getblocktemplate '{"rules": ["segwit", "signet"]}' | \ + miner --cli="bitcoin-cli -datadir=/root/.bitcoin" generate --grind-cmd="bitcoin-util grind" \ + --address=$ADDR --nbits=$NBITS --set-block-time=$(date +%s) +``` + +- **--cli** : commande `bitcoin-cli` (avec `-datadir` dans le conteneur). +- **generate** : sous-commande pour miner des blocs (boucle getblocktemplate → signer PSBT signet → grind → submitblock). +- **--grind-cmd** : commande pour le proof-of-work (grind du header). +- **--address** : adresse de récompense de bloc. +- **--nbits** : difficulté cible (ex. `1e0377ae`). +- **--set-block-time** : timestamp du bloc. + +La variable d’environnement **PRIVKEY** doit être définie (exportée par `mine.sh` depuis `.env`) pour que le miner puisse signer le PSBT signet via `descriptorprocesspsbt` / `walletprocesspsbt`. + +## Comportement de `bitcoin-cli` et option `-signet` + +Le miner appelle Bitcoin RPC via une fonction interne qui construisait toujours la commande en ajoutant **-signet** aux arguments de `bitcoin-cli`. Lorsque `--cli="bitcoin-cli -datadir=/root/.bitcoin"` est utilisé, la config du datadir (ex. `bitcoin.conf` avec `signet=1`) définit déjà le réseau signet. Ajouter `-signet` en plus pouvait provoquer un échec de connexion RPC (exit 1). + +**Correctif appliqué (dans le script `miner`)** : ne pas ajouter `-signet` lorsque la commande cli contient déjà `-datadir=`. + +```python +# Quand --cli inclut -datadir=, la config du datadir sélectionne déjà le signet +if not any("-datadir=" in x for x in basecmd): + basecmd = basecmd + ["-signet"] +cmd = basecmd + args +``` + +Référence : correctif documenté dans le dépôt (rechercher « bitcoin_cli » et « -signet » dans le fichier `miner`). + +## Descriptor wallet et clé P2PK + +Le miner signet doit signer des transactions vers le SIGNETCHALLENGE (script P2PK). La clé privée doit être importée dans le wallet comme descriptor **pk()** (P2PK), et non **wpkh()**, pour que `walletprocesspsbt` puisse signer. L’import est fait dans `run.sh` au démarrage du conteneur lorsque `MINERENABLED=1`. + +Détails et dépannage : voir **docs/TROUBLESHOOTING_MINING.md** et **docs/SOLUTION_MINING.md** à la racine du projet. + +## Références + +- **Racine du projet** : `/home/ncantu/Bureau/code/bitcoin/` +- **Script miner** : `miner` (racine) +- **Boucle de minage** : `mine.sh` (racine) +- **Démarrage conteneur** : `run.sh`, **docs/MAINTENANCE.md** (section Mining) +- **Dépannage mining** : **docs/TROUBLESHOOTING_MINING.md** +- **Solution PSBT / descriptor** : **docs/SOLUTION_MINING.md** +- **Configuration** : **docs/ENVIRONMENT.md** (MINERENABLED, BLOCKPRODUCTIONDELAY, NBITS, PRIVKEY, etc.) diff --git a/restore-signet-from-backup.sh b/restore-signet-from-backup.sh new file mode 100755 index 0000000..9ad0df2 --- /dev/null +++ b/restore-signet-from-backup.sh @@ -0,0 +1,65 @@ +#!/bin/bash +# +# Restore the Bitcoin Signet chain from a full datadir backup (tar.gz). +# Use this to resume on the previous chain (e.g. height ~11535) after data loss. +# +# Prerequisite: a backup created with save-signet-datadir-backup.sh +# (or: docker exec bitcoin-signet-instance tar czf /tmp/bitcoin-backup.tar.gz /root/.bitcoin/ +# docker cp bitcoin-signet-instance:/tmp/bitcoin-backup.tar.gz ./backups/) +# +# Usage: ./restore-signet-from-backup.sh +# Example: ./restore-signet-from-backup.sh backups/bitcoin-backup-20260124.tar.gz +# +# Author: 4NK Team +# Date: 2026-02-02 + +set -e + +CONTAINER_NAME="bitcoin-signet-instance" +VOLUME_NAME="signet-bitcoin-data" +BACKUP_PATH="${1:?Usage: $0 }" + +if [[ ! -f "$BACKUP_PATH" ]]; then + echo "Error: Backup file not found: $BACKUP_PATH" + exit 1 +fi + +BACKUP_PATH=$(realpath "$BACKUP_PATH") +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +echo "=== Restore Signet from backup ===" +echo "Backup: $BACKUP_PATH" +echo "Volume: $VOLUME_NAME" +echo "" + +if docker ps -a --format "{{.Names}}" | grep -q "^${CONTAINER_NAME}$"; then + echo "Stopping and removing current container..." + sudo docker stop "$CONTAINER_NAME" 2>/dev/null || true + sudo docker rm "$CONTAINER_NAME" 2>/dev/null || true +fi + +echo "Creating volume $VOLUME_NAME if needed..." +sudo docker volume create "$VOLUME_NAME" 2>/dev/null || true + +echo "Extracting backup into volume..." +# Archive may contain root/.bitcoin/... (from tar czf -C / root/.bitcoin) or .bitcoin/... +# Volume is mounted at /root/.bitcoin; extract to / then ensure content is under /root/.bitcoin +sudo docker run --rm \ + -v "$VOLUME_NAME":/restore_target \ + -v "$BACKUP_PATH":/backup.tar.gz:ro \ + debian:bookworm-slim \ + sh -c "cd / && tar xzf /backup.tar.gz && if [ -d /root/.bitcoin ] && [ -n \"\$(ls -A /root/.bitcoin 2>/dev/null)\" ]; then cp -a /root/.bitcoin/. /restore_target/; elif [ -d /.bitcoin ]; then cp -a /.bitcoin/. /restore_target/; else echo 'Unexpected archive layout' && exit 1; fi" + +echo "Starting container with restored data..." +sudo docker run --env-file .env -d \ + --name "$CONTAINER_NAME" \ + -v "$VOLUME_NAME":/root/.bitcoin \ + -p 38332:38332 -p 38333:38333 \ + -p 28332:28332 -p 28333:28333 -p 28334:28334 \ + bitcoin-signet + +echo "" +echo "Container started. Check height with:" +echo " sudo docker exec $CONTAINER_NAME bitcoin-cli -datadir=\$(docker exec $CONTAINER_NAME printenv BITCOIN_DIR 2>/dev/null || echo /root/.bitcoin) getblockchaininfo" +echo "" diff --git a/save-signet-datadir-backup.sh b/save-signet-datadir-backup.sh new file mode 100755 index 0000000..70dbd24 --- /dev/null +++ b/save-signet-datadir-backup.sh @@ -0,0 +1,59 @@ +#!/bin/bash +# +# Create a full datadir backup (blocks + chainstate + config) from the running +# Bitcoin Signet container. Use this on the machine that has the chain you want +# to keep (e.g. height ~11535), then copy the archive and run restore-signet-from-backup.sh +# on the target machine. +# +# Usage: ./save-signet-datadir-backup.sh [output_dir] +# Default output: backups/signet-datadir-YYYYMMDD-HHMMSS.tar.gz +# +# Author: 4NK Team +# Date: 2026-02-02 + +set -e + +CONTAINER_NAME="bitcoin-signet-instance" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" +OUTPUT_DIR="${1:-$SCRIPT_DIR/backups}" +mkdir -p "$OUTPUT_DIR" +TIMESTAMP=$(date +%Y%m%d-%H%M%S) +BACKUP_FILE="$OUTPUT_DIR/signet-datadir-$TIMESTAMP.tar.gz" + +if ! docker ps --format "{{.Names}}" | grep -q "^${CONTAINER_NAME}$"; then + echo "Error: Container $CONTAINER_NAME is not running." + exit 1 +fi + +# Datadir inside container: use BITCOIN_DIR from container so backup matches actual bitcoind data +DATADIR=$(docker exec "$CONTAINER_NAME" printenv BITCOIN_DIR 2>/dev/null || echo "/root/.bitcoin") +DATADIR_REL="${DATADIR#/}" + +echo "=== Full Signet datadir backup ===" +echo "Container: $CONTAINER_NAME" +echo "Datadir: $DATADIR" +echo "Output: $BACKUP_FILE" +echo "" + +echo "Creating archive inside container..." +# tar may exit 1 if a file changed during read (e.g. wallet.dat); archive is usually still usable +sudo docker exec "$CONTAINER_NAME" tar czf /tmp/signet-datadir-backup.tar.gz -C / "$DATADIR_REL" || { e=$?; [ "$e" -eq 1 ] || exit "$e"; } + +echo "Copying archive to host..." +sudo docker cp "$CONTAINER_NAME:/tmp/signet-datadir-backup.tar.gz" "$BACKUP_FILE" + +echo "Removing temp file in container..." +sudo docker exec "$CONTAINER_NAME" rm -f /tmp/signet-datadir-backup.tar.gz + +echo "" +echo "Backup saved: $BACKUP_FILE" +echo "Size: $(du -h "$BACKUP_FILE" | cut -f1)" +# Symlink for easy download (signet-datadir-latest.tar.gz) +ln -sf "$(basename "$BACKUP_FILE")" "$OUTPUT_DIR/signet-datadir-latest.tar.gz" +echo "Latest symlink: $OUTPUT_DIR/signet-datadir-latest.tar.gz -> $(basename "$BACKUP_FILE")" +echo "" +echo "To restore on this or another machine:" +echo " ./restore-signet-from-backup.sh $BACKUP_FILE" +echo " # or: ./restore-signet-from-backup.sh $OUTPUT_DIR/signet-datadir-latest.tar.gz" +echo "" diff --git a/signet-dashboard/.env.example b/signet-dashboard/.env.example index c39c944..0ee7034 100644 --- a/signet-dashboard/.env.example +++ b/signet-dashboard/.env.example @@ -1,5 +1,5 @@ -# Bitcoin RPC Configuration -BITCOIN_RPC_HOST=localhost +# Bitcoin RPC Configuration (nœud local = même chaîne que Mempool sur la machine bitcoin) +BITCOIN_RPC_HOST=127.0.0.1 BITCOIN_RPC_PORT=38332 BITCOIN_RPC_USER=bitcoin BITCOIN_RPC_PASSWORD=bitcoin diff --git a/signet-dashboard/public/api-docs.html b/signet-dashboard/public/api-docs.html index 692e0e2..d5f955d 100644 --- a/signet-dashboard/public/api-docs.html +++ b/signet-dashboard/public/api-docs.html @@ -1668,6 +1668,34 @@ + +
+
+
+ GET + /api/mining/status +
+ +
+

État du miner : inféré depuis l’âge du dernier bloc. active si le dernier bloc a moins de 30 minutes, sinon inactive.

+

Base URL : https://dashboard.certificator.4nkweb.com

+
+ +
+

Réponse (200 OK)

+
+
{
+  "status": "active",
+  "blocks": 11535,
+  "lastBlockTime": 1769730315,
+  "lastBlockAgeSeconds": 120,
+  "message": "Dernier bloc récent, miner probablement actif"
+}
+
+
+
+
+
diff --git a/signet-dashboard/public/app.js b/signet-dashboard/public/app.js index 0b01572..4690dc9 100644 --- a/signet-dashboard/public/app.js +++ b/signet-dashboard/public/app.js @@ -63,6 +63,7 @@ function startBlockPolling() { loadAvgFee(), loadAvgTxAmount(), loadMiningDifficulty(), + loadMinerStatus(), loadAvgBlockTime(), ]); } @@ -88,6 +89,7 @@ async function loadData() { loadAnchorCount(), loadNetworkPeers(), loadMiningDifficulty(), + loadMinerStatus(), ]); updateLastUpdateTime(); @@ -111,18 +113,43 @@ async function loadData() { * Charge les informations de la blockchain */ async function loadBlockchainInfo() { + const blockHeightEl = document.getElementById('block-height'); + const chainWarningEl = document.getElementById('chain-warning'); try { const response = await fetch(`${API_BASE_URL}/api/blockchain/info`); const data = await response.json(); - document.getElementById('block-height').textContent = data.blocks || 0; - // Initialiser lastBlockHeight si ce n'est pas déjà fait + if (!response.ok) { + blockHeightEl.textContent = 'Erreur'; + if (chainWarningEl) { + chainWarningEl.textContent = data.error || `Erreur RPC (${response.status})`; + chainWarningEl.style.display = 'block'; + } + return; + } + + const blocks = data.blocks !== undefined ? data.blocks : 0; + blockHeightEl.textContent = blocks; + + if (chainWarningEl) { + if (blocks > 0 && blocks < 10000) { + chainWarningEl.textContent = 'Chaîne anormale (hauteur < 10000). Vérifier le nœud et le volume persistant sur la machine bitcoin.'; + chainWarningEl.style.display = 'block'; + } else { + chainWarningEl.style.display = 'none'; + } + } + if (lastBlockHeight === null && data.blocks !== undefined) { lastBlockHeight = data.blocks; } } catch (error) { console.error('Error loading blockchain info:', error); - document.getElementById('block-height').textContent = 'Erreur'; + blockHeightEl.textContent = 'Erreur'; + if (chainWarningEl) { + chainWarningEl.textContent = 'Impossible de joindre le backend. Vérifier le nœud (127.0.0.1:38332) sur la machine bitcoin.'; + chainWarningEl.style.display = 'block'; + } } } @@ -134,6 +161,12 @@ async function loadLatestBlock() { const response = await fetch(`${API_BASE_URL}/api/blockchain/latest-block`); const data = await response.json(); + if (!response.ok) { + document.getElementById('last-block-time').textContent = 'Erreur'; + document.getElementById('last-block-tx-count').textContent = 'Erreur'; + return; + } + if (data.time) { const date = new Date(data.time * 1000); document.getElementById('last-block-time').textContent = date.toLocaleString('fr-FR'); @@ -141,7 +174,7 @@ async function loadLatestBlock() { document.getElementById('last-block-time').textContent = 'Aucun bloc'; } - document.getElementById('last-block-tx-count').textContent = data.tx_count || 0; + document.getElementById('last-block-tx-count').textContent = data.tx_count ?? 0; } catch (error) { console.error('Error loading latest block:', error); document.getElementById('last-block-time').textContent = 'Erreur'; @@ -157,8 +190,14 @@ async function loadWalletBalance() { const response = await fetch(`${API_BASE_URL}/api/wallet/balance`); const data = await response.json(); - document.getElementById('balance-mature').textContent = formatBTC(data.mature || 0); - document.getElementById('balance-immature').textContent = formatBTC(data.immature || 0); + if (!response.ok) { + document.getElementById('balance-mature').textContent = 'Erreur'; + document.getElementById('balance-immature').textContent = 'Erreur'; + return; + } + + document.getElementById('balance-mature').textContent = formatBTC(data.mature ?? 0); + document.getElementById('balance-immature').textContent = formatBTC(data.immature ?? 0); } catch (error) { console.error('Error loading wallet balance:', error); document.getElementById('balance-mature').textContent = 'Erreur'; @@ -303,6 +342,37 @@ async function loadMiningDifficulty() { } } +/** + * Charge l'état du miner (actif / inactif / inconnu, inféré depuis l'âge du dernier bloc). + */ +async function loadMinerStatus() { + const el = document.getElementById('miner-status'); + if (!el) { + return; + } + try { + const response = await fetch(`${API_BASE_URL}/api/mining/status`); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const data = await response.json(); + if (data.status === 'active') { + el.textContent = 'Actif'; + el.setAttribute('aria-label', data.message ?? 'Miner actif'); + } else if (data.status === 'inactive') { + el.textContent = 'Inactif'; + el.setAttribute('aria-label', data.message ?? 'Miner inactif'); + } else { + el.textContent = data.message ?? 'Inconnu'; + el.setAttribute('aria-label', data.message ?? 'État du miner inconnu'); + } + } catch (error) { + console.error('Error loading miner status:', error); + el.textContent = 'Erreur'; + el.setAttribute('aria-label', 'Impossible de charger l\'état du miner'); + } +} + /** * Charge le temps moyen entre blocs */ diff --git a/signet-dashboard/public/hash-list.html b/signet-dashboard/public/hash-list.html index a6a4e1e..2d078f3 100644 --- a/signet-dashboard/public/hash-list.html +++ b/signet-dashboard/public/hash-list.html @@ -432,19 +432,24 @@ try { // Utiliser l'endpoint proxy du dashboard const response = await fetch(`${API_BASE_URL}/api/anchor/health/detailed`); - if (!response.ok) { - throw new Error(`HTTP error! status: ${response.status}`); - } - const health = await response.json(); + // On 503 the API still returns full health JSON (ok, bitcoin, mutex, utxos) so we can show the reason + if (!response.ok && (!health.mutex || !health.utxos || !health.bitcoin)) { + const reason = health.error || health.message || `HTTP ${response.status}`; + healthDiv.innerHTML = `
✗ [anchor-api] Health check a échoué (HTTP ${response.status}). ${reason}
`; + unlockButton.style.display = 'none'; + return; + } + let html = '
'; - // État général + // État général (on 503 we still render full panel so user sees Bitcoin/mutex/UTXOs reason) const statusClass = health.ok ? 'ok' : 'error'; + const statusText = health.ok ? '✓ OK' : (response.status === 503 ? '✗ Problème (503 - voir détails ci-dessous)' : '✗ Problème'); html += `
-
${health.ok ? '✓ OK' : '✗ Problème'}
+
${statusText}
`; // Mutex @@ -510,7 +515,7 @@ } catch (error) { console.error('Error loading health status:', error); - healthDiv.innerHTML = `
Erreur lors du chargement de l'état : ${error.message}
`; + healthDiv.innerHTML = `
✗ [anchor-api] L'API d'ancrage n'est pas accessible. Erreur : ${error.message}
`; unlockButton.style.display = 'none'; } finally { refreshButton.disabled = false; diff --git a/signet-dashboard/public/index.html b/signet-dashboard/public/index.html index ab66a80..5a4d5b9 100644 --- a/signet-dashboard/public/index.html +++ b/signet-dashboard/public/index.html @@ -34,6 +34,7 @@
+

État de la Blockchain

@@ -80,6 +81,10 @@

Difficulté de Minage

-

+
+

État du Miner

+

-

+

Temps Moyen entre Blocs

diff --git a/signet-dashboard/public/join-signet.html b/signet-dashboard/public/join-signet.html index 7cbee8e..d2d1065 100644 --- a/signet-dashboard/public/join-signet.html +++ b/signet-dashboard/public/join-signet.html @@ -58,60 +58,6 @@ transform: scale(0.98); } - .payment-section { - background: var(--card-background); - padding: 30px; - border-radius: 10px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3); - margin-bottom: 30px; - text-align: center; - border: 1px solid var(--border-color); - } - - .payment-section h2 { - color: var(--primary-color); - margin-bottom: 20px; - font-size: 1.8em; - } - - .payment-info { - margin: 20px 0; - } - - .payment-amount { - font-size: 1.5em; - font-weight: bold; - color: var(--primary-color); - margin: 10px 0; - } - - .payment-address { - font-family: 'Courier New', monospace; - background: var(--card-background); - color: var(--text-color); - padding: 15px; - border-radius: 5px; - margin: 15px 0; - word-break: break-all; - font-size: 0.9em; - border: 1px solid var(--border-color); - } - - .nostr-profile-link { - display: inline-block; - margin-top: 15px; - padding: 10px 20px; - background: var(--primary-color); - color: white; - text-decoration: none; - border-radius: 5px; - transition: background 0.3s; - } - - .nostr-profile-link:hover { - background: #e0820d; - } - .wallet-section { background: var(--card-background); padding: 30px; @@ -127,28 +73,6 @@ font-size: 1.8em; } - .wallet-checkbox { - display: flex; - align-items: center; - margin: 20px 0; - padding: 15px; - background: var(--card-background); - border-radius: 5px; - border: 1px solid var(--border-color); - } - - .wallet-checkbox input[type="checkbox"] { - width: 20px; - height: 20px; - margin-right: 15px; - cursor: pointer; - } - - .wallet-checkbox label { - cursor: pointer; - font-size: 1.1em; - } - .info-box { background: rgba(13, 202, 240, 0.15); border-left: 4px solid #0dcaf0; @@ -162,16 +86,6 @@ margin: 5px 0; } - .success-message { - background: rgba(40, 167, 69, 0.2); - border-left: 4px solid var(--success-color); - padding: 15px; - margin: 20px 0; - border-radius: 5px; - color: #90ee90; - display: none; - } - .back-link { display: inline-block; margin-bottom: 20px; @@ -244,80 +158,6 @@ addnode=anchorage.certificator.4nkweb.com:38333

- -
-
-

💳 Accès au Wallet de Mining

-

Pour recevoir le wallet de mining et les clés nécessaires pour miner sur le réseau, effectuez un paiement de :

- -
0,0065 BTC
- -

Envoyez le paiement via Nostr à :

- -
npub18s03s39fa80ce2n3cmm0zme3jqehc82h6ld9sxq03uejqm3d05gsae0fuu
- - - - - 🔗 Voir le profil Nostr - - -
- - -
- -
-

Instructions :

-

1. Effectuez le paiement de 0,0065 BTC via Nostr à la npub ci-dessus

-

2. Cochez la case ci-dessus si vous souhaitez recevoir le wallet de mining

-

3. Après confirmation du paiement, vous recevrez le wallet de mining sur Nostr

-
- -
-

✅ Paiement reçu !

-

Votre demande a été enregistrée. Vous recevrez le wallet de mining sur Nostr sous peu.

-
-
-
- - -
-
-

🔑 Accès à une Clé API

-

Pour recevoir une clé API permettant d'utiliser les services d'ancrage et de filigrane, effectuez un paiement de :

- -
0,0065 BTC
- -

Envoyez le paiement via Nostr à :

- -
npub18s03s39fa80ce2n3cmm0zme3jqehc82h6ld9sxq03uejqm3d05gsae0fuu
- - - - - 🔗 Voir le profil Nostr - - -
- - -
- -
-

Instructions :

-

1. Effectuez le paiement de 0,0065 BTC via Nostr à la npub ci-dessus

-

2. Cochez la case ci-dessus si vous souhaitez recevoir la clé API

-

3. Après confirmation du paiement, vous recevrez la clé API sur Nostr

-
- -
-

✅ Paiement reçu !

-

Votre demande a été enregistrée. Vous recevrez la clé API sur Nostr sous peu.

-
-
-
-
@@ -333,17 +173,6 @@ addnode=anchorage.certificator.4nkweb.com:38333
-
-

Que se passe-t-il après le paiement ?

-

Une fois le paiement confirmé, vous recevrez sur Nostr :

-
    -
  • Les fichiers de configuration complets
  • -
  • La clé privée du signet (si vous avez coché la case pour le wallet de mining)
  • -
  • La clé API (si vous avez coché la case pour la clé API)
  • -
  • Les instructions détaillées pour démarrer votre nœud ou utiliser l'API
  • -
-
-

Besoin d'aide ?

Pour toute question, consultez la documentation complète dans le dépôt GitHub ou contactez l'équipe.

@@ -363,10 +192,6 @@ addnode=anchorage.certificator.4nkweb.com:38333
diff --git a/signet-dashboard/public/learn.html b/signet-dashboard/public/learn.html index 7bedeec..64a2ff3 100644 --- a/signet-dashboard/public/learn.html +++ b/signet-dashboard/public/learn.html @@ -390,6 +390,42 @@ Le bloc est ajouté à la blockchain, le mineur reçoit la récompense + +

Comment fonctionne le Miner ?

+
+

Le miner (mineur) est le logiciel ou le nœud qui produit effectivement les blocs. Il écoute le réseau, construit des blocs candidats et les diffuse une fois qu’ils sont valides.

+

Sur le réseau principal Bitcoin, le miner :

+
    +
  • Obtient un modèle de bloc (block template) depuis son nœud Bitcoin Core : en-tête du bloc, transactions du mempool, difficulté cible
  • +
  • Sélectionne les transactions à inclure (souvent par ordre de frais par octet)
  • +
  • Construit le bloc (transaction coinbase + liste de transactions)
  • +
  • Cherche un nonce (et varie le coinbase si besoin) pour que le hash du bloc soit sous la cible de difficulté (proof-of-work)
  • +
  • Diffuse le bloc miné au réseau ; les autres nœuds le valident et l’ajoutent à leur chaîne
  • +
+

Sur Bitcoin Signet (réseau de test comme celui de ce dashboard), le principe est le même, mais la « preuve » change : au lieu d’un proof-of-work coûteux, un bloc valide doit contenir une signature d’une clé autorisée (signet). Le miner Signet récupère un template, construit le bloc, le signe avec la clé du signet, puis le diffuse. La récompense par bloc est fixe (par ex. 50 000 sats sur ce Signet).

+
+ +
+
+ 1. Modèle de bloc
+ Le nœud (Bitcoin Core) fournit un template : bloc précédent, mempool, difficulté +
+
+
+ 2. Construction du bloc
+ Le miner ajoute la transaction coinbase (récompense vers son adresse) et des transactions du mempool +
+
+
+ 3. Preuve
+ Mainnet : recherche d’un nonce (proof-of-work). Signet : signature du bloc avec la clé autorisée +
+
+
+ 4. Diffusion
+ Le bloc valide est envoyé au réseau ; les nœuds le valident et l’ajoutent à la blockchain +
+
diff --git a/signet-dashboard/public/styles.css b/signet-dashboard/public/styles.css index 767ee6a..33220ec 100644 --- a/signet-dashboard/public/styles.css +++ b/signet-dashboard/public/styles.css @@ -75,6 +75,16 @@ main { margin-bottom: 40px; } +.chain-warning { + margin-bottom: 20px; + padding: 14px 18px; + background-color: rgba(220, 53, 69, 0.2); + border: 1px solid var(--error-color); + border-radius: 8px; + color: var(--error-color); + font-weight: 500; +} + section { margin-bottom: 40px; } diff --git a/signet-dashboard/signet-dashboard.service b/signet-dashboard/signet-dashboard.service index 8b5e9b1..4d49985 100644 --- a/signet-dashboard/signet-dashboard.service +++ b/signet-dashboard/signet-dashboard.service @@ -8,7 +8,12 @@ User=ncantu WorkingDirectory=/home/ncantu/Bureau/code/bitcoin/signet-dashboard Environment=NODE_ENV=production Environment=DASHBOARD_PORT=3020 -Environment=DASHBOARD_HOST=0.0.0.0 +# Bind IPv4 only: machine bitcoin (192.168.1.105). Accept only from proxy 192.168.1.100. +Environment=DASHBOARD_HOST=192.168.1.105 +Environment=ALLOWED_SOURCE_IP=192.168.1.100 +# RPC = même machine : 127.0.0.1:38332 = host.docker.internal:38332 (Mempool) = bitcoin-signet-instance +Environment=BITCOIN_RPC_HOST=127.0.0.1 +Environment=BITCOIN_RPC_PORT=38332 ExecStart=/usr/bin/node /home/ncantu/Bureau/code/bitcoin/signet-dashboard/src/server.js Restart=always RestartSec=10 diff --git a/signet-dashboard/src/server.js b/signet-dashboard/src/server.js index c77efa1..caef31a 100644 --- a/signet-dashboard/src/server.js +++ b/signet-dashboard/src/server.js @@ -63,13 +63,21 @@ function makeHttpRequest(baseUrl, path, options = {}) { res.on('end', () => { try { const jsonData = JSON.parse(data); + if (options.preserveStatus) { + resolve({ statusCode: res.statusCode, body: jsonData }); + return; + } if (res.statusCode >= 200 && res.statusCode < 300) { resolve(jsonData); } else { resolve({ error: jsonData.error || 'Request failed', message: jsonData.message || `HTTP ${res.statusCode}` }); } } catch (e) { - resolve({ error: 'Invalid JSON response', message: data.substring(0, 100) }); + if (options.preserveStatus) { + resolve({ statusCode: res.statusCode, body: { error: 'Invalid JSON response', message: data.substring(0, 100) } }); + } else { + resolve({ error: 'Invalid JSON response', message: data.substring(0, 100) }); + } } }); }); @@ -126,6 +134,34 @@ if (envResult.error) { const app = express(); const PORT = process.env.DASHBOARD_PORT || 3020; const HOST = process.env.DASHBOARD_HOST || '0.0.0.0'; +const ALLOWED_SOURCE_IP = process.env.ALLOWED_SOURCE_IP ?? ''; + +/** + * Normalize remote address: IPv6-mapped IPv4 (::ffff:192.168.1.100) -> 192.168.1.100 + * @param {string} addr - req.socket.remoteAddress + * @returns {string} + */ +function normalizeRemoteAddress(addr) { + if (!addr) return ''; + if (addr.startsWith('::ffff:')) return addr.slice(7); + return addr; +} + +// Middleware: accept only requests from proxy when ALLOWED_SOURCE_IP is set (IPv4 only) +app.use((req, res, next) => { + if (!ALLOWED_SOURCE_IP) return next(); + const remote = normalizeRemoteAddress(req.socket.remoteAddress ?? ''); + if (remote !== ALLOWED_SOURCE_IP) { + logger.warn('Request rejected: source not allowed', { + remoteAddress: req.socket.remoteAddress, + allowedSourceIp: ALLOWED_SOURCE_IP, + path: req.path, + }); + res.status(403).json({ error: 'Forbidden', message: 'Source not allowed' }); + return; + } + next(); +}); // Middleware app.use(cors()); @@ -595,6 +631,35 @@ app.get('/api/mining/difficulty', async (req, res) => { } }); +/** État du miner : inféré depuis l’âge du dernier bloc (actif si dernier bloc < 30 min). */ +const MINER_ACTIVE_THRESHOLD_SECONDS = 30 * 60; + +app.get('/api/mining/status', async (req, res) => { + try { + const blockchainInfo = await bitcoinRPC.getBlockchainInfo(); + const latestBlock = await bitcoinRPC.getLatestBlock(); + const nowSeconds = Math.floor(Date.now() / 1000); + const lastBlockTime = latestBlock.time; + const ageSeconds = nowSeconds - lastBlockTime; + const status = ageSeconds <= MINER_ACTIVE_THRESHOLD_SECONDS ? 'active' : 'inactive'; + res.json({ + status, + blocks: blockchainInfo.blocks, + lastBlockTime: lastBlockTime, + lastBlockAgeSeconds: ageSeconds, + message: status === 'active' + ? 'Dernier bloc récent, miner probablement actif' + : 'Dernier bloc ancien, miner probablement inactif ou en attente du délai', + }); + } catch (error) { + logger.error('Error getting mining status', { error: error.message }); + res.status(500).json({ + status: 'unknown', + message: error.message, + }); + } +}); + app.get('/api/mining/avg-block-time', async (req, res) => { try { // Utiliser l'API mempool pour obtenir le temps moyen entre blocs @@ -985,6 +1050,7 @@ app.post('/api/anchor/unlock-utxos', async (req, res) => { }); // Route pour obtenir l'état de santé détaillé (appelle l'API d'ancrage externe) +// Forward status code and body so client sees 503 and exact reason (Bitcoin, mutex, UTXOs). app.get('/api/anchor/health/detailed', async (req, res) => { try { // Toujours utiliser l'URL publique HTTPS @@ -998,9 +1064,10 @@ app.get('/api/anchor/health/detailed', async (req, res) => { headers: { 'Content-Type': 'application/json', }, + preserveStatus: true, }); - res.json(result); + res.status(result.statusCode).json(result.body); } catch (error) { logger.error('Error getting detailed health', { error: error.message }); res.status(500).json({ error: error.message }); diff --git a/test-mempool-rpc-config.sh b/test-mempool-rpc-config.sh new file mode 100755 index 0000000..26c6be9 --- /dev/null +++ b/test-mempool-rpc-config.sh @@ -0,0 +1,72 @@ +#!/bin/bash +# +# Test the RPC configuration that Mempool uses (host.docker.internal:38332 +# = host's 127.0.0.1:38332 when Mempool runs in Docker on that host). +# Run on the machine where Mempool runs to verify the node has ~11535 blocks; +# then align Dashboard, Anchorage and Faucet to the same host:port. +# +# Usage: ./test-mempool-rpc-config.sh [RPC_HOST] [RPC_PORT] +# Default: 127.0.0.1 38332 +# Example (node on another machine): ./test-mempool-rpc-config.sh 192.168.1.105 38332 +# +# Author: 4NK Team +# Date: 2026-02-02 + +set -e + +RPC_HOST="${1:-127.0.0.1}" +RPC_PORT="${2:-38332}" +RPC_USER="${BITCOIN_RPC_USER:-bitcoin}" +RPC_PASS="${BITCOIN_RPC_PASSWORD:-bitcoin}" +EXPECTED_MIN="${EXPECTED_HEIGHT_MIN:-11000}" +EXPECTED_MAX="${EXPECTED_HEIGHT_MAX:-12000}" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TMP_JSON="$SCRIPT_DIR/.test-mempool-rpc-$$.json" +trap 'rm -f "$TMP_JSON"' EXIT + +echo "=== Test Mempool RPC config (same node as Mempool uses) ===" +echo "Host: $RPC_HOST Port: $RPC_PORT" +echo "" + +# JSON-RPC getblockchaininfo +HTTP_CODE=$(curl -s -o "$TMP_JSON" -w "%{http_code}" \ + --user "$RPC_USER:$RPC_PASS" \ + --data-binary '{"jsonrpc":"1.0","id":"test","method":"getblockchaininfo","params":[]}' \ + -H "content-type: text/plain;" \ + "http://${RPC_HOST}:${RPC_PORT}/" 2>/dev/null || echo "000") + +if [ "$HTTP_CODE" != "200" ]; then + echo "FAIL: RPC returned HTTP $HTTP_CODE (node unreachable or auth failed)." + exit 1 +fi + +if [ ! -f "$TMP_JSON" ] || ! grep -q '"result"' "$TMP_JSON" 2>/dev/null; then + echo "FAIL: RPC response missing result." + exit 1 +fi + +CHAIN=$(grep -o '"chain"[[:space:]]*:[[:space:]]*"[^"]*"' "$TMP_JSON" | sed 's/.*:[[:space:]]*"\([^"]*\)".*/\1/') +BLOCKS=$(grep -o '"blocks"[[:space:]]*:[[:space:]]*[0-9]*' "$TMP_JSON" | head -1 | grep -o '[0-9]*$') + +if [ -z "$BLOCKS" ]; then + echo "FAIL: Could not parse 'blocks' from getblockchaininfo." + exit 1 +fi + +echo "Node: chain=$CHAIN blocks=$BLOCKS" + +if [ "$CHAIN" != "signet" ]; then + echo "WARN: chain is not 'signet' (got '$CHAIN')." +fi + +if [ "$BLOCKS" -ge "$EXPECTED_MIN" ] && [ "$BLOCKS" -le "$EXPECTED_MAX" ]; then + echo "OK: Height $BLOCKS is in expected range [$EXPECTED_MIN..$EXPECTED_MAX] (same as Mempool)." + echo "" + echo "Align Dashboard, Anchorage and Faucet to this node: BITCOIN_RPC_HOST=$RPC_HOST BITCOIN_RPC_PORT=$RPC_PORT" + exit 0 +else + echo "WARN: Height $BLOCKS is outside expected range [$EXPECTED_MIN..$EXPECTED_MAX]." + echo " This may not be the node Mempool uses, or the chain is not synced." + exit 1 +fi diff --git a/update-signet.sh b/update-signet.sh index c75fac3..d6de7ef 100755 --- a/update-signet.sh +++ b/update-signet.sh @@ -130,6 +130,13 @@ rebuild_image() { fi } +# Volume persistant : même que restore-signet-from-backup.sh (une seule chaîne pour Mempool, dashboard, APIs) +VOLUME_NAME="signet-bitcoin-data" + +# Volume contenant la chaîne Signet complète (~11530 blocs). Utilisé par défaut s'il existe pour éviter de repartir sur une chaîne vide. +# Voir docs/MAINTENANCE.md et fixKnowledge/dashboard-anchor-wrong-chain-insufficient-balance.md +SIGNET_VOLUME_FULL_CHAIN="4b5dca4d940b9f6e5db67b460f40f230a5ef1195a3769e5f91fa02be6edde649" + # Redémarrage du conteneur restart_container() { info "Redémarrage du conteneur..." @@ -141,10 +148,20 @@ restart_container() { sudo docker rm bitcoin-signet-instance fi - # Démarrer le nouveau conteneur - info "Démarrage du nouveau conteneur..." + # Utiliser le volume "chaîne complète" s'il existe, sinon le volume nommé (créé si besoin) + local volume_to_use="$VOLUME_NAME" + if sudo docker volume inspect "$SIGNET_VOLUME_FULL_CHAIN" &>/dev/null; then + volume_to_use="$SIGNET_VOLUME_FULL_CHAIN" + info "Utilisation du volume chaîne complète (signet ~11530 blocs)" + else + sudo docker volume create "$VOLUME_NAME" 2>/dev/null || true + fi + + # Démarrer le nouveau conteneur avec volume persistant (une seule chaîne pour tous les services) + info "Démarrage du nouveau conteneur (volume $volume_to_use)..." sudo docker run --env-file .env -d \ --name bitcoin-signet-instance \ + -v "$volume_to_use":/root/.bitcoin \ -p 38332:38332 \ -p 38333:38333 \ -p 28332:28332 \ @@ -167,10 +184,12 @@ verify_update() { return 1 fi - # Vérifier la version de Bitcoin + # Vérifier la version de Bitcoin (datadir = BITCOIN_DIR du conteneur) + local bitcoin_datadir + bitcoin_datadir=$(sudo docker exec bitcoin-signet-instance printenv BITCOIN_DIR 2>/dev/null || echo "/root/.bitcoin") local version_info - version_info=$(sudo docker exec bitcoin-signet-instance bitcoin-cli -datadir=/root/.bitcoin -version 2>/dev/null || \ - sudo docker exec bitcoin-signet-instance bitcoin-cli -datadir=/root/.bitcoin getnetworkinfo 2>/dev/null | grep -oP '"subversion": "\K[^"]+' || echo "unknown") + version_info=$(sudo docker exec bitcoin-signet-instance bitcoin-cli -datadir="$bitcoin_datadir" -version 2>/dev/null || \ + sudo docker exec bitcoin-signet-instance bitcoin-cli -datadir="$bitcoin_datadir" getnetworkinfo 2>/dev/null | grep -oP '"subversion": "\K[^"]+' || echo "unknown") if [ "$version_info" != "unknown" ]; then success "Version Bitcoin détectée: $version_info" @@ -180,7 +199,7 @@ verify_update() { # Vérifier l'état de la blockchain local blockchain_info - blockchain_info=$(sudo docker exec bitcoin-signet-instance bitcoin-cli -datadir=/root/.bitcoin getblockchaininfo 2>/dev/null | grep -oP '"chain": "\K[^"]+' || echo "unknown") + blockchain_info=$(sudo docker exec bitcoin-signet-instance bitcoin-cli -datadir="$bitcoin_datadir" getblockchaininfo 2>/dev/null | grep -oP '"chain": "\K[^"]+' || echo "unknown") if [ "$blockchain_info" = "signet" ]; then success "Blockchain signet opérationnelle" @@ -252,7 +271,7 @@ main() { echo "" success "Mise à jour terminée avec succès!" info "Pour voir les logs: sudo docker logs -f bitcoin-signet-instance" - info "Pour vérifier l'état: sudo docker exec bitcoin-signet-instance bitcoin-cli -datadir=/root/.bitcoin getblockchaininfo" + info "Pour vérifier l'état: sudo docker exec bitcoin-signet-instance bitcoin-cli -datadir=\$(docker exec bitcoin-signet-instance printenv BITCOIN_DIR 2>/dev/null || echo /root/.bitcoin) getblockchaininfo" } # Gestion des arguments diff --git a/userwallet/docs/ports.md b/userwallet/docs/ports.md index 9e43c59..e92c968 100644 --- a/userwallet/docs/ports.md +++ b/userwallet/docs/ports.md @@ -61,6 +61,6 @@ const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : ; Les ports suivants sont évités car potentiellement occupés : - **3007** : Utilisé par d'autres services - **8080** : Port commun, souvent occupé -- **3015** : Occupé (mempool1.4nkweb.com) +- **3015** : Occupé (mempool.4nkweb.com, machine bitcoin) - **3016** : Réservé (git1.4nkweb.com) - **3017** : Réservé (rocket1.4nkweb.com) diff --git a/userwallet/src/components/GlobalActionBar.tsx b/userwallet/src/components/GlobalActionBar.tsx index de6aee4..76f4271 100644 --- a/userwallet/src/components/GlobalActionBar.tsx +++ b/userwallet/src/components/GlobalActionBar.tsx @@ -183,7 +183,7 @@ export function GlobalActionBar(): JSX.Element { margin: '1rem 0', }} > - {mots.join(' ')} + {mots.map((mot, i) => `${i + 1}. ${mot}`).join(' ')}

) : (

diff --git a/userwallet/src/components/HomeScreen.tsx b/userwallet/src/components/HomeScreen.tsx index 7d95882..1c903c0 100644 --- a/userwallet/src/components/HomeScreen.tsx +++ b/userwallet/src/components/HomeScreen.tsx @@ -1,11 +1,17 @@ -import { useEffect, useRef } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { useIdentity } from '../hooks/useIdentity'; -import { isPairingSatisfied, hasRemotePair } from '../utils/pairing'; +import { + isPairingSatisfied, + hasRemotePair, + getStoredPairs, + removePair, + updatePairLabel, +} from '../utils/pairing'; import { usePairingConnected } from '../hooks/usePairingConnected'; import { getStoredRelays } from '../utils/relay'; import { isInIframe, sendToChannel } from '../utils/iframeChannel'; -import type { LocalIdentity } from '../types/identity'; +import type { LocalIdentity, PairConfig } from '../types/identity'; import { PairingSetupBlock } from './PairingSetupBlock'; function logHomeStatus( @@ -42,12 +48,19 @@ export function HomeScreen(): JSX.Element { const { identity, isLoading } = useIdentity(); const { connected: pairingConnected } = usePairingConnected(); const pairingSatisfied = isPairingSatisfied(); - const showSetupBlock = !hasRemotePair(); + const hasRemote = hasRemotePair(); + const showSetupBlock = !hasRemote; + const [pairs, setPairs] = useState(() => getStoredPairs()); + const [addingDevice, setAddingDevice] = useState(false); const relays = getStoredRelays(); const relayStatus = relays.length > 0 ? 'OK' : 'Non configuré'; const pairingSetupRef = useRef(null); const hasScrolledAndLoggedRef = useRef(false); + const refreshPairs = (): void => { + setPairs(getStoredPairs()); + }; + useEffect(() => { if (identity === null) { return; @@ -88,6 +101,12 @@ export function HomeScreen(): JSX.Element { return () => window.clearTimeout(t); }, [showSetupBlock, identity]); + useEffect(() => { + if (!addingDevice) { + refreshPairs(); + } + }, [addingDevice]); + if (isLoading) { return (

@@ -112,22 +131,169 @@ export function HomeScreen(): JSX.Element { ); } + const handleRemovePair = (pairUuid: string): void => { + const ok = typeof window !== 'undefined' && window.confirm('Supprimer cet appareil du pairing ?'); + if (!ok) { + return; + } + removePair(pairUuid); + setPairs(getStoredPairs()); + }; + + const handleLabelChange = (pairUuid: string, label: string): void => { + updatePairLabel(pairUuid, label); + refreshPairs(); + }; + + const getDeviceDisplayLabel = (pair: PairConfig): string => { + if (pair.label !== undefined && pair.label.trim() !== '') { + return pair.label.trim(); + } + if (pair.is_local) { + return 'Appareil local'; + } + const remoteIndex = pairs.filter((p) => !p.is_local).indexOf(pair) + 1; + return `Appareil distant ${remoteIndex}`; + }; + return ( -
+
{pairingConnected && (

Connecté

)} - {showSetupBlock && ( -
- -
- )} +
+

+ Pairing +

+

+ Statut pairing +
+ Requis: Oui +
+ Satisfait: {pairingSatisfied ? 'Oui' : 'Non'} +

+ {showSetupBlock && !addingDevice && ( + + )} + {hasRemote && !addingDevice && ( + <> +

Appareils appariés

+
    + {pairs.map((pair) => ( +
  • + + handleLabelChange(pair.uuid, e.target.value)} + onBlur={(e) => handleLabelChange(pair.uuid, e.target.value)} + placeholder={getDeviceDisplayLabel(pair)} + aria-label={ + pair.is_local ? 'Éditer le label appareil local' : 'Éditer le label appareil distant' + } + style={{ + flex: 1, + minWidth: 0, + padding: '0.25rem 0.5rem', + fontSize: '1rem', + border: '1px solid var(--color-border)', + borderRadius: '4px', + backgroundColor: 'var(--color-surface)', + color: 'var(--color-text)', + }} + /> + {!pair.is_local && ( + + )} +
  • + ))} +
+

+ +

+ + )} + {addingDevice && ( + <> +

+ +

+ { + setAddingDevice(false); + refreshPairs(); + }} + isAddingDevice + /> + + )} +
); } diff --git a/userwallet/src/components/PairingDisplayScreen.tsx b/userwallet/src/components/PairingDisplayScreen.tsx index a244ae8..f207b6d 100644 --- a/userwallet/src/components/PairingDisplayScreen.tsx +++ b/userwallet/src/components/PairingDisplayScreen.tsx @@ -5,6 +5,7 @@ import { ensureLocalPairForSetup, getStoredPairs, parseAndValidatePairingWords, + removePair, } from '../utils/pairing'; import { usePairingWordsContext } from '../contexts/PairingWordsContext'; import { useIdentity } from '../hooks/useIdentity'; @@ -37,6 +38,15 @@ export function PairingDisplayScreen(): JSX.Element { }); }; + const handleGenerateAnotherKey = (): void => { + const pairs = getStoredPairs(); + const local = pairs.find((p) => p.is_local); + if (local !== undefined) { + removePair(local.uuid); + } + createNewIdentity(); + }; + useEffect(() => { if (identity !== null && identity.publicKey !== undefined) { const w = ensureLocalPairForSetup(identity.publicKey); @@ -146,6 +156,15 @@ export function PairingDisplayScreen(): JSX.Element {

Mots du 2ᵉ appareil — à copier sur le 1ᵉʳ

+

+ +

{words2nd.length > 0 ? (
void; + isAddingDevice?: boolean; +} + +export function PairingSetupBlock({ + onDone, + isAddingDevice = false, +}: PairingSetupBlockProps): JSX.Element { const navigate = useNavigate(); - const { identity } = useIdentity(); + const { identity, createNewIdentity } = useIdentity(); const [words, setWords] = useState([]); const [qrDataUrl, setQrDataUrl] = useState(null); const [remoteWordsInput, setRemoteWordsInput] = useState([]); @@ -42,6 +51,15 @@ export function PairingSetupBlock(): JSX.Element { }); }; + const handleGenerateAnotherKey = (): void => { + const pairs = getStoredPairs(); + const local = pairs.find((p) => p.is_local); + if (local !== undefined) { + removePair(local.uuid); + } + createNewIdentity(); + }; + useEffect(() => { if (identity !== null && identity.publicKey !== undefined) { const w = ensureLocalPairForSetup(identity.publicKey); @@ -71,22 +89,20 @@ export function PairingSetupBlock(): JSX.Element { setRemoteError('Mots invalides. 17 mots requis.'); return; } - const pair = addRemotePairFromWords(parsed, []); - if (pair === null) { + const newPair = addRemotePairFromWords(parsed, []); + if (newPair === null) { setRemoteError('Mots invalides. Vérifiez la saisie.'); return; } const pairs = getStoredPairs(); const local = pairs.find((p) => p.is_local); - const remote = pairs.find((p) => !p.is_local); - if ( - identity === null || - local === undefined || - remote === undefined || - identity.privateKey === undefined - ) { + if (identity === null || local === undefined || identity.privateKey === undefined) { setRemoteWordsInput([]); - navigate('/manage-pairs'); + if (onDone !== undefined) { + onDone(); + } else { + navigate('/manage-pairs'); + } return; } const relays = getStoredRelays().filter((r) => r.enabled); @@ -94,13 +110,12 @@ export function PairingSetupBlock(): JSX.Element { setRemoteError('Aucun relais activé. Configurez les relais pour finaliser le pairing.'); return; } - // Use pair's publicKey if available (will be updated from signatures if not) - const remotePublicKey = remote.publicKey; + const remotePublicKey = newPair.publicKey; setIsConfirming(true); try { await runDevice1Confirmation({ pairLocal: local.uuid, - pairRemote: remote.uuid, + pairRemote: newPair.uuid, identity, relays, remotePublicKey, @@ -115,12 +130,20 @@ export function PairingSetupBlock(): JSX.Element { } setIsConfirming(false); setRemoteWordsInput([]); - navigate('/manage-pairs'); + if (onDone !== undefined) { + onDone(); + } else { + navigate('/manage-pairs'); + } }; + const setupHeading = isAddingDevice + ? 'Ajouter un appareil' + : 'Configurer le pairing avec un 2ᵉ appareil'; + return ( -
-

Configurer le pairing avec un 2ᵉ appareil

+
+

{setupHeading}

{words.length > 0 && ( <> {!hasCopiedToSecondDevice ? ( @@ -128,6 +151,15 @@ export function PairingSetupBlock(): JSX.Element {

Mots du 1ᵉʳ appareil — à saisir sur le 2ᵉ (QR) :

+

+ +

setHasCopiedToSecondDevice(true)} > - J'ai copié ces mots sur mon deuxième device + {isAddingDevice + ? "J'ai copié ces mots sur le nouvel appareil" + : "J'ai copié ces mots sur mon deuxième device"}

) : (
-

Mots du 2ᵉ appareil

+

+ {isAddingDevice ? 'Mots du nouvel appareil' : 'Mots du 2ᵉ appareil'} +

void handleSubmitRemote(ev)} aria-label="Saisir les mots du 2e appareil" diff --git a/userwallet/src/components/WordInputGrid.tsx b/userwallet/src/components/WordInputGrid.tsx index 44e8f58..b5ee14a 100644 --- a/userwallet/src/components/WordInputGrid.tsx +++ b/userwallet/src/components/WordInputGrid.tsx @@ -1,5 +1,5 @@ import { useState, useRef, useEffect, KeyboardEvent, ChangeEvent, FocusEvent } from 'react'; -import { BIP32_WORDLIST } from '../utils/bip32'; +import { BIP32_MAX_WORD_LENGTH, BIP32_WORDLIST } from '../utils/bip32'; interface WordInputGridProps { value: string[]; @@ -189,100 +189,113 @@ export function WordInputGrid({ } }; + const allVisible = visibleWords.size === WORD_COUNT; + const toggleAllVisibility = (): void => { + setVisibleWords(allVisible ? new Set() : new Set(Array.from({ length: WORD_COUNT }, (_, i) => i))); + }; + return (
+
+ +
0 ? '0.5rem' : '0', }} > {words.map((word, index) => (
- -
- { - inputRefs.current[index] = el; - }} - id={`${id ?? 'word'}-${index}`} - type={visibleWords.has(index) ? 'text' : 'password'} - value={word} - onChange={(e: ChangeEvent) => { - handleInputChange(index, e.target.value); - }} - onKeyDown={(e) => { - handleKeyDown(index, e); - }} - onFocus={() => { - handleFocus(index); - }} - onBlur={handleBlur} - autoComplete="off" - spellCheck={false} - aria-describedby={ - index === 0 && ariaDescribedBy !== undefined - ? ariaDescribedBy - : undefined - } +
{focusedIndex === index && suggestions.length > 0 && (
w.length), +); + /** * Convert UUID to BIP32 word list. * UUID is converted to bytes, then to BIP32 path, then to words. diff --git a/userwallet/src/utils/crypto.ts b/userwallet/src/utils/crypto.ts index 1225179..db38ed1 100644 --- a/userwallet/src/utils/crypto.ts +++ b/userwallet/src/utils/crypto.ts @@ -10,6 +10,10 @@ import { hmac } from '@noble/hashes/hmac'; import { sha256 } from '@noble/hashes/sha256'; import { bytesToHex, hexToBytes } from '@noble/hashes/utils'; +/** secp256k1 curve order (number of points on the curve). */ +const SECP256K1_ORDER = + 0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n; + // Require hmacSha256Sync for secp256k1 sign() (RFC6979). Set once at module load. if (secpEtc.hmacSha256Sync === undefined) { secpEtc.hmacSha256Sync = (k: Uint8Array, ...m: Uint8Array[]): Uint8Array => @@ -44,6 +48,67 @@ export function publicKeyFromPrivateKey(privateKeyHex: string): string { return bytesToHex(pub); } +const DERIVE_DOMAIN = 'userwallet-derive-v1'; + +/** + * Derives a child key pair deterministically from a parent private key. + * Index must be >= 0. Same index always yields the same key pair. + * Uses HMAC-SHA256(parentKey, domain || index) then reduces to valid secp256k1 scalar. + */ +export function deriveChildKeyPair( + parentPrivateKeyHex: string, + index: number, +): KeyPair { + const parentBytes = hexToBytes(parentPrivateKeyHex); + const indexBytes = new TextEncoder().encode(`${DERIVE_DOMAIN}-${index}`); + const h = hmac(sha256, parentBytes, indexBytes); + const num = BigInt('0x' + bytesToHex(h)); + const scalar = (num % (SECP256K1_ORDER - 1n)) + 1n; + const privHex = scalar.toString(16).padStart(64, '0'); + const privateKeyBytes = hexToBytes(privHex); + const publicKey = bytesToHex(getPublicKey(privateKeyBytes, true)); + return { privateKey: privHex, publicKey }; +} + +/** + * Returns the main public key plus derived public keys for indices 0..count-1. + * Index 0 is the first derived key, etc. Total length is 1 + count. + */ +export function getDerivedPublicKeys( + parentPrivateKeyHex: string, + count: number, +): string[] { + const main = publicKeyFromPrivateKey(parentPrivateKeyHex); + const out: string[] = [main]; + for (let i = 0; i < count; i++) { + const { publicKey } = deriveChildKeyPair(parentPrivateKeyHex, i); + out.push(publicKey); + } + return out; +} + +/** + * Returns true if the given public key is the main key or any derived key (indices 0..maxDerived-1). + * Fast: main key check O(1), then at most maxDerived derivations. Pass maxDerived to limit search. + */ +export function publicKeyBelongsToIdentity( + identityPrivateKeyHex: string, + publicKeyHex: string, + maxDerived: number = 0, +): boolean { + const main = publicKeyFromPrivateKey(identityPrivateKeyHex); + if (main === publicKeyHex) { + return true; + } + for (let i = 0; i < maxDerived; i++) { + const { publicKey } = deriveChildKeyPair(identityPrivateKeyHex, i); + if (publicKey === publicKeyHex) { + return true; + } + } + return false; +} + /** * Signs a message with a private key using secp256k1. * The message is hashed with SHA-256 before signing. diff --git a/userwallet/src/utils/pairing.ts b/userwallet/src/utils/pairing.ts index 52cb145..68b4586 100644 --- a/userwallet/src/utils/pairing.ts +++ b/userwallet/src/utils/pairing.ts @@ -4,6 +4,10 @@ import { publicKeyToBip32Words, bip32WordsToPublicKey, } from './bip32'; +import { + getDerivedPublicKeys, + publicKeyBelongsToIdentity, +} from './crypto'; import type { PairConfig } from '../types/identity'; const STORAGE_KEY_PAIRS = 'userwallet_pairs'; @@ -192,3 +196,84 @@ export function updatePairPublicKey(pairUuid: string, publicKey: string): void { pair.publicKey = publicKey; storePairs(pairs); } + +/** + * Update a pair's persistent label (for display). + */ +export function updatePairLabel(pairUuid: string, label: string): void { + const pairs = getStoredPairs(); + const pair = pairs.find((p) => p.uuid === pairUuid); + if (pair === undefined) { + return; + } + pair.label = label; + storePairs(pairs); +} + +/** + * Returns all public keys for a pair. For local pair, pass identity private key to include + * derived keys (main + indices 0..derivedCount-1). For remote, returns publicKey + publicKeys. + */ +export function getPairPublicKeys( + pair: PairConfig, + identityPrivateKeyHex?: string, + derivedCount: number = 0, +): string[] { + if (pair.is_local && identityPrivateKeyHex !== undefined) { + return getDerivedPublicKeys(identityPrivateKeyHex, derivedCount); + } + const keys: string[] = []; + if (pair.publicKey !== undefined) { + keys.push(pair.publicKey); + } + if (pair.publicKeys !== undefined) { + for (const k of pair.publicKeys) { + if (k !== pair.publicKey && !keys.includes(k)) { + keys.push(k); + } + } + } + return keys; +} + +/** + * Returns true if the pair "owns" the given public key. Fast for local pair when identity + * private key is passed (main key O(1), then at most maxDerived derivations). + */ +export function pairContainsPublicKey( + pair: PairConfig, + publicKeyHex: string, + identityPrivateKeyHex?: string, + maxDerived: number = 0, +): boolean { + if (pair.is_local && identityPrivateKeyHex !== undefined) { + return publicKeyBelongsToIdentity( + identityPrivateKeyHex, + publicKeyHex, + maxDerived, + ); + } + if (pair.publicKey === publicKeyHex) { + return true; + } + return pair.publicKeys?.includes(publicKeyHex) ?? false; +} + +/** + * Add a derived public key to a pair (appends to publicKeys). Does not duplicate publicKey. + */ +export function addPairPublicKey(pairUuid: string, publicKeyHex: string): void { + const pairs = getStoredPairs(); + const pair = pairs.find((p) => p.uuid === pairUuid); + if (pair === undefined) { + return; + } + const existing = pair.publicKey === publicKeyHex || pair.publicKeys?.includes(publicKeyHex); + if (existing) { + return; + } + const next = pair.publicKeys ?? []; + next.push(publicKeyHex); + pair.publicKeys = next; + storePairs(pairs); +} diff --git a/userwallet/vite.config.ts b/userwallet/vite.config.ts index f8ca8e8..7604830 100644 --- a/userwallet/vite.config.ts +++ b/userwallet/vite.config.ts @@ -9,7 +9,7 @@ export default defineConfig({ }, server: { port: 3018, - strictPort: false, + strictPort: true, }, preview: { port: 3018, diff --git a/verify-chain-alignment.sh b/verify-chain-alignment.sh new file mode 100755 index 0000000..b2b120f --- /dev/null +++ b/verify-chain-alignment.sh @@ -0,0 +1,98 @@ +#!/bin/bash +# +# Verify that Dashboard, Miner and Signet node are aligned on the same chain +# (expected height ~11535). Run on the bitcoin machine (192.168.1.105). +# +# Usage: ./verify-chain-alignment.sh +# +# Author: 4NK Team +# Date: 2026-02-02 + +set -e + +CONTAINER_NAME="bitcoin-signet-instance" +DASHBOARD_URL="${DASHBOARD_URL:-http://localhost:3020}" +EXPECTED_HEIGHT_MIN="${EXPECTED_HEIGHT_MIN:-11000}" +EXPECTED_HEIGHT_MAX="${EXPECTED_HEIGHT_MAX:-12000}" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" +DASHBOARD_TMP="$SCRIPT_DIR/.verify-chain-dashboard-$$.json" +trap 'rm -f "$DASHBOARD_TMP"' EXIT + +echo "=== Chain alignment check (Dashboard / Miner / Signet) ===" +echo "" + +# 1. Node: getblockchaininfo (datadir = BITCOIN_DIR from container, default /root/.bitcoin) +if ! docker ps --format "{{.Names}}" | grep -q "^${CONTAINER_NAME}$"; then + echo "FAIL: Container $CONTAINER_NAME is not running." + exit 1 +fi +BITCOIN_DATADIR=$(docker exec "$CONTAINER_NAME" printenv BITCOIN_DIR 2>/dev/null || echo "/root/.bitcoin") + +NODE_JSON=$(sudo docker exec "$CONTAINER_NAME" bitcoin-cli -datadir="$BITCOIN_DATADIR" getblockchaininfo 2>/dev/null || true) +if [ -z "$NODE_JSON" ]; then + echo "FAIL: Could not get getblockchaininfo from node." + exit 1 +fi + +NODE_CHAIN=$(echo "$NODE_JSON" | grep -o '"chain"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*:[[:space:]]*"\([^"]*\)".*/\1/') +NODE_BLOCKS=$(echo "$NODE_JSON" | grep -o '"blocks"[[:space:]]*:[[:space:]]*[0-9]*' | grep -o '[0-9]*$') + +if [ -z "$NODE_BLOCKS" ]; then + echo "FAIL: Could not parse blocks from node getblockchaininfo." + exit 1 +fi + +echo "Node (signet): chain=$NODE_CHAIN blocks=$NODE_BLOCKS" + +if [ "$NODE_CHAIN" != "signet" ]; then + echo "WARN: Node chain is not 'signet' (got '$NODE_CHAIN')." +fi + +# 2. Dashboard: /api/blockchain/info +DASHBOARD_HTTP_CODE=$(curl -s -o "$DASHBOARD_TMP" -w "%{http_code}" "$DASHBOARD_URL/api/blockchain/info" 2>/dev/null || echo "000") +DASHBOARD_JSON="" +if [ -f "$DASHBOARD_TMP" ]; then + DASHBOARD_JSON=$(cat "$DASHBOARD_TMP") +fi + +if [ "$DASHBOARD_HTTP_CODE" = "403" ]; then + echo "Dashboard: (403 Forbidden - if ALLOWED_SOURCE_IP is set, use DASHBOARD_URL from proxy or allowed host)" +elif [ -n "$DASHBOARD_JSON" ] && [ "$DASHBOARD_HTTP_CODE" = "200" ]; then + DASHBOARD_BLOCKS=$(echo "$DASHBOARD_JSON" | grep -o '"blocks"[[:space:]]*:[[:space:]]*[0-9]*' | grep -o '[0-9]*$') + if [ -n "$DASHBOARD_BLOCKS" ]; then + echo "Dashboard: blocks=$DASHBOARD_BLOCKS" + if [ "$DASHBOARD_BLOCKS" != "$NODE_BLOCKS" ]; then + echo "WARN: Dashboard blocks ($DASHBOARD_BLOCKS) != Node blocks ($NODE_BLOCKS)." + else + echo "OK: Dashboard and Node show same height." + fi + else + echo "Dashboard: (could not parse blocks)" + fi +else + if [ "$DASHBOARD_HTTP_CODE" != "000" ]; then + echo "Dashboard: (HTTP $DASHBOARD_HTTP_CODE at $DASHBOARD_URL)" + else + echo "Dashboard: (not reachable at $DASHBOARD_URL)" + fi +fi + +# 3. Expected height range (~11535) +echo "" +if [ -n "$NODE_BLOCKS" ] && [ "$NODE_BLOCKS" -eq "$NODE_BLOCKS" ] 2>/dev/null; then + if [ "$NODE_BLOCKS" -ge "$EXPECTED_HEIGHT_MIN" ] && [ "$NODE_BLOCKS" -le "$EXPECTED_HEIGHT_MAX" ]; then + echo "OK: Node height $NODE_BLOCKS is in expected range [$EXPECTED_HEIGHT_MIN..$EXPECTED_HEIGHT_MAX]." + else + echo "WARN: Node height $NODE_BLOCKS is outside expected range [$EXPECTED_HEIGHT_MIN..$EXPECTED_HEIGHT_MAX]." + echo " If the chain was lost (e.g. container recreated without persistent volume), see:" + echo " fixKnowledge/signet-chain-lost-volume-persistent.md" + echo " restore-signet-from-backup.sh" + fi +else + echo "WARN: Could not validate node height (got '$NODE_BLOCKS')." +fi + +echo "" +echo "Done." diff --git a/verify-dashboard-signet.sh b/verify-dashboard-signet.sh new file mode 100755 index 0000000..cd5b9fc --- /dev/null +++ b/verify-dashboard-signet.sh @@ -0,0 +1,117 @@ +#!/bin/bash +# +# Verify that https://dashboard.certificator.4nkweb.com/ shows the custom signet +# (~11535 blocks). Run on the machine bitcoin (192.168.1.105). +# +# Checks: +# 1. Node RPC (127.0.0.1:38332) returns getblockchaininfo with ~11535 blocks +# 2. Dashboard service (signet-dashboard) is running and uses same RPC +# 3. Dashboard API /api/blockchain/info returns blocks +# +# Usage: ./verify-dashboard-signet.sh +# +# Author: 4NK Team +# Date: 2026-02-02 + +set -e + +RPC_HOST="${BITCOIN_RPC_HOST:-127.0.0.1}" +RPC_PORT="${BITCOIN_RPC_PORT:-38332}" +RPC_USER="${BITCOIN_RPC_USER:-bitcoin}" +RPC_PASS="${BITCOIN_RPC_PASSWORD:-bitcoin}" +DASHBOARD_URL="${DASHBOARD_URL:-http://localhost:3020}" +EXPECTED_MIN="${EXPECTED_HEIGHT_MIN:-11000}" +EXPECTED_MAX="${EXPECTED_HEIGHT_MAX:-12000}" + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +echo "=== Verify dashboard shows custom signet ===" +echo "RPC: $RPC_HOST:$RPC_PORT Dashboard: $DASHBOARD_URL" +echo "" + +# 0. One node only: bitcoin-signet-instance on 38332 (same as Mempool host.docker.internal:38332) +echo "0. Checking single node (bitcoin-signet-instance = Mempool host.docker.internal:$RPC_PORT)..." +if command -v docker &>/dev/null && docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^bitcoin-signet-instance$'; then + if ss -tlnp 2>/dev/null | grep -q ":${RPC_PORT}"; then + echo " OK: Port $RPC_PORT in use; bitcoin-signet-instance is running (same node as Mempool)." + else + echo " WARN: bitcoin-signet-instance running but port $RPC_PORT not found in ss (check mapping)." + fi + # Prefer named volume signet-bitcoin-data (same as restore-signet-from-backup.sh and update-signet.sh) + if docker inspect bitcoin-signet-instance --format '{{range .Mounts}}{{.Name}} {{end}}' 2>/dev/null | grep -q 'signet-bitcoin-data'; then + echo " OK: Container uses volume signet-bitcoin-data (persistent chain)." + else + echo " WARN: Container not using volume signet-bitcoin-data (chain may be lost on container replace). Run update-signet.sh or restore-signet-from-backup.sh for correct config." + fi +else + echo " WARN: bitcoin-signet-instance not running; another process may be on $RPC_PORT (verify it is the signet node)." +fi +echo "" + +# 1. Test RPC (same node as Mempool / Dashboard) +echo "1. Testing node RPC ($RPC_HOST:$RPC_PORT)..." +RPC_JSON=$(curl -s --user "$RPC_USER:$RPC_PASS" \ + --data-binary '{"jsonrpc":"1.0","id":"test","method":"getblockchaininfo","params":[]}' \ + -H "content-type: text/plain;" "http://${RPC_HOST}:${RPC_PORT}/" 2>/dev/null || true) +BLOCKS=$(echo "$RPC_JSON" | grep -o '"blocks"[[:space:]]*:[[:space:]]*[0-9]*' | head -1 | grep -o '[0-9]*$' || echo "") +if [ -z "$BLOCKS" ]; then + echo " FAIL: Cannot reach node at $RPC_HOST:$RPC_PORT (dashboard will show '-'). + Ensure: bitcoin-signet-instance or bitcoind is running and listening on 38332." + exit 1 +fi +echo " Node: blocks=$BLOCKS (expected ~11535)" +if [ "$BLOCKS" -ge "$EXPECTED_MIN" ] && [ "$BLOCKS" -le "$EXPECTED_MAX" ]; then + echo " OK: Height in expected range (custom signet, same as Mempool)." +else + echo " WARN: Height outside [$EXPECTED_MIN..$EXPECTED_MAX]. Dashboard will show this height." +fi +echo "" + +# 2. Dashboard service +echo "2. Checking signet-dashboard service..." +if systemctl is-active --quiet signet-dashboard 2>/dev/null; then + echo " OK: signet-dashboard is running." +else + echo " FAIL: signet-dashboard is not running. + Start: sudo systemctl start signet-dashboard" + exit 1 +fi +echo "" + +# 3. Dashboard API +echo "3. Checking Dashboard API ($DASHBOARD_URL/api/blockchain/info)..." +TMP_JSON="$SCRIPT_DIR/.verify-dashboard-$$.json" +trap 'rm -f "$TMP_JSON"' EXIT +DASH_HTTP=$(curl -s -o "$TMP_JSON" -w "%{http_code}" "$DASHBOARD_URL/api/blockchain/info" 2>/dev/null || echo "000") +if [ "$DASH_HTTP" = "200" ]; then + DASH_BLOCKS=$(grep -o '"blocks"[[:space:]]*:[[:space:]]*[0-9]*' "$TMP_JSON" | head -1 | grep -o '[0-9]*$' || echo "") + if [ -n "$DASH_BLOCKS" ]; then + echo " OK: Dashboard API returns blocks=$DASH_BLOCKS." + if [ "$DASH_BLOCKS" -ge "$EXPECTED_MIN" ] && [ "$DASH_BLOCKS" -le "$EXPECTED_MAX" ]; then + echo " OK: Height in expected range (custom signet)." + else + echo " WARN: Height $DASH_BLOCKS outside [$EXPECTED_MIN..$EXPECTED_MAX]." + fi + else + echo " WARN: Could not parse blocks from Dashboard API." + fi +else + if [ "$DASH_HTTP" = "403" ]; then + echo " INFO: Dashboard returned 403 (ALLOWED_SOURCE_IP: call from proxy or allowed host). + From proxy: curl https://dashboard.certificator.4nkweb.com/api/blockchain/info" + else + echo " FAIL: Dashboard API returned HTTP $DASH_HTTP. + Check: sudo journalctl -u signet-dashboard -n 50" + exit 1 + fi +fi +echo "" + +echo "=== Summary ===" +echo "For https://dashboard.certificator.4nkweb.com/ to show the custom signet:" +echo " - Proxy nginx: dashboard.certificator.4nkweb.com -> 192.168.1.105:3020 (configure-nginx-proxy.sh)" +echo " - Machine 105: signet-dashboard listening on 192.168.1.105:3020, BITCOIN_RPC_HOST=127.0.0.1, BITCOIN_RPC_PORT=38332" +echo " - Node on 105: listening on 0.0.0.0:38332 (bitcoin.conf: signet=1, rpcbind=0.0.0.0:38332)" +echo " - Run fix: ./fix-dashboard-anchor-chain.sh (tests RPC, restarts dashboard and anchorage)" +echo "" diff --git a/website-skeleton/cryptographie.html b/website-skeleton/cryptographie.html index e6a90bf..029ec42 100644 --- a/website-skeleton/cryptographie.html +++ b/website-skeleton/cryptographie.html @@ -199,6 +199,51 @@
+

Dérivation de clés et clés multiples par pair

+

+ Chaque pair (appareil local ou distant) peut être associé à plusieurs clés publiques : + une clé principale et, optionnellement, une liste de clés supplémentaires. Cela permet d’utiliser + plusieurs clés pour un même pair sans multiplier les secrets à gérer. +

+

Dérivation déterministe à partir d’une clé privée

+

+ À partir d’une seule clé privée (celle de l’identité), le système peut calculer d’autres paires + clé privée / clé publique de façon déterministe : pour un index entier (0, 1, 2, …), + le calcul produit toujours la même paire. Aucune donnée aléatoire n’est utilisée pour cette dérivation. +

+
    +
  • Entrée : la clé privée parente (64 caractères hexadécimaux) et un index (entier ≥ 0).
  • +
  • Procédé : une fonction de dérivation (HMAC-SHA256 avec un domaine fixe et l’index) + produit 32 octets, puis une réduction modulo l’ordre de la courbe secp256k1 donne un nombre + valide comme clé privée sur la courbe ; la clé publique enfant est obtenue par multiplication + du point de base de la courbe par ce nombre (format compressé, 66 caractères hex).
  • +
  • Sortie : une paire (clé privée enfant, clé publique enfant). La clé « principale » + est celle dérivée directement de la clé privée de l’identité (sans index enfant).
  • +
+

+ Ainsi, une même identité peut exposer plusieurs clés publiques (la principale et les dérivées 0, 1, 2, …) + tout en ne stockant qu’une seule clé privée ; les autres sont recalculées à la demande. +

+

Vérification rapide : une clé publique appartient-elle à mon identité ?

+

+ Pour savoir si une clé publique donnée correspond à votre clé privée (identité), le système fait : +

+
    +
  1. Calculer la clé publique à partir de votre clé privée (une opération sur la courbe).
  2. +
  3. Comparer le résultat à la clé publique donnée. Si elles sont égales, la clé appartient à votre identité.
  4. +
  5. Si on autorise les clés dérivées : calculer les clés publiques dérivées pour les indices 0, 1, 2, … + jusqu’à une borne fixée, et comparer chacune à la clé donnée. Dès qu’une égalité est trouvée, la réponse est oui.
  6. +
+

+ Le coût est donc une seule opération pour la clé principale, puis au plus N opérations si on teste + N clés dérivées. En limitant N (par exemple 100), la vérification reste rapide. +

+

+ Pour un pair distant (autre appareil), les clés sont celles enregistrées pour ce pair (clé principale + et liste optionnelle) : la vérification consiste à tester si la clé donnée est dans cet ensemble, + sans dérivation côté local. +

+

Le workflow complet

Voici ce qui se passe quand vous envoyez un message sécurisé, étape par étape : @@ -414,12 +459,14 @@

Fichiers source

  • userwallet/src/utils/encryption.ts : encryptWithECDH, decryptWithECDH
  • -
  • userwallet/src/utils/crypto.ts : signMessage, verifySignature, generateChallenge
  • +
  • userwallet/src/utils/crypto.ts : signMessage, verifySignature, generateChallenge, deriveChildKeyPair, getDerivedPublicKeys, publicKeyBelongsToIdentity
  • +
  • userwallet/src/utils/pairing.ts : getPairPublicKeys, pairContainsPublicKey, addPairPublicKey
  • userwallet/src/utils/relay.ts : postMessageChiffre, postSignature, postKey
  • userwallet/src/utils/collectSignatures.ts : runCollectLoop, fetchSignaturesForHash
  • userwallet/src/utils/loginValidation.ts : hasEnoughSignatures, checkDependenciesSatisfied
  • service-login-verify/ : verifyLoginProof (côté parent)
+

Dérivation de clés et clés multiples par pair : docs/USERWALLET_KEY_DERIVATION.md.

Collecte des signatures