Daily backup to git cron, backup/restore scripts, docs

**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
This commit is contained in:
ncantu 2026-02-04 03:07:57 +01:00
parent f9fe0e3419
commit 937646cc45
71 changed files with 2802 additions and 610 deletions

2
.gitignore vendored
View File

@ -1,2 +1,4 @@
.env
backups/
data/backup-to-git.log
.verify-chain-dashboard-*.json

View File

@ -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

View File

@ -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

View File

@ -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',

View File

@ -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

View File

@ -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());

View File

@ -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

View File

@ -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

View File

@ -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());

View File

@ -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

View File

@ -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());

130
data/backup-to-git-cron.sh Executable file
View File

@ -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"

View File

@ -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

View File

@ -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...

View File

@ -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

View File

@ -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

View File

@ -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 denviron **11535 blocs**. Pour vérifier lalignement Dashboard / Miner / Signet, voir [MAINTENANCE.md - Vérification de lalignement](./MAINTENANCE.md#vérification-de-lalignement-dashboard--miner--signet-chaîne-11535).
### Vérifier que le dashboard fonctionne
```bash

View File

@ -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 nacceptent les flux que **reçus de la machine proxy (192.168.1.100)** :
- **Bind** : chaque service écoute sur ladresse 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 nest 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

View File

@ -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 naccepter 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 naccepter 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 naccepter 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 naccepter 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 naccepter 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` à ladresse 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) nest 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 :

View File

@ -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 dune 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
```

View File

@ -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)

View File

@ -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 <co
## Gestion du Conteneur
### Persistance du datadir (chaîne Bitcoin)
**Important :** Sans volume persistant, la recréation du conteneur (`docker rm` puis `docker run`) supprime les données Bitcoin et le nœud repart sur une **nouvelle chaîne** (hauteur 0). Pour conserver la chaîne (ex. hauteur ~11530), utilisez **toujours** un volume persistant.
**Volume par défaut (chaîne complète) :** Le script `update-signet.sh` utilise par défaut le volume contenant la chaîne Signet complète (~11530 blocs) s'il existe sur la machine : volume Docker d'ID **4b5dca4d940b9f6e5db67b460f40f230a5ef1195a3769e5f91fa02be6edde649** (variable `SIGNET_VOLUME_FULL_CHAIN` dans le script). Ainsi, après une mise à jour ou un redémarrage via `update-signet.sh`, le nœud repart sur la bonne chaîne sans recherche. Si ce volume n'existe pas, le script utilise le volume nommé `signet-bitcoin-data`.
- **Volume « chaîne complète »** (prioritaire si présent) : ID `4b5dca4d940b9f6e5db67b460f40f230a5ef1195a3769e5f91fa02be6edde649`
- **Volume nommé** (sinon) : `-v signet-bitcoin-data:/root/.bitcoin`
- **Montage host** : `-v /chemin/sur/hote/signet-data:/root/.bitcoin` (créer le répertoire avant)
**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`). À utiliser pour restaurer la chaîne sur une autre machine ou après perte du volume. Voir [Sauvegarde et Restauration](#sauvegarde-et-restauration).
Les commandes `docker run` ci-dessous utilisent le volume approprié (voir `update-signet.sh` pour la logique par défaut).
### Démarrage
```bash
cd /home/ncantu/Bureau/code/bitcoin
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 \
@ -256,6 +271,7 @@ sudo docker restart bitcoin-signet-instance
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
```
@ -607,7 +623,7 @@ Le script `update-signet.sh` effectue automatiquement :
4. **Sauvegarde automatique** : Données du conteneur et fichier `.env`
5. **Mise à jour du Dockerfile** : Modification de la version Bitcoin Core
6. **Reconstruction de l'image** : Build de la nouvelle image Docker
7. **Redémarrage du conteneur** : Arrêt propre et redémarrage avec la nouvelle version
7. **Redémarrage du conteneur** : Arrêt propre et redémarrage avec la nouvelle version (volume **signet-bitcoin-data** pour une seule chaîne Mempool/dashboard/APIs)
8. **Vérification post-mise à jour** : Contrôle de l'état du nœud
#### Procédure Manuelle de Mise à Jour
@ -671,9 +687,10 @@ sudo docker build -t bitcoin-signet .
sudo docker stop bitcoin-signet-instance
sudo docker rm bitcoin-signet-instance
# Démarrer avec la nouvelle image
# Démarrer avec la nouvelle image (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 \
@ -736,8 +753,9 @@ sudo docker exec bitcoin-signet-instance tar xzf /tmp/signet-backup-YYYYMMDD-HHM
# Reconstruire avec l'ancienne version
sudo docker build -t bitcoin-signet .
# Redémarrer
# Redé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
```
@ -850,8 +868,9 @@ sudo docker volume rm $(sudo docker volume ls -q | grep bitcoin)
# Ou supprimer manuellement les données
sudo rm -rf /var/lib/docker/volumes/*/bitcoin-signet-instance/_data
# Relancer avec un nouveau .env (clés seront régénérées)
# Relancer avec un nouveau .env (clés seront régénérées ; utiliser un nouveau volume ou supprimer signet-bitcoin-data avant)
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
```
@ -894,6 +913,39 @@ sudo docker run --env-file .env -d --name bitcoin-signet-instance \
### Sauvegarde
**Sauvegarde datadir (chaîne complète, prête à télécharger) :**
```bash
# Créer une archive complète du datadir (blocs + chainstate + config)
./save-signet-datadir-backup.sh
# Crée backups/signet-datadir-YYYYMMDD-HHMMSS.tar.gz et met à jour le symlink :
# backups/signet-datadir-latest.tar.gz -> 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 denviron **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 lAPI dancrage 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 quelle 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

View File

@ -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';

View File

@ -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 dune 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 dancrage
./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

View File

@ -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`.

View File

@ -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

View File

@ -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) davoir **plusieurs clés publiques** et de **dériver** de nouvelles paires de clés de façon déterministe à partir dune clé privée. Vérifier rapidement si une clé publique donnée « appartient » à une clé privée (clé principale ou lune 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 dobtenir 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 lidentité.
### API (userwallet)
| Fonction | Rôle |
|----------|------|
| `deriveChildKeyPair(parentPrivateKeyHex, index)` | Retourne la paire (privée, publique) pour lindex donné. |
| `getDerivedPublicKeys(parentPrivateKeyHex, count)` | Retourne [clé principale, dérivée(0), …, dérivée(count1)]. Longueur = 1 + count. |
| `publicKeyBelongsToIdentity(identityPrivateKeyHex, publicKeyHex, maxDerived?)` | Vrai si la clé publique est la clé principale ou lune des dérivées dindice 0..maxDerived1. 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..derivedCount1. 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 dappartenance à `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 quune clé appartient à lidentité** (avec au plus 100 dérivées) : `publicKeyBelongsToIdentity(identity.privateKey, somePubKey, 100)`.
- **Vérifier quun pair possède une clé** : `pairContainsPublicKey(pair, somePubKey, identity?.privateKey, 100)`.

View File

@ -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=<Add node IP, if applicable>
ADDNODE=<Add node IP:port, if applicable (not needed for single node on this machine)>
EXTERNAL_IP=<Your external IP, if applicable>

View File

@ -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

86
export-mining-wallet.sh Executable file
View File

@ -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 <this-file.json> 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 ""

View File

@ -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 dexé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 dexport 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 laccè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 danalyse
- 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 derreur `git clone` : créer le dépôt sur git.4nkweb.com.
- En cas derreur `git push` : vérifier credential helper / accès réseau.

View File

@ -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 dexposition (pas dIPv6, pas daccès direct depuis dautres 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 ladresse IPv4 de la machine
Chaque service écoute sur ladresse 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 denvironnement `ALLOWED_SOURCE_IP=192.168.1.100` est définie, le service rejette toute requête dont ladresse source (après normalisation IPv6-mapped → IPv4) nest 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 à lIP 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 nest 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é ; cest 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 danalyse
- Vérifier quaucun service nécoute sur `[::]` : `ss -tlnp` / `netstat -tlnp` sur chaque machine.
- Vérifier que les services écoutent sur lIP LAN attendue : `ss -tlnp | grep <port>`.
- 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.

91
fix-dashboard-anchor-chain.sh Executable file
View File

@ -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 ""

View File

@ -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 lAPI dancrage renvoie HTTP 503, avec des messages du type « Health check a échoué (HTTP 503) » ou « LAPI dancrage nest 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 lAPI comme indisponible sans distinguer « processus arrêté » et « pas prêt » (Bitcoin déconnecté, UTXOs verrouillés).
- Lutilisateur 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 lauth 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 lAPI dancrage 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 na pas la structure health (mutex, utxos, bitcoin), affichage dun message derreur 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 dexception (réseau, parse), affichage de « LAPI dancrage nest pas accessible » avec le message derreur.
## 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 lAPI 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 lAPI est de nouveau joignable.
## Modalités danalyse
- 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`.

View File

@ -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 denviron 11535 blocs.
- **Clients de lAPI dancrage** : reçoivent `{"error":"Insufficient Balance","message":"Insufficient balance. Required: 0.00001 BTC, Available: 0 BTC"}` et lAPI dancrage ne fonctionne pas correctement.
## Impacts
- Les utilisateurs ne voient pas létat réel de la blockchain.
- Lancrage de documents échoue (solde 0 côté nœud utilisé par lAPI).
## Cause
Une seule cause racine couvre les deux symptômes : **le nœud Bitcoin Signet auquel se connectent le Dashboard et lAPI dancrage na pas la bonne chaîne ou na 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 lAPI dancrage 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 ny 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 lAPI nest 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 dancrage, lancer la vérification dalignement
./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 quil ne sagit pas dun 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 lAPI dancrage
- **Dashboard** : doit être sur la **machine bitcoin (192.168.1.105)**. Vérifier le service `signet-dashboard` sur cette machine.
- **API dancrage** (`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 na 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 lhôte.
Pour corriger la machine dont le nœud na que quelques blocs (ex. 6) : soit **restaurer** depuis une archive issue de 105 ou de `backups/` sous ncantu, soit **pointer** le Dashboard / lAPI dancrage vers le RPC du nœud sur 105 (ex. `BITCOIN_RPC_HOST=192.168.1.105`) si larchitecture 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 larchive 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 lalignement 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 lAPI :
- Le miner utilise en général le wallet `custom_signet`.
- LAPI dancrage appelle `getBalance()` sans nom de wallet → utilise le **wallet par défaut** du nœud.
- Si le nœud a plusieurs wallets, sassurer 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 danalyse
- Consulter les logs du Dashboard : `sudo journalctl -u signet-dashboard -f` (erreurs RPC).
- Consulter les logs de lAPI dancrage : `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)

View File

@ -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)

View File

@ -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

View File

@ -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 nest 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 labsence 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 nest 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 larchive 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 nexiste** : la chaîne à 11535 nest plus récupérable sur ce nœud. Il faut quun autre nœud (ex. machine bitcoin) ait encore cette chaîne et quon en tire une sauvegarde avec `save-signet-datadir-backup.sh`, puis quon 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 danalyse
- 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 <chemin-vers-archive.tar.gz>`.
## 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)

99
import-mining-wallet.sh Executable file
View File

@ -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 <path-to-export.json> [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 <path-to-export.json> [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 ""

View File

@ -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

11
miner
View File

@ -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):

View File

@ -0,0 +1,76 @@
# Miner Bitcoin Signet Documentation
**Auteur** : Équipe 4NK
**Date** : 2026-02-02
**Version** : 1.0
## Vue densemble
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 dentré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 nest dans `miner_imports/` : lexé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 denvironnement **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. Limport 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.)

65
restore-signet-from-backup.sh Executable file
View File

@ -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 <path-to-backup.tar.gz>
# 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 <path-to-backup.tar.gz>}"
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 ""

59
save-signet-datadir-backup.sh Executable file
View File

@ -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 ""

View File

@ -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

View File

@ -1668,6 +1668,34 @@
</div>
</section>
<!-- Endpoint: Mining Status -->
<section class="api-docs-section">
<div class="endpoint-card">
<div class="endpoint-header">
<span class="method-badge method-get">GET</span>
<span class="endpoint-path">/api/mining/status</span>
</div>
<div class="endpoint-description">
<p>État du miner : inféré depuis lâge du dernier bloc. <code>active</code> si le dernier bloc a moins de 30 minutes, sinon <code>inactive</code>.</p>
<p><strong>Base URL :</strong> <code>https://dashboard.certificator.4nkweb.com</code></p>
</div>
<div class="response-example">
<h4>Réponse (200 OK)</h4>
<div class="code-block">
<pre>{
"status": "active",
"blocks": 11535,
"lastBlockTime": 1769730315,
"lastBlockAgeSeconds": 120,
"message": "Dernier bloc récent, miner probablement actif"
}</pre>
</div>
</div>
</div>
</section>
<!-- Endpoint: Mining Avg Block Time -->
<section class="api-docs-section">
<div class="endpoint-card">

View File

@ -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
*/

View File

@ -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 = `<div class="error">✗ [anchor-api] Health check a échoué (HTTP ${response.status}). ${reason}</div>`;
unlockButton.style.display = 'none';
return;
}
let html = '<div class="health-status">';
// É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 += `<div class="health-item">
<label>État général</label>
<div class="value ${statusClass}">${health.ok ? '✓ OK' : '✗ Problème'}</div>
<div class="value ${statusClass}">${statusText}</div>
</div>`;
// Mutex
@ -510,7 +515,7 @@
} catch (error) {
console.error('Error loading health status:', error);
healthDiv.innerHTML = `<div class="error">Erreur lors du chargement de l'état : ${error.message}</div>`;
healthDiv.innerHTML = `<div class="error">✗ [anchor-api] L'API d'ancrage n'est pas accessible. Erreur : ${error.message}</div>`;
unlockButton.style.display = 'none';
} finally {
refreshButton.disabled = false;

View File

@ -34,6 +34,7 @@
</header>
<main>
<div id="chain-warning" class="chain-warning" style="display: none;" role="alert"></div>
<!-- Section Supervision -->
<section class="supervision-section">
<h2>État de la Blockchain</h2>
@ -80,6 +81,10 @@
<h3>Difficulté de Minage</h3>
<p class="value" id="mining-difficulty">-</p>
</div>
<div class="card">
<h3>État du Miner</h3>
<p class="value" id="miner-status">-</p>
</div>
<div class="card">
<h3>Temps Moyen entre Blocs</h3>
<p class="value" id="avg-block-time">

View File

@ -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</div>
</div>
</section>
<!-- Section Paiement Wallet de Mining -->
<section class="join-section">
<div class="payment-section">
<h2>💳 Accès au Wallet de Mining</h2>
<p>Pour recevoir le wallet de mining et les clés nécessaires pour miner sur le réseau, effectuez un paiement de :</p>
<div class="payment-amount">0,0065 BTC</div>
<p>Envoyez le paiement via Nostr à :</p>
<div class="payment-address" id="wallet-npub">npub18s03s39fa80ce2n3cmm0zme3jqehc82h6ld9sxq03uejqm3d05gsae0fuu</div>
<button class="copy-button" onclick="copyNpub('wallet-npub')">📋 Copier la npub</button>
<a href="https://yakihonne.com/profile/fancy-wallaby-90@rizful.com" target="_blank" rel="noopener noreferrer" class="nostr-profile-link">
🔗 Voir le profil Nostr
</a>
<div class="wallet-checkbox">
<input type="checkbox" id="wallet-request" onchange="updatePaymentMessage('wallet')">
<label for="wallet-request">Je souhaite recevoir le wallet de mining après le paiement</label>
</div>
<div class="info-box" id="wallet-payment-info">
<p><strong>Instructions :</strong></p>
<p>1. Effectuez le paiement de 0,0065 BTC via Nostr à la npub ci-dessus</p>
<p>2. Cochez la case ci-dessus si vous souhaitez recevoir le wallet de mining</p>
<p>3. Après confirmation du paiement, vous recevrez le wallet de mining sur Nostr</p>
</div>
<div class="success-message" id="wallet-payment-success">
<p><strong>✅ Paiement reçu !</strong></p>
<p>Votre demande a été enregistrée. Vous recevrez le wallet de mining sur Nostr sous peu.</p>
</div>
</div>
</section>
<!-- Section Paiement Clé API -->
<section class="join-section">
<div class="payment-section">
<h2>🔑 Accès à une Clé API</h2>
<p>Pour recevoir une clé API permettant d'utiliser les services d'ancrage et de filigrane, effectuez un paiement de :</p>
<div class="payment-amount">0,0065 BTC</div>
<p>Envoyez le paiement via Nostr à :</p>
<div class="payment-address" id="api-npub">npub18s03s39fa80ce2n3cmm0zme3jqehc82h6ld9sxq03uejqm3d05gsae0fuu</div>
<button class="copy-button" onclick="copyNpub('api-npub')">📋 Copier la npub</button>
<a href="https://yakihonne.com/profile/fancy-wallaby-90@rizful.com" target="_blank" rel="noopener noreferrer" class="nostr-profile-link">
🔗 Voir le profil Nostr
</a>
<div class="wallet-checkbox">
<input type="checkbox" id="api-request" onchange="updatePaymentMessage('api')">
<label for="api-request">Je souhaite recevoir la clé API après le paiement</label>
</div>
<div class="info-box" id="api-payment-info">
<p><strong>Instructions :</strong></p>
<p>1. Effectuez le paiement de 0,0065 BTC via Nostr à la npub ci-dessus</p>
<p>2. Cochez la case ci-dessus si vous souhaitez recevoir la clé API</p>
<p>3. Après confirmation du paiement, vous recevrez la clé API sur Nostr</p>
</div>
<div class="success-message" id="api-payment-success">
<p><strong>✅ Paiement reçu !</strong></p>
<p>Votre demande a été enregistrée. Vous recevrez la clé API sur Nostr sous peu.</p>
</div>
</div>
</section>
<!-- Section Informations Supplémentaires -->
<section class="join-section">
<div class="wallet-section">
@ -333,17 +173,6 @@ addnode=anchorage.certificator.4nkweb.com:38333</div>
</ul>
</div>
<div class="info-box">
<p><strong>Que se passe-t-il après le paiement ?</strong></p>
<p>Une fois le paiement confirmé, vous recevrez sur Nostr :</p>
<ul style="margin-left: 20px; margin-top: 10px;">
<li>Les fichiers de configuration complets</li>
<li>La clé privée du signet (si vous avez coché la case pour le wallet de mining)</li>
<li>La clé API (si vous avez coché la case pour la clé API)</li>
<li>Les instructions détaillées pour démarrer votre nœud ou utiliser l'API</li>
</ul>
</div>
<div class="info-box">
<p><strong>Besoin d'aide ?</strong></p>
<p>Pour toute question, consultez la documentation complète dans le dépôt GitHub ou contactez l'équipe.</p>
@ -363,10 +192,6 @@ addnode=anchorage.certificator.4nkweb.com:38333</div>
</div>
<script>
const NOSTR_NPUB = 'npub18s03s39fa80ce2n3cmm0zme3jqehc82h6ld9sxq03uejqm3d05gsae0fuu';
const NOSTR_PROFILE_URL = 'https://yakihonne.com/profile/fancy-wallaby-90@rizful.com';
const PAYMENT_AMOUNT = 0.0065;
function copyConfig() {
const configText = document.getElementById('bitcoin-config')?.textContent || '';
navigator.clipboard.writeText(configText).then(() => {
@ -383,70 +208,6 @@ addnode=anchorage.certificator.4nkweb.com:38333</div>
alert('Erreur lors de la copie. Veuillez sélectionner et copier manuellement.');
});
}
function copyNpub(elementId) {
const npubElement = document.getElementById(elementId);
const npub = npubElement?.textContent || NOSTR_NPUB;
navigator.clipboard.writeText(npub).then(() => {
const button = event?.target;
if (button) {
const originalText = button.textContent;
button.textContent = '✅ Copié !';
setTimeout(() => {
button.textContent = originalText;
}, 2000);
}
}).catch(err => {
console.error('Erreur lors de la copie:', err);
alert('Erreur lors de la copie. Veuillez sélectionner et copier manuellement.');
});
}
function updatePaymentMessage(type) {
if (type === 'wallet') {
const checkbox = document.getElementById('wallet-request');
const paymentInfo = document.getElementById('wallet-payment-info');
if (checkbox && paymentInfo) {
if (checkbox.checked) {
paymentInfo.innerHTML = `
<p><strong>Instructions :</strong></p>
<p>1. Effectuez le paiement de 0,0065 BTC via Nostr à la npub ci-dessus</p>
<p>2. ✅ Vous recevrez le wallet de mining après confirmation du paiement</p>
<p>3. Le wallet vous sera envoyé sur Nostr</p>
`;
} else {
paymentInfo.innerHTML = `
<p><strong>Instructions :</strong></p>
<p>1. Effectuez le paiement de 0,0065 BTC via Nostr à la npub ci-dessus</p>
<p>2. Cochez la case ci-dessus si vous souhaitez recevoir le wallet de mining</p>
<p>3. Après confirmation du paiement, vous recevrez le wallet de mining sur Nostr</p>
`;
}
}
} else if (type === 'api') {
const checkbox = document.getElementById('api-request');
const paymentInfo = document.getElementById('api-payment-info');
if (checkbox && paymentInfo) {
if (checkbox.checked) {
paymentInfo.innerHTML = `
<p><strong>Instructions :</strong></p>
<p>1. Effectuez le paiement de 0,0065 BTC via Nostr à la npub ci-dessus</p>
<p>2. ✅ Vous recevrez la clé API après confirmation du paiement</p>
<p>3. La clé API vous sera envoyée sur Nostr</p>
`;
} else {
paymentInfo.innerHTML = `
<p><strong>Instructions :</strong></p>
<p>1. Effectuez le paiement de 0,0065 BTC via Nostr à la npub ci-dessus</p>
<p>2. Cochez la case ci-dessus si vous souhaitez recevoir la clé API</p>
<p>3. Après confirmation du paiement, vous recevrez la clé API sur Nostr</p>
`;
}
}
}
}
</script>
</body>
</html>

View File

@ -390,6 +390,42 @@
Le bloc est ajouté à la blockchain, le mineur reçoit la récompense
</div>
</div>
<h3>Comment fonctionne le Miner ?</h3>
<div class="concept-explanation">
<p>Le <span class="highlight">miner</span> (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 quils sont valides.</p>
<p><strong>Sur le réseau principal Bitcoin</strong>, le miner :</p>
<ul>
<li>Obtient un <strong>modèle de bloc</strong> (block template) depuis son nœud Bitcoin Core : en-tête du bloc, transactions du mempool, difficulté cible</li>
<li>Sélectionne les transactions à inclure (souvent par ordre de frais par octet)</li>
<li>Construit le bloc (transaction coinbase + liste de transactions)</li>
<li>Cherche un <strong>nonce</strong> (et varie le coinbase si besoin) pour que le hash du bloc soit sous la cible de difficulté (proof-of-work)</li>
<li>Diffuse le bloc miné au réseau ; les autres nœuds le valident et lajoutent à leur chaîne</li>
</ul>
<p><strong>Sur Bitcoin Signet</strong> (réseau de test comme celui de ce dashboard), le principe est le même, mais la « preuve » change : au lieu dun proof-of-work coûteux, un bloc valide doit contenir une <strong>signature</strong> dune 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).</p>
</div>
<div class="flow-diagram">
<div class="flow-step">
<strong>1. Modèle de bloc</strong><br>
Le nœud (Bitcoin Core) fournit un template : bloc précédent, mempool, difficulté
</div>
<div class="flow-arrow"></div>
<div class="flow-step">
<strong>2. Construction du bloc</strong><br>
Le miner ajoute la transaction coinbase (récompense vers son adresse) et des transactions du mempool
</div>
<div class="flow-arrow"></div>
<div class="flow-step">
<strong>3. Preuve</strong><br>
Mainnet : recherche dun nonce (proof-of-work). Signet : signature du bloc avec la clé autorisée
</div>
<div class="flow-arrow"></div>
<div class="flow-step">
<strong>4. Diffusion</strong><br>
Le bloc valide est envoyé au réseau ; les nœuds le valident et lajoutent à la blockchain
</div>
</div>
</section>
<!-- Section: Entrée de Transaction -->

View File

@ -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;
}

View File

@ -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

View File

@ -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 });

72
test-mempool-rpc-config.sh Executable file
View File

@ -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

View File

@ -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

View File

@ -61,6 +61,6 @@ const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : <nouveau_port>;
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)

View File

@ -183,7 +183,7 @@ export function GlobalActionBar(): JSX.Element {
margin: '1rem 0',
}}
>
{mots.join(' ')}
{mots.map((mot, i) => `${i + 1}. ${mot}`).join(' ')}
</p>
) : (
<p style={{ margin: '1rem 0' }}>

View File

@ -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<PairConfig[]>(() => getStoredPairs());
const [addingDevice, setAddingDevice] = useState(false);
const relays = getStoredRelays();
const relayStatus = relays.length > 0 ? 'OK' : 'Non configuré';
const pairingSetupRef = useRef<HTMLElement | null>(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 (
<div role="status" aria-live="polite" aria-busy="true">
@ -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 (
<main>
<main style={{ width: '100%', maxWidth: '100%', minWidth: 0 }}>
{pairingConnected && (
<p role="status" style={{ fontWeight: 'bold', color: 'var(--color-ok, var(--color-success))' }}>
Connecté
</p>
)}
{showSetupBlock && (
<section
ref={pairingSetupRef}
id="pairing-setup"
aria-labelledby="pairing-setup-heading"
>
<PairingSetupBlock />
</section>
)}
<section
ref={pairingSetupRef}
id="pairing-setup"
aria-labelledby="pairing-section-heading"
style={{ width: '100%', maxWidth: '100%', minWidth: 0, boxSizing: 'border-box' }}
>
<h2 id="pairing-section-heading" style={{ marginTop: 0 }}>
Pairing
</h2>
<p>
<strong>Statut pairing</strong>
<br />
Requis: Oui
<br />
Satisfait: {pairingSatisfied ? 'Oui' : 'Non'}
</p>
{showSetupBlock && !addingDevice && (
<PairingSetupBlock onDone={refreshPairs} isAddingDevice={false} />
)}
{hasRemote && !addingDevice && (
<>
<h3 id="devices-paired-heading">Appareils appariés</h3>
<ul
aria-labelledby="devices-paired-heading"
style={{ listStyle: 'none', padding: 0, marginTop: '0.5rem' }}
>
{pairs.map((pair) => (
<li
key={pair.uuid}
style={{
display: 'flex',
alignItems: 'center',
gap: '0.75rem',
marginBottom: '0.5rem',
padding: '0.5rem',
border: '1px solid var(--color-border)',
borderRadius: '4px',
backgroundColor: 'var(--color-background)',
}}
>
<span
style={{
flex: '0 0 auto',
minWidth: '8rem',
fontSize: '0.875rem',
color: 'var(--color-text-secondary)',
}}
aria-hidden="true"
>
{getDeviceDisplayLabel(pair)}
</span>
<input
id={`pair-label-${pair.uuid}`}
type="text"
value={pair.label ?? ''}
onChange={(e) => 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 && (
<button
type="button"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
handleRemovePair(pair.uuid);
}}
aria-label={`Supprimer ${getDeviceDisplayLabel(pair)}`}
style={{
flexShrink: 0,
width: 'auto',
padding: '0.25rem 0.5rem',
fontSize: '0.875rem',
color: 'var(--color-error)',
backgroundColor: 'transparent',
border: '1px solid var(--color-error)',
borderRadius: '4px',
cursor: 'pointer',
}}
>
Supprimer
</button>
)}
</li>
))}
</ul>
<p style={{ marginTop: '0.75rem' }}>
<button
type="button"
onClick={() => setAddingDevice(true)}
aria-label="Ajouter un appareil"
>
Ajouter un device
</button>
</p>
</>
)}
{addingDevice && (
<>
<p>
<button
type="button"
onClick={() => setAddingDevice(false)}
aria-label="Annuler l'ajout d'un appareil"
>
Annuler
</button>
</p>
<PairingSetupBlock
onDone={() => {
setAddingDevice(false);
refreshPairs();
}}
isAddingDevice
/>
</>
)}
</section>
</main>
);
}

View File

@ -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 {
<h2 id="words-2nd-heading" style={{ marginTop: 0, marginBottom: '0.5rem' }}>
Mots du 2 appareil à copier sur le 1ʳ
</h2>
<p style={{ marginTop: 0, marginBottom: '0.5rem' }}>
<button
type="button"
onClick={handleGenerateAnotherKey}
aria-label="Générer une autre clé publique"
>
Générer une autre clé publique
</button>
</p>
{words2nd.length > 0 ? (
<div
aria-label="Mots 2e appareil"

View File

@ -6,6 +6,7 @@ import {
addRemotePairFromWords,
getStoredPairs,
parseAndValidatePairingWords,
removePair,
} from '../utils/pairing';
import { useIdentity } from '../hooks/useIdentity';
import { getStoredRelays } from '../utils/relay';
@ -19,9 +20,17 @@ function buildPairingDisplayUrl(): string {
return `${window.location.origin}${PAIRING_DISPLAY_PATH}`;
}
export function PairingSetupBlock(): JSX.Element {
export interface PairingSetupBlockProps {
onDone?: () => 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<string[]>([]);
const [qrDataUrl, setQrDataUrl] = useState<string | null>(null);
const [remoteWordsInput, setRemoteWordsInput] = useState<string[]>([]);
@ -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 (
<div role="region" aria-labelledby="pairing-setup-heading">
<h3 id="pairing-setup-heading">Configurer le pairing avec un 2 appareil</h3>
<div role="region" aria-labelledby="pairing-setup-block-heading">
<h3 id="pairing-setup-block-heading">{setupHeading}</h3>
{words.length > 0 && (
<>
{!hasCopiedToSecondDevice ? (
@ -128,6 +151,15 @@ export function PairingSetupBlock(): JSX.Element {
<p>
<strong>Mots du 1ʳ appareil</strong> à saisir sur le 2 (QR) :
</p>
<p style={{ marginTop: 0, marginBottom: '0.5rem' }}>
<button
type="button"
onClick={handleGenerateAnotherKey}
aria-label="Générer une autre clé publique"
>
Générer une autre clé publique
</button>
</p>
<div
aria-label="Mots 1er appareil"
style={{
@ -199,13 +231,17 @@ export function PairingSetupBlock(): JSX.Element {
type="button"
onClick={() => setHasCopiedToSecondDevice(true)}
>
J&apos;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"}
</button>
</p>
</>
) : (
<section aria-labelledby="remote-words-heading">
<h4 id="remote-words-heading">Mots du 2 appareil</h4>
<h4 id="remote-words-heading">
{isAddingDevice ? 'Mots du nouvel appareil' : 'Mots du 2ᵉ appareil'}
</h4>
<form
onSubmit={(ev) => void handleSubmitRemote(ev)}
aria-label="Saisir les mots du 2e appareil"

View File

@ -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 (
<div style={{ position: 'relative' }}>
<div style={{ marginBottom: '0.5rem' }}>
<button
type="button"
onClick={toggleAllVisibility}
aria-label={allVisible ? 'Masquer les mots' : 'Afficher les mots'}
title={allVisible ? 'Masquer les mots' : 'Afficher les mots'}
>
{allVisible ? 'Masquer les mots' : 'Afficher les mots'}
</button>
</div>
<div
role="group"
aria-label={ariaLabel}
style={{
display: 'grid',
gridTemplateColumns: 'repeat(4, 1fr)',
gridTemplateColumns: '1fr',
gap: '0.5rem',
marginBottom: focusedIndex !== null && suggestions.length > 0 ? '0.5rem' : '0',
}}
>
{words.map((word, index) => (
<div key={index} style={{ position: 'relative' }}>
<label
htmlFor={`${id ?? 'word'}-${index}`}
<div
style={{
display: 'block',
fontSize: '0.875rem',
marginBottom: '0.25rem',
color: 'var(--color-text-secondary, #666)',
display: 'flex',
alignItems: 'stretch',
gap: '0',
border: '1px solid var(--color-border, #ccc)',
borderRadius: '4px',
backgroundColor: 'var(--color-background)',
}}
>
{index + 1}
</label>
<div style={{ position: 'relative' }}>
<input
ref={(el) => {
inputRefs.current[index] = el;
}}
id={`${id ?? 'word'}-${index}`}
type={visibleWords.has(index) ? 'text' : 'password'}
value={word}
onChange={(e: ChangeEvent<HTMLInputElement>) => {
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
}
<span
aria-hidden="true"
style={{
width: '100%',
padding: '0.5rem',
paddingRight: word.length > 0 ? '2.5rem' : '0.5rem',
display: 'flex',
alignItems: 'center',
padding: '0.5rem 0.5rem 0.5rem 0.75rem',
fontSize: '1rem',
fontFamily: 'monospace',
border: '1px solid var(--color-border, #ccc)',
borderRadius: '4px',
backgroundColor: 'var(--color-background)',
color: 'var(--color-text)',
color: 'var(--color-text-secondary, #666)',
borderRight: '1px solid var(--color-border, #ccc)',
minWidth: '2.25rem',
}}
/>
{word.length > 0 && (
<button
type="button"
onClick={() => {
setVisibleWords((prev) => {
const next = new Set(prev);
if (next.has(index)) {
next.delete(index);
} else {
next.add(index);
}
return next;
});
>
{index + 1}.
</span>
<label
htmlFor={`${id ?? 'word'}-${index}`}
style={{
flex: 1,
margin: 0,
minWidth: 0,
display: 'block',
}}
>
<input
ref={(el) => {
inputRefs.current[index] = el;
}}
id={`${id ?? 'word'}-${index}`}
type={visibleWords.has(index) ? 'text' : 'password'}
value={word}
onChange={(e: ChangeEvent<HTMLInputElement>) => {
handleInputChange(index, e.target.value);
}}
onKeyDown={(e) => {
handleKeyDown(index, e);
}}
onFocus={() => {
handleFocus(index);
}}
onBlur={handleBlur}
autoComplete="off"
spellCheck={false}
name={`bip32-w-${id ?? 'w'}-${index}`}
data-lpignore="true"
data-form-type="other"
data-1p-ignore="true"
aria-describedby={
index === 0 && ariaDescribedBy !== undefined
? ariaDescribedBy
: undefined
}
size={BIP32_MAX_WORD_LENGTH}
style={{
position: 'absolute',
right: '0.25rem',
top: '50%',
transform: 'translateY(-50%)',
background: 'none',
width: `${BIP32_MAX_WORD_LENGTH}ch`,
minWidth: `${BIP32_MAX_WORD_LENGTH}ch`,
maxWidth: `${BIP32_MAX_WORD_LENGTH}ch`,
padding: '0.5rem 0.75rem',
fontSize: '1rem',
fontFamily: 'monospace',
border: 'none',
cursor: 'pointer',
padding: '0.25rem',
fontSize: '0.875rem',
color: 'var(--color-text-secondary, #666)',
borderRadius: 0,
backgroundColor: 'transparent',
color: 'var(--color-text)',
boxSizing: 'content-box',
}}
aria-label={`${visibleWords.has(index) ? 'Masquer' : 'Afficher'} le mot ${index + 1}`}
title={`${visibleWords.has(index) ? 'Masquer' : 'Afficher'} le mot ${index + 1}`}
>
{visibleWords.has(index) ? '👁' : '👁‍🗨'}
</button>
)}
/>
</label>
</div>
{focusedIndex === index && suggestions.length > 0 && (
<div

View File

@ -284,7 +284,8 @@ a:focus-visible {
}
/* Screen-reader only (e.g. login state machine status) */
.sr-only {
.sr-only,
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;

View File

@ -23,7 +23,10 @@ export interface RelayConfig {
/**
* Pair configuration (local device or remote).
* publicKey: remote device identity public key (hex), for ECDH encryption of pairing messages.
* publicKey: primary public key (hex), for ECDH encryption of pairing messages.
* publicKeys: optional list of additional public keys (e.g. derived from identity). When present,
* the pair is considered to "own" any of these keys for ownership checks.
* label: optional persistent user-defined label for display (e.g. "Mon téléphone").
*/
export interface PairConfig {
uuid: string;
@ -31,6 +34,9 @@ export interface PairConfig {
is_local: boolean;
can_sign: boolean;
publicKey?: string;
/** Additional public keys (e.g. derived). Ownership = publicKey or any of publicKeys. */
publicKeys?: string[];
label?: string;
}
/**

View File

@ -265,6 +265,14 @@ export const BIP32_WORDLIST = [
'young', 'youth', 'zebra', 'zero', 'zone', 'zoo',
];
/**
* Maximum character length of any word in BIP32_WORDLIST.
* Used to size word input fields uniformly.
*/
export const BIP32_MAX_WORD_LENGTH: number = Math.max(
...BIP32_WORDLIST.map((w) => w.length),
);
/**
* Convert UUID to BIP32 word list.
* UUID is converted to bytes, then to BIP32 path, then to words.

View File

@ -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.

View File

@ -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);
}

View File

@ -9,7 +9,7 @@ export default defineConfig({
},
server: {
port: 3018,
strictPort: false,
strictPort: true,
},
preview: {
port: 3018,

98
verify-chain-alignment.sh Executable file
View File

@ -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."

117
verify-dashboard-signet.sh Executable file
View File

@ -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 ""

View File

@ -199,6 +199,51 @@
</div>
</div>
<h2>Dérivation de clés et clés multiples par pair</h2>
<p>
Chaque pair (appareil local ou distant) peut être associé à <strong>plusieurs clés publiques</strong> :
une clé principale et, optionnellement, une liste de clés supplémentaires. Cela permet dutiliser
plusieurs clés pour un même pair sans multiplier les secrets à gérer.
</p>
<h3>Dérivation déterministe à partir dune clé privée</h3>
<p>
À partir dune seule clé privée (celle de lidentité), le système peut calculer dautres paires
clé privée / clé publique de façon <strong>déterministe</strong> : pour un index entier (0, 1, 2, …),
le calcul produit toujours la même paire. Aucune donnée aléatoire nest utilisée pour cette dérivation.
</p>
<ul>
<li><strong>Entrée</strong> : la clé privée parente (64 caractères hexadécimaux) et un index (entier ≥ 0).</li>
<li><strong>Procédé</strong> : une fonction de dérivation (HMAC-SHA256 avec un domaine fixe et lindex)
produit 32 octets, puis une réduction modulo lordre 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).</li>
<li><strong>Sortie</strong> : une paire (clé privée enfant, clé publique enfant). La clé « principale »
est celle dérivée directement de la clé privée de lidentité (sans index enfant).</li>
</ul>
<p>
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 quune seule clé privée ; les autres sont recalculées à la demande.
</p>
<h3>Vérification rapide : une clé publique appartient-elle à mon identité ?</h3>
<p>
Pour savoir si une clé publique donnée correspond à votre clé privée (identité), le système fait :
</p>
<ol>
<li>Calculer la clé publique à partir de votre clé privée (une opération sur la courbe).</li>
<li>Comparer le résultat à la clé publique donnée. Si elles sont égales, la clé appartient à votre identité.</li>
<li>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 quune égalité est trouvée, la réponse est oui.</li>
</ol>
<p>
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.
</p>
<p>
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.
</p>
<h2>Le workflow complet</h2>
<p>
Voici ce qui se passe quand vous envoyez un message sécurisé, étape par étape :
@ -414,12 +459,14 @@
<h3>Fichiers source</h3>
<ul>
<li><code>userwallet/src/utils/encryption.ts</code> : encryptWithECDH, decryptWithECDH</li>
<li><code>userwallet/src/utils/crypto.ts</code> : signMessage, verifySignature, generateChallenge</li>
<li><code>userwallet/src/utils/crypto.ts</code> : signMessage, verifySignature, generateChallenge, deriveChildKeyPair, getDerivedPublicKeys, publicKeyBelongsToIdentity</li>
<li><code>userwallet/src/utils/pairing.ts</code> : getPairPublicKeys, pairContainsPublicKey, addPairPublicKey</li>
<li><code>userwallet/src/utils/relay.ts</code> : postMessageChiffre, postSignature, postKey</li>
<li><code>userwallet/src/utils/collectSignatures.ts</code> : runCollectLoop, fetchSignaturesForHash</li>
<li><code>userwallet/src/utils/loginValidation.ts</code> : hasEnoughSignatures, checkDependenciesSatisfied</li>
<li><code>service-login-verify/</code> : verifyLoginProof (côté parent)</li>
</ul>
<p>Dérivation de clés et clés multiples par pair : <code>docs/USERWALLET_KEY_DERIVATION.md</code>.</p>
<h3>Collecte des signatures</h3>
<table class="algo-table">