Optimize sync-utxos RPC calls and document bitcoind crash issues

**Motivations:**
- Prevent bitcoind crashes caused by heavy RPC calls without timeout
- Document bitcoind crash and wallet loading stuck issues
- Clean up obsolete files (configure-nginx-proxy.sh, userwallet components, website-skeleton, old fixKnowledge docs)

**Root causes:**
- RPC calls without timeout causing bitcoind crashes
- No pre-check of bitcoind health before heavy operations
- Large wallet (315MB) causing long loading times and potential hangs
- Missing retry mechanism for transient errors

**Correctifs:**
- Add timeouts on RPC calls (5 minutes for listunspent, 10 seconds for healthcheck)
- Add bitcoind health check before synchronization
- Implement retry with exponential backoff
- Reduce maximumCount limit from 9999999 to 500000 UTXOs
- Improve cron script with pre-checks and better error handling
- Add container status verification before script execution

**Evolutions:**
- New check-services-status.sh script for service diagnostics
- Documentation of crash issues in fixKnowledge
- Improved logging with timestamps
- Better error messages and handling

**Pages affectées:**
- data/sync-utxos-spent-status.mjs
- data/sync-utxos-cron.sh
- data/restart-services-cron.sh
- data/check-services-status.sh (new)
- fixKnowledge/sync-utxos-rpc-optimization.md (new)
- fixKnowledge/signet-bitcoind-crash-mining-stopped.md (new)
- fixKnowledge/signet-bitcoind-crash-wallet-loading-stuck.md (new)
- Removed obsolete files: configure-nginx-proxy.sh, userwallet components, website-skeleton files, old fixKnowledge docs

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ncantu 2026-02-08 08:18:26 +01:00
parent 937646cc45
commit e0ce7a9d83
20 changed files with 682 additions and 2803 deletions

View File

@ -1,391 +0,0 @@
#!/bin/bash
# Script de configuration Nginx pour les sous-domaines certificator.4nkweb.com
# Usage: ./configure-nginx-proxy.sh
set -e
PROXY_HOST="192.168.1.100"
PROXY_USER="ncantu"
NGINX_SITES_AVAILABLE="/etc/nginx/sites-available"
NGINX_SITES_ENABLED="/etc/nginx/sites-enabled"
CERTBOT_BIN="/usr/bin/certbot"
echo "=== Configuration Nginx pour certificator.4nkweb.com ==="
echo ""
# Vérifier que nous sommes sur le proxy ou que nous pouvons y accéder
# Note: Le script peut être exécuté localement ou via SSH
CURRENT_IP=$(hostname -I 2>/dev/null | awk '{print $1}' || echo "")
if [ "$CURRENT_IP" != "192.168.1.100" ] && [ -z "$SSH_CONNECTION" ]; then
echo " Ce script peut être exécuté sur le proxy (192.168.1.100)"
echo " Ou via SSH: ssh ${PROXY_USER}@${PROXY_HOST} 'sudo bash -s' < $0"
echo ""
fi
# Vérifier les permissions (sudo disponible pour ncantu)
if [ "$EUID" -ne 0 ]; then
if command -v sudo &> /dev/null && sudo -n true 2>/dev/null; then
echo "✅ Utilisation de sudo (droits non interactifs)"
# Le script continuera avec sudo pour les commandes nécessitant root
else
echo "⚠️ Ce script nécessite les permissions root pour configurer Nginx"
echo " Utilisez: sudo $0"
exit 1
fi
fi
# Fonction pour exécuter les commandes nécessitant root
SUDO_CMD=""
if [ "$EUID" -ne 0 ]; then
SUDO_CMD="sudo"
fi
echo "✅ Vérification de Nginx..."
# Vérifier Nginx (peut être dans /usr/sbin/nginx)
NGINX_BIN=""
if command -v nginx &> /dev/null; then
NGINX_BIN="nginx"
elif [ -f /usr/sbin/nginx ]; then
NGINX_BIN="/usr/sbin/nginx"
elif [ -f /usr/bin/nginx ]; then
NGINX_BIN="/usr/bin/nginx"
else
echo "❌ Nginx n'est pas installé"
exit 1
fi
echo " Nginx trouvé: ${NGINX_BIN}"
echo "✅ Vérification de Certbot..."
# Vérifier Certbot (peut être dans /usr/bin/certbot)
CERTBOT_BIN=""
if command -v certbot &> /dev/null; then
CERTBOT_BIN="certbot"
elif [ -f /usr/bin/certbot ]; then
CERTBOT_BIN="/usr/bin/certbot"
else
echo "⚠️ Certbot n'est pas installé. Installation..."
${SUDO_CMD} apt-get update
${SUDO_CMD} apt-get install -y certbot python3-certbot-nginx
CERTBOT_BIN="certbot"
fi
echo " Certbot trouvé: ${CERTBOT_BIN}"
# Créer les configurations Nginx pour chaque sous-domaine
# 1. Dashboard (port 3020)
echo ""
echo "📝 Configuration de dashboard.certificator.4nkweb.com..."
${SUDO_CMD} tee "${NGINX_SITES_AVAILABLE}/dashboard.certificator.4nkweb.com" > /dev/null << 'EOF'
# Dashboard Bitcoin Signet
server {
listen 80;
server_name dashboard.certificator.4nkweb.com;
# Logs
access_log /var/log/nginx/dashboard.certificator.4nkweb.com.access.log;
error_log /var/log/nginx/dashboard.certificator.4nkweb.com.error.log;
# Proxy vers le service Node.js (port 3020)
# Note: Les services tournent sur 192.168.1.105
location / {
proxy_pass http://192.168.1.105:3020;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 300s;
proxy_connect_timeout 75s;
}
}
EOF
# 2. Faucet (port 3021)
echo "📝 Configuration de faucet.certificator.4nkweb.com..."
${SUDO_CMD} tee "${NGINX_SITES_AVAILABLE}/faucet.certificator.4nkweb.com" > /dev/null << 'EOF'
# API Faucet Bitcoin Signet
server {
listen 80;
server_name faucet.certificator.4nkweb.com;
# Logs
access_log /var/log/nginx/faucet.certificator.4nkweb.com.access.log;
error_log /var/log/nginx/faucet.certificator.4nkweb.com.error.log;
# Proxy vers le service Node.js (port 3021)
# Note: Les services tournent sur 192.168.1.105
location / {
proxy_pass http://192.168.1.105:3021;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 300s;
proxy_connect_timeout 75s;
}
}
EOF
# 3. Anchorage (port 3010)
echo "📝 Configuration de anchorage.certificator.4nkweb.com..."
${SUDO_CMD} tee "${NGINX_SITES_AVAILABLE}/anchorage.certificator.4nkweb.com" > /dev/null << 'EOF'
# API Anchorage Bitcoin Signet
server {
listen 80;
server_name anchorage.certificator.4nkweb.com;
# Logs
access_log /var/log/nginx/anchorage.certificator.4nkweb.com.access.log;
error_log /var/log/nginx/anchorage.certificator.4nkweb.com.error.log;
# Proxy vers le service Node.js (port 3010)
# Note: Les services tournent sur 192.168.1.105
location / {
proxy_pass http://192.168.1.105:3010;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 300s;
proxy_connect_timeout 75s;
}
}
EOF
# 4. Watermark (port 3022)
echo "📝 Configuration de watermark.certificator.4nkweb.com..."
${SUDO_CMD} tee "${NGINX_SITES_AVAILABLE}/watermark.certificator.4nkweb.com" > /dev/null << 'EOF'
# API Watermark Bitcoin Signet
server {
listen 80;
server_name watermark.certificator.4nkweb.com;
# Logs
access_log /var/log/nginx/watermark.certificator.4nkweb.com.access.log;
error_log /var/log/nginx/watermark.certificator.4nkweb.com.error.log;
# Proxy vers le service Node.js (port 3022)
# Note: Les services tournent sur 192.168.1.105
location / {
proxy_pass http://192.168.1.105:3022;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 300s;
proxy_connect_timeout 75s;
}
}
EOF
# 5. UserWallet (port 3018)
echo "📝 Configuration de userwallet.certificator.4nkweb.com..."
${SUDO_CMD} tee "${NGINX_SITES_AVAILABLE}/userwallet.certificator.4nkweb.com" > /dev/null << 'EOF'
# UserWallet frontend (Vite)
server {
listen 80;
server_name userwallet.certificator.4nkweb.com;
# Logs
access_log /var/log/nginx/userwallet.certificator.4nkweb.com.access.log;
error_log /var/log/nginx/userwallet.certificator.4nkweb.com.error.log;
# Proxy vers le frontend UserWallet (port 3018) sur 192.168.1.105
location / {
proxy_pass http://192.168.1.105:3018;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 300s;
proxy_connect_timeout 75s;
}
}
EOF
# 6. Website skeleton (port 3024)
echo "📝 Configuration de skeleton.certificator.4nkweb.com..."
${SUDO_CMD} tee "${NGINX_SITES_AVAILABLE}/skeleton.certificator.4nkweb.com" > /dev/null << 'EOF'
# Website skeleton (UserWallet iframe)
server {
listen 80;
server_name skeleton.certificator.4nkweb.com;
access_log /var/log/nginx/skeleton.certificator.4nkweb.com.access.log;
error_log /var/log/nginx/skeleton.certificator.4nkweb.com.error.log;
location / {
proxy_pass http://192.168.1.105:3024;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 300s;
proxy_connect_timeout 75s;
}
}
EOF
# 7. Website data (port 3025)
echo "📝 Configuration de data.certificator.4nkweb.com..."
${SUDO_CMD} tee "${NGINX_SITES_AVAILABLE}/data.certificator.4nkweb.com" > /dev/null << 'EOF'
# Website data (iframe data, non clés)
server {
listen 80;
server_name data.certificator.4nkweb.com;
access_log /var/log/nginx/data.certificator.4nkweb.com.access.log;
error_log /var/log/nginx/data.certificator.4nkweb.com.error.log;
location / {
proxy_pass http://192.168.1.105:3025;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 300s;
proxy_connect_timeout 75s;
}
}
EOF
# 8. Relay / api-relay (port 3019)
echo "📝 Configuration de relay.certificator.4nkweb.com..."
${SUDO_CMD} tee "${NGINX_SITES_AVAILABLE}/relay.certificator.4nkweb.com" > /dev/null << 'EOF'
# API Relay (UserWallet)
server {
listen 80;
server_name relay.certificator.4nkweb.com;
# Logs
access_log /var/log/nginx/relay.certificator.4nkweb.com.access.log;
error_log /var/log/nginx/relay.certificator.4nkweb.com.error.log;
# Proxy vers api-relay (port 3019) sur 192.168.1.105
location / {
proxy_pass http://192.168.1.105:3019;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
proxy_read_timeout 300s;
proxy_connect_timeout 75s;
}
}
EOF
# Activer les sites
echo ""
echo "🔗 Activation des sites..."
${SUDO_CMD} ln -sf "${NGINX_SITES_AVAILABLE}/dashboard.certificator.4nkweb.com" "${NGINX_SITES_ENABLED}/dashboard.certificator.4nkweb.com"
${SUDO_CMD} ln -sf "${NGINX_SITES_AVAILABLE}/faucet.certificator.4nkweb.com" "${NGINX_SITES_ENABLED}/faucet.certificator.4nkweb.com"
${SUDO_CMD} ln -sf "${NGINX_SITES_AVAILABLE}/anchorage.certificator.4nkweb.com" "${NGINX_SITES_ENABLED}/anchorage.certificator.4nkweb.com"
${SUDO_CMD} ln -sf "${NGINX_SITES_AVAILABLE}/watermark.certificator.4nkweb.com" "${NGINX_SITES_ENABLED}/watermark.certificator.4nkweb.com"
${SUDO_CMD} ln -sf "${NGINX_SITES_AVAILABLE}/userwallet.certificator.4nkweb.com" "${NGINX_SITES_ENABLED}/userwallet.certificator.4nkweb.com"
${SUDO_CMD} ln -sf "${NGINX_SITES_AVAILABLE}/skeleton.certificator.4nkweb.com" "${NGINX_SITES_ENABLED}/skeleton.certificator.4nkweb.com"
${SUDO_CMD} ln -sf "${NGINX_SITES_AVAILABLE}/data.certificator.4nkweb.com" "${NGINX_SITES_ENABLED}/data.certificator.4nkweb.com"
${SUDO_CMD} ln -sf "${NGINX_SITES_AVAILABLE}/relay.certificator.4nkweb.com" "${NGINX_SITES_ENABLED}/relay.certificator.4nkweb.com"
# Tester la configuration Nginx
echo ""
echo "🔍 Test de la configuration Nginx..."
if ${SUDO_CMD} ${NGINX_BIN} -t; then
echo "✅ Configuration Nginx valide"
else
echo "❌ Erreur dans la configuration Nginx"
exit 1
fi
# Recharger Nginx (configuration HTTP uniquement pour l'instant)
echo ""
echo "🔄 Rechargement de Nginx (configuration HTTP)..."
${SUDO_CMD} systemctl reload nginx || ${SUDO_CMD} service nginx reload
# Générer les certificats SSL avec Certbot
echo ""
echo "🔐 Génération des certificats SSL avec Certbot..."
echo " Note: Certbot va automatiquement créer les configurations HTTPS"
echo ""
# Générer les certificats (un par un pour éviter les erreurs)
DOMAINS=(
"dashboard.certificator.4nkweb.com"
"faucet.certificator.4nkweb.com"
"anchorage.certificator.4nkweb.com"
"watermark.certificator.4nkweb.com"
"userwallet.certificator.4nkweb.com"
"skeleton.certificator.4nkweb.com"
"data.certificator.4nkweb.com"
"relay.certificator.4nkweb.com"
)
for domain in "${DOMAINS[@]}"; do
echo "📜 Génération du certificat pour ${domain}..."
# Certbot va automatiquement modifier la config pour ajouter HTTPS et redirection
if ${SUDO_CMD} ${CERTBOT_BIN} --nginx -d "${domain}" --non-interactive --agree-tos --email admin@4nkweb.com --redirect; then
echo "✅ Certificat généré et configuration HTTPS créée pour ${domain}"
else
echo "⚠️ Erreur lors de la génération du certificat pour ${domain}"
echo " Vous pouvez le générer manuellement avec:"
echo " sudo ${CERTBOT_BIN} --nginx -d ${domain}"
fi
done
# Recharger Nginx final
echo ""
echo "🔄 Rechargement final de Nginx..."
${SUDO_CMD} systemctl reload nginx || ${SUDO_CMD} service nginx reload
echo ""
echo "✅ Configuration terminée !"
echo ""
echo "📋 Résumé:"
echo " - dashboard.certificator.4nkweb.com -> http://192.168.1.105:3020"
echo " - faucet.certificator.4nkweb.com -> http://192.168.1.105:3021"
echo " - anchorage.certificator.4nkweb.com -> http://192.168.1.105:3010"
echo " - watermark.certificator.4nkweb.com -> http://192.168.1.105:3022"
echo " - userwallet.certificator.4nkweb.com -> http://192.168.1.105:3018"
echo " - skeleton.certificator.4nkweb.com -> http://192.168.1.105:3024"
echo " - data.certificator.4nkweb.com -> http://192.168.1.105:3025"
echo " - relay.certificator.4nkweb.com -> http://192.168.1.105:3019"
echo ""
echo "⚠️ Note: Si les services tournent sur une autre machine,"
echo " modifiez les IP dans les fichiers de configuration Nginx"
echo ""
echo "🔍 Vérification:"
echo " - Test Nginx: nginx -t"
echo " - Status: systemctl status nginx"
echo " - Logs: tail -f /var/log/nginx/*.error.log"
echo ""

81
data/check-services-status.sh Executable file
View File

@ -0,0 +1,81 @@
#!/bin/bash
# Check status of Docker services (bitcoind signet, mempool) and mining.
# Local only: no SSH. Run on the machine where Docker runs.
# Usage: ./data/check-services-status.sh
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
BITCOIND_CONTAINER="bitcoin-signet-instance"
DATADIR="/root/.bitcoin"
echo "=== État des services (${BITCOIND_CONTAINER}, mempool) ==="
echo ""
# Docker containers
echo "--- Conteneurs Docker ---"
docker ps -a --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" 2>/dev/null || echo "Docker non disponible ou erreur"
echo ""
# Bitcoind
if docker ps -q -f "name=^${BITCOIND_CONTAINER}$" 2>/dev/null | grep -q .; then
echo "--- Bitcoind (${BITCOIND_CONTAINER}) ---"
BITCOIN_DIR=$(docker exec "$BITCOIND_CONTAINER" printenv BITCOIN_DIR 2>/dev/null || echo "$DATADIR")
if docker exec "$BITCOIND_CONTAINER" bitcoin-cli -datadir="$BITCOIN_DIR" getblockchaininfo &>/dev/null; then
BCI=$(docker exec "$BITCOIND_CONTAINER" bitcoin-cli -datadir="$BITCOIN_DIR" getblockchaininfo 2>/dev/null)
BLOCKS=$(echo "$BCI" | jq -r '.blocks // "?"')
CHAIN=$(echo "$BCI" | jq -r '.chain // "?"')
HEADERS=$(echo "$BCI" | jq -r '.headers // "?"')
echo " RPC: OK"
echo " Chaîne: $CHAIN | Blocs: $BLOCKS | Headers: $HEADERS"
if [ "$BLOCKS" != "?" ]; then
TIP_TIME=$(docker exec "$BITCOIND_CONTAINER" bitcoin-cli -datadir="$BITCOIN_DIR" getblock "$(docker exec "$BITCOIND_CONTAINER" bitcoin-cli -datadir="$BITCOIN_DIR" getblockhash "$BLOCKS" 2>/dev/null)" 2>/dev/null | jq -r '.time // 0')
if [ -n "$TIP_TIME" ] && [ "$TIP_TIME" != "0" ]; then
echo " Dernier bloc (time): $TIP_TIME ($(date -d "@${TIP_TIME}" 2>/dev/null || echo "N/A"))"
fi
fi
else
echo " RPC: HORS SERVICE (bitcoind ne répond pas)"
RPC_ERROR=$(docker exec "$BITCOIND_CONTAINER" bitcoin-cli -datadir="$BITCOIN_DIR" getblockchaininfo 2>&1 || true)
if echo "$RPC_ERROR" | grep -q "Loading wallet"; then
echo " ⚠️ Bitcoind est bloqué en chargement de wallet"
echo " Cause probable: wallet volumineux ou corrompu"
echo " Solution: Attendre quelques minutes ou redémarrer: docker restart ${BITCOIND_CONTAINER}"
elif echo "$RPC_ERROR" | grep -q "Could not connect"; then
echo " ⚠️ Bitcoind ne répond pas du tout"
echo " Cause probable: bitcoind a planté dans le conteneur"
echo " Solution: Redémarrer le conteneur: docker restart ${BITCOIND_CONTAINER}"
else
echo " Erreur RPC: $RPC_ERROR"
echo " Solution: Redémarrer le conteneur: docker restart ${BITCOIND_CONTAINER}"
fi
fi
echo ""
echo " Processus dans le conteneur:"
docker exec "$BITCOIND_CONTAINER" ps aux 2>/dev/null | head -20 || true
# Vérifier si bitcoind est présent dans les processus
if ! docker exec "$BITCOIND_CONTAINER" ps aux 2>/dev/null | grep -q "[b]itcoind"; then
echo " ⚠️ ATTENTION: Le processus bitcoind n'est pas présent dans le conteneur"
echo " Le conteneur est actif mais bitcoind a planté"
echo " Solution immédiate: docker restart ${BITCOIND_CONTAINER}"
fi
echo ""
echo " Dernières lignes debug.log:"
docker exec "$BITCOIND_CONTAINER" tail -3 "${BITCOIN_DIR}/signet/debug.log" 2>/dev/null || true
else
echo "--- Bitcoind: conteneur ${BITCOIND_CONTAINER} non trouvé ou arrêté ---"
fi
echo ""
# Mempool stack
echo "--- Mempool (docker-compose.signet.yml) ---"
if [ -f "${PROJECT_DIR}/mempool/docker-compose.signet.yml" ]; then
(cd "${PROJECT_DIR}/mempool" && docker compose -f docker-compose.signet.yml ps 2>/dev/null) || \
(cd "${PROJECT_DIR}/mempool" && docker-compose -f docker-compose.signet.yml ps 2>/dev/null) || true
else
echo " Fichier compose non trouvé"
fi
echo ""
echo "=== Fin du rapport ==="

View File

@ -81,20 +81,33 @@ if docker ps -a -q -f "name=^${BITCOIND_CONTAINER}$" 2>/dev/null | grep -q .; th
if [ "$bitcoind_ok" -eq 1 ]; then if [ "$bitcoind_ok" -eq 1 ]; then
log " $BITCOIND_CONTAINER started, waiting for RPC to be ready..." log " $BITCOIND_CONTAINER started, waiting for RPC to be ready..."
# Wait for bitcoind RPC to be ready (max 60 seconds) # Wait for bitcoind RPC to be ready (max 300 seconds = 5 minutes)
max_wait=60 # Increased timeout to handle large wallet loading (e.g., 315MB wallet can take several minutes)
max_wait=300
wait_count=0 wait_count=0
while [ $wait_count -lt $max_wait ]; do while [ $wait_count -lt $max_wait ]; do
BITCOIN_DATADIR=$(docker exec "$BITCOIND_CONTAINER" printenv BITCOIN_DIR 2>/dev/null || echo "/root/.bitcoin") 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 RPC_RESPONSE=$(docker exec "$BITCOIND_CONTAINER" bitcoin-cli -datadir="$BITCOIN_DATADIR" getblockchaininfo 2>&1)
log " $BITCOIND_CONTAINER RPC ready" if echo "$RPC_RESPONSE" | grep -q '"chain"'; then
log " $BITCOIND_CONTAINER RPC ready after ${wait_count}s"
break break
elif echo "$RPC_RESPONSE" | grep -q "Loading wallet"; then
# Wallet is still loading, continue waiting
if [ $((wait_count % 30)) -eq 0 ]; then
log " Wallet still loading... (${wait_count}s elapsed)"
fi
elif echo "$RPC_RESPONSE" | grep -q "Could not connect"; then
# Bitcoind not started yet, continue waiting
if [ $((wait_count % 30)) -eq 0 ]; then
log " Bitcoind starting... (${wait_count}s elapsed)"
fi
fi fi
sleep 1 sleep 2
wait_count=$((wait_count + 1)) wait_count=$((wait_count + 2))
done done
if [ $wait_count -ge $max_wait ]; then if [ $wait_count -ge $max_wait ]; then
log " WARN: $BITCOIND_CONTAINER RPC not ready after ${max_wait}s" log " WARN: $BITCOIND_CONTAINER RPC not ready after ${max_wait}s (wallet may still be loading)"
log " Check with: docker exec $BITCOIND_CONTAINER bitcoin-cli -datadir=$BITCOIN_DATADIR getblockchaininfo"
fi fi
else else
log " $BITCOIND_CONTAINER FAILED" log " $BITCOIND_CONTAINER FAILED"

View File

@ -0,0 +1,13 @@
2026-02-07T02:07:15+01:00 === Start Docker services at boot ===
2026-02-07T02:07:15+01:00 Starting bitcoin-signet-instance...
2026-02-07T02:07:15+01:00 bitcoin-signet-instance started
2026-02-07T02:32:36+01:00 === Start Docker services at boot ===
2026-02-07T02:32:37+01:00 bitcoin-signet-instance already running
2026-02-07T02:32:37+01:00 Starting mempool stack...
2026-02-07T02:32:37+01:00 mempool stack started
2026-02-07T02:32:37+01:00 === Done ===
2026-02-08T08:08:57+01:00 === Start Docker services at boot ===
2026-02-08T08:08:58+01:00 bitcoin-signet-instance already running
2026-02-08T08:08:58+01:00 Starting mempool stack...
2026-02-08T08:08:58+01:00 mempool stack started
2026-02-08T08:08:58+01:00 === Done ===

View File

@ -2,15 +2,50 @@
# Script de synchronisation des UTXOs dépensés # Script de synchronisation des UTXOs dépensés
# À exécuter via cron pour maintenir la synchronisation # À exécuter via cron pour maintenir la synchronisation
# Vérifie que bitcoind est disponible avant d'exécuter la synchronisation
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")" PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
LOG_FILE="$SCRIPT_DIR/sync-utxos.log" LOG_FILE="$SCRIPT_DIR/sync-utxos.log"
BITCOIND_CONTAINER="bitcoin-signet-instance"
log() {
echo "$(date -Iseconds) $*" | tee -a "$LOG_FILE"
}
cd "$PROJECT_DIR" || exit 1 cd "$PROJECT_DIR" || exit 1
log "=== Synchronisation des UTXOs dépensés ==="
# Vérifier que bitcoind est disponible avant de commencer
if docker ps -q -f "name=^${BITCOIND_CONTAINER}$" 2>/dev/null | grep -q .; then
log "Vérification de la disponibilité de bitcoind..."
if docker exec "$BITCOIND_CONTAINER" bitcoin-cli -datadir=/root/.bitcoin getblockchaininfo &>/dev/null; then
log " ✅ Bitcoind disponible, démarrage de la synchronisation..."
else
log " ❌ Bitcoind ne répond pas (RPC non disponible)"
log " ⚠️ Arrêt de la synchronisation pour éviter de surcharger bitcoind"
log " Le script sera réexécuté à la prochaine heure"
exit 1
fi
else
log " ❌ Conteneur bitcoind non trouvé ou arrêté"
log " ⚠️ Arrêt de la synchronisation"
exit 1
fi
# Exécuter le script de synchronisation # Exécuter le script de synchronisation
log "Exécution du script de synchronisation..."
node "$SCRIPT_DIR/sync-utxos-spent-status.mjs" >> "$LOG_FILE" 2>&1 node "$SCRIPT_DIR/sync-utxos-spent-status.mjs" >> "$LOG_FILE" 2>&1
EXIT_CODE=$?
if [ $EXIT_CODE -eq 0 ]; then
log "✅ Synchronisation terminée avec succès"
else
log "❌ Synchronisation échouée (code: $EXIT_CODE)"
fi
# Garder seulement les 100 dernières lignes du log # Garder seulement les 100 dernières lignes du log
tail -n 100 "$LOG_FILE" > "$LOG_FILE.tmp" && mv "$LOG_FILE.tmp" "$LOG_FILE" tail -n 100 "$LOG_FILE" > "$LOG_FILE.tmp" && mv "$LOG_FILE.tmp" "$LOG_FILE"
exit $EXIT_CODE

View File

@ -33,9 +33,43 @@ const RPC_WALLET = process.env.BITCOIN_RPC_WALLET || 'custom_signet';
const DB_PATH = join(__dirname, 'signet.db'); const DB_PATH = join(__dirname, 'signet.db');
/** /**
* Effectue un appel RPC Bitcoin * Effectue un appel RPC Bitcoin avec timeout et retry
*/ */
function rpcCall(method, params = []) { function rpcCall(method, params = [], timeoutMs = 300000, maxRetries = 3) {
return new Promise(async (resolve, reject) => {
let lastError;
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const result = await rpcCallOnce(method, params, timeoutMs);
resolve(result);
return;
} catch (error) {
lastError = error;
const isTimeoutError = error.message.includes('timeout') ||
error.message.includes('ETIMEDOUT') ||
error.message.includes('ECONNRESET') ||
error.message.includes('socket hang up');
if (attempt < maxRetries - 1 && isTimeoutError) {
const delay = Math.min(1000 * Math.pow(2, attempt), 10000); // Backoff exponentiel, max 10s
console.log(` ⚠️ Tentative ${attempt + 1}/${maxRetries} échouée, nouvelle tentative dans ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
} else {
reject(error);
return;
}
}
}
reject(lastError);
});
}
/**
* Effectue un appel RPC Bitcoin unique avec timeout
*/
function rpcCallOnce(method, params = [], timeoutMs = 300000) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const url = new URL(RPC_URL); const url = new URL(RPC_URL);
const isHttps = url.protocol === 'https:'; const isHttps = url.protocol === 'https:';
@ -60,6 +94,7 @@ function rpcCall(method, params = []) {
'Authorization': `Basic ${auth}`, 'Authorization': `Basic ${auth}`,
'Content-Length': Buffer.byteLength(postData), 'Content-Length': Buffer.byteLength(postData),
}, },
timeout: timeoutMs,
}; };
const req = httpModule.request(options, (res) => { const req = httpModule.request(options, (res) => {
@ -87,17 +122,44 @@ function rpcCall(method, params = []) {
reject(error); reject(error);
}); });
req.on('timeout', () => {
req.destroy();
reject(new Error(`RPC timeout after ${timeoutMs}ms for method ${method}`));
});
req.write(postData); req.write(postData);
req.end(); req.end();
}); });
} }
/**
* Vérifie que bitcoind est disponible et répond
*/
async function checkBitcoindHealth() {
try {
await rpcCall('getblockchaininfo', [], 10000, 1); // Timeout court pour le healthcheck
return true;
} catch (error) {
console.error(` ❌ Bitcoind non disponible: ${error.message}`);
return false;
}
}
/** /**
* Synchronise les UTXOs dépensés * Synchronise les UTXOs dépensés
*/ */
async function syncSpentUtxos() { async function syncSpentUtxos() {
console.log('🔍 Démarrage de la synchronisation des UTXOs dépensés...\n'); console.log('🔍 Démarrage de la synchronisation des UTXOs dépensés...\n');
// Vérifier que bitcoind est disponible avant de commencer
console.log('🔍 Vérification de la disponibilité de bitcoind...');
const bitcoindAvailable = await checkBitcoindHealth();
if (!bitcoindAvailable) {
console.error('❌ Bitcoind n\'est pas disponible. Arrêt de la synchronisation.');
process.exit(1);
}
console.log(' ✅ Bitcoind disponible\n');
// Ouvrir la base de données // Ouvrir la base de données
const db = new Database(DB_PATH); const db = new Database(DB_PATH);
@ -117,11 +179,17 @@ async function syncSpentUtxos() {
} }
// Récupérer tous les UTXOs disponibles depuis Bitcoin // Récupérer tous les UTXOs disponibles depuis Bitcoin
// Note: listunspent ne supporte pas la pagination avec skip
// Limiter maximumCount pour éviter les problèmes de mémoire avec de gros wallets
// Timeout augmenté à 5 minutes pour les gros wallets
console.log('📡 Récupération des UTXOs depuis Bitcoin...'); console.log('📡 Récupération des UTXOs depuis Bitcoin...');
console.log(' ⏳ Cela peut prendre plusieurs minutes avec un wallet volumineux...');
// Limite à 500000 UTXOs pour éviter les problèmes de mémoire
// Timeout de 5 minutes (300000ms) pour permettre le traitement des gros wallets
const unspent = await rpcCall('listunspent', [0, 9999999, [], false, { const unspent = await rpcCall('listunspent', [0, 9999999, [], false, {
minimumAmount: 0, minimumAmount: 0,
maximumCount: 9999999, maximumCount: 500000, // Limite réduite de 9999999 à 500000 pour éviter les problèmes de mémoire
}]); }], 300000, 2); // Timeout 5 minutes, max 2 retries
console.log(`📊 UTXOs disponibles dans Bitcoin: ${unspent.length}`); console.log(`📊 UTXOs disponibles dans Bitcoin: ${unspent.length}`);
@ -195,6 +263,15 @@ async function syncSpentUtxos() {
} catch (error) { } catch (error) {
console.error('❌ Erreur lors de la synchronisation:', error.message); console.error('❌ Erreur lors de la synchronisation:', error.message);
// Ne pas faire échouer le cron si c'est un problème temporaire de bitcoind
// Le script sera réexécuté à l'heure suivante
if (error.message.includes('timeout') ||
error.message.includes('ECONNRESET') ||
error.message.includes('socket hang up') ||
error.message.includes('Could not connect')) {
console.error(' ⚠️ Problème de connexion avec bitcoind (peut être temporaire)');
console.error(' Le script sera réexécuté à la prochaine heure');
}
process.exit(1); process.exit(1);
} finally { } finally {
db.close(); db.close();

View File

@ -1,100 +1,100 @@
📊 UTXOs à vérifier: 80378
📡 Récupération des UTXOs depuis Bitcoin...
📊 UTXOs disponibles dans Bitcoin: 268376
💾 Création de la table temporaire...
💾 Insertion des UTXOs disponibles par batch...
⏳ Traitement: 10000/268376 UTXOs insérés...
⏳ Traitement: 20000/268376 UTXOs insérés...
⏳ Traitement: 30000/268376 UTXOs insérés...
⏳ Traitement: 40000/268376 UTXOs insérés...
⏳ Traitement: 50000/268376 UTXOs insérés...
⏳ Traitement: 60000/268376 UTXOs insérés...
⏳ Traitement: 70000/268376 UTXOs insérés...
⏳ Traitement: 80000/268376 UTXOs insérés...
⏳ Traitement: 90000/268376 UTXOs insérés...
⏳ Traitement: 100000/268376 UTXOs insérés...
⏳ Traitement: 110000/268376 UTXOs insérés...
⏳ Traitement: 120000/268376 UTXOs insérés...
⏳ Traitement: 130000/268376 UTXOs insérés...
⏳ Traitement: 140000/268376 UTXOs insérés...
⏳ Traitement: 150000/268376 UTXOs insérés...
⏳ Traitement: 160000/268376 UTXOs insérés...
⏳ Traitement: 170000/268376 UTXOs insérés...
⏳ Traitement: 180000/268376 UTXOs insérés...
⏳ Traitement: 190000/268376 UTXOs insérés...
⏳ Traitement: 200000/268376 UTXOs insérés...
⏳ Traitement: 210000/268376 UTXOs insérés...
⏳ Traitement: 220000/268376 UTXOs insérés...
⏳ Traitement: 230000/268376 UTXOs insérés...
⏳ Traitement: 240000/268376 UTXOs insérés...
⏳ Traitement: 250000/268376 UTXOs insérés...
⏳ Traitement: 260000/268376 UTXOs insérés...
💾 Mise à jour des UTXOs dépensés...
📊 Résumé:
- UTXOs vérifiés: 80378
- UTXOs toujours disponibles: 80378
- UTXOs dépensés détectés: 0 - UTXOs dépensés détectés: 0
📈 Statistiques finales: 📈 Statistiques finales:
- Total UTXOs: 283165 - Total UTXOs: 283165
- Dépensés: 202127 - Dépensés: 202787
- Non dépensés: 81038 - Non dépensés: 80378
✅ Synchronisation terminée ✅ Synchronisation terminée
🔍 Démarrage de la synchronisation des UTXOs dépensés... 🔍 Démarrage de la synchronisation des UTXOs dépensés...
📊 UTXOs à vérifier: 80978 📊 UTXOs à vérifier: 80378
📡 Récupération des UTXOs depuis Bitcoin... 📡 Récupération des UTXOs depuis Bitcoin...
📊 UTXOs disponibles dans Bitcoin: 267500 📊 UTXOs disponibles dans Bitcoin: 268385
💾 Création de la table temporaire... 💾 Création de la table temporaire...
💾 Insertion des UTXOs disponibles par batch... 💾 Insertion des UTXOs disponibles par batch...
⏳ Traitement: 10000/267500 UTXOs insérés... ⏳ Traitement: 10000/268385 UTXOs insérés...
⏳ Traitement: 20000/267500 UTXOs insérés... ⏳ Traitement: 20000/268385 UTXOs insérés...
⏳ Traitement: 30000/267500 UTXOs insérés... ⏳ Traitement: 30000/268385 UTXOs insérés...
⏳ Traitement: 40000/267500 UTXOs insérés... ⏳ Traitement: 40000/268385 UTXOs insérés...
⏳ Traitement: 50000/267500 UTXOs insérés... ⏳ Traitement: 50000/268385 UTXOs insérés...
⏳ Traitement: 60000/267500 UTXOs insérés... ⏳ Traitement: 60000/268385 UTXOs insérés...
⏳ Traitement: 70000/267500 UTXOs insérés... ⏳ Traitement: 70000/268385 UTXOs insérés...
⏳ Traitement: 80000/267500 UTXOs insérés... ⏳ Traitement: 80000/268385 UTXOs insérés...
⏳ Traitement: 90000/267500 UTXOs insérés... ⏳ Traitement: 90000/268385 UTXOs insérés...
⏳ Traitement: 100000/267500 UTXOs insérés... ⏳ Traitement: 100000/268385 UTXOs insérés...
⏳ Traitement: 110000/267500 UTXOs insérés... ⏳ Traitement: 110000/268385 UTXOs insérés...
⏳ Traitement: 120000/267500 UTXOs insérés... ⏳ Traitement: 120000/268385 UTXOs insérés...
⏳ Traitement: 130000/267500 UTXOs insérés... ⏳ Traitement: 130000/268385 UTXOs insérés...
⏳ Traitement: 140000/267500 UTXOs insérés... ⏳ Traitement: 140000/268385 UTXOs insérés...
⏳ Traitement: 150000/267500 UTXOs insérés... ⏳ Traitement: 150000/268385 UTXOs insérés...
⏳ Traitement: 160000/267500 UTXOs insérés... ⏳ Traitement: 160000/268385 UTXOs insérés...
⏳ Traitement: 170000/267500 UTXOs insérés... ⏳ Traitement: 170000/268385 UTXOs insérés...
⏳ Traitement: 180000/267500 UTXOs insérés... ⏳ Traitement: 180000/268385 UTXOs insérés...
⏳ Traitement: 190000/267500 UTXOs insérés... ⏳ Traitement: 190000/268385 UTXOs insérés...
⏳ Traitement: 200000/267500 UTXOs insérés... ⏳ Traitement: 200000/268385 UTXOs insérés...
⏳ Traitement: 210000/267500 UTXOs insérés... ⏳ Traitement: 210000/268385 UTXOs insérés...
⏳ Traitement: 220000/267500 UTXOs insérés... ⏳ Traitement: 220000/268385 UTXOs insérés...
⏳ Traitement: 230000/267500 UTXOs insérés... ⏳ Traitement: 230000/268385 UTXOs insérés...
⏳ Traitement: 240000/267500 UTXOs insérés... ⏳ Traitement: 240000/268385 UTXOs insérés...
⏳ Traitement: 250000/267500 UTXOs insérés... ⏳ Traitement: 250000/268385 UTXOs insérés...
⏳ Traitement: 260000/267500 UTXOs insérés... ⏳ Traitement: 260000/268385 UTXOs insérés...
💾 Mise à jour des UTXOs dépensés... 💾 Mise à jour des UTXOs dépensés...
📊 Résumé: 📊 Résumé:
- UTXOs vérifiés: 80978 - UTXOs vérifiés: 80378
- UTXOs toujours disponibles: 80978 - UTXOs toujours disponibles: 80378
- UTXOs dépensés détectés: 0 - UTXOs dépensés détectés: 0
📈 Statistiques finales: 📈 Statistiques finales:
- Total UTXOs: 283165 - Total UTXOs: 283165
- Dépensés: 202187 - Dépensés: 202787
- Non dépensés: 80978 - Non dépensés: 80378
✅ Synchronisation terminée ✅ Synchronisation terminée
🔍 Démarrage de la synchronisation des UTXOs dépensés... 🔍 Démarrage de la synchronisation des UTXOs dépensés...
📊 UTXOs à vérifier: 80978 📊 UTXOs à vérifier: 80378
📡 Récupération des UTXOs depuis Bitcoin... 📡 Récupération des UTXOs depuis Bitcoin...
📊 UTXOs disponibles dans Bitcoin: 267516 ❌ Erreur lors de la synchronisation: socket hang up
💾 Création de la table temporaire... 🔍 Démarrage de la synchronisation des UTXOs dépensés...
💾 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érifier: 80378
- UTXOs vérifiés: 80978 📡 Récupération des UTXOs depuis Bitcoin...
- UTXOs toujours disponibles: 80978 ❌ Erreur lors de la synchronisation: read ECONNRESET
- 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

@ -1,124 +0,0 @@
# 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

@ -1,88 +0,0 @@
# Fix: Mempool API Healthcheck - curl not found
**Date:** 2026-01-27
**Auteur:** Équipe 4NK
## Problème
Le conteneur Docker `mempool_api_1` était marqué comme "unhealthy" avec un FailingStreak de 2963 échecs consécutifs.
### Symptômes
- Statut Docker: `unhealthy`
- Erreur répétée: `/bin/sh: 1: curl: not found`
- Le healthcheck ne pouvait pas s'exécuter car `curl` n'est pas installé dans l'image `mempool/backend:latest`
### Impact
- Le conteneur fonctionnait normalement (les logs montraient une synchronisation correcte des index Bitcoin)
- Le statut "unhealthy" générait des alertes et masquait l'état réel du service
- Pas d'impact fonctionnel direct, mais confusion sur l'état réel du service
## Root cause
Le healthcheck dans `docker-compose.signet.yml` utilisait la commande `curl` qui n'est pas disponible dans l'image Docker `mempool/backend:latest`. L'image ne contient que les dépendances minimales nécessaires au backend Node.js.
## Correctifs
### Modification du healthcheck
**Fichier modifié:** `mempool/docker-compose.signet.yml`
**Avant:**
```yaml
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8999/api/v1/backend-info | grep -q . || exit 1"]
```
**Après:**
```yaml
healthcheck:
test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:8999/api/v1/backend-info', (r) => { process.exit(r.statusCode === 200 ? 0 : 1); }).on('error', () => process.exit(1));\""]
```
### Justification
- `node` est disponible dans l'image (backend Node.js)
- Utilisation de l'API HTTP native de Node.js au lieu de `curl`
- Même logique de vérification: requête HTTP vers `/api/v1/backend-info` avec vérification du code de statut 200
## Modifications
- `mempool/docker-compose.signet.yml`: Modification du healthcheck du service `api`
## Modalités de déploiement
1. Modifier le fichier `docker-compose.signet.yml`
2. Recréer le conteneur pour appliquer la nouvelle configuration:
```bash
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)
## Modalités d'analyse
### Vérification du statut
```bash
docker inspect mempool_api_1 --format='{{.State.Health.Status}}'
```
### Vérification des logs du healthcheck
```bash
docker inspect mempool_api_1 --format='{{json .State.Health}}' | python3 -m json.tool
```
### Test manuel du healthcheck
```bash
docker exec mempool_api_1 node -e "require('http').get('http://localhost:8999/api/v1/backend-info', (r) => { console.log('Status:', r.statusCode); process.exit(r.statusCode === 200 ? 0 : 1); }).on('error', (e) => { console.error('Error:', e.message); process.exit(1); });"
```
## Résultat
- Le conteneur `mempool_api_1` est maintenant marqué comme "healthy"
- Le healthcheck fonctionne correctement avec Node.js
- Aucun impact sur le fonctionnement du service

View File

@ -1,106 +0,0 @@
# Correction: Mempool affiche "hors connexion"
**Date**: 2026-01-26
**Auteur**: Équipe 4NK
## Motivations
- Le mempool affichait le site mais indiquait "hors connexion"
- Le WebSocket ne pouvait pas se connecter au backend
- L'utilisateur ne pouvait pas utiliser l'explorateur blockchain
## Root causes
- Le backend mempool était en état "unhealthy" mais fonctionnait partiellement
- Le backend avait des problèmes de connexion avec Bitcoin RPC (erreurs `ECONNRESET`)
- Le processus de mise à jour des blocs était bloqué (`$updateBlocks stalled`)
- Le backend nécessitait un redémarrage pour se resynchroniser correctement
## Correctifs
- Redémarrage du backend mempool pour résoudre les problèmes de connexion
- Vérification de la configuration nginx du frontend (proxy WebSocket vers `http://api:8999/`)
- Vérification de la connectivité entre le frontend et le backend via le réseau Docker
- Restauration de la configuration nginx après des modifications incorrectes
## 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 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
- `mempool/diagnose-mempool.sh` : Script de diagnostic local
- `mempool/fix-mempool-websocket.sh` : Script de diagnostic et correction distant
- `mempool/nginx-mempool.conf` : Configuration nginx pour le WebSocket (amélioration des headers)
## Modalités de déploiement
1. **Diagnostic local** (depuis la racine du projet `/home/ncantu/Bureau/code/bitcoin/`) :
```bash
cd mempool
./diagnose-mempool.sh
```
2. **Redémarrage du backend si nécessaire** :
```bash
docker-compose -f docker-compose.signet.yml restart api
```
3. **Vérification de la connectivité** :
```bash
docker-compose -f docker-compose.signet.yml exec -T web curl -f -s http://api:8999/api/v1/backend-info
```
4. **Redémarrage du frontend si la configuration nginx a été modifiée** :
```bash
docker-compose -f docker-compose.signet.yml restart web
```
## Modalités d'analyse
### Vérifier l'état des services
```bash
docker-compose -f docker-compose.signet.yml ps
```
### Vérifier les logs du backend
```bash
docker-compose -f docker-compose.signet.yml logs --tail=50 api | grep -E "(ERROR|ERR|WARN|failed|error)"
```
### Tester la connectivité backend
```bash
# Depuis le conteneur frontend
docker-compose -f docker-compose.signet.yml exec -T web curl -f -s http://api:8999/api/v1/backend-info
# Depuis l'extérieur
curl -s http://localhost:3015/api/v1/backend-info
```
### Vérifier la configuration nginx WebSocket
```bash
docker-compose -f docker-compose.signet.yml exec -T web cat /etc/nginx/conf.d/nginx-mempool.conf | grep -A 5 "api/v1/ws"
```
La configuration doit être :
```nginx
location /api/v1/ws {
proxy_pass http://api:8999/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
}
```
## Notes
- Le backend mempool écoute sur toutes les routes pour les WebSockets (WebSocket.Server créé directement sur le serveur HTTP)
- Le nginx du frontend proxyfie `/api/v1/ws` vers `http://api:8999/` (racine du backend)
- Le backend peut être en état "unhealthy" mais fonctionner quand même si le healthcheck échoue pour une raison technique (par exemple, `curl` non disponible dans le conteneur)
- Les erreurs `ECONNRESET` avec Bitcoin RPC peuvent être temporaires et se résoudre avec un redémarrage

View File

@ -0,0 +1,55 @@
# Signet: bitcoind crash, mining stopped
**Date:** 2026-02-05
**Auteur:** Équipe 4NK
## Problème
La chaîne signet ne mine plus. Les logs du miner affichent en boucle:
`error: timeout on transient error: Could not connect to the server 127.0.0.1:38332`.
## Cause
Le processus **bitcoind** a cessé de tourner à lintérieur du conteneur `bitcoin-signet-instance`. Le conteneur reste « Up », mais seul le script `run.sh` et `mine.sh` sont actifs; bitcoind napparaît plus dans `ps aux`. Le RPC bitcoind (port 38332) ne répond donc pas.
Dans `signet/debug.log`, la dernière ligne utile est un `CreateNewBlock()` ou `UpdateTip`; aucune ligne derreur ou de shutdown explicite (crash ou kill possible, ex. OOM).
## Diagnostic
1. Lancer le script détat:
```bash
./data/check-services-status.sh
```
2. Si « RPC: HORS SERVICE » pour bitcoind alors que le conteneur est Up: confirmer avec
```bash
docker exec bitcoin-signet-instance ps aux
```
(absence du processus `bitcoind`).
3. Dernières lignes du log bitcoind:
```bash
docker exec bitcoin-signet-instance tail -20 /root/.bitcoin/signet/debug.log
```
## Correction
Redémarrer le conteneur pour relancer bitcoind (et donc le minage):
```bash
docker restart bitcoin-signet-instance
```
Attendre ~3060 s que bitcoind et le wallet soient prêts, puis vérifier:
```bash
docker exec bitcoin-signet-instance bitcoin-cli -datadir=/root/.bitcoin getblockchaininfo
```
## Prévention / suivi
- Utiliser `./data/check-services-status.sh` régulièrement (ou via cron) pour détecter RPC hors service.
- En cas de répétition des crashes, investiguer (RAM, disque, `dmesg` OOM, logs bitcoind complets).
## Pages affectées
- `data/check-services-status.sh` (script de vérification)
- `fixKnowledge/signet-bitcoind-crash-mining-stopped.md` (ce document)

View File

@ -0,0 +1,186 @@
# Signet: bitcoind crash puis blocage en chargement de wallet
**Date:** 2026-02-08
**Auteur:** Équipe 4NK
## Problème
Bitcoind a planté silencieusement dans le conteneur `bitcoin-signet-instance` vers 3h08 du matin. Le conteneur reste actif, mais le processus bitcoind n'est plus en cours d'exécution. Au redémarrage du conteneur à 07:12, bitcoind est bloqué en chargement de wallet ("Loading wallet…").
### Symptômes
1. **Conteneur actif mais bitcoind planté** :
- Le conteneur Docker est "Up" mais bitcoind n'est plus en cours d'exécution
- `ps aux` dans le conteneur montre uniquement `entrypoint.sh`, `run.sh`, `mine.sh`, `tail -f /dev/null`
- Pas de processus `bitcoind`
2. **RPC inaccessible** :
- `bitcoin-cli` retourne : `Could not connect to the server 127.0.0.1:38332`
- `curl` vers le RPC depuis l'hôte : connexion reset (exit 56)
- Le port 38332 est toujours en écoute côté hôte (mapping Docker) mais il n'y a plus de processus RPC derrière
3. **Blocage en chargement de wallet** :
- Après redémarrage, bitcoind est bloqué en "Loading wallet…"
- `bitcoin-cli getblockchaininfo` retourne : `error code: -28 error message: Loading wallet…`
- CPU à 100% mais aucun progrès visible
- Wallet volumineux : 315MB (`wallet.dat`)
4. **Erreurs des services dépendants** :
- Dashboard : erreurs "socket hang up" et "read ECONNRESET"
- Script `sync-utxos-cron.sh` : erreurs de connexion RPC
### Impact
- **Fonctionnalité** : Tous les services dépendants de bitcoind sont hors service
- **Mining** : Le minage est arrêté
- **API** : L'API d'ancrage ne peut plus fonctionner
- **Dashboard** : Le dashboard ne peut plus afficher les données de la blockchain
## Root causes
1. **Crash silencieux de bitcoind** :
- Bitcoind a planté silencieusement vers 3h08 sans message d'erreur dans les logs
- Dernière activité normale : bloc miné à 02:54:29 (height 13139)
- Nouveau bloc reçu à 03:08:48 (height 13140)
- Après cela, plus aucune activité jusqu'au redémarrage à 07:12:16
2. **Wallet volumineux** :
- Le wallet `custom_signet` fait 315MB
- Le chargement du wallet peut prendre beaucoup de temps ou bloquer si le wallet est corrompu ou trop volumineux
3. **Absence de détection automatique** :
- Le conteneur Docker reste actif même si bitcoind plante
- Aucun mécanisme de redémarrage automatique de bitcoind dans le conteneur
- Le script `check-services-status.sh` détecte le problème mais ne le corrige pas automatiquement
## Diagnostic
### Vérifier l'état actuel
1. **Vérifier les processus dans le conteneur** :
```bash
docker exec bitcoin-signet-instance ps aux
```
Si bitcoind n'apparaît pas, il a planté.
2. **Vérifier l'état du RPC** :
```bash
docker exec bitcoin-signet-instance bitcoin-cli -datadir=/root/.bitcoin getblockchaininfo
```
Si erreur "Could not connect" ou "Loading wallet…", bitcoind est planté ou bloqué.
3. **Vérifier les logs** :
```bash
docker logs bitcoin-signet-instance --tail 100
docker exec bitcoin-signet-instance tail -50 /root/.bitcoin/signet/debug.log
```
4. **Vérifier la taille du wallet** :
```bash
docker exec bitcoin-signet-instance du -sh /root/.bitcoin/signet/wallets/custom_signet/
```
### Utiliser le script de diagnostic
```bash
./data/check-services-status.sh
```
Si "RPC: HORS SERVICE", bitcoind a planté.
## Correctifs
### Solution immédiate : Redémarrer le conteneur
```bash
docker restart bitcoin-signet-instance
```
Attendre 60-120 secondes que bitcoind démarre et charge le wallet, puis vérifier :
```bash
docker exec bitcoin-signet-instance bitcoin-cli -datadir=/root/.bitcoin getblockchaininfo
```
### Si le wallet reste bloqué en chargement
Si après redémarrage, bitcoind reste bloqué en "Loading wallet…" pendant plus de 5 minutes :
1. **Arrêter le conteneur** :
```bash
docker stop bitcoin-signet-instance
```
2. **Vérifier l'intégrité du wallet** :
```bash
docker exec bitcoin-signet-instance ls -lah /root/.bitcoin/signet/wallets/custom_signet/
```
Vérifier la présence de `wallet.dat-journal` (fichier de journalisation)
3. **Redémarrer avec vérification** :
```bash
docker start bitcoin-signet-instance
docker logs bitcoin-signet-instance -f
```
Surveiller les logs pour voir si le wallet se charge correctement.
4. **Si le wallet est corrompu** :
- Sauvegarder le wallet actuel
- Essayer de réparer avec `bitcoin-wallet` (si disponible)
- Ou restaurer depuis une sauvegarde
### Solution préventive : Améliorer la détection et le redémarrage automatique
1. **Améliorer le script `check-services-status.sh`** :
- Détecter si bitcoind est planté même si le conteneur est actif
- Proposer automatiquement un redémarrage si le problème est détecté
2. **Ajouter un healthcheck Docker** :
- Créer un healthcheck qui vérifie que le RPC bitcoind répond
- Configurer Docker pour redémarrer automatiquement le conteneur si le healthcheck échoue
3. **Surveillance proactive** :
- Ajouter un cron job qui exécute `check-services-status.sh` toutes les 5 minutes
- Envoyer une alerte si bitcoind est hors service
## Prévention / suivi
- **Surveillance régulière** : Exécuter `./data/check-services-status.sh` régulièrement (ou via cron toutes les 5 minutes)
- **Logs** : Surveiller les logs bitcoind pour détecter les problèmes avant qu'ils ne causent un crash
- **Sauvegardes** : Sauvegarder régulièrement le wallet et la chaîne
- **Investigation** : En cas de répétition des crashes, investiguer (RAM, disque, `dmesg` OOM, logs bitcoind complets)
## Note importante sur les crons et la cause probable
**Le script `restart-services-cron.sh` n'est PAS actuellement dans les crontabs.**
**Cause probable du crash récurrent vers 3h** : Le script `sync-utxos-cron.sh` s'exécute à 3h00 et fait un appel RPC très lourd (`listunspent` avec `maximumCount: 9999999`). Avec un wallet volumineux (315MB) et potentiellement des centaines de milliers d'UTXOs, cet appel peut :
- Consommer beaucoup de mémoire
- Prendre beaucoup de temps
- Causer un crash de bitcoind si la mémoire est insuffisante ou si bitcoind est déjà dans un état fragile
**Recommandation pour ajouter `restart-services-cron.sh` à 4h** :
**OUI, c'est prudent d'ajouter `restart-services-cron.sh` à 4h**, car :
- Le script a été amélioré pour gérer les wallets volumineux (timeout de 5 minutes, détection du chargement de wallet)
- Cela permettra de redémarrer bitcoind automatiquement après un crash potentiel vers 3h
- Le redémarrage à 4h laisse le temps au script `sync-utxos-cron.sh` de se terminer (ou d'échouer) avant le redémarrage
⚠️ **Mais il faut aussi** :
- Surveiller les logs pour confirmer que le crash est bien lié au script `sync-utxos-cron.sh`
- Considérer d'optimiser le script `sync-utxos-spent-status.mjs` pour réduire la charge sur bitcoind
- Surveiller la mémoire disponible pour bitcoind
**Configuration recommandée** :
```bash
# Ajouter dans crontab
0 4 * * * /home/ncantu/Bureau/code/bitcoin/data/restart-services-cron.sh
```
Cela redémarrera bitcoind tous les jours à 4h, ce qui permettra de récupérer automatiquement après un crash potentiel vers 3h.
## Pages affectées
- `data/check-services-status.sh` (script de vérification)
- `fixKnowledge/signet-bitcoind-crash-wallet-loading-stuck.md` (ce document)
- `fixKnowledge/signet-bitcoind-crash-mining-stopped.md` (documentation similaire)

View File

@ -0,0 +1,135 @@
# Optimisation du script sync-utxos pour éviter les crashes RPC
**Date:** 2026-02-08
**Auteur:** Équipe 4NK
## Problème
Le script `sync-utxos-cron.sh` s'exécute toutes les heures (à 3h00 notamment) et fait un appel RPC très lourd (`listunspent` avec `maximumCount: 9999999`). Avec un wallet volumineux (315MB) et potentiellement des centaines de milliers d'UTXOs, cet appel peut :
- Consommer beaucoup de mémoire
- Prendre beaucoup de temps (plusieurs minutes)
- Causer un crash de bitcoind si la mémoire est insuffisante ou si bitcoind est déjà dans un état fragile
- Bloquer le RPC pendant plusieurs minutes, empêchant les autres services de fonctionner
### Symptômes observés
- Crash silencieux de bitcoind vers 3h08 (après l'exécution du script à 3h00)
- Erreurs "socket hang up" et "read ECONNRESET" dans les logs du dashboard
- Bitcoind bloqué en chargement de wallet après redémarrage
- Wallet volumineux (315MB) causant des chargements longs
## Root causes
1. **Appel RPC sans timeout** : L'appel `listunspent` n'avait pas de timeout, pouvant bloquer indéfiniment
2. **Pas de vérification préalable** : Le script ne vérifiait pas si bitcoind était disponible avant de faire l'appel lourd
3. **Pas de retry avec backoff** : En cas d'échec temporaire, le script échouait immédiatement
4. **Limite trop élevée** : `maximumCount: 9999999` pouvait charger des centaines de milliers d'UTXOs en mémoire d'un coup
5. **Pas de gestion d'erreurs** : Les erreurs n'étaient pas gérées de manière gracieuse
## Correctifs appliqués
### 1. Ajout de timeout sur les appels RPC
**Fichier** : `data/sync-utxos-spent-status.mjs`
- **Timeout de 5 minutes** (300000ms) pour l'appel `listunspent` avec gros wallet
- **Timeout de 10 secondes** pour le healthcheck `getblockchaininfo`
- Les timeouts sont configurables via les paramètres de la fonction `rpcCall`
### 2. Vérification de santé de bitcoind avant synchronisation
**Fichier** : `data/sync-utxos-spent-status.mjs`
- Ajout de la fonction `checkBitcoindHealth()` qui vérifie que bitcoind répond avant de commencer
- Si bitcoind n'est pas disponible, le script s'arrête immédiatement sans faire d'appel RPC lourd
### 3. Retry avec backoff exponentiel
**Fichier** : `data/sync-utxos-spent-status.mjs`
- Ajout de retry automatique avec backoff exponentiel (1s, 2s, 4s, max 10s)
- Maximum 3 tentatives pour les erreurs de timeout/connexion
- Maximum 2 tentatives pour l'appel `listunspent` (pour éviter de surcharger bitcoind)
### 4. Réduction de la limite maximumCount
**Fichier** : `data/sync-utxos-spent-status.mjs`
- Réduction de `maximumCount` de 9999999 à 500000 UTXOs
- Limite la consommation mémoire et réduit le temps de traitement
- Si vous avez plus de 500000 UTXOs, il faudra augmenter cette limite ou optimiser l'approche
### 5. Amélioration du script cron
**Fichier** : `data/sync-utxos-cron.sh`
- Vérification que le conteneur bitcoind est actif avant d'exécuter le script Node.js
- Vérification que le RPC bitcoind répond avant de commencer
- Logs améliorés avec timestamps
- Gestion des codes de sortie pour permettre la détection d'échecs
### 6. Gestion d'erreurs améliorée
**Fichier** : `data/sync-utxos-spent-status.mjs`
- Distinction entre erreurs temporaires (timeout, connexion) et erreurs permanentes
- Messages d'erreur plus informatifs
- Le script ne fait pas échouer le cron si c'est un problème temporaire de bitcoind
## Modifications
**Fichiers modifiés :**
- `data/sync-utxos-spent-status.mjs` :
- Ajout de `rpcCall()` avec timeout et retry
- Ajout de `rpcCallOnce()` avec timeout HTTP
- Ajout de `checkBitcoindHealth()`
- Réduction de `maximumCount` à 500000
- Timeout de 5 minutes pour `listunspent`
- Gestion d'erreurs améliorée
- `data/sync-utxos-cron.sh` :
- Vérification de disponibilité de bitcoind avant exécution
- Logs améliorés avec timestamps
- Gestion des codes de sortie
## Modalités de déploiement
1. **Vérifier que les scripts sont exécutables** :
```bash
chmod +x data/sync-utxos-cron.sh
```
2. **Tester manuellement** :
```bash
./data/sync-utxos-cron.sh
```
3. **Vérifier les logs** :
```bash
tail -f data/sync-utxos.log
```
4. **Surveiller la prochaine exécution** (à 3h00) pour vérifier que le problème est résolu
## Modalités d'analyse
- **Logs du script** : `data/sync-utxos.log` (100 dernières lignes conservées)
- **Vérifier les timeouts** : Chercher "timeout" dans les logs
- **Vérifier les retries** : Chercher "Tentative" dans les logs
- **Vérifier les erreurs** : Chercher "❌" dans les logs
## Prévention / suivi
- **Surveillance** : Surveiller les logs après chaque exécution pour détecter les problèmes
- **Ajustement** : Si vous avez plus de 500000 UTXOs, augmenter `maximumCount` ou optimiser l'approche
- **Timeout** : Si le timeout de 5 minutes est insuffisant, l'augmenter progressivement
- **Investigation** : En cas de répétition des problèmes, investiguer la mémoire disponible pour bitcoind
## Pages affectées
- `data/sync-utxos-spent-status.mjs` (script de synchronisation amélioré)
- `data/sync-utxos-cron.sh` (script cron amélioré)
- `fixKnowledge/sync-utxos-rpc-optimization.md` (ce document)
- `fixKnowledge/signet-bitcoind-crash-wallet-loading-stuck.md` (documentation du problème)

View File

@ -1,269 +0,0 @@
import { useState, FormEvent, useEffect } from 'react';
import { Link } from 'react-router-dom';
import {
addRemotePairFromWords,
ensureLocalPairForSetup,
getStoredPairs,
parseAndValidatePairingWords,
removePair,
} from '../utils/pairing';
import { usePairingWordsContext } from '../contexts/PairingWordsContext';
import { useIdentity } from '../hooks/useIdentity';
import { usePairingConnected } from '../hooks/usePairingConnected';
import { getStoredRelays } from '../utils/relay';
import { runDevice2Confirmation } from '../services/pairingConfirm';
import { WordInputGrid } from './WordInputGrid';
export function PairingDisplayScreen(): JSX.Element {
const ctx = usePairingWordsContext();
const { identity, isLoading, createNewIdentity } = useIdentity();
const { connected: pairingConnected } = usePairingConnected();
const [words2nd, setWords2nd] = useState<string[]>([]);
const [wordInput, setWordInput] = useState<string[]>([]);
const [error, setError] = useState<string | null>(null);
const [success, setSuccess] = useState(false);
const [isConfirming, setIsConfirming] = useState(false);
const [justConnected, setJustConnected] = useState(false);
const [visibleWordIndices, setVisibleWordIndices] = useState<Set<number>>(new Set());
const toggleWordVisibility = (index: number): void => {
setVisibleWordIndices((prev) => {
const next = new Set(prev);
if (next.has(index)) {
next.delete(index);
} else {
next.add(index);
}
return next;
});
};
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);
setWords2nd(w);
}
}, [identity]); // eslint-disable-line react-hooks/exhaustive-deps
useEffect(() => {
if (isLoading || identity !== null) {
return;
}
createNewIdentity();
}, [isLoading, identity, createNewIdentity]);
useEffect(() => {
ctx?.setOfferWords(words2nd);
return () => {
ctx?.setOfferWords(null);
};
}, [ctx, words2nd]);
const handleSubmit = (e: FormEvent<HTMLFormElement>): void => {
e.preventDefault();
void (async (): Promise<void> => {
setError(null);
const wordsText = wordInput.join(' ');
const parsed = parseAndValidatePairingWords(wordsText);
if (parsed === null) {
setError('Mots invalides. 17 mots requis.');
return;
}
const pair = addRemotePairFromWords(parsed, []);
if (pair === null) {
setError('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
) {
setSuccess(true);
return;
}
const relays = getStoredRelays().filter((r) => r.enabled);
if (relays.length === 0) {
setError('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;
setIsConfirming(true);
try {
const ok = await runDevice2Confirmation({
pairLocal: local.uuid,
pairRemote: remote.uuid,
identity,
relays,
start: identity.t0_anniversaire,
end: Date.now(),
remotePublicKey,
});
setJustConnected(ok);
} catch (err) {
console.error('Pairing confirmation (device 2):', err);
setError(
err instanceof Error ? err.message : 'Erreur lors de la confirmation du pairing.',
);
setIsConfirming(false);
return;
}
setIsConfirming(false);
setSuccess(true);
})();
};
if (success) {
const showConnected = pairingConnected || justConnected;
return (
<main>
{showConnected && (
<p role="status" style={{ fontWeight: 'bold', color: 'var(--color-ok, green)' }}>
Connecté
</p>
)}
<h1>Pair associé</h1>
<p>Le pair du 1ʳ appareil a é ajouté.</p>
<div
role="region"
aria-labelledby="words-2nd-heading"
id="mots-2e-appareil"
style={{
display: 'block',
marginTop: '1.5rem',
marginBottom: '1.5rem',
padding: '1rem',
backgroundColor: 'var(--color-info-bg, #dbeafe)',
border: '2px solid var(--color-info-border, #93c5fd)',
borderRadius: '8px',
color: 'var(--color-text)',
}}
>
<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"
style={{
display: 'flex',
flexWrap: 'wrap',
gap: '0.5rem',
fontFamily: 'monospace',
fontSize: '1rem',
marginTop: '0.5rem',
padding: '0.75rem',
backgroundColor: 'var(--color-background)',
borderRadius: '4px',
border: '1px solid var(--color-info-border, #93c5fd)',
}}
>
{words2nd.map((word, index) => {
const visible = visibleWordIndices.has(index);
return (
<div
key={index}
style={{
display: 'flex',
alignItems: 'center',
gap: '0.25rem',
padding: '0.25rem 0.5rem',
backgroundColor: 'var(--color-info-bg, #dbeafe)',
borderRadius: '4px',
border: '1px solid var(--color-info-border, #93c5fd)',
}}
>
<span style={{ fontWeight: 'bold' }}>{index + 1}.</span>
<span style={{ minWidth: '80px' }}>
{visible ? word : '•••••'}
</span>
<button
type="button"
onClick={() => toggleWordVisibility(index)}
style={{
background: 'none',
border: 'none',
cursor: 'pointer',
padding: '0.125rem',
fontSize: '0.875rem',
color: 'var(--color-text-secondary, #666)',
}}
aria-label={`${visible ? 'Masquer' : 'Afficher'} le mot ${index + 1}`}
title={`${visible ? 'Masquer' : 'Afficher'} le mot ${index + 1}`}
>
{visible ? '👁' : '👁‍🗨'}
</button>
</div>
);
})}
</div>
) : (
<p style={{ margin: 0, padding: '0.75rem' }}></p>
)}
</div>
<p>
<Link to="/">Accueil</Link> <Link to="/manage-pairs">Gérer les pairs</Link>
</p>
</main>
);
}
return (
<main>
<h1>Saisir les mots du 1ʳ appareil</h1>
<p>
Saisissez les 17 mots affichés par le 1ʳ appareil.
</p>
<form
onSubmit={(ev) => void handleSubmit(ev)}
aria-label="Saisir les mots du 1er appareil"
>
<label htmlFor="pairing-words-display">
Mots du 1ʳ appareil
<WordInputGrid
id="pairing-words-display"
value={wordInput}
onChange={setWordInput}
aria-describedby={error !== null ? 'pairing-display-err' : undefined}
aria-label="Saisir les 17 mots du 1er appareil"
wordCount={17}
/>
</label>
{error !== null && (
<p id="pairing-display-err" role="alert" style={{ color: 'var(--color-error)' }}>
{error}
</p>
)}
<button type="submit" disabled={isConfirming}>
{isConfirming ? 'Finalisation…' : 'Valider'}
</button>
</form>
<p>
<Link to="/">Accueil</Link> <Link to="/manage-pairs">Gérer les pairs</Link>
</p>
</main>
);
}

View File

@ -1,275 +0,0 @@
import { useEffect, useMemo, useState, FormEvent } from 'react';
import { useNavigate } from 'react-router-dom';
import QRCode from 'qrcode';
import {
ensureLocalPairForSetup,
addRemotePairFromWords,
getStoredPairs,
parseAndValidatePairingWords,
removePair,
} from '../utils/pairing';
import { useIdentity } from '../hooks/useIdentity';
import { getStoredRelays } from '../utils/relay';
import { runDevice1Confirmation } from '../services/pairingConfirm';
import { WordInputGrid } from './WordInputGrid';
const PAIRING_DISPLAY_PATH = '/pairing-display';
const QR_SIZE = 256;
function buildPairingDisplayUrl(): string {
return `${window.location.origin}${PAIRING_DISPLAY_PATH}`;
}
export interface PairingSetupBlockProps {
onDone?: () => void;
isAddingDevice?: boolean;
}
export function PairingSetupBlock({
onDone,
isAddingDevice = false,
}: PairingSetupBlockProps): JSX.Element {
const navigate = useNavigate();
const { identity, createNewIdentity } = useIdentity();
const [words, setWords] = useState<string[]>([]);
const [qrDataUrl, setQrDataUrl] = useState<string | null>(null);
const [remoteWordsInput, setRemoteWordsInput] = useState<string[]>([]);
const [remoteError, setRemoteError] = useState<string | null>(null);
const [hasCopiedToSecondDevice, setHasCopiedToSecondDevice] = useState(false);
const [isConfirming, setIsConfirming] = useState(false);
const [visibleWordIndices, setVisibleWordIndices] = useState<Set<number>>(new Set());
const toggleWordVisibility = (index: number): void => {
setVisibleWordIndices((prev) => {
const next = new Set(prev);
if (next.has(index)) {
next.delete(index);
} else {
next.add(index);
}
return next;
});
};
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);
setWords(w);
}
}, [identity]);
const url = useMemo(() => buildPairingDisplayUrl(), []);
useEffect(() => {
if (words.length === 0) {
return;
}
QRCode.toDataURL(url, { width: QR_SIZE })
.then(setQrDataUrl)
.catch((err: unknown) => {
console.error('QR generation failed:', err);
});
}, [words, url]);
const handleSubmitRemote = async (e: FormEvent<HTMLFormElement>): Promise<void> => {
e.preventDefault();
setRemoteError(null);
const wordsText = remoteWordsInput.join(' ');
const parsed = parseAndValidatePairingWords(wordsText);
if (parsed === null) {
setRemoteError('Mots invalides. 17 mots requis.');
return;
}
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);
if (identity === null || local === undefined || identity.privateKey === undefined) {
setRemoteWordsInput([]);
if (onDone !== undefined) {
onDone();
} else {
navigate('/manage-pairs');
}
return;
}
const relays = getStoredRelays().filter((r) => r.enabled);
if (relays.length === 0) {
setRemoteError('Aucun relais activé. Configurez les relais pour finaliser le pairing.');
return;
}
const remotePublicKey = newPair.publicKey;
setIsConfirming(true);
try {
await runDevice1Confirmation({
pairLocal: local.uuid,
pairRemote: newPair.uuid,
identity,
relays,
remotePublicKey,
});
} catch (err) {
console.error('Pairing confirmation (device 1):', err);
setRemoteError(
err instanceof Error ? err.message : 'Erreur lors de la confirmation du pairing.',
);
setIsConfirming(false);
return;
}
setIsConfirming(false);
setRemoteWordsInput([]);
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-block-heading">
<h3 id="pairing-setup-block-heading">{setupHeading}</h3>
{words.length > 0 && (
<>
{!hasCopiedToSecondDevice ? (
<>
<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={{
display: 'flex',
flexWrap: 'wrap',
gap: '0.5rem',
fontFamily: 'monospace',
fontSize: '1rem',
marginTop: '0.5rem',
}}
>
{words.map((word, index) => {
const visible = visibleWordIndices.has(index);
return (
<div
key={index}
style={{
display: 'flex',
alignItems: 'center',
gap: '0.25rem',
padding: '0.25rem 0.5rem',
backgroundColor: 'var(--color-info-bg, #e8f4fd)',
borderRadius: '4px',
border: '1px solid var(--color-info-border, #93c5fd)',
}}
>
<span style={{ fontWeight: 'bold' }}>{index + 1}.</span>
<span style={{ minWidth: '80px' }}>
{visible ? word : '•••••'}
</span>
<button
type="button"
onClick={() => toggleWordVisibility(index)}
style={{
background: 'none',
border: 'none',
cursor: 'pointer',
padding: '0.125rem',
fontSize: '0.875rem',
color: 'var(--color-text-secondary, #666)',
}}
aria-label={`${visible ? 'Masquer' : 'Afficher'} le mot ${index + 1}`}
title={`${visible ? 'Masquer' : 'Afficher'} le mot ${index + 1}`}
>
{visible ? '👁' : '👁‍🗨'}
</button>
</div>
);
})}
</div>
{qrDataUrl !== null && (
<p>
<img
src={qrDataUrl}
alt="QR code : URL pour ouvrir la page de pairing sur le 2e appareil"
width={QR_SIZE}
height={QR_SIZE}
/>
</p>
)}
<p>
<strong>URL pour le 2 appareil :</strong>{' '}
<a href={url} target="_blank" rel="noopener noreferrer">
{url}
</a>
</p>
<p>
<button
type="button"
onClick={() => setHasCopiedToSecondDevice(true)}
>
{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">
{isAddingDevice ? 'Mots du nouvel appareil' : 'Mots du 2ᵉ appareil'}
</h4>
<form
onSubmit={(ev) => void handleSubmitRemote(ev)}
aria-label="Saisir les mots du 2e appareil"
>
<label htmlFor="remote-pairing-words">
Mots affichés par le 2 appareil
<WordInputGrid
id="remote-pairing-words"
value={remoteWordsInput}
onChange={setRemoteWordsInput}
aria-describedby={remoteError !== null ? 'remote-words-err' : undefined}
aria-label="Saisir les 17 mots du 2e appareil"
wordCount={17}
/>
</label>
{remoteError !== null && (
<p id="remote-words-err" role="alert" style={{ color: 'var(--color-error)' }}>
{remoteError}
</p>
)}
<button type="submit" disabled={isConfirming}>
{isConfirming ? 'Finalisation…' : 'Associer le pair'}
</button>
</form>
</section>
)}
</>
)}
</div>
);
}

View File

@ -1,242 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Le contrat 4NK un nouveau web</title>
<style>
* { box-sizing: border-box; }
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 900px;
margin: 0 auto;
padding: 1rem;
line-height: 1.6;
color: #333;
}
h1 { font-size: 1.5rem; margin-bottom: 1rem; color: #222; }
h2 { font-size: 1.2rem; margin-top: 1.5rem; margin-bottom: 0.5rem; color: #333; }
h3 { font-size: 1rem; margin-top: 1rem; margin-bottom: 0.35rem; font-weight: 600; }
h4 { font-size: 0.95rem; margin-top: 0.75rem; margin-bottom: 0.25rem; font-weight: 600; color: #555; }
p { margin: 0.75rem 0; }
ul { margin: 0.5rem 0; padding-left: 1.5rem; }
li { margin: 0.4rem 0; }
a { color: #007bff; }
a:hover { text-decoration: underline; }
.highlight { background: #e8f4fd; padding: 1rem; border-radius: 8px; border-left: 4px solid #007bff; margin: 1rem 0; }
.commitment { background: #d4edda; padding: 1rem; border-radius: 8px; border-left: 4px solid #28a745; margin: 1rem 0; }
.your-part { background: #fff3cd; padding: 1rem; border-radius: 8px; border-left: 4px solid #ffc107; margin: 1rem 0; }
details { margin: 1rem 0; }
summary { cursor: pointer; font-weight: 600; color: #555; }
.meta { font-family: ui-monospace, monospace; font-size: 0.85rem; color: #666; background: #f5f5f5; padding: 0.2em 0.4em; border-radius: 4px; word-break: break-all; }
.member-list { list-style: none; padding-left: 0; }
.member-list > li { background: #f8f9fa; padding: 0.75rem 1rem; border-radius: 8px; border: 1px solid #e0e0e0; margin: 0.5rem 0; }
.member-list > li > em { color: #666; font-size: 0.9rem; }
.pair-list { list-style: none; margin-top: 0.5rem; padding-left: 1rem; border-left: 3px solid #007bff; }
.pair-list > li { background: #fff; padding: 0.5rem 0.75rem; border-radius: 6px; margin: 0.4rem 0; border: 1px solid #e8e8e8; }
.roles-intro { margin-bottom: 1rem; }
.role-block { background: #f8f9fa; border: 1px solid #e0e0e0; border-radius: 8px; padding: 0.75rem 1rem; margin: 0.5rem 0; }
.role-block h4 { margin-top: 0; }
.role-block .usage, .role-block .validation { font-size: 0.9rem; color: #555; margin: 0.25rem 0; }
.role-block .usage strong, .role-block .validation strong { color: #333; }
@media (max-width: 768px) {
body { padding: 0.75rem; }
h1 { font-size: 1.25rem; }
}
</style>
</head>
<body>
<h1>Le contrat</h1>
<p><a href="index.html">← Retour à l'accueil</a> · <a href="membre.html">Qui êtes-vous ?</a> · <a href="technique.html">Réseau P2P</a> · <a href="cryptographie.html">Cryptographie</a></p>
<div class="highlight">
<strong>En résumé :</strong> Ce contrat définit les règles de confiance entre vous et le service.
Il précise <strong>qui peut faire quoi</strong> et <strong>comment prouver son identité</strong>.
Tout est vérifiable et transparent.
</div>
<h2>Qu'est-ce que ce contrat ?</h2>
<p>
Ce n'est pas un contrat papier, mais un <strong>accord numérique</strong> qui établit les règles du jeu.
Il définit :
</p>
<ul>
<li><strong>Le service</strong> : Website Skeleton (ce site de démonstration).</li>
<li><strong>Les actions possibles</strong> : se connecter (login).</li>
<li><strong>Les validateurs</strong> : qui a le droit de vérifier les connexions.</li>
</ul>
<h2>Ce que le service s'engage à faire</h2>
<div class="commitment">
<ul>
<li><strong>Ne jamais stocker vos clés privées</strong> — elles restent sur votre appareil.</li>
<li><strong>Vérifier votre identité de façon transparente</strong> — via une preuve cryptographique que vous fournissez.</li>
<li><strong>Respecter les règles du contrat</strong> — publiquement vérifiables.</li>
<li><strong>Utiliser une clé de service déclarée</strong> — jamais exposée, mais vérifiable.</li>
</ul>
</div>
<h2>Ce que vous vous engagez à faire</h2>
<div class="your-part">
<ul>
<li><strong>Protéger votre appareil</strong> — c'est votre coffre-fort numérique.</li>
<li><strong>Fournir une preuve valide</strong> — en signant avec vos clés lors de la connexion.</li>
<li><strong>Être responsable de vos clés</strong> — si vous les perdez, personne ne peut les récupérer.</li>
</ul>
</div>
<h2>Les parties prenantes</h2>
<h3>Le service (Website Skeleton)</h3>
<p>
C'est le site que vous utilisez. Il possède sa propre identité (un « validateur ») qui permet
de vérifier que les connexions sont légitimes.
</p>
<h3>Vous (le membre connecté)</h3>
<p>
Vous êtes l'utilisateur qui souhaite accéder au service. Votre identité est prouvée par
la signature de votre appareil (<a href="membre.html">en savoir plus</a>).
</p>
<h2>Comment fonctionne la validation ?</h2>
<ol>
<li>Vous demandez à vous connecter.</li>
<li>Votre appareil crée une <strong>preuve</strong> (login-proof) signée avec vos clés.</li>
<li>Le service vérifie cette preuve grâce aux règles définies dans ce contrat.</li>
<li>Si la preuve est valide, vous êtes connecté. Sinon, l'accès est refusé.</li>
</ol>
<p>
Ce système est <strong>plus sûr</strong> qu'un mot de passe classique car il n'y a rien à voler côté serveur.
</p>
<h2>Pourquoi c'est plus sûr ?</h2>
<ul>
<li><strong>Pas de base de mots de passe</strong> à pirater.</li>
<li><strong>Vos clés ne transitent jamais</strong> sur le réseau.</li>
<li><strong>Chaque connexion est unique</strong> (signature à usage unique).</li>
<li><strong>Vérifiable par tous</strong> — les règles du contrat sont publiques.</li>
</ul>
<h2>Infrastructure, gouvernance et preuves</h2>
<p>
L'<strong>infrastructure</strong> (relais, stockage des messages et des signatures) est <strong>décentralisée et neutre</strong> :
elle ne dépend pas d'un acteur unique.
</p>
<p>
Les <strong>membres</strong> ont une <strong>liberté de gouvernance</strong>. Certains peuvent choisir une organisation
<strong>centralisée</strong> lorsque c'est préférable (équipe, processus, conformité). D'autres restent décentralisés.
</p>
<p>
L'important est d'avoir les <strong>preuves de la bonne exécution du contrat entre les parties</strong> :
signatures, nonces, règles publiques. Ces preuves restent vérifiables quelle que soit l'organisation
(décentralisée ou centralisée) des membres.
</p>
<details>
<summary>Détails techniques du contrat</summary>
<h3>Identifiants</h3>
<ul>
<li>Contrat UUID : <span class="meta">f9b9b336-4282-4c1c-b70b-e5197aeae3fa</span></li>
<li>Service UUID : <span class="meta">32b9095a-562d-4239-ae45-2d7ffb1a40de</span></li>
<li>Action login UUID : <span class="meta">0ac7de59-9e81-4bdc-bd19-c07750fad48e</span></li>
<li>Validateur (membre) : <span class="meta">0e865301-362f-4951-bfbc-531b7bddf820</span></li>
</ul>
<h3>Rôles par défaut (types de Champ)</h3>
<p class="roles-intro">
Les <strong>11 rôles</strong> cidessous existent par défaut. Chacun peut être <strong>vide</strong> (aucun membre)
ou <strong>plein</strong> (membres et pairs définis). Tous sont <strong>connus des participants</strong> ;
les conditions d'usage et de validation s'appliquent dès qu'un rôle est rempli.
</p>
<div class="role-block">
<h4>1. Partage avec les institutions</h4>
<p class="usage"><strong>Usage :</strong> Partager des données avec des institutions.</p>
<p class="validation"><strong>Validation :</strong> Conformité aux validateurs du champ ; signatures requises selon <code>membres_du_role</code>.</p>
</div>
<div class="role-block">
<h4>2. Messages au RSSI</h4>
<p class="usage"><strong>Usage :</strong> Messages au RSSI de la société responsable du service.</p>
<p class="validation"><strong>Validation :</strong> Signatures des membres du rôle conformes aux <code>signatures_obligatoires</code>.</p>
</div>
<div class="role-block">
<h4>3. Messages au Correspondant CNIL</h4>
<p class="usage"><strong>Usage :</strong> Messages au Correspondant CNIL de la société responsable du service.</p>
<p class="validation"><strong>Validation :</strong> Signatures des membres du rôle ; attributs CNIL requis si applicables.</p>
</div>
<div class="role-block">
<h4>4. Messages au Responsable cybersécurité</h4>
<p class="usage"><strong>Usage :</strong> Messages au Responsable cybersécurité de la société responsable du service.</p>
<p class="validation"><strong>Validation :</strong> Signatures conformes aux validateurs du champ.</p>
</div>
<div class="role-block">
<h4>5. Messages de support infogérant</h4>
<p class="usage"><strong>Usage :</strong> Support infogérant du service (peut inclure un membre du miner pour la gestion des clés API).</p>
<p class="validation"><strong>Validation :</strong> Signatures des membres du rôle ; <code>membre_miner_uuid</code> optionnel dans <code>datajson</code> si applicable.</p>
</div>
<div class="role-block">
<h4>6. Messages de support administrateur système</h4>
<p class="usage"><strong>Usage :</strong> Support administrateur système du service.</p>
<p class="validation"><strong>Validation :</strong> Signatures conformes aux validateurs du champ.</p>
</div>
<div class="role-block">
<h4>7. Messages de support niveau 1</h4>
<p class="usage"><strong>Usage :</strong> Support niveau 1 du service.</p>
<p class="validation"><strong>Validation :</strong> Signatures conformes aux validateurs du champ.</p>
</div>
<div class="role-block">
<h4>8. Messages de support niveau 2</h4>
<p class="usage"><strong>Usage :</strong> Support niveau 2 du service.</p>
<p class="validation"><strong>Validation :</strong> Signatures conformes aux validateurs du champ.</p>
</div>
<div class="role-block">
<h4>9. Messages de support niveau 3</h4>
<p class="usage"><strong>Usage :</strong> Support niveau 3 du service.</p>
<p class="validation"><strong>Validation :</strong> Signatures conformes aux validateurs du champ.</p>
</div>
<div class="role-block">
<h4>10. Validation (contrat)</h4>
<p class="usage"><strong>Usage :</strong> Valider le contrat ; définir qui peut signer pour le contrat (<code>contrat.validateurs.membres_du_role</code>).</p>
<p class="validation"><strong>Validation :</strong> Au moins une signature valide par membre du rôle ; clés publiques autorisées dans <code>signatures_obligatoires</code>.</p>
</div>
<div class="role-block">
<h4>11. Validation du login</h4>
<p class="usage"><strong>Usage :</strong> Valider le login ; définir qui peut signer pour l'action login (<code>action.validateurs_action.membres_du_role</code>).</p>
<p class="validation"><strong>Validation :</strong> Preuve de login (hash, nonce) signée par les pairs des membres du rôle ; nonce unique ; clés autorisées.</p>
</div>
<h3>Membres par rôles (ce contrat)</h3>
<p>Ce contrat skeleton remplit uniquement les rôles <strong>Validation (contrat)</strong> et <strong>Validation du login</strong>. Les autres rôles sont vides mais connus.</p>
<h4>Validation (contrat)</h4>
<ul class="member-list">
<li>
<strong>Membre</strong> : <span class="meta">0e865301-362f-4951-bfbc-531b7bddf820</span>
<br><em>Signatures obligatoires : 1</em>
<ul class="pair-list">
<li>
<strong>Pair 1</strong> : <span class="meta">f2779304-0d9b-4139-9aee-8d3347819d98</span>
<br>Clé publique : <span class="meta">0244f299538f4a091d93561dcee0c77de3e0d8bb917c9378405653c57f7800f174</span>
</li>
</ul>
</li>
</ul>
<h4>Validation du login</h4>
<ul class="member-list">
<li>
<strong>Membre</strong> : <span class="meta">0e865301-362f-4951-bfbc-531b7bddf820</span>
<br><em>Signatures obligatoires : 1</em>
<ul class="pair-list">
<li>
<strong>Pair 1</strong> : <span class="meta">f2779304-0d9b-4139-9aee-8d3347819d98</span>
<br>Clé publique : <span class="meta">0244f299538f4a091d93561dcee0c77de3e0d8bb917c9378405653c57f7800f174</span>
</li>
</ul>
</li>
</ul>
</details>
<p><a href="index.html">← Retour à l'accueil</a> · <a href="membre.html">Qui êtes-vous ?</a> · <a href="technique.html">Réseau P2P</a> · <a href="cryptographie.html">Cryptographie</a></p>
</body>
</html>

View File

@ -1,495 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Cryptographie expliquée 4NK un nouveau web</title>
<style>
* { box-sizing: border-box; }
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 900px;
margin: 0 auto;
padding: 1rem;
line-height: 1.6;
color: #333;
}
h1 { font-size: 1.5rem; margin-bottom: 1rem; color: #222; }
h2 { font-size: 1.2rem; margin-top: 1.5rem; margin-bottom: 0.5rem; color: #333; }
h3 { font-size: 1rem; margin-top: 1rem; margin-bottom: 0.35rem; font-weight: 600; }
p { margin: 0.75rem 0; }
ul, ol { margin: 0.5rem 0; padding-left: 1.5rem; }
li { margin: 0.4rem 0; }
a { color: #007bff; }
a:hover { text-decoration: underline; }
.highlight { background: #e8f4fd; padding: 1rem; border-radius: 8px; border-left: 4px solid #007bff; margin: 1rem 0; }
.info { background: #d4edda; padding: 1rem; border-radius: 8px; border-left: 4px solid #28a745; margin: 1rem 0; }
.warning { background: #fff3cd; padding: 1rem; border-radius: 8px; border-left: 4px solid #ffc107; margin: 1rem 0; }
.algo { background: #f8f9fa; padding: 1rem; border-radius: 8px; border: 1px solid #e0e0e0; margin: 1rem 0; }
.algo-title { font-weight: 700; color: #007bff; margin-bottom: 0.5rem; }
details { margin: 1rem 0; }
summary { cursor: pointer; font-weight: 600; color: #555; }
.meta { font-family: ui-monospace, monospace; font-size: 0.85rem; color: #666; background: #f5f5f5; padding: 0.2em 0.4em; border-radius: 4px; word-break: break-all; }
.workflow { background: #f5f5f5; padding: 1rem; border-radius: 8px; margin: 1rem 0; font-family: ui-monospace, monospace; font-size: 0.85rem; white-space: pre-wrap; line-height: 1.4; overflow-x: auto; }
.step { background: #fff; border: 1px solid #ddd; border-radius: 8px; padding: 1rem; margin: 0.75rem 0; }
.step-num { display: inline-block; width: 28px; height: 28px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border-radius: 50%; text-align: center; line-height: 28px; font-weight: 700; margin-right: 0.75rem; font-size: 0.9rem; }
.algo-table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
.algo-table th, .algo-table td { padding: 0.5rem; text-align: left; border-bottom: 1px solid #ddd; }
.algo-table th { background: #f5f5f5; font-weight: 600; }
.algo-table code { background: #f5f5f5; padding: 0.2em 0.4em; border-radius: 4px; font-size: 0.9em; }
@media (max-width: 768px) {
body { padding: 0.75rem; }
h1 { font-size: 1.25rem; }
.workflow { font-size: 0.75rem; padding: 0.75rem; }
.algo-table { font-size: 0.9rem; }
}
</style>
</head>
<body>
<h1>Cryptographie expliquée</h1>
<p><a href="index.html">← Retour à l'accueil</a> · <a href="contrat.html">Le contrat</a> · <a href="membre.html">Qui êtes-vous ?</a> · <a href="technique.html">Réseau P2P</a></p>
<div class="highlight">
<strong>En résumé :</strong> Vos messages sont protégés par plusieurs couches de cryptographie.
Seul le destinataire peut les lire (chiffrement), et il peut vérifier que c'est bien vous qui les avez envoyés (signature).
Tout cela sans que personne ne connaisse vos clés secrètes.
</div>
<h2>Comment ça marche en simple ?</h2>
<p>
Imaginez que vous voulez envoyer une lettre secrète à un ami. Dans le monde réel, vous pourriez :
</p>
<ol>
<li><strong>Mettre la lettre dans une enveloppe fermée</strong> (chiffrement) — seul celui qui a la clé peut l'ouvrir</li>
<li><strong>Signer l'enveloppe</strong> (signature) — pour prouver que c'est bien vous qui l'avez envoyée</li>
<li><strong>Donner la clé au destinataire</strong> (échange de clé) — mais sans que personne d'autre ne puisse l'intercepter</li>
</ol>
<p>
C'est exactement ce que fait la cryptographie numérique, mais de façon <strong>mathématiquement impossible à falsifier</strong>.
</p>
<h2>Les algorithmes utilisés</h2>
<p>
Voici les « outils » cryptographiques du système. Chacun a un rôle précis :
</p>
<table class="algo-table">
<thead>
<tr>
<th>Algorithme</th>
<th>Rôle</th>
<th>Analogie</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>SHA-256</code></td>
<td>Empreinte unique du message</td>
<td>Comme une empreinte digitale : unique et impossible à inverser</td>
</tr>
<tr>
<td><code>ECDH secp256k1</code></td>
<td>Échange de clé sécurisé</td>
<td>Comme mélanger deux couleurs en public pour créer un secret commun</td>
</tr>
<tr>
<td><code>HKDF-SHA256</code></td>
<td>Dérivation de clé</td>
<td>Transformer le secret partagé en une clé utilisable</td>
</tr>
<tr>
<td><code>AES-256-GCM</code></td>
<td>Chiffrement du message</td>
<td>Un coffre-fort numérique ultra-sécurisé</td>
</tr>
<tr>
<td><code>Schnorr secp256k1</code></td>
<td>Signature numérique</td>
<td>Votre signature manuscrite, mais impossible à falsifier</td>
</tr>
</tbody>
</table>
<h2>Détail de chaque algorithme</h2>
<div class="algo">
<div class="algo-title">SHA-256 — L'empreinte digitale</div>
<p>
SHA-256 crée une « empreinte » unique de 64 caractères hexadécimaux (256 bits) pour n'importe quel message.
Deux messages différents auront toujours des empreintes différentes.
</p>
<ul>
<li><strong>Entrée</strong> : n'importe quel texte ou données</li>
<li><strong>Sortie</strong> : 64 caractères hexadécimaux (ex: <span class="meta">a7f3c9...</span>)</li>
<li><strong>Propriété clé</strong> : impossible de retrouver le message à partir de l'empreinte</li>
</ul>
<p><em>Usage</em> : identifier chaque message de façon unique sur le relais.</p>
</div>
<div class="algo">
<div class="algo-title">ECDH secp256k1 — Le secret partagé</div>
<p>
ECDH (Elliptic Curve Diffie-Hellman) permet à deux personnes de créer un <strong>secret commun</strong>
sans jamais l'échanger directement. C'est comme de la magie mathématique !
</p>
<div class="info">
<strong>L'analogie des couleurs :</strong> Imaginez qu'Alice et Bob veulent créer une couleur secrète.
<ul>
<li>Alice mélange une couleur publique avec sa couleur secrète → obtient « couleur A »</li>
<li>Bob mélange la même couleur publique avec sa couleur secrète → obtient « couleur B »</li>
<li>Ils échangent « couleur A » et « couleur B » (publiquement, tout le monde peut voir)</li>
<li>Alice mélange « couleur B » + sa couleur secrète → obtient la couleur finale</li>
<li>Bob mélange « couleur A » + sa couleur secrète → obtient <strong>la même couleur finale</strong></li>
</ul>
Résultat : Alice et Bob ont la même couleur secrète, sans l'avoir jamais échangée !
Un espion qui a vu « couleur A » et « couleur B » ne peut pas retrouver la couleur finale.
</div>
<ul>
<li><strong>Courbe</strong> : secp256k1 (la même que Bitcoin)</li>
<li><strong>Entrée</strong> : votre clé privée + la clé publique du destinataire</li>
<li><strong>Sortie</strong> : un secret partagé (32 octets)</li>
</ul>
<p><em>Usage</em> : créer un secret pour chiffrer les messages entre deux personnes.</p>
</div>
<div class="algo">
<div class="algo-title">HKDF-SHA256 — Le raffineur de clé</div>
<p>
HKDF (HMAC-based Key Derivation Function) transforme le secret partagé ECDH en une clé
de chiffrement de haute qualité.
</p>
<ul>
<li><strong>Entrée</strong> : le secret partagé ECDH (32 octets)</li>
<li><strong>Sortie</strong> : une clé AES-256 (32 octets) de qualité cryptographique</li>
<li><strong>Pourquoi</strong> : s'assurer que la clé est uniformément aléatoire</li>
</ul>
</div>
<div class="algo">
<div class="algo-title">AES-256-GCM — Le coffre-fort</div>
<p>
AES-256-GCM est l'algorithme de chiffrement qui protège le contenu du message.
Il offre à la fois la <strong>confidentialité</strong> (personne ne peut lire) et
l'<strong>intégrité</strong> (personne ne peut modifier sans qu'on le sache).
</p>
<ul>
<li><strong>Taille de clé</strong> : 256 bits (très sécurisé)</li>
<li><strong>Mode</strong> : GCM (Galois/Counter Mode) avec authentification</li>
<li><strong>IV</strong> : vecteur d'initialisation de 12 octets (différent à chaque chiffrement)</li>
<li><strong>Tag</strong> : code d'authentification de 16 octets (détecte les modifications)</li>
</ul>
<p><em>Usage</em> : chiffrer le message pour que seul le destinataire puisse le lire.</p>
</div>
<div class="algo">
<div class="algo-title">Schnorr secp256k1 — La signature</div>
<p>
La signature Schnorr prouve que vous êtes bien l'auteur du message, sans révéler votre clé privée.
Elle est <strong>plus efficace</strong> et <strong>plus simple</strong> que les signatures ECDSA traditionnelles.
</p>
<ul>
<li><strong>Entrée</strong> : le hash du message + votre clé privée</li>
<li><strong>Sortie</strong> : une signature de 64 octets (128 caractères hex)</li>
<li><strong>Vérification</strong> : n'importe qui peut vérifier avec votre clé publique</li>
</ul>
<div class="warning">
<strong>Important :</strong> La signature prouve que vous avez signé, mais elle ne révèle
jamais votre clé privée. Même en voyant 1000 de vos signatures, personne ne peut
retrouver votre clé secrète.
</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 :
</p>
<h3>Phase 1 : Préparation</h3>
<div class="step">
<span class="step-num">1</span>
<strong>Création du message</strong><br>
Votre message est structuré avec : les données, un horodatage, un identifiant unique (UUID), et les règles de validation.
</div>
<div class="step">
<span class="step-num">2</span>
<strong>Calcul de l'empreinte (hash)</strong><br>
<span class="meta">Message → SHA-256 → Hash (64 caractères)</span><br>
Cette empreinte unique identifie le message sur le réseau.
</div>
<div class="step">
<span class="step-num">3</span>
<strong>Génération du nonce</strong><br>
Un nombre aléatoire unique (nonce) est créé pour éviter qu'un même message soit rejoué.
</div>
<h3>Phase 2 : Chiffrement</h3>
<div class="step">
<span class="step-num">4</span>
<strong>Échange de clé ECDH</strong><br>
<span class="meta">Votre clé privée + Clé publique du destinataire → ECDH → Secret partagé</span><br>
Un secret commun est calculé mathématiquement, sans jamais être transmis.
</div>
<div class="step">
<span class="step-num">5</span>
<strong>Dérivation de la clé AES</strong><br>
<span class="meta">Secret partagé → HKDF-SHA256 → Clé AES-256</span><br>
Le secret est transformé en une clé de chiffrement de qualité.
</div>
<div class="step">
<span class="step-num">6</span>
<strong>Chiffrement du message</strong><br>
<span class="meta">Clé AES + IV aléatoire + Message → AES-256-GCM → Message chiffré + Tag</span><br>
Le message est enfermé dans un « coffre-fort » numérique.
</div>
<h3>Phase 3 : Signature</h3>
<div class="step">
<span class="step-num">7</span>
<strong>Signature Schnorr</strong><br>
<span class="meta">Hash + Nonce + Votre clé privée → Schnorr → Signature</span><br>
Vous signez l'empreinte du message avec votre clé privée.
</div>
<h3>Phase 4 : Publication sur le relais</h3>
<div class="step">
<span class="step-num">8</span>
<strong>Envoi au relais</strong><br>
Trois éléments sont envoyés séparément :
<ul style="margin-top: 0.5rem;">
<li><strong>MsgChiffre</strong> : le message chiffré + son hash</li>
<li><strong>MsgCle</strong> : l'IV + votre clé publique (pour que le destinataire puisse déchiffrer)</li>
<li><strong>MsgSignature</strong> : votre signature + clé publique</li>
</ul>
</div>
<h3>Phase 5 : Collecte des signatures (multi-signature)</h3>
<div class="step">
<span class="step-num">9</span>
<strong>Attente des co-signataires</strong><br>
Si le contrat exige plusieurs signatures (ex: 2 appareils sur 3), le système attend que les autres signent :
<ul style="margin-top: 0.5rem;">
<li>Interrogation du relais toutes les 2 secondes</li>
<li>Timeout après 5 minutes si signatures manquantes</li>
<li>Progression affichée (ex: "2/3 signatures")</li>
</ul>
</div>
<div class="step">
<span class="step-num">10</span>
<strong>Validation</strong><br>
Le message est validé quand :
<ul style="margin-top: 0.5rem;">
<li>Toutes les signatures requises sont présentes (cardinalité)</li>
<li>Les dépendances sont respectées (ex: "A doit signer avant B")</li>
<li>Les clés publiques correspondent aux signataires autorisés</li>
</ul>
</div>
<h3>Phase 6 : Réception et déchiffrement</h3>
<div class="step">
<span class="step-num">11</span>
<strong>Scan des clés</strong><br>
Le destinataire interroge le relais pour récupérer les MsgCle récentes.
</div>
<div class="step">
<span class="step-num">12</span>
<strong>Récupération du message</strong><br>
Avec le hash trouvé dans la MsgCle, il récupère le message chiffré.
</div>
<div class="step">
<span class="step-num">13</span>
<strong>Déchiffrement ECDH inverse</strong><br>
<span class="meta">Sa clé privée + Clé publique de l'émetteur (df) → ECDH → Même secret partagé</span><br>
Il recalcule le même secret, puis déchiffre avec AES-GCM.
</div>
<div class="step">
<span class="step-num">14</span>
<strong>Vérification des signatures</strong><br>
Chaque signature est vérifiée avec la clé publique du signataire.
</div>
<h2>Schéma récapitulatif</h2>
<div class="workflow">┌─────────────────────────────────────────────────────────────────┐
│ ÉMETTEUR │
├─────────────────────────────────────────────────────────────────┤
│ Message → SHA-256 → Hash │
│ │
│ Clé privée émetteur ─┐ │
│ ├──→ ECDH → Secret partagé │
│ Clé publique dest. ──┘ │
│ │
│ Secret partagé → HKDF-SHA256 → Clé AES-256 │
│ │
│ Clé AES + IV + Message → AES-256-GCM → Message chiffré │
│ │
│ Hash + Nonce + Clé privée → Schnorr → Signature │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ RELAIS │
├─────────────────────────────────────────────────────────────────┤
│ POST /messages ← MsgChiffre (hash, message_chiffre) │
│ POST /keys ← MsgCle (hash, iv, df_ecdh = clé pub émett.) │
│ POST /signatures← MsgSignature (hash, signature, clé publique) │
│ │
│ ... attente des co-signatures (GET /signatures/:hash) ... │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ DESTINATAIRE │
├─────────────────────────────────────────────────────────────────┤
│ GET /keys?start=&end= → Liste des MsgCle récentes │
│ GET /messages/:hash → MsgChiffre │
│ GET /signatures/:hash → MsgSignature[] │
│ │
│ Clé privée dest. ────┐ │
│ ├──→ ECDH → Même secret partagé │
│ Clé publique émett. ─┘ (df_ecdh_scannable) │
│ │
│ Secret partagé → HKDF-SHA256 → Clé AES-256 │
│ │
│ Clé AES + IV + Ciphertext → AES-256-GCM → Message original │
│ │
│ Vérification : Signature + Clé publique → Schnorr verify → OK │
└─────────────────────────────────────────────────────────────────┘</div>
<h2>Pourquoi c'est sécurisé ?</h2>
<h3>Confidentialité</h3>
<ul>
<li><strong>Seul le destinataire peut lire</strong> : le secret partagé ECDH ne peut être calculé que par l'émetteur et le destinataire</li>
<li><strong>AES-256</strong> : considéré incassable avec les technologies actuelles (2²⁵⁶ combinaisons possibles)</li>
<li><strong>IV unique</strong> : même si vous envoyez le même message deux fois, le résultat chiffré sera différent</li>
</ul>
<h3>Intégrité</h3>
<ul>
<li><strong>GCM</strong> : le mode Galois/Counter détecte toute modification du message chiffré</li>
<li><strong>Hash</strong> : l'empreinte SHA-256 garantit que le message n'a pas été altéré</li>
</ul>
<h3>Authenticité</h3>
<ul>
<li><strong>Signature Schnorr</strong> : prouve mathématiquement que c'est bien vous qui avez signé</li>
<li><strong>Multi-signature</strong> : plusieurs appareils peuvent être requis pour valider</li>
<li><strong>Anti-rejeu</strong> : le nonce empêche de réutiliser une ancienne signature</li>
</ul>
<h3>Séparation des données</h3>
<ul>
<li><strong>Message, clé et signature séparés</strong> : même si quelqu'un intercepte un élément, il ne peut rien faire sans les autres</li>
<li><strong>Clé privée jamais transmise</strong> : seules les clés publiques et les signatures circulent</li>
</ul>
<details>
<summary>Détails techniques pour les développeurs</summary>
<h3>Paramètres cryptographiques</h3>
<table class="algo-table">
<tr><th>Paramètre</th><th>Valeur</th></tr>
<tr><td>Courbe elliptique</td><td>secp256k1 (256 bits)</td></tr>
<tr><td>Taille clé AES</td><td>256 bits</td></tr>
<tr><td>Taille IV (AES-GCM)</td><td>96 bits (12 octets)</td></tr>
<tr><td>Taille Tag (AES-GCM)</td><td>128 bits (16 octets)</td></tr>
<tr><td>Hash</td><td>SHA-256 (256 bits)</td></tr>
<tr><td>KDF</td><td>HKDF-SHA256</td></tr>
<tr><td>Signature</td><td>Schnorr secp256k1 (64 octets)</td></tr>
</table>
<h3>Bibliothèques utilisées</h3>
<ul>
<li><code>@noble/secp256k1</code> : ECDH, Schnorr, manipulation de clés</li>
<li><code>@noble/hashes</code> : SHA-256, HKDF</li>
<li><code>Web Crypto API</code> : AES-256-GCM (navigateur)</li>
</ul>
<h3>Format des clés</h3>
<ul>
<li><strong>Clé privée</strong> : 32 octets (64 caractères hex)</li>
<li><strong>Clé publique (compressée)</strong> : 33 octets (66 caractères hex, préfixe 02 ou 03)</li>
<li><strong>Signature Schnorr</strong> : 64 octets (128 caractères hex)</li>
</ul>
<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, 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">
<tr><th>Constante</th><th>Valeur</th><th>Description</th></tr>
<tr><td>COLLECT_POLL_MS</td><td>2 000 ms</td><td>Intervalle entre chaque interrogation</td></tr>
<tr><td>COLLECT_TIMEOUT_MS</td><td>300 000 ms</td><td>Timeout global (5 minutes)</td></tr>
<tr><td>COLLECT_FETCH_TIMEOUT_MS</td><td>15 000 ms</td><td>Timeout par requête</td></tr>
</table>
</details>
<h2>En résumé</h2>
<div class="info">
<strong>Le système combine plusieurs couches de protection :</strong>
<ol>
<li><strong>ECDH</strong> pour créer un secret sans jamais l'échanger</li>
<li><strong>AES-256-GCM</strong> pour chiffrer le message</li>
<li><strong>Schnorr</strong> pour prouver votre identité</li>
<li><strong>SHA-256</strong> pour identifier chaque message de façon unique</li>
<li><strong>Multi-signature</strong> pour exiger plusieurs validations</li>
</ol>
Résultat : vos messages sont <strong>confidentiels</strong>, <strong>authentiques</strong> et <strong>infalsifiables</strong>.
</div>
<p><a href="index.html">← Retour à l'accueil</a> · <a href="contrat.html">Le contrat</a> · <a href="membre.html">Qui êtes-vous ?</a> · <a href="technique.html">Réseau P2P</a></p>
</body>
</html>

View File

@ -1,232 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>4NK un nouveau web - site d'exemple</title>
<style>
* {
box-sizing: border-box;
}
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 900px;
margin: 0 auto;
padding: 1rem;
line-height: 1.5;
}
h1 {
font-size: 1.5rem;
margin-bottom: 1rem;
}
.button-group {
margin: 1rem 0;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
button {
padding: 0.625rem 1.25rem;
font-size: 1rem;
border: 1px solid #ccc;
border-radius: 6px;
background: #fff;
cursor: pointer;
transition: all 0.2s;
min-height: 44px;
}
button:hover {
background: #f5f5f5;
border-color: #999;
}
button:active {
background: #e0e0e0;
}
button.primary {
background: #007bff;
color: white;
border-color: #007bff;
}
button.primary:hover {
background: #0056b3;
border-color: #0056b3;
}
button.danger {
background: #dc3545;
color: white;
border-color: #dc3545;
}
button.danger:hover {
background: #c82333;
border-color: #c82333;
}
#iframe-container {
margin: 1.5rem 0;
border-radius: 12px;
overflow: hidden;
background: #fff;
box-shadow: 0 4px 24px rgba(0, 0, 0, 0.15);
border: 1px solid #e0e0e0;
}
.iframe-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 0.75rem 1rem;
display: flex;
align-items: center;
gap: 0.75rem;
}
.iframe-header-icon {
width: 24px;
height: 24px;
background: rgba(255,255,255,0.2);
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
}
.iframe-header-title {
font-weight: 600;
font-size: 0.95rem;
}
.iframe-header-subtitle {
font-size: 0.8rem;
opacity: 0.85;
margin-left: auto;
}
#iframe-container iframe {
width: 100%;
height: 550px;
border: 0;
display: block;
background: #fafafa;
}
#connected-section {
padding: 0;
margin: 1rem 0;
}
.connected-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.75rem 0;
margin-bottom: 1rem;
border-bottom: 1px solid #e0e0e0;
}
.connected-avatar {
width: 48px;
height: 48px;
border-radius: 50%;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-size: 1.25rem;
flex-shrink: 0;
}
.connected-notifications {
width: 44px;
height: 44px;
min-width: 44px;
min-height: 44px;
border: 1px solid #ccc;
border-radius: 8px;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
}
.connected-notifications:hover {
background: #f5f5f5;
border-color: #999;
}
.connected-notifications svg {
width: 24px;
height: 24px;
}
#user-info {
margin: 1rem 0;
padding: 1rem;
background: white;
border-radius: 4px;
border: 1px solid #ddd;
}
@media (max-width: 768px) {
body {
padding: 0.75rem;
}
h1 {
font-size: 1.25rem;
}
#iframe-container {
margin: 1rem 0;
border-radius: 8px;
}
#iframe-container iframe {
height: 500px;
}
.iframe-header {
padding: 0.6rem 0.8rem;
}
.iframe-header-subtitle {
display: none;
}
button {
width: 100%;
margin-bottom: 0.5rem;
}
.button-group {
flex-direction: column;
}
}
@media (max-width: 480px) {
#iframe-container iframe {
height: 450px;
}
}
</style>
</head>
<body>
<h1>4NK un nouveau web - site d'exemple</h1>
<div id="login-section">
<p><a href="contrat.html">Le contrat</a> · <a href="membre.html">Qui êtes-vous ?</a> · <a href="technique.html">Réseau P2P</a> · <a href="cryptographie.html">Cryptographie</a></p>
<p id="waiting-status" style="display: none;" role="status" aria-live="polite">Vérification du statut pairing et relais…</p>
<div class="button-group">
<button type="button" id="btn-login" class="primary">Se connecter</button>
</div>
<div id="iframe-container" style="display: none;">
<div class="iframe-header">
<div class="iframe-header-icon">🔐</div>
<span class="iframe-header-title">UserWallet</span>
<span class="iframe-header-subtitle">Connexion sécurisée</span>
</div>
<iframe id="userwallet" title="UserWallet"></iframe>
</div>
</div>
<div id="connected-section" style="display: none;">
<header class="connected-header" aria-label="Zone connectée">
<div class="connected-avatar" aria-hidden="true" title="Avatar">👤</div>
<button type="button" class="connected-notifications" aria-label="Notifications" title="Notifications">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M18 8A6 6 0 0 0 6 8c0 7-3 9-3 9h18s-3-2-3-9"></path>
<path d="M13.73 21a2 2 0 0 1-3.46 0"></path>
</svg>
</button>
</header>
<h2>Vous êtes connecté</h2>
<div id="user-info"></div>
<p><a href="contrat.html">Le contrat</a> · <a href="membre.html">Qui êtes-vous ?</a> · <a href="technique.html">Réseau P2P</a> · <a href="cryptographie.html">Cryptographie</a></p>
<div class="button-group">
<button type="button" id="btn-logout" class="danger">Se déconnecter</button>
</div>
</div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

View File

@ -1,182 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Qui êtes-vous ? 4NK un nouveau web</title>
<style>
* { box-sizing: border-box; }
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 900px;
margin: 0 auto;
padding: 1rem;
line-height: 1.6;
color: #333;
}
h1 { font-size: 1.5rem; margin-bottom: 1rem; color: #222; }
h2 { font-size: 1.2rem; margin-top: 1.5rem; margin-bottom: 0.5rem; color: #333; }
p { margin: 0.75rem 0; }
ul { margin: 0.5rem 0; padding-left: 1.5rem; }
li { margin: 0.4rem 0; }
a { color: #007bff; }
a:hover { text-decoration: underline; }
.highlight { background: #e8f4fd; padding: 1rem; border-radius: 8px; border-left: 4px solid #007bff; margin: 1rem 0; }
.warning { background: #fff3cd; padding: 1rem; border-radius: 8px; border-left: 4px solid #ffc107; margin: 1rem 0; }
details { margin: 1rem 0; }
summary { cursor: pointer; font-weight: 600; color: #555; }
.meta { font-family: ui-monospace, monospace; font-size: 0.85rem; color: #666; background: #f5f5f5; padding: 0.2em 0.4em; border-radius: 4px; word-break: break-all; }
details h3 { font-size: 0.95rem; margin-top: 1rem; margin-bottom: 0.5rem; font-weight: 600; color: #555; }
#user-pairs-info ul { list-style: none; padding-left: 0; }
#user-pairs-info li { background: #f9f9f9; padding: 0.75rem; border-radius: 6px; margin: 0.5rem 0; border: 1px solid #e0e0e0; }
@media (max-width: 768px) {
body { padding: 0.75rem; }
h1 { font-size: 1.25rem; }
}
</style>
</head>
<body>
<h1>Qui êtes-vous ?</h1>
<p><a href="index.html">← Retour à l'accueil</a> · <a href="contrat.html">Voir le contrat</a> · <a href="technique.html">Réseau P2P</a> · <a href="cryptographie.html">Cryptographie</a></p>
<div class="highlight">
<strong>En résumé :</strong> Vous êtes un <strong>membre</strong> qui peut avoir plusieurs appareils (Pairs).
Chaque appareil possède ses propres clés et peut signer selon <strong>vos règles</strong>.
Vos données sont stockées selon les membres du contrat — vous gardez le contrôle total.
</div>
<h2>Vous êtes le « membre connecté »</h2>
<p>
Quand vous vous connectez à ce service, vous devenez un <strong>membre</strong>.
Contrairement aux sites classiques où vos identifiants sont stockés sur un serveur distant,
ici votre identité reste <strong>chez vous</strong>.
</p>
<h2>Un membre = plusieurs appareils</h2>
<p>
Un membre n'est pas limité à un seul appareil. Vous pouvez avoir <strong>plusieurs appareils</strong>
(ordinateur, téléphone, tablette) qui forment ensemble votre identité :
</p>
<ul>
<li>Chaque appareil s'appelle un « <strong>Pair</strong> » (device).</li>
<li>Chaque Pair possède <strong>sa propre paire de clés</strong> cryptographiques.</li>
<li>Tous vos Pairs peuvent signer en votre nom, selon les règles que <strong>vous</strong> définissez.</li>
</ul>
<p>
<em>Exemple :</em> Vous pouvez configurer votre ordinateur principal et votre téléphone comme deux Pairs.
Si l'un est perdu, vous gardez l'accès via l'autre.
</p>
<h2>Vous définissez les règles</h2>
<p>
Chaque membre a un <strong>contrat</strong> qui définit les règles de signature et de validation.
C'est <strong>vous</strong> qui contrôlez ces règles :
</p>
<ul>
<li><strong>Quels Pairs peuvent signer</strong> — vous décidez quels appareils sont autorisés.</li>
<li><strong>Combien de signatures sont requises</strong> — une seule, ou plusieurs pour plus de sécurité.</li>
<li><strong>Pour quelles actions</strong> — certaines actions peuvent nécessiter plus de validations.</li>
</ul>
<div class="highlight">
<strong>Exemple :</strong> Vous pouvez exiger qu'une action sensible (comme un paiement) soit signée
par <strong>2 de vos 3 appareils</strong> — c'est le principe du « multi-signature ».
</div>
<h2>Où sont stockées vos données ?</h2>
<p>
Les données du service sont stockées selon les <strong>membres définis dans le contrat</strong>.
Chaque membre a ses propres données, séparées des autres :
</p>
<ul>
<li><strong>Vos clés privées</strong> — sur vos appareils (Pairs), jamais sur le serveur.</li>
<li><strong>Vos données utilisateur</strong> — associées à votre identité de membre.</li>
<li><strong>Les preuves de signature</strong> — vérifiables publiquement, liées à vos Pairs.</li>
</ul>
<h2>Votre appareil = votre coffre-fort</h2>
<p>
Chaque appareil (Pair) joue le rôle de <strong>coffre-fort numérique</strong> :
</p>
<ul>
<li><strong>Vos clés de sécurité</strong> sont créées directement dans votre navigateur (dans la fenêtre de connexion).</li>
<li><strong>Elles ne quittent jamais votre appareil</strong> — elles sont stockées localement (IndexedDB).</li>
<li><strong>Personne d'autre n'y a accès</strong>, pas même le service.</li>
</ul>
<h2>Comment ça fonctionne ?</h2>
<ol>
<li>Vous cliquez sur « Se connecter ».</li>
<li>Une fenêtre s'ouvre (UserWallet) où vous déverrouillez votre identité.</li>
<li>Votre appareil <strong>signe</strong> une preuve que c'est bien vous (comme une signature manuscrite, mais numérique).</li>
<li>Le service vérifie cette preuve et vous donne accès.</li>
</ol>
<p>
À aucun moment vos clés secrètes ne sont transmises — seule la <strong>preuve</strong> de votre identité l'est.
</p>
<div class="warning">
<strong>Important :</strong> Si vous perdez l'accès à votre appareil (panne, vol, perte),
vous perdez vos clés. Pensez à configurer un second appareil ou une sauvegarde.
</div>
<h2>Quelle différence avec un mot de passe classique ?</h2>
<ul>
<li><strong>Mot de passe classique</strong> : stocké sur le serveur du site → risque de fuite en cas de piratage.</li>
<li><strong>Ici</strong> : vos clés restent sur votre appareil → même si le service est piraté, vos clés sont en sécurité.</li>
</ul>
<details>
<summary>Détails techniques (pour les curieux)</summary>
<ul>
<li>Vos clés utilisent la cryptographie <strong>secp256k1</strong> (la même que Bitcoin).</li>
<li>Elles sont stockées dans <strong>IndexedDB</strong> de votre navigateur.</li>
<li>La connexion utilise l'authentification multi-facteur (<strong>MFA</strong>).</li>
<li>Le service possède son propre portefeuille (wallet) séparé du vôtre, jamais exposé.</li>
</ul>
<h3>Vos Pairs et clés publiques</h3>
<div id="user-pairs-info">
<p><em>Non connecté — connectez-vous pour voir vos Pairs.</em></p>
</div>
</details>
<script>
(function() {
const SESSION_STORAGE_KEY = 'website-skeleton-session';
const container = document.getElementById('user-pairs-info');
if (!container) return;
const stored = sessionStorage.getItem(SESSION_STORAGE_KEY);
if (!stored) return;
try {
const session = JSON.parse(stored);
if (!session || !session.proof || !session.proof.signatures) return;
const signatures = session.proof.signatures;
if (signatures.length === 0) {
container.innerHTML = '<p><em>Aucun Pair enregistré.</em></p>';
return;
}
let html = '<ul>';
signatures.forEach(function(sig, index) {
const pairUuid = sig.pair_uuid || 'Non spécifié';
const pubKey = sig.cle_publique || 'Non disponible';
html += '<li>';
html += '<strong>Pair ' + (index + 1) + '</strong><br>';
html += 'UUID : <span class="meta">' + pairUuid + '</span><br>';
html += 'Clé publique : <span class="meta">' + pubKey + '</span>';
html += '</li>';
});
html += '</ul>';
container.innerHTML = html;
} catch (e) {
// Session parsing error - keep default message
}
})();
</script>
<p><a href="index.html">← Retour à l'accueil</a> · <a href="contrat.html">Voir le contrat</a> · <a href="technique.html">Réseau P2P</a> · <a href="cryptographie.html">Cryptographie</a></p>
</body>
</html>

View File

@ -1,312 +0,0 @@
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Réseau P2P 4NK un nouveau web</title>
<style>
* { box-sizing: border-box; }
body {
font-family: system-ui, -apple-system, sans-serif;
max-width: 900px;
margin: 0 auto;
padding: 1rem;
line-height: 1.6;
color: #333;
}
h1 { font-size: 1.5rem; margin-bottom: 1rem; color: #222; }
h2 { font-size: 1.2rem; margin-top: 1.5rem; margin-bottom: 0.5rem; color: #333; }
h3 { font-size: 1rem; margin-top: 1rem; margin-bottom: 0.35rem; font-weight: 600; }
p { margin: 0.75rem 0; }
ul { margin: 0.5rem 0; padding-left: 1.5rem; }
li { margin: 0.4rem 0; }
a { color: #007bff; }
a:hover { text-decoration: underline; }
.highlight { background: #e8f4fd; padding: 1rem; border-radius: 8px; border-left: 4px solid #007bff; margin: 1rem 0; }
.info { background: #d4edda; padding: 1rem; border-radius: 8px; border-left: 4px solid #28a745; margin: 1rem 0; }
details { margin: 1rem 0; }
summary { cursor: pointer; font-weight: 600; color: #555; }
.meta { font-family: ui-monospace, monospace; font-size: 0.85rem; color: #666; background: #f5f5f5; padding: 0.2em 0.4em; border-radius: 4px; word-break: break-all; }
.relay-list { list-style: none; padding-left: 0; }
.relay-list > li { background: #f8f9fa; padding: 0.75rem 1rem; border-radius: 8px; border: 1px solid #e0e0e0; margin: 0.5rem 0; }
.relay-list > li > strong { color: #007bff; }
.endpoint-table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
.endpoint-table th, .endpoint-table td { padding: 0.5rem; text-align: left; border-bottom: 1px solid #ddd; }
.endpoint-table th { background: #f5f5f5; font-weight: 600; }
.endpoint-table code { background: #f5f5f5; padding: 0.2em 0.4em; border-radius: 4px; font-size: 0.9em; }
@media (max-width: 768px) {
body { padding: 0.75rem; }
h1 { font-size: 1.25rem; }
.endpoint-table { font-size: 0.9rem; }
.endpoint-table th, .endpoint-table td { padding: 0.4rem; }
}
</style>
</head>
<body>
<h1>Réseau P2P</h1>
<p><a href="index.html">← Retour à l'accueil</a> · <a href="contrat.html">Le contrat</a> · <a href="membre.html">Qui êtes-vous ?</a> · <a href="cryptographie.html">Cryptographie</a></p>
<div class="highlight">
<strong>En résumé :</strong> Au lieu d'utiliser un seul serveur central (comme Gmail ou Facebook),
le système utilise plusieurs <strong>relais</strong> qui fonctionnent comme des boîtes aux lettres publiques.
Vos messages chiffrés sont stockés dans ces relais, et vous pouvez choisir lesquels utiliser.
</div>
<h2>Qu'est-ce qu'un réseau P2P ?</h2>
<p>
Imaginez que vous voulez envoyer une lettre. Dans un système classique (comme la poste traditionnelle),
tous les courriers passent par un seul bureau central. Si ce bureau tombe en panne, plus rien ne fonctionne.
</p>
<p>
Avec un réseau pair-à-pair (P2P), c'est différent : il y a plusieurs <strong>relais</strong> (comme plusieurs
boîtes aux lettres) répartis sur Internet. Vous pouvez choisir où déposer vos messages, et si un relais
ne fonctionne plus, vous pouvez utiliser un autre.
</p>
<p>
Les avantages de cette approche :
</p>
<ul>
<li><strong>Plus de robustesse</strong> : si un relais tombe en panne, d'autres continuent de fonctionner</li>
<li><strong>Pas de point unique de défaillance</strong> : personne ne contrôle tout le système</li>
<li><strong>Vous choisissez</strong> : vous décidez quels relais utiliser, comme choisir votre opérateur téléphonique</li>
<li><strong>Copies multiples</strong> : vos messages peuvent être copiés sur plusieurs relais pour plus de sécurité</li>
</ul>
<h2>Comment fonctionnent les relais ?</h2>
<p>
Un relais, c'est comme une boîte aux lettres publique sur Internet. Quand vous voulez communiquer avec quelqu'un,
vous déposez trois choses séparément dans cette boîte :
</p>
<ol>
<li><strong>Le message chiffré</strong> : votre message codé (comme une lettre dans une enveloppe scellée)</li>
<li><strong>La signature</strong> : une preuve que c'est bien vous qui avez envoyé le message (comme votre signature manuscrite)</li>
<li><strong>La clé de déchiffrement</strong> : la clé pour décoder le message (comme la clé de votre boîte aux lettres)</li>
</ol>
<div class="info">
<strong>Pourquoi séparer ces trois éléments ?</strong> C'est comme mettre votre lettre, votre signature et votre clé
dans trois enveloppes différentes. Même si quelqu'un trouve une enveloppe, il ne peut pas tout faire sans les autres.
C'est plus sûr !
</div>
<h3>Comment récupérer vos messages ?</h3>
<p>
Le système fonctionne comme une boîte aux lettres classique : vous allez vérifier régulièrement s'il y a du courrier.
C'est ce qu'on appelle le modèle <strong>"pull-only"</strong> (vous tirez les informations, elles ne vous sont pas poussées).
</p>
<ul>
<li>Votre appareil vérifie périodiquement les relais pour voir s'il y a de nouveaux messages</li>
<li>Pas de notification instantanée (comme une alerte SMS), mais une vérification régulière</li>
<li>Communication simple via Internet, comme consulter une page web</li>
</ul>
<p>
Cette méthode fonctionne partout, même dans les environnements les plus restrictifs, car elle utilise
simplement le protocole HTTP (comme quand vous visitez un site web).
</p>
<h3>Les relais se parlent entre eux</h3>
<p>
Les relais peuvent être configurés pour se copier mutuellement les messages :
</p>
<ul>
<li>Quand vous déposez un message sur un relais, il peut automatiquement le copier vers d'autres relais</li>
<li>Un système intelligent évite de copier plusieurs fois le même message (grâce à une empreinte unique)</li>
<li>Résultat : vos messages sont disponibles sur plusieurs relais, comme avoir plusieurs copies de sauvegarde</li>
</ul>
<p>
<em>Exemple :</em> Si vous déposez un message sur le relais A, et que A est configuré pour copier vers B et C,
votre message sera disponible sur les trois relais. Si A tombe en panne, vous pouvez toujours récupérer votre message depuis B ou C.
</p>
<h2>Quels relais utiliser ?</h2>
<p>
Par défaut, un relais principal est configuré. Vous pouvez en ajouter d'autres, jusqu'à <strong>8 relais au maximum</strong>.
L'application tente de se connecter à tous les relais activés pour déposer et récupérer les messages.
</p>
<ul class="relay-list">
<li>
<strong>Relais principal (configuré automatiquement)</strong>
<br>Adresse : <span class="meta">https://relay.certificator.4nkweb.com</span>
<br>Ce relais est déjà configuré quand vous utilisez le système pour la première fois.
</li>
</ul>
<div class="info">
<strong>Jusqu'à 8 relais :</strong> Dans les paramètres de UserWallet, vous pouvez ajouter vos propres relais
(comme plusieurs boîtes aux lettres). Maximum 8 au total. Vous activez ou désactivez chaque relais,
et choisissez l'ordre de priorité. L'application utilise tous les relais activés pour déposer et récupérer.
</div>
<h2>Pourquoi dupliquer les relais et les flux ?</h2>
<p>
Plus vous utilisez de relais, plus vos messages ont de chances d'être disponibles et de circuler.
</p>
<ul>
<li><strong>Déposer sur plusieurs relais</strong> : quand vous envoyez un message, l'application le dépose sur chaque relais activé.
Si un relais est indisponible, les autres reçoivent quand même le message. Comme poster la même lettre à plusieurs boîtes.</li>
<li><strong>Récupérer depuis plusieurs relais</strong> : pour lire vos messages, l'application interroge chaque relais.
Si un relais ne répond pas ou n'a pas le message, un autre peut l'avoir. Comme vérifier plusieurs boîtes aux lettres.</li>
<li><strong>Résultat</strong> : plus de robustesse (un relais en panne ne bloque pas tout), plus de chances que vos messages
soient bien livrés, et les relais peuvent se copier entre eux pour multiplier les copies.</li>
</ul>
<p>
En résumé : configurer jusqu'à 8 relais et tous les activer, c'est maximiser les chemins pour envoyer et recevoir,
sans dépendre d'un seul point.
</p>
<h2>Les actions possibles avec un relais</h2>
<p>
Un relais offre plusieurs "actions" (appelées endpoints) que vous pouvez utiliser. C'est comme une boîte aux lettres
avec plusieurs fonctions : déposer, récupérer, vérifier l'état, etc.
</p>
<p>
<em>Note technique :</em> Ces actions utilisent le protocole HTTP (comme les sites web) avec des méthodes GET (lire)
et POST (écrire).
</p>
<table class="endpoint-table">
<thead>
<tr>
<th>Méthode</th>
<th>Endpoint</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>GET</code></td>
<td><code>/health</code></td>
<td>Vérifier si le relais fonctionne correctement</td>
</tr>
<tr>
<td><code>GET</code></td>
<td><code>/messages?start=&lt;ts&gt;&end=&lt;ts&gt;&service=&lt;uuid&gt;</code></td>
<td>Récupérer les messages entre deux dates (optionnel : filtrer par service)</td>
</tr>
<tr>
<td><code>POST</code></td>
<td><code>/messages</code></td>
<td>Déposer un message chiffré dans le relais</td>
</tr>
<tr>
<td><code>GET</code></td>
<td><code>/messages/:hash</code></td>
<td>Récupérer un message précis grâce à son identifiant unique (hash)</td>
</tr>
<tr>
<td><code>GET</code></td>
<td><code>/signatures/:hash</code></td>
<td>Récupérer les signatures associées à un message</td>
</tr>
<tr>
<td><code>POST</code></td>
<td><code>/signatures</code></td>
<td>Déposer une signature dans le relais</td>
</tr>
<tr>
<td><code>GET</code></td>
<td><code>/keys/:hash</code></td>
<td>Récupérer les clés pour déchiffrer un message</td>
</tr>
<tr>
<td><code>POST</code></td>
<td><code>/keys</code></td>
<td>Déposer une clé de déchiffrement dans le relais</td>
</tr>
<tr>
<td><code>GET</code></td>
<td><code>/metrics</code></td>
<td>Consulter les statistiques du relais (nombre de messages, signatures, clés stockées)</td>
</tr>
<tr>
<td><code>GET</code></td>
<td><code>/bloom</code></td>
<td>Obtenir une liste des messages déjà vus (pour éviter de demander plusieurs fois le même message)</td>
</tr>
</tbody>
</table>
<h2>Comment les relais stockent les données</h2>
<h3>Organisation du stockage</h3>
<p>
Les relais organisent les données comme une bibliothèque bien rangée :
</p>
<ul>
<li><strong>Messages</strong> : rangés par identifiant unique (hash) pour les retrouver rapidement</li>
<li><strong>Signatures</strong> : classées par message associé</li>
<li><strong>Clés</strong> : classées par message associé</li>
<li><strong>Éviter les doublons</strong> : le système se souvient des messages déjà vus pour ne pas les stocker plusieurs fois</li>
<li><strong>Indexation</strong> : un système de classement permet de retrouver rapidement les messages d'un service particulier</li>
</ul>
<h3>Protection contre les abus</h3>
<p>
Les relais sont protégés contre les utilisations abusives :
</p>
<ul>
<li><strong>Limitation des requêtes</strong> : chaque adresse IP ne peut faire qu'un nombre limité de requêtes par minute</li>
<li><strong>Contrôle d'accès</strong> : seuls les sites autorisés peuvent utiliser le relais</li>
<li><strong>Timeout</strong> : si une requête prend trop de temps, elle est annulée automatiquement</li>
<li><strong>Compression</strong> : les réponses sont compressées pour être plus rapides</li>
</ul>
<h3>Éviter les doublons</h3>
<p>
Chaque message a une empreinte unique (comme une empreinte digitale). Le système utilise cette empreinte
pour s'assurer qu'un même message n'est pas stocké plusieurs fois, même s'il arrive depuis plusieurs relais.
Cela évite aussi que les messages tournent en boucle entre les relais.
</p>
<details>
<summary>Détails techniques pour les développeurs</summary>
<h3>Configuration des relais pairs</h3>
<p>
Les administrateurs de relais peuvent configurer leurs relais pour se copier mutuellement les messages.
Cela se fait via une variable de configuration :
</p>
<pre class="meta">PEER_RELAYS=http://relay1:3019,http://relay2:3019</pre>
<p>
Quand un message arrive sur un relais, il est automatiquement copié vers tous
les relais pairs configurés (si le message n'a pas déjà été vu).
</p>
<h3>Bloom filter</h3>
<p>
Le endpoint <code>/bloom</code> retourne une liste compacte des messages déjà vus par le relais.
Les applications peuvent utiliser cette liste pour éviter de demander plusieurs fois le même message,
ce qui réduit la charge sur le réseau et améliore les performances.
</p>
<h3>Statistiques du relais</h3>
<p>
Le endpoint <code>/metrics</code> expose des statistiques au format Prometheus (pour le monitoring) :
</p>
<ul>
<li>Nombre de messages stockés</li>
<li>Nombre de signatures stockées</li>
<li>Nombre de clés stockées</li>
</ul>
<h3>Gestion des erreurs et timeouts</h3>
<p>
Les applications utilisent un délai d'attente de 15 secondes pour toutes les requêtes vers les relais.
Si une requête échoue ou prend trop de temps, l'application peut essayer un autre relais configuré,
ou réessayer plus tard avec un délai progressif (backoff exponentiel).
</p>
</details>
<h2>Pourquoi cette architecture ?</h2>
<p>
Cette façon de faire a été choisie pour plusieurs raisons importantes :
</p>
<ul>
<li><strong>Simplicité</strong> : utilise le protocole HTTP standard (comme les sites web), donc ça fonctionne partout</li>
<li><strong>Fiabilité</strong> : pas besoin de maintenir une connexion permanente, comme consulter une page web</li>
<li><strong>Capacité</strong> : chaque relais peut gérer de nombreux utilisateurs en même temps</li>
<li><strong>Contrôle</strong> : vous choisissez quels relais utiliser, personne ne vous impose un choix</li>
<li><strong>Sécurité</strong> : les messages, signatures et clés sont séparés, ce qui rend le système plus sûr</li>
</ul>
<p>
En résumé, c'est un système simple, fiable et qui vous donne le contrôle, tout en étant sécurisé.
</p>
<p><a href="index.html">← Retour à l'accueil</a> · <a href="contrat.html">Le contrat</a> · <a href="membre.html">Qui êtes-vous ?</a> · <a href="cryptographie.html">Cryptographie</a></p>
</body>
</html>