Daily backup to git cron, backup/restore scripts, docs
**Motivations:** - Export Signet and mining wallet backups to git with only 2 versions kept - Document and add backup/restore scripts for signet and mining wallet **Correctifs:** - Backup-to-git uses SSH URL for passwordless cron; copy timestamped files only; prune to 2 versions; remove *-latest from backup repo **Evolutions:** - data/backup-to-git-cron.sh: daily export to git.4nkweb.com/4nk/backup - save-signet-datadir-backup.sh, restore-signet-from-backup.sh, export-mining-wallet.sh, import-mining-wallet.sh - features/backup-to-git-daily-cron.md, docs/MAINTENANCE.md backup section - .gitignore: data/backup-to-git.log **Pages affectées:** - .gitignore, data/backup-to-git-cron.sh, docs/MAINTENANCE.md, features/backup-to-git-daily-cron.md - save-signet-datadir-backup.sh, restore-signet-from-backup.sh, export-mining-wallet.sh, import-mining-wallet.sh - Plus autres fichiers modifiés ou non suivis déjà présents dans le working tree
This commit is contained in:
parent
f9fe0e3419
commit
937646cc45
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,2 +1,4 @@
|
||||
.env
|
||||
backups/
|
||||
data/backup-to-git.log
|
||||
.verify-chain-dashboard-*.json
|
||||
@ -8,7 +8,15 @@ User=ncantu
|
||||
WorkingDirectory=/home/ncantu/Bureau/code/bitcoin/api-anchorage
|
||||
Environment=NODE_ENV=production
|
||||
Environment=API_PORT=3010
|
||||
Environment=API_HOST=0.0.0.0
|
||||
# Bind IPv4 only: machine bitcoin (192.168.1.105). Accept only from proxy 192.168.1.100.
|
||||
Environment=API_HOST=192.168.1.105
|
||||
Environment=ALLOWED_SOURCE_IP=192.168.1.100
|
||||
# Same node as Mempool: 127.0.0.1:38332 = host.docker.internal:38332 = bitcoin-signet-instance
|
||||
Environment=BITCOIN_RPC_HOST=127.0.0.1
|
||||
Environment=BITCOIN_RPC_PORT=38332
|
||||
Environment=BITCOIN_RPC_USER=bitcoin
|
||||
Environment=BITCOIN_RPC_PASSWORD=bitcoin
|
||||
Environment=BITCOIN_RPC_WALLET=custom_signet
|
||||
ExecStart=/usr/bin/node /home/ncantu/Bureau/code/bitcoin/api-anchorage/src/server.js
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
@ -9,6 +9,19 @@ import { getDatabase } from '../database.js';
|
||||
|
||||
export const healthRouter = express.Router();
|
||||
|
||||
/**
|
||||
* GET /health/live
|
||||
* Liveness probe: returns 200 while the process is running.
|
||||
* Use for monitoring/load-balancer to distinguish "process down" from "not ready".
|
||||
*/
|
||||
healthRouter.get('/live', (req, res) => {
|
||||
res.status(200).json({
|
||||
ok: true,
|
||||
service: 'anchor-api',
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /health
|
||||
* Vérifie l'état de l'API et de la connexion Bitcoin
|
||||
|
||||
@ -34,6 +34,34 @@ dotenv.config({ path: envPath });
|
||||
const app = express();
|
||||
const PORT = process.env.API_PORT || 3010;
|
||||
const HOST = process.env.API_HOST || '0.0.0.0';
|
||||
const ALLOWED_SOURCE_IP = process.env.ALLOWED_SOURCE_IP ?? '';
|
||||
|
||||
/**
|
||||
* Normalize remote address: IPv6-mapped IPv4 (::ffff:192.168.1.100) -> 192.168.1.100
|
||||
* @param {string} addr - req.socket.remoteAddress
|
||||
* @returns {string}
|
||||
*/
|
||||
function normalizeRemoteAddress(addr) {
|
||||
if (!addr) return '';
|
||||
if (addr.startsWith('::ffff:')) return addr.slice(7);
|
||||
return addr;
|
||||
}
|
||||
|
||||
// Middleware: accept only requests from proxy when ALLOWED_SOURCE_IP is set (IPv4 only)
|
||||
app.use((req, res, next) => {
|
||||
if (!ALLOWED_SOURCE_IP) return next();
|
||||
const remote = normalizeRemoteAddress(req.socket.remoteAddress ?? '');
|
||||
if (remote !== ALLOWED_SOURCE_IP) {
|
||||
logger.warn('Request rejected: source not allowed', {
|
||||
remoteAddress: req.socket.remoteAddress,
|
||||
allowedSourceIp: ALLOWED_SOURCE_IP,
|
||||
path: req.path,
|
||||
});
|
||||
res.status(403).json({ error: 'Forbidden', message: 'Source not allowed' });
|
||||
return;
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// Middleware
|
||||
app.use(cors());
|
||||
@ -52,7 +80,7 @@ app.use((req, res, next) => {
|
||||
// Middleware d'authentification API Key
|
||||
app.use((req, res, next) => {
|
||||
// Exclure /health, /health/detailed et /api/anchor/locked-utxos de l'authentification
|
||||
if (req.path === '/health' || req.path === '/health/detailed' || req.path === '/' || req.path.startsWith('/api/anchor/locked-utxos')) {
|
||||
if (req.path === '/health' || req.path === '/health/live' || req.path === '/health/detailed' || req.path === '/' || req.path.startsWith('/api/anchor/locked-utxos')) {
|
||||
return next();
|
||||
}
|
||||
|
||||
@ -83,6 +111,7 @@ app.get('/', (req, res) => {
|
||||
version: '1.0.0',
|
||||
endpoints: {
|
||||
health: '/health',
|
||||
healthLive: '/health/live',
|
||||
healthDetailed: '/health/detailed',
|
||||
anchor: '/api/anchor/document',
|
||||
verify: '/api/anchor/verify',
|
||||
|
||||
@ -8,7 +8,9 @@ User=ncantu
|
||||
WorkingDirectory=/home/ncantu/Bureau/code/bitcoin/api-clamav
|
||||
Environment=NODE_ENV=production
|
||||
Environment=CLAMAV_API_PORT=3023
|
||||
Environment=CLAMAV_API_HOST=0.0.0.0
|
||||
# Bind IPv4 only: machine prod (192.168.1.103). Accept only from proxy 192.168.1.100.
|
||||
Environment=CLAMAV_API_HOST=192.168.1.103
|
||||
Environment=ALLOWED_SOURCE_IP=192.168.1.100
|
||||
Environment=CLAMAV_HOST=localhost
|
||||
Environment=CLAMAV_PORT=3310
|
||||
ExecStart=/usr/bin/node /home/ncantu/Bureau/code/bitcoin/api-clamav/src/server.js
|
||||
|
||||
@ -31,6 +31,34 @@ const app = express();
|
||||
// Port fixe : 3023
|
||||
const PORT = 3023;
|
||||
const HOST = process.env.CLAMAV_API_HOST || '0.0.0.0';
|
||||
const ALLOWED_SOURCE_IP = process.env.ALLOWED_SOURCE_IP ?? '';
|
||||
|
||||
/**
|
||||
* Normalize remote address: IPv6-mapped IPv4 (::ffff:192.168.1.100) -> 192.168.1.100
|
||||
* @param {string} addr - req.socket.remoteAddress
|
||||
* @returns {string}
|
||||
*/
|
||||
function normalizeRemoteAddress(addr) {
|
||||
if (!addr) return '';
|
||||
if (addr.startsWith('::ffff:')) return addr.slice(7);
|
||||
return addr;
|
||||
}
|
||||
|
||||
// Middleware: accept only requests from proxy when ALLOWED_SOURCE_IP is set (IPv4 only)
|
||||
app.use((req, res, next) => {
|
||||
if (!ALLOWED_SOURCE_IP) return next();
|
||||
const remote = normalizeRemoteAddress(req.socket.remoteAddress ?? '');
|
||||
if (remote !== ALLOWED_SOURCE_IP) {
|
||||
logger.warn('Request rejected: source not allowed', {
|
||||
remoteAddress: req.socket.remoteAddress,
|
||||
allowedSourceIp: ALLOWED_SOURCE_IP,
|
||||
path: req.path,
|
||||
});
|
||||
res.status(403).json({ error: 'Forbidden', message: 'Source not allowed' });
|
||||
return;
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// Middleware
|
||||
app.use(cors());
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
# Bitcoin RPC Configuration
|
||||
BITCOIN_RPC_HOST=localhost
|
||||
# Bitcoin RPC Configuration (same node as Mempool: 127.0.0.1:38332 = bitcoin-signet-instance)
|
||||
BITCOIN_RPC_HOST=127.0.0.1
|
||||
BITCOIN_RPC_PORT=38332
|
||||
BITCOIN_RPC_USER=bitcoin
|
||||
BITCOIN_RPC_PASSWORD=bitcoin
|
||||
|
||||
@ -8,7 +8,14 @@ User=ncantu
|
||||
WorkingDirectory=/home/ncantu/Bureau/code/bitcoin/api-faucet
|
||||
Environment=NODE_ENV=production
|
||||
Environment=FAUCET_API_PORT=3021
|
||||
Environment=FAUCET_API_HOST=0.0.0.0
|
||||
# Bind IPv4 only: machine bitcoin (192.168.1.105). Accept only from proxy 192.168.1.100.
|
||||
Environment=FAUCET_API_HOST=192.168.1.105
|
||||
Environment=ALLOWED_SOURCE_IP=192.168.1.100
|
||||
# Same node as Mempool: 127.0.0.1:38332 = bitcoin-signet-instance (same machine)
|
||||
Environment=BITCOIN_RPC_HOST=127.0.0.1
|
||||
Environment=BITCOIN_RPC_PORT=38332
|
||||
Environment=BITCOIN_RPC_USER=bitcoin
|
||||
Environment=BITCOIN_RPC_PASSWORD=bitcoin
|
||||
ExecStart=/usr/bin/node /home/ncantu/Bureau/code/bitcoin/api-faucet/src/server.js
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
@ -23,6 +23,34 @@ dotenv.config();
|
||||
const app = express();
|
||||
const PORT = process.env.FAUCET_API_PORT || 3021;
|
||||
const HOST = process.env.FAUCET_API_HOST || '0.0.0.0';
|
||||
const ALLOWED_SOURCE_IP = process.env.ALLOWED_SOURCE_IP ?? '';
|
||||
|
||||
/**
|
||||
* Normalize remote address: IPv6-mapped IPv4 (::ffff:192.168.1.100) -> 192.168.1.100
|
||||
* @param {string} addr - req.socket.remoteAddress
|
||||
* @returns {string}
|
||||
*/
|
||||
function normalizeRemoteAddress(addr) {
|
||||
if (!addr) return '';
|
||||
if (addr.startsWith('::ffff:')) return addr.slice(7);
|
||||
return addr;
|
||||
}
|
||||
|
||||
// Middleware: accept only requests from proxy when ALLOWED_SOURCE_IP is set (IPv4 only)
|
||||
app.use((req, res, next) => {
|
||||
if (!ALLOWED_SOURCE_IP) return next();
|
||||
const remote = normalizeRemoteAddress(req.socket.remoteAddress ?? '');
|
||||
if (remote !== ALLOWED_SOURCE_IP) {
|
||||
logger.warn('Request rejected: source not allowed', {
|
||||
remoteAddress: req.socket.remoteAddress,
|
||||
allowedSourceIp: ALLOWED_SOURCE_IP,
|
||||
path: req.path,
|
||||
});
|
||||
res.status(403).json({ error: 'Forbidden', message: 'Source not allowed' });
|
||||
return;
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// Middleware
|
||||
app.use(cors());
|
||||
|
||||
@ -8,7 +8,9 @@ User=ncantu
|
||||
WorkingDirectory=/home/ncantu/Bureau/code/bitcoin/api-filigrane
|
||||
Environment=NODE_ENV=production
|
||||
Environment=WATERMARK_API_PORT=3022
|
||||
Environment=WATERMARK_API_HOST=0.0.0.0
|
||||
# Bind IPv4 only: machine prod (192.168.1.103). Accept only from proxy 192.168.1.100.
|
||||
Environment=WATERMARK_API_HOST=192.168.1.103
|
||||
Environment=ALLOWED_SOURCE_IP=192.168.1.100
|
||||
ExecStart=/usr/bin/node /home/ncantu/Bureau/code/bitcoin/api-filigrane/src/server.js
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
@ -29,6 +29,34 @@ dotenv.config({ path: envPath });
|
||||
const app = express();
|
||||
const PORT = process.env.WATERMARK_API_PORT || 3022;
|
||||
const HOST = process.env.WATERMARK_API_HOST || '0.0.0.0';
|
||||
const ALLOWED_SOURCE_IP = process.env.ALLOWED_SOURCE_IP ?? '';
|
||||
|
||||
/**
|
||||
* Normalize remote address: IPv6-mapped IPv4 (::ffff:192.168.1.100) -> 192.168.1.100
|
||||
* @param {string} addr - req.socket.remoteAddress
|
||||
* @returns {string}
|
||||
*/
|
||||
function normalizeRemoteAddress(addr) {
|
||||
if (!addr) return '';
|
||||
if (addr.startsWith('::ffff:')) return addr.slice(7);
|
||||
return addr;
|
||||
}
|
||||
|
||||
// Middleware: accept only requests from proxy when ALLOWED_SOURCE_IP is set (IPv4 only)
|
||||
app.use((req, res, next) => {
|
||||
if (!ALLOWED_SOURCE_IP) return next();
|
||||
const remote = normalizeRemoteAddress(req.socket.remoteAddress ?? '');
|
||||
if (remote !== ALLOWED_SOURCE_IP) {
|
||||
logger.warn('Request rejected: source not allowed', {
|
||||
remoteAddress: req.socket.remoteAddress,
|
||||
allowedSourceIp: ALLOWED_SOURCE_IP,
|
||||
path: req.path,
|
||||
});
|
||||
res.status(403).json({ error: 'Forbidden', message: 'Source not allowed' });
|
||||
return;
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// Middleware
|
||||
app.use(cors());
|
||||
|
||||
130
data/backup-to-git-cron.sh
Executable file
130
data/backup-to-git-cron.sh
Executable file
@ -0,0 +1,130 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Daily backup of Signet datadir and mining wallet to git (https://git.4nkweb.com/4nk/backup).
|
||||
# Runs save-signet-datadir-backup.sh and export-mining-wallet.sh, then pushes to the
|
||||
# backup repo. Keeps only 2 versions of the full chain (signet-datadir) and 2 of the
|
||||
# mining wallet export.
|
||||
#
|
||||
# Run via cron. Requires: docker (for backup scripts), git, passwordless push to backup repo.
|
||||
# Log: data/backup-to-git.log
|
||||
#
|
||||
# Author: 4NK Team
|
||||
# Date: 2026-02-04
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
|
||||
BACKUP_DIR="${PROJECT_DIR}/backups"
|
||||
LOG_FILE="${SCRIPT_DIR}/backup-to-git.log"
|
||||
BACKUP_REPO_URL="git@git.4nkweb.com:4nk/backup.git"
|
||||
BACKUP_GIT_WORKSPACE="${BACKUP_GIT_WORKSPACE:-$HOME/.4nk-backup-git}"
|
||||
KEEP_VERSIONS=2
|
||||
|
||||
SIGNET_SUBDIR="signet-datadir"
|
||||
WALLET_SUBDIR="mining-wallet"
|
||||
|
||||
log() { echo "$(date -Iseconds) $*" | tee -a "$LOG_FILE"; }
|
||||
|
||||
cd "$PROJECT_DIR" || exit 1
|
||||
|
||||
log "=== Backup to git cron ==="
|
||||
|
||||
# 1. Run signet datadir backup
|
||||
if [ ! -x "${PROJECT_DIR}/save-signet-datadir-backup.sh" ]; then
|
||||
log "ERROR: save-signet-datadir-backup.sh not executable"
|
||||
exit 1
|
||||
fi
|
||||
log "Running save-signet-datadir-backup.sh..."
|
||||
if ! ./save-signet-datadir-backup.sh >> "$LOG_FILE" 2>&1; then
|
||||
log "ERROR: save-signet-datadir-backup.sh failed"
|
||||
exit 1
|
||||
fi
|
||||
log " signet datadir backup OK"
|
||||
|
||||
# 2. Run mining wallet export
|
||||
if [ ! -x "${PROJECT_DIR}/export-mining-wallet.sh" ]; then
|
||||
log "ERROR: export-mining-wallet.sh not executable"
|
||||
exit 1
|
||||
fi
|
||||
log "Running export-mining-wallet.sh..."
|
||||
if ! ./export-mining-wallet.sh >> "$LOG_FILE" 2>&1; then
|
||||
log "ERROR: export-mining-wallet.sh failed"
|
||||
exit 1
|
||||
fi
|
||||
log " mining wallet export OK"
|
||||
|
||||
# 3. Clone or pull backup repo
|
||||
if [ -d "$BACKUP_GIT_WORKSPACE/.git" ]; then
|
||||
log "Pulling backup repo..."
|
||||
(cd "$BACKUP_GIT_WORKSPACE" && git pull) >> "$LOG_FILE" 2>&1 || {
|
||||
log "WARN: git pull failed, continuing with local state"
|
||||
}
|
||||
else
|
||||
log "Cloning backup repo..."
|
||||
mkdir -p "$(dirname "$BACKUP_GIT_WORKSPACE")"
|
||||
if ! git clone "$BACKUP_REPO_URL" "$BACKUP_GIT_WORKSPACE" >> "$LOG_FILE" 2>&1; then
|
||||
log "ERROR: git clone failed. Create repo at $BACKUP_REPO_URL first."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p "${BACKUP_GIT_WORKSPACE}/${SIGNET_SUBDIR}"
|
||||
mkdir -p "${BACKUP_GIT_WORKSPACE}/${WALLET_SUBDIR}"
|
||||
|
||||
# 4. Copy latest signet-datadir backup (use real file, not symlink, for timestamped filename)
|
||||
LATEST_SIGNET=$(readlink -f "$BACKUP_DIR/signet-datadir-latest.tar.gz" 2>/dev/null)
|
||||
if [ -z "$LATEST_SIGNET" ] || [ ! -f "$LATEST_SIGNET" ]; then
|
||||
log "ERROR: no signet-datadir backup found in $BACKUP_DIR"
|
||||
exit 1
|
||||
fi
|
||||
cp "$LATEST_SIGNET" "${BACKUP_GIT_WORKSPACE}/${SIGNET_SUBDIR}/$(basename "$LATEST_SIGNET")"
|
||||
log " Copied $(basename "$LATEST_SIGNET") to ${SIGNET_SUBDIR}/"
|
||||
|
||||
# 5. Copy latest mining wallet export (use real file, not symlink, for timestamped filename)
|
||||
LATEST_WALLET=$(readlink -f "$BACKUP_DIR/mining-wallet-export-latest.json" 2>/dev/null)
|
||||
if [ -z "$LATEST_WALLET" ] || [ ! -f "$LATEST_WALLET" ]; then
|
||||
log "ERROR: no mining-wallet export found in $BACKUP_DIR"
|
||||
exit 1
|
||||
fi
|
||||
cp "$LATEST_WALLET" "${BACKUP_GIT_WORKSPACE}/${WALLET_SUBDIR}/$(basename "$LATEST_WALLET")"
|
||||
log " Copied $(basename "$LATEST_WALLET") to ${WALLET_SUBDIR}/"
|
||||
|
||||
# 6. Remove any *-latest* files from backup repo (only timestamped versions belong)
|
||||
rm -f "${BACKUP_GIT_WORKSPACE}/${SIGNET_SUBDIR}/signet-datadir-latest.tar.gz"
|
||||
rm -f "${BACKUP_GIT_WORKSPACE}/${WALLET_SUBDIR}/mining-wallet-export-latest.json"
|
||||
|
||||
# 7. Prune signet-datadir: keep only KEEP_VERSIONS most recent (by filename timestamp)
|
||||
cd "${BACKUP_GIT_WORKSPACE}/${SIGNET_SUBDIR}"
|
||||
while [ "$(ls -1 signet-datadir-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-*.tar.gz 2>/dev/null | wc -l)" -gt "$KEEP_VERSIONS" ]; do
|
||||
OLDEST=$(ls signet-datadir-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-*.tar.gz 2>/dev/null | sort | head -1)
|
||||
rm -f "$OLDEST"
|
||||
log " Pruned signet: $OLDEST"
|
||||
done
|
||||
|
||||
# 8. Prune mining-wallet: keep only KEEP_VERSIONS most recent (by filename timestamp)
|
||||
cd "${BACKUP_GIT_WORKSPACE}/${WALLET_SUBDIR}"
|
||||
while [ "$(ls -1 mining-wallet-export-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-*.json 2>/dev/null | wc -l)" -gt "$KEEP_VERSIONS" ]; do
|
||||
OLDEST=$(ls mining-wallet-export-[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-*.json 2>/dev/null | sort | head -1)
|
||||
rm -f "$OLDEST"
|
||||
log " Pruned wallet: $OLDEST"
|
||||
done
|
||||
|
||||
# 9. Commit and push
|
||||
cd "$BACKUP_GIT_WORKSPACE"
|
||||
if git status --porcelain | grep -q .; then
|
||||
git add "${SIGNET_SUBDIR}/" "${WALLET_SUBDIR}/"
|
||||
git commit -m "backup: signet datadir and mining wallet $(date -Iseconds)" >> "$LOG_FILE" 2>&1
|
||||
log "Pushing to $BACKUP_REPO_URL..."
|
||||
if git push >> "$LOG_FILE" 2>&1; then
|
||||
log " Push OK"
|
||||
else
|
||||
log "ERROR: git push failed"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
log "No changes to commit (backups identical)"
|
||||
fi
|
||||
|
||||
log "=== Done ==="
|
||||
tail -n 150 "$LOG_FILE" > "$LOG_FILE.tmp" && mv "$LOG_FILE.tmp" "$LOG_FILE"
|
||||
@ -85,7 +85,8 @@ if docker ps -a -q -f "name=^${BITCOIND_CONTAINER}$" 2>/dev/null | grep -q .; th
|
||||
max_wait=60
|
||||
wait_count=0
|
||||
while [ $wait_count -lt $max_wait ]; do
|
||||
if docker exec "$BITCOIND_CONTAINER" bitcoin-cli -datadir=/root/.bitcoin getblockchaininfo &>/dev/null; then
|
||||
BITCOIN_DATADIR=$(docker exec "$BITCOIND_CONTAINER" printenv BITCOIN_DIR 2>/dev/null || echo "/root/.bitcoin")
|
||||
if docker exec "$BITCOIND_CONTAINER" bitcoin-cli -datadir="$BITCOIN_DATADIR" getblockchaininfo &>/dev/null; then
|
||||
log " $BITCOIND_CONTAINER RPC ready"
|
||||
break
|
||||
fi
|
||||
|
||||
@ -1,100 +1,123 @@
|
||||
config Manage Docker configs
|
||||
container Manage containers
|
||||
context Manage contexts
|
||||
image Manage images
|
||||
manifest Manage Docker image manifests and manifest lists
|
||||
network Manage networks
|
||||
node Manage Swarm nodes
|
||||
plugin Manage plugins
|
||||
secret Manage Docker secrets
|
||||
service Manage services
|
||||
stack Manage Docker stacks
|
||||
swarm Manage Swarm
|
||||
system Manage Docker
|
||||
trust Manage trust on Docker images
|
||||
volume Manage volumes
|
||||
|
||||
Commands:
|
||||
attach Attach local standard input, output, and error streams to a running container
|
||||
build Build an image from a Dockerfile
|
||||
commit Create a new image from a container's changes
|
||||
cp Copy files/folders between a container and the local filesystem
|
||||
create Create a new container
|
||||
diff Inspect changes to files or directories on a container's filesystem
|
||||
events Get real time events from the server
|
||||
exec Run a command in a running container
|
||||
export Export a container's filesystem as a tar archive
|
||||
history Show the history of an image
|
||||
images List images
|
||||
import Import the contents from a tarball to create a filesystem image
|
||||
info Display system-wide information
|
||||
inspect Return low-level information on Docker objects
|
||||
kill Kill one or more running containers
|
||||
load Load an image from a tar archive or STDIN
|
||||
login Log in to a Docker registry
|
||||
logout Log out from a Docker registry
|
||||
logs Fetch the logs of a container
|
||||
pause Pause all processes within one or more containers
|
||||
port List port mappings or a specific mapping for the container
|
||||
ps List containers
|
||||
pull Pull an image or a repository from a registry
|
||||
push Push an image or a repository to a registry
|
||||
rename Rename a container
|
||||
restart Restart one or more containers
|
||||
rm Remove one or more containers
|
||||
rmi Remove one or more images
|
||||
run Run a command in a new container
|
||||
save Save one or more images to a tar archive (streamed to STDOUT by default)
|
||||
search Search the Docker Hub for images
|
||||
start Start one or more stopped containers
|
||||
stats Display a live stream of container(s) resource usage statistics
|
||||
stop Stop one or more running containers
|
||||
tag Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE
|
||||
top Display the running processes of a container
|
||||
unpause Unpause all processes within one or more containers
|
||||
update Update configuration of one or more containers
|
||||
version Show the Docker version information
|
||||
wait Block until one or more containers stop, then print their exit codes
|
||||
|
||||
Run 'docker COMMAND --help' for more information on a command.
|
||||
|
||||
To get more help with docker, check out our guides at https://docs.docker.com/go/guides/
|
||||
|
||||
Restarting mempool_api_1 ...
|
||||
Restarting mempool_electrs_1 ...
|
||||
Restarting mempool_web_1 ...
|
||||
Restarting mempool_db_1 ...
|
||||
Restarting mempool_electrs_1 ... done
|
||||
Restarting mempool_db_1 ... done
|
||||
Restarting mempool_api_1 ... done
|
||||
Restarting mempool_web_1 ... done
|
||||
2026-01-28T00:43:59+01:00 mempool OK
|
||||
2026-01-28T00:43:59+01:00 Restarting bitcoin-signet-instance...
|
||||
2026-01-28T00:44:06+01:00 bitcoin-signet-instance OK
|
||||
2026-01-28T00:44:06+01:00 === Done ===
|
||||
2026-01-28T00:45:24+01:00 === Restart services cron (local only, no SSH) ===
|
||||
2026-01-28T00:45:24+01:00 WARN: bitcoin-signet is not enabled (will not start at boot)
|
||||
2026-01-28T00:45:24+01:00 WARN: bitcoin is not enabled (will not start at boot)
|
||||
2026-01-28T00:45:24+01:00 SKIP: bitcoin-signet not active (not installed or not running on this machine)
|
||||
2026-01-28T00:45:24+01:00 SKIP: bitcoin not active (not installed or not running on this machine)
|
||||
2026-01-28T00:45:24+01:00 Restarting anchorage-api...
|
||||
2026-01-28T00:45:24+01:00 anchorage-api OK
|
||||
2026-01-28T00:45:24+01:00 Restarting api-relay...
|
||||
2026-01-28T00:45:24+01:00 api-relay OK
|
||||
2026-01-28T00:45:24+01:00 Restarting clamav-api...
|
||||
2026-01-28T00:45:24+01:00 clamav-api OK
|
||||
2026-01-28T00:45:24+01:00 Restarting faucet-api...
|
||||
2026-01-28T00:45:24+01:00 faucet-api OK
|
||||
2026-01-28T00:45:24+01:00 Restarting filigrane-api...
|
||||
2026-01-28T00:45:24+01:00 filigrane-api OK
|
||||
2026-01-28T00:45:24+01:00 Restarting signet-dashboard...
|
||||
2026-01-28T00:45:25+01:00 signet-dashboard OK
|
||||
2026-01-28T00:45:25+01:00 Restarting userwallet...
|
||||
2026-01-28T00:45:25+01:00 userwallet OK
|
||||
2026-01-28T00:45:25+01:00 Restarting website-skeleton...
|
||||
2026-01-28T00:45:25+01:00 website-skeleton OK
|
||||
2026-01-28T00:45:25+01:00 Restarting mempool (docker)...
|
||||
2026-01-28T00:46:26+01:00 mempool OK
|
||||
2026-01-28T00:46:26+01:00 Restarting bitcoin-signet-instance...
|
||||
2026-01-28T00:46:33+01:00 bitcoin-signet-instance OK
|
||||
2026-01-28T00:46:33+01:00 === Done ===
|
||||
2026-02-02T11:35:06+01:00 api-relay OK
|
||||
2026-02-02T11:35:06+01:00 Restarting clamav-api...
|
||||
2026-02-02T11:35:06+01:00 clamav-api OK
|
||||
2026-02-02T11:35:06+01:00 Restarting faucet-api...
|
||||
2026-02-02T11:35:06+01:00 faucet-api OK
|
||||
2026-02-02T11:35:06+01:00 Restarting filigrane-api...
|
||||
2026-02-02T11:35:06+01:00 filigrane-api OK
|
||||
2026-02-02T11:35:06+01:00 Restarting signet-dashboard...
|
||||
2026-02-02T11:35:06+01:00 signet-dashboard OK
|
||||
2026-02-02T11:35:06+01:00 Restarting userwallet...
|
||||
2026-02-02T11:35:06+01:00 userwallet OK
|
||||
2026-02-02T11:35:06+01:00 Restarting website-skeleton...
|
||||
2026-02-02T11:35:06+01:00 website-skeleton OK
|
||||
2026-02-02T11:35:06+01:00 Starting/restarting bitcoin-signet-instance...
|
||||
2026-02-02T11:35:13+01:00 bitcoin-signet-instance started, waiting for RPC to be ready...
|
||||
2026-02-02T11:35:14+01:00 bitcoin-signet-instance RPC ready
|
||||
2026-02-02T11:35:14+01:00 Starting/restarting mempool (docker)...
|
||||
2026-02-02T11:35:15+01:00 mempool started
|
||||
2026-02-02T11:35:20+01:00 === Done ===
|
||||
2026-02-02T11:43:03+01:00 === Restart services cron (local only, no SSH) ===
|
||||
2026-02-02T11:43:03+01:00 WARN: bitcoin-signet is not enabled (will not start at boot)
|
||||
2026-02-02T11:43:03+01:00 WARN: bitcoin is not enabled (will not start at boot)
|
||||
2026-02-02T11:43:03+01:00 SKIP: bitcoin-signet not active (not installed or not running on this machine)
|
||||
2026-02-02T11:43:03+01:00 SKIP: bitcoin not active (not installed or not running on this machine)
|
||||
2026-02-02T11:43:03+01:00 Restarting anchorage-api...
|
||||
2026-02-02T11:43:03+01:00 anchorage-api OK
|
||||
2026-02-02T11:43:03+01:00 Restarting api-relay...
|
||||
2026-02-02T11:43:04+01:00 api-relay OK
|
||||
2026-02-02T11:43:04+01:00 Restarting clamav-api...
|
||||
2026-02-02T11:43:04+01:00 clamav-api OK
|
||||
2026-02-02T11:43:04+01:00 Restarting faucet-api...
|
||||
2026-02-02T11:43:04+01:00 faucet-api OK
|
||||
2026-02-02T11:43:04+01:00 Restarting filigrane-api...
|
||||
2026-02-02T11:43:04+01:00 filigrane-api OK
|
||||
2026-02-02T11:43:04+01:00 Restarting signet-dashboard...
|
||||
2026-02-02T11:43:04+01:00 signet-dashboard OK
|
||||
2026-02-02T11:43:04+01:00 Restarting userwallet...
|
||||
2026-02-02T11:43:04+01:00 userwallet OK
|
||||
2026-02-02T11:43:04+01:00 Restarting website-skeleton...
|
||||
2026-02-02T11:43:04+01:00 website-skeleton OK
|
||||
2026-02-02T11:43:04+01:00 Starting/restarting bitcoin-signet-instance...
|
||||
2026-02-02T11:43:10+01:00 bitcoin-signet-instance started, waiting for RPC to be ready...
|
||||
2026-02-02T11:43:12+01:00 bitcoin-signet-instance RPC ready
|
||||
2026-02-02T11:43:12+01:00 Starting/restarting mempool (docker)...
|
||||
2026-02-02T11:43:12+01:00 mempool started
|
||||
2026-02-02T11:43:17+01:00 === Done ===
|
||||
2026-02-02T11:50:48+01:00 === Restart services cron (local only, no SSH) ===
|
||||
2026-02-02T11:50:48+01:00 WARN: bitcoin-signet is not enabled (will not start at boot)
|
||||
2026-02-02T11:50:48+01:00 WARN: bitcoin is not enabled (will not start at boot)
|
||||
2026-02-02T11:50:48+01:00 SKIP: bitcoin-signet not active (not installed or not running on this machine)
|
||||
2026-02-02T11:50:48+01:00 SKIP: bitcoin not active (not installed or not running on this machine)
|
||||
2026-02-02T11:50:48+01:00 Restarting anchorage-api...
|
||||
2026-02-02T11:50:48+01:00 anchorage-api OK
|
||||
2026-02-02T11:50:48+01:00 Restarting api-relay...
|
||||
2026-02-02T11:50:48+01:00 api-relay OK
|
||||
2026-02-02T11:50:48+01:00 Restarting clamav-api...
|
||||
2026-02-02T11:50:48+01:00 clamav-api OK
|
||||
2026-02-02T11:50:48+01:00 Restarting faucet-api...
|
||||
2026-02-02T11:50:48+01:00 faucet-api OK
|
||||
2026-02-02T11:50:48+01:00 Restarting filigrane-api...
|
||||
2026-02-02T11:50:48+01:00 filigrane-api OK
|
||||
2026-02-02T11:50:48+01:00 Restarting signet-dashboard...
|
||||
2026-02-02T11:50:48+01:00 signet-dashboard OK
|
||||
2026-02-02T11:50:48+01:00 Restarting userwallet...
|
||||
2026-02-02T11:50:49+01:00 userwallet OK
|
||||
2026-02-02T11:50:49+01:00 Restarting website-skeleton...
|
||||
2026-02-02T11:50:49+01:00 website-skeleton OK
|
||||
2026-02-02T11:50:49+01:00 Starting/restarting bitcoin-signet-instance...
|
||||
2026-02-02T11:50:55+01:00 bitcoin-signet-instance started, waiting for RPC to be ready...
|
||||
2026-02-02T11:50:56+01:00 bitcoin-signet-instance RPC ready
|
||||
2026-02-02T11:50:56+01:00 Starting/restarting mempool (docker)...
|
||||
2026-02-02T11:50:57+01:00 mempool started
|
||||
2026-02-02T11:51:02+01:00 === Done ===
|
||||
2026-02-02T11:57:11+01:00 === Restart services cron (local only, no SSH) ===
|
||||
2026-02-02T11:57:11+01:00 WARN: bitcoin-signet is not enabled (will not start at boot)
|
||||
2026-02-02T11:57:11+01:00 WARN: bitcoin is not enabled (will not start at boot)
|
||||
2026-02-02T11:57:11+01:00 SKIP: bitcoin-signet not active (not installed or not running on this machine)
|
||||
2026-02-02T11:57:11+01:00 SKIP: bitcoin not active (not installed or not running on this machine)
|
||||
2026-02-02T11:57:11+01:00 Restarting anchorage-api...
|
||||
2026-02-02T11:57:11+01:00 anchorage-api OK
|
||||
2026-02-02T11:57:11+01:00 Restarting api-relay...
|
||||
2026-02-02T11:57:11+01:00 api-relay OK
|
||||
2026-02-02T11:57:11+01:00 Restarting clamav-api...
|
||||
2026-02-02T11:57:11+01:00 clamav-api OK
|
||||
2026-02-02T11:57:11+01:00 Restarting faucet-api...
|
||||
2026-02-02T11:57:12+01:00 faucet-api OK
|
||||
2026-02-02T11:57:12+01:00 Restarting filigrane-api...
|
||||
2026-02-02T11:57:12+01:00 filigrane-api OK
|
||||
2026-02-02T11:57:12+01:00 Restarting signet-dashboard...
|
||||
2026-02-02T11:57:12+01:00 signet-dashboard OK
|
||||
2026-02-02T11:57:12+01:00 Restarting userwallet...
|
||||
2026-02-02T11:57:12+01:00 userwallet OK
|
||||
2026-02-02T11:57:12+01:00 Restarting website-skeleton...
|
||||
2026-02-02T11:57:12+01:00 website-skeleton OK
|
||||
2026-02-02T11:57:12+01:00 Starting/restarting bitcoin-signet-instance...
|
||||
2026-02-02T11:57:18+01:00 bitcoin-signet-instance started, waiting for RPC to be ready...
|
||||
2026-02-02T11:57:49+01:00 bitcoin-signet-instance RPC ready
|
||||
2026-02-02T11:57:49+01:00 Starting/restarting mempool (docker)...
|
||||
2026-02-02T11:57:49+01:00 mempool started
|
||||
2026-02-02T11:57:54+01:00 === Done ===
|
||||
2026-02-03T00:29:32+01:00 === Restart services cron (local only, no SSH) ===
|
||||
2026-02-03T00:29:32+01:00 WARN: bitcoin-signet is not enabled (will not start at boot)
|
||||
2026-02-03T00:29:32+01:00 WARN: bitcoin is not enabled (will not start at boot)
|
||||
2026-02-03T00:29:32+01:00 SKIP: bitcoin-signet not active (not installed or not running on this machine)
|
||||
2026-02-03T00:29:32+01:00 SKIP: bitcoin not active (not installed or not running on this machine)
|
||||
2026-02-03T00:29:32+01:00 Restarting anchorage-api...
|
||||
2026-02-03T00:29:32+01:00 anchorage-api OK
|
||||
2026-02-03T00:29:32+01:00 Restarting api-relay...
|
||||
2026-02-03T00:29:32+01:00 api-relay OK
|
||||
2026-02-03T00:29:32+01:00 Restarting clamav-api...
|
||||
2026-02-03T00:29:32+01:00 clamav-api OK
|
||||
2026-02-03T00:29:32+01:00 Restarting faucet-api...
|
||||
2026-02-03T00:29:32+01:00 faucet-api OK
|
||||
2026-02-03T00:29:32+01:00 Restarting filigrane-api...
|
||||
2026-02-03T00:29:32+01:00 filigrane-api OK
|
||||
2026-02-03T00:29:32+01:00 Restarting signet-dashboard...
|
||||
2026-02-03T00:29:33+01:00 signet-dashboard OK
|
||||
2026-02-03T00:29:33+01:00 Restarting userwallet...
|
||||
2026-02-03T00:29:33+01:00 userwallet OK
|
||||
2026-02-03T00:29:33+01:00 Restarting website-skeleton...
|
||||
2026-02-03T00:29:33+01:00 website-skeleton OK
|
||||
2026-02-03T00:29:33+01:00 Starting/restarting bitcoin-signet-instance...
|
||||
2026-02-03T00:29:40+01:00 bitcoin-signet-instance started, waiting for RPC to be ready...
|
||||
|
||||
@ -36,7 +36,8 @@ if docker ps -a -q -f "name=^${BITCOIND_CONTAINER}$" 2>/dev/null | grep -q .; th
|
||||
max_wait=120
|
||||
wait_count=0
|
||||
while [ $wait_count -lt $max_wait ]; do
|
||||
if docker exec "$BITCOIND_CONTAINER" bitcoin-cli -datadir=/root/.bitcoin getblockchaininfo &>/dev/null; then
|
||||
BITCOIN_DATADIR=$(docker exec "$BITCOIND_CONTAINER" printenv BITCOIN_DIR 2>/dev/null || echo "/root/.bitcoin")
|
||||
if docker exec "$BITCOIND_CONTAINER" bitcoin-cli -datadir="$BITCOIN_DATADIR" getblockchaininfo &>/dev/null; then
|
||||
log " $BITCOIND_CONTAINER RPC ready after ${wait_count}s"
|
||||
break
|
||||
fi
|
||||
|
||||
@ -1,100 +1,100 @@
|
||||
📈 Statistiques finales:
|
||||
- Total UTXOs: 283165
|
||||
- Dépensés: 124307
|
||||
- Non dépensés: 158858
|
||||
|
||||
✅ Synchronisation terminée
|
||||
🔍 Démarrage de la synchronisation des UTXOs dépensés...
|
||||
|
||||
📊 UTXOs à vérifier: 141938
|
||||
📡 Récupération des UTXOs depuis Bitcoin...
|
||||
📊 UTXOs disponibles dans Bitcoin: 272415
|
||||
💾 Création de la table temporaire...
|
||||
💾 Insertion des UTXOs disponibles par batch...
|
||||
⏳ Traitement: 10000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 20000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 30000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 40000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 50000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 60000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 70000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 80000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 90000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 100000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 110000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 120000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 130000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 140000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 150000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 160000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 170000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 180000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 190000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 200000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 210000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 220000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 230000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 240000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 250000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 260000/272415 UTXOs insérés...
|
||||
⏳ Traitement: 270000/272415 UTXOs insérés...
|
||||
💾 Mise à jour des UTXOs dépensés...
|
||||
|
||||
📊 Résumé:
|
||||
- UTXOs vérifiés: 141938
|
||||
- UTXOs toujours disponibles: 141938
|
||||
- UTXOs dépensés détectés: 0
|
||||
|
||||
📈 Statistiques finales:
|
||||
- Total UTXOs: 283165
|
||||
- Dépensés: 141287
|
||||
- Non dépensés: 141878
|
||||
- Dépensés: 202127
|
||||
- Non dépensés: 81038
|
||||
|
||||
✅ Synchronisation terminée
|
||||
🔍 Démarrage de la synchronisation des UTXOs dépensés...
|
||||
|
||||
📊 UTXOs à vérifier: 124298
|
||||
📊 UTXOs à vérifier: 80978
|
||||
📡 Récupération des UTXOs depuis Bitcoin...
|
||||
📊 UTXOs disponibles dans Bitcoin: 270669
|
||||
📊 UTXOs disponibles dans Bitcoin: 267500
|
||||
💾 Création de la table temporaire...
|
||||
💾 Insertion des UTXOs disponibles par batch...
|
||||
⏳ Traitement: 10000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 20000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 30000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 40000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 50000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 60000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 70000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 80000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 90000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 100000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 110000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 120000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 130000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 140000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 150000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 160000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 170000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 180000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 190000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 200000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 210000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 220000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 230000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 240000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 250000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 260000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 270000/270669 UTXOs insérés...
|
||||
⏳ Traitement: 10000/267500 UTXOs insérés...
|
||||
⏳ Traitement: 20000/267500 UTXOs insérés...
|
||||
⏳ Traitement: 30000/267500 UTXOs insérés...
|
||||
⏳ Traitement: 40000/267500 UTXOs insérés...
|
||||
⏳ Traitement: 50000/267500 UTXOs insérés...
|
||||
⏳ Traitement: 60000/267500 UTXOs insérés...
|
||||
⏳ Traitement: 70000/267500 UTXOs insérés...
|
||||
⏳ Traitement: 80000/267500 UTXOs insérés...
|
||||
⏳ Traitement: 90000/267500 UTXOs insérés...
|
||||
⏳ Traitement: 100000/267500 UTXOs insérés...
|
||||
⏳ Traitement: 110000/267500 UTXOs insérés...
|
||||
⏳ Traitement: 120000/267500 UTXOs insérés...
|
||||
⏳ Traitement: 130000/267500 UTXOs insérés...
|
||||
⏳ Traitement: 140000/267500 UTXOs insérés...
|
||||
⏳ Traitement: 150000/267500 UTXOs insérés...
|
||||
⏳ Traitement: 160000/267500 UTXOs insérés...
|
||||
⏳ Traitement: 170000/267500 UTXOs insérés...
|
||||
⏳ Traitement: 180000/267500 UTXOs insérés...
|
||||
⏳ Traitement: 190000/267500 UTXOs insérés...
|
||||
⏳ Traitement: 200000/267500 UTXOs insérés...
|
||||
⏳ Traitement: 210000/267500 UTXOs insérés...
|
||||
⏳ Traitement: 220000/267500 UTXOs insérés...
|
||||
⏳ Traitement: 230000/267500 UTXOs insérés...
|
||||
⏳ Traitement: 240000/267500 UTXOs insérés...
|
||||
⏳ Traitement: 250000/267500 UTXOs insérés...
|
||||
⏳ Traitement: 260000/267500 UTXOs insérés...
|
||||
💾 Mise à jour des UTXOs dépensés...
|
||||
|
||||
📊 Résumé:
|
||||
- UTXOs vérifiés: 124298
|
||||
- UTXOs toujours disponibles: 124298
|
||||
- UTXOs vérifiés: 80978
|
||||
- UTXOs toujours disponibles: 80978
|
||||
- UTXOs dépensés détectés: 0
|
||||
|
||||
📈 Statistiques finales:
|
||||
- Total UTXOs: 283165
|
||||
- Dépensés: 158927
|
||||
- Non dépensés: 124238
|
||||
- Dépensés: 202187
|
||||
- Non dépensés: 80978
|
||||
|
||||
✅ Synchronisation terminée
|
||||
🔍 Démarrage de la synchronisation des UTXOs dépensés...
|
||||
|
||||
📊 UTXOs à vérifier: 80978
|
||||
📡 Récupération des UTXOs depuis Bitcoin...
|
||||
📊 UTXOs disponibles dans Bitcoin: 267516
|
||||
💾 Création de la table temporaire...
|
||||
💾 Insertion des UTXOs disponibles par batch...
|
||||
⏳ Traitement: 10000/267516 UTXOs insérés...
|
||||
⏳ Traitement: 20000/267516 UTXOs insérés...
|
||||
⏳ Traitement: 30000/267516 UTXOs insérés...
|
||||
⏳ Traitement: 40000/267516 UTXOs insérés...
|
||||
⏳ Traitement: 50000/267516 UTXOs insérés...
|
||||
⏳ Traitement: 60000/267516 UTXOs insérés...
|
||||
⏳ Traitement: 70000/267516 UTXOs insérés...
|
||||
⏳ Traitement: 80000/267516 UTXOs insérés...
|
||||
⏳ Traitement: 90000/267516 UTXOs insérés...
|
||||
⏳ Traitement: 100000/267516 UTXOs insérés...
|
||||
⏳ Traitement: 110000/267516 UTXOs insérés...
|
||||
⏳ Traitement: 120000/267516 UTXOs insérés...
|
||||
⏳ Traitement: 130000/267516 UTXOs insérés...
|
||||
⏳ Traitement: 140000/267516 UTXOs insérés...
|
||||
⏳ Traitement: 150000/267516 UTXOs insérés...
|
||||
⏳ Traitement: 160000/267516 UTXOs insérés...
|
||||
⏳ Traitement: 170000/267516 UTXOs insérés...
|
||||
⏳ Traitement: 180000/267516 UTXOs insérés...
|
||||
⏳ Traitement: 190000/267516 UTXOs insérés...
|
||||
⏳ Traitement: 200000/267516 UTXOs insérés...
|
||||
⏳ Traitement: 210000/267516 UTXOs insérés...
|
||||
⏳ Traitement: 220000/267516 UTXOs insérés...
|
||||
⏳ Traitement: 230000/267516 UTXOs insérés...
|
||||
⏳ Traitement: 240000/267516 UTXOs insérés...
|
||||
⏳ Traitement: 250000/267516 UTXOs insérés...
|
||||
⏳ Traitement: 260000/267516 UTXOs insérés...
|
||||
💾 Mise à jour des UTXOs dépensés...
|
||||
|
||||
📊 Résumé:
|
||||
- UTXOs vérifiés: 80978
|
||||
- UTXOs toujours disponibles: 80978
|
||||
- UTXOs dépensés détectés: 0
|
||||
|
||||
📈 Statistiques finales:
|
||||
- Total UTXOs: 283165
|
||||
- Dépensés: 202187
|
||||
- Non dépensés: 80978
|
||||
|
||||
✅ Synchronisation terminée
|
||||
|
||||
@ -145,7 +145,7 @@ Le Dashboard Bitcoin Signet est une interface web de supervision et de test acce
|
||||
- Documentation complète de toutes les APIs
|
||||
- Endpoints documentés :
|
||||
- API d'Ancrage (`/api/anchor/document`, `/api/anchor/verify`), Faucet, Filigrane, ClamAV
|
||||
- **API Dashboard** : `/api/utxo/count`, `/api/utxo/list` (pagination, catégories), `/api/utxo/fees`, `POST /api/utxo/fees/update`, `/api/utxo/small-info`, `POST /api/utxo/consolidate`, `/api/hash/list` (pagination), `POST /api/hash/generate`, `/api/mining/difficulty`, `/api/mining/avg-block-time`, `/api/transactions/avg-fee`, `/api/transactions/avg-amount`, `/api/anchor/example`, etc.
|
||||
- **API Dashboard** : `/api/utxo/count`, `/api/utxo/list` (pagination, catégories), `/api/utxo/fees`, `POST /api/utxo/fees/update`, `/api/utxo/small-info`, `POST /api/utxo/consolidate`, `/api/hash/list` (pagination), `POST /api/hash/generate`, `/api/mining/difficulty`, `/api/mining/status`, `/api/mining/avg-block-time`, `/api/transactions/avg-fee`, `/api/transactions/avg-amount`, `/api/anchor/example`, etc.
|
||||
- Exemples de requêtes curl, paramètres (query/body), réponses
|
||||
- Codes de statut HTTP, authentification (APIs externes), format des réponses
|
||||
|
||||
@ -245,6 +245,7 @@ Le dashboard utilise les endpoints suivants. Tous les endpoints internes sont se
|
||||
- `GET /api/hash/list` : Liste des hash ancrés (pagination : `page`, `limit`)
|
||||
- `POST /api/hash/generate` : Génère un hash SHA256 (body : `text` ou `fileContent`, optionnel `isBase64`)
|
||||
- `GET /api/mining/difficulty` : Difficulté de minage
|
||||
- `GET /api/mining/status` : État du miner (actif / inactif, inféré depuis l’âge du dernier bloc)
|
||||
- `GET /api/mining/avg-block-time` : Temps moyen entre blocs (Mempool)
|
||||
- `GET /api/transactions/avg-fee` : Frais moyen ancrages (1200 sats)
|
||||
- `GET /api/transactions/avg-amount` : Montant moyen ancrages (1000 sats)
|
||||
@ -283,6 +284,10 @@ Le Dashboard n'expose pas de route `/health`. Pour vérifier qu'il répond, util
|
||||
|
||||
## Maintenance
|
||||
|
||||
### Alignement avec la chaîne Signet (~11535 blocs)
|
||||
|
||||
Le Dashboard lit la hauteur de la chaîne via RPC vers le nœud Bitcoin Signet (127.0.0.1:38332). Il affiche donc la même chaîne que le miner et Mempool (même nœud). La hauteur attendue est d’environ **11535 blocs**. Pour vérifier l’alignement Dashboard / Miner / Signet, voir [MAINTENANCE.md - Vérification de l’alignement](./MAINTENANCE.md#vérification-de-lalignement-dashboard--miner--signet-chaîne-11535).
|
||||
|
||||
### Vérifier que le dashboard fonctionne
|
||||
|
||||
```bash
|
||||
|
||||
@ -20,10 +20,19 @@ Ce document liste tous les domaines, ports et services de l'infrastructure Certi
|
||||
| `antivir.certificator.4nkweb.com` | API ClamAV | 3023 | API REST pour scanner les fichiers (antivirus) |
|
||||
| `dashboard.certificator.4nkweb.com` | Dashboard | 3020 | Interface web de supervision |
|
||||
| `faucet.certificator.4nkweb.com` | API Faucet | 3021 | API REST pour distribuer des sats |
|
||||
| `mempool.4nkweb.com` | Mempool | 3015 | Explorateur de blockchain Bitcoin |
|
||||
| `mempool.4nkweb.com` | Mempool | 3015 | Explorateur de blockchain Bitcoin (machine bitcoin 192.168.1.105) |
|
||||
| `skeleton.certificator.4nkweb.com` | Website skeleton | 3024 | Site squelette iframe UserWallet |
|
||||
| `data.certificator.4nkweb.com` | Website data | 3025 | Iframe data (non clés), site ↔ data ↔ userwallet |
|
||||
|
||||
### Écoute des services : IPv4 uniquement, flux reçus du proxy (192.168.1.100)
|
||||
|
||||
Les services backend écoutent en **IPv4 uniquement** (pas d’écoute sur `[::]`) et n’acceptent les flux que **reçus de la machine proxy (192.168.1.100)** :
|
||||
|
||||
- **Bind** : chaque service écoute sur l’adresse IPv4 de sa machine (`DASHBOARD_HOST=192.168.1.105` sur bitcoin, `FAUCET_API_HOST=192.168.1.103` sur prod, etc.).
|
||||
- **Restriction à la source** : si `ALLOWED_SOURCE_IP=192.168.1.100` est défini, toute requête dont la source n’est pas le proxy est rejetée (403).
|
||||
|
||||
Voir `features/services-ecoute-ipv4-proxy.md` et `docs/ENVIRONMENT.md`.
|
||||
|
||||
### Configuration Nginx
|
||||
|
||||
Tous les domaines sont routés via le proxy Nginx sur le serveur `192.168.1.100` (proxy).
|
||||
@ -116,10 +125,10 @@ Internet
|
||||
│ ├─→ certificator.4nkweb.com → 192.168.1.103:3004 (API LeCoffre Anchor)
|
||||
│ ├─→ watermark.certificator.4nkweb.com → 192.168.1.103:3022 (API Filigrane)
|
||||
│ ├─→ antivir.certificator.4nkweb.com → 192.168.1.103:3023 (API ClamAV)
|
||||
│ ├─→ dashboard.certificator.4nkweb.com → 192.168.1.103:3020 (Dashboard)
|
||||
│ ├─→ dashboard.certificator.4nkweb.com → 192.168.1.105:3020 (Dashboard, machine bitcoin)
|
||||
│ ├─→ skeleton.certificator.4nkweb.com → 192.168.1.105:3024 (Website skeleton)
|
||||
│ ├─→ faucet.certificator.4nkweb.com → 192.168.1.103:3021 (API Faucet)
|
||||
│ └─→ mempool.4nkweb.com → 192.168.1.104:3015 (Mempool)
|
||||
│ └─→ mempool.4nkweb.com → 192.168.1.105:3015 (Mempool, machine bitcoin)
|
||||
```
|
||||
|
||||
## Vérification des Ports
|
||||
|
||||
@ -78,7 +78,7 @@ LOG_LEVEL=info # error, warn, info, debug
|
||||
NODE_ENV=production
|
||||
```
|
||||
|
||||
**Note :** Le port est fixe (3010) et défini aussi dans le service systemd.
|
||||
**Note :** Le port est fixe (3010) et défini aussi dans le service systemd. En production, `API_HOST=192.168.1.105` (machine bitcoin) et `ALLOWED_SOURCE_IP=192.168.1.100` pour n’accepter que le proxy en IPv4.
|
||||
|
||||
### 3. Fichier `.env` de l'API Faucet
|
||||
|
||||
@ -110,7 +110,7 @@ LOG_LEVEL=info
|
||||
NODE_ENV=production
|
||||
```
|
||||
|
||||
**Note :** Le port est fixe (3021) et défini aussi dans le service systemd.
|
||||
**Note :** Le port est fixe (3021) et défini aussi dans le service systemd. En production, `FAUCET_API_HOST=192.168.1.103` (machine prod) et `ALLOWED_SOURCE_IP=192.168.1.100` pour n’accepter que le proxy en IPv4.
|
||||
|
||||
### 4. Fichier `.env` de l'API Filigrane
|
||||
|
||||
@ -143,7 +143,7 @@ LOG_LEVEL=info
|
||||
NODE_ENV=production
|
||||
```
|
||||
|
||||
**Note :** Le port est fixe (3022) et défini aussi dans le service systemd.
|
||||
**Note :** Le port est fixe (3022) et défini aussi dans le service systemd. En production, `WATERMARK_API_HOST=192.168.1.103` (machine prod) et `ALLOWED_SOURCE_IP=192.168.1.100` pour n’accepter que le proxy en IPv4.
|
||||
|
||||
### 5. Fichier `.env` de l'API ClamAV
|
||||
|
||||
@ -166,7 +166,7 @@ LOG_LEVEL=info
|
||||
NODE_ENV=production
|
||||
```
|
||||
|
||||
**Note :** Le port est fixe (3023) et défini directement dans le code (`src/server.js`).
|
||||
**Note :** Le port est fixe (3023) et défini directement dans le code (`src/server.js`). En production, `CLAMAV_API_HOST=192.168.1.103` (machine prod) et `ALLOWED_SOURCE_IP=192.168.1.100` pour n’accepter que le proxy en IPv4.
|
||||
|
||||
### 6. Fichier `.env` du Dashboard
|
||||
|
||||
@ -179,8 +179,8 @@ NODE_ENV=production
|
||||
DASHBOARD_PORT=3020 # Port fixe (défini aussi dans systemd)
|
||||
DASHBOARD_HOST=0.0.0.0
|
||||
|
||||
# Bitcoin RPC Configuration
|
||||
BITCOIN_RPC_HOST=localhost
|
||||
# Bitcoin RPC Configuration (sur la machine bitcoin : 127.0.0.1 = même nœud que Mempool)
|
||||
BITCOIN_RPC_HOST=127.0.0.1
|
||||
BITCOIN_RPC_PORT=38332
|
||||
BITCOIN_RPC_USER=bitcoin
|
||||
BITCOIN_RPC_PASSWORD=bitcoin
|
||||
@ -207,7 +207,7 @@ LOG_LEVEL=info
|
||||
NODE_ENV=production
|
||||
```
|
||||
|
||||
**Note :** Le port est fixe (3020) et défini aussi dans le service systemd.
|
||||
**Note :** Le port est fixe (3020) et défini aussi dans le service systemd. En production, `DASHBOARD_HOST=192.168.1.105` (machine bitcoin) et `ALLOWED_SOURCE_IP=192.168.1.100` pour n’accepter que le proxy en IPv4.
|
||||
|
||||
## Variables d'Environnement dans les Services Systemd
|
||||
|
||||
@ -217,28 +217,32 @@ Les services systemd définissent aussi des variables d'environnement pour garan
|
||||
|
||||
```ini
|
||||
Environment=API_PORT=3010
|
||||
Environment=API_HOST=0.0.0.0
|
||||
Environment=API_HOST=192.168.1.105
|
||||
Environment=ALLOWED_SOURCE_IP=192.168.1.100
|
||||
```
|
||||
|
||||
### API Faucet (`api-faucet/faucet-api.service`)
|
||||
|
||||
```ini
|
||||
Environment=FAUCET_API_PORT=3021
|
||||
Environment=FAUCET_API_HOST=0.0.0.0
|
||||
Environment=FAUCET_API_HOST=192.168.1.103
|
||||
Environment=ALLOWED_SOURCE_IP=192.168.1.100
|
||||
```
|
||||
|
||||
### API Filigrane (`api-filigrane/filigrane-api.service`)
|
||||
|
||||
```ini
|
||||
Environment=WATERMARK_API_PORT=3022
|
||||
Environment=WATERMARK_API_HOST=0.0.0.0
|
||||
Environment=WATERMARK_API_HOST=192.168.1.103
|
||||
Environment=ALLOWED_SOURCE_IP=192.168.1.100
|
||||
```
|
||||
|
||||
### API ClamAV (`api-clamav/clamav-api.service`)
|
||||
|
||||
```ini
|
||||
Environment=CLAMAV_API_PORT=3023
|
||||
Environment=CLAMAV_API_HOST=0.0.0.0
|
||||
Environment=CLAMAV_API_HOST=192.168.1.103
|
||||
Environment=ALLOWED_SOURCE_IP=192.168.1.100
|
||||
Environment=CLAMAV_HOST=localhost
|
||||
Environment=CLAMAV_PORT=3310
|
||||
```
|
||||
@ -247,9 +251,22 @@ Environment=CLAMAV_PORT=3310
|
||||
|
||||
```ini
|
||||
Environment=DASHBOARD_PORT=3020
|
||||
Environment=DASHBOARD_HOST=0.0.0.0
|
||||
Environment=DASHBOARD_HOST=192.168.1.105
|
||||
Environment=ALLOWED_SOURCE_IP=192.168.1.100
|
||||
```
|
||||
|
||||
## Écoute réseau : IPv4 uniquement, flux reçus du proxy (192.168.1.100)
|
||||
|
||||
Les services backend doivent :
|
||||
|
||||
1. **Écouter en IPv4 uniquement** : pas d’écoute sur `[::]`. En fixant `*_HOST` à l’adresse IPv4 de la machine (ex. `192.168.1.105` pour la machine bitcoin), le service n’écoute que sur cette interface (IPv4).
|
||||
2. **Accepter les flux reçus du proxy uniquement** : si `ALLOWED_SOURCE_IP=192.168.1.100` est défini, le service rejette (403) toute requête dont la source (après normalisation IPv6-mapped → IPv4) n’est pas 192.168.1.100.
|
||||
|
||||
- **Machine bitcoin (192.168.1.105)** : `DASHBOARD_HOST=192.168.1.105`, `API_HOST=192.168.1.105` (anchorage), `ALLOWED_SOURCE_IP=192.168.1.100`.
|
||||
- **Machine prod (192.168.1.103)** : `FAUCET_API_HOST=192.168.1.103`, `WATERMARK_API_HOST=192.168.1.103`, `CLAMAV_API_HOST=192.168.1.103`, `ALLOWED_SOURCE_IP=192.168.1.100`.
|
||||
|
||||
Voir `features/services-ecoute-ipv4-proxy.md` pour le détail.
|
||||
|
||||
## Ordre de Priorité
|
||||
|
||||
Les variables d'environnement sont chargées dans cet ordre :
|
||||
|
||||
@ -218,6 +218,7 @@ sudo docker exec bitcoin-signet-instance bash -c "echo 60 > /root/.bitcoin/BLOCK
|
||||
```bash
|
||||
sudo docker run --env-file .env -d \
|
||||
--name bitcoin-signet-instance \
|
||||
-v signet-bitcoin-data:/root/.bitcoin \
|
||||
-p 38332:38332 \
|
||||
-p 38333:38333 \
|
||||
-p 28332:28332 \
|
||||
@ -226,6 +227,8 @@ sudo docker run --env-file .env -d \
|
||||
bitcoin-signet
|
||||
```
|
||||
|
||||
Le volume `signet-bitcoin-data` conserve la chaîne ; sans lui, une recréation du conteneur repart d’une nouvelle chaîne.
|
||||
|
||||
### Vérifier les Logs
|
||||
|
||||
```bash
|
||||
@ -252,11 +255,12 @@ sudo docker stop bitcoin-signet-instance && sudo docker rm bitcoin-signet-instan
|
||||
# Redémarrer le conteneur
|
||||
sudo docker restart bitcoin-signet-instance
|
||||
|
||||
# Ou recréer le conteneur
|
||||
# Ou recréer le conteneur (volume persistant pour conserver la chaîne)
|
||||
sudo docker stop bitcoin-signet-instance
|
||||
sudo docker rm bitcoin-signet-instance
|
||||
sudo docker run --env-file .env -d \
|
||||
--name bitcoin-signet-instance \
|
||||
-v signet-bitcoin-data:/root/.bitcoin \
|
||||
-p 38332:38332 -p 38333:38333 -p 28332:28332 -p 28333:28333 -p 28334:28334 \
|
||||
bitcoin-signet
|
||||
```
|
||||
|
||||
@ -377,9 +377,10 @@ Commandes Docker pour gérer le conteneur Bitcoin Signet.
|
||||
|
||||
#### Gestion du Conteneur
|
||||
```bash
|
||||
# Démarrer
|
||||
# Démarrer (volume persistant pour conserver la chaîne)
|
||||
sudo docker run --env-file .env -d \
|
||||
--name bitcoin-signet-instance \
|
||||
-v signet-bitcoin-data:/root/.bitcoin \
|
||||
-p 38332:38332 -p 38333:38333 \
|
||||
-p 28332:28332 -p 28333:28333 -p 28334:28334 \
|
||||
bitcoin-signet
|
||||
@ -502,7 +503,7 @@ pm2 logs anchor-api
|
||||
Interface web et API REST pour explorer la blockchain Bitcoin Signet.
|
||||
|
||||
### Accès
|
||||
- **URL Web** : `http://localhost:3015` (local) ou `https://mempool1.4nkweb.com` (production)
|
||||
- **URL Web** : `http://localhost:3015` (local) ou `https://mempool.4nkweb.com` (production, machine bitcoin)
|
||||
- **API Backend** : `http://localhost:8999/api/v1/`
|
||||
- **Protocole** : HTTP/HTTPS
|
||||
- **Format** : HTML (web) / JSON (API)
|
||||
|
||||
@ -85,7 +85,7 @@ bitcoin/
|
||||
|
||||
- `/root/.bitcoin/` : Répertoire de données Bitcoin
|
||||
- `signet/` : Données de la chaîne signet
|
||||
- `bitcoin.conf` : Configuration du nœud
|
||||
- `bitcoin.conf` : Configuration du nœud (générée par `gen-bitcoind-conf.sh` ; référence : [SIGNET-CUSTOM-CONFIG.md](./SIGNET-CUSTOM-CONFIG.md))
|
||||
- `PRIVKEY.txt` : Clé privée du signer
|
||||
- `SIGNETCHALLENGE.txt` : Challenge du signet
|
||||
- `MAGIC.txt` : Magic number du réseau
|
||||
@ -222,12 +222,27 @@ sudo docker exec bitcoin-signet-instance bitcoin-cli -datadir=/root/.bitcoin <co
|
||||
|
||||
## Gestion du Conteneur
|
||||
|
||||
### Persistance du datadir (chaîne Bitcoin)
|
||||
|
||||
**Important :** Sans volume persistant, la recréation du conteneur (`docker rm` puis `docker run`) supprime les données Bitcoin et le nœud repart sur une **nouvelle chaîne** (hauteur 0). Pour conserver la chaîne (ex. hauteur ~11530), utilisez **toujours** un volume persistant.
|
||||
|
||||
**Volume par défaut (chaîne complète) :** Le script `update-signet.sh` utilise par défaut le volume contenant la chaîne Signet complète (~11530 blocs) s'il existe sur la machine : volume Docker d'ID **4b5dca4d940b9f6e5db67b460f40f230a5ef1195a3769e5f91fa02be6edde649** (variable `SIGNET_VOLUME_FULL_CHAIN` dans le script). Ainsi, après une mise à jour ou un redémarrage via `update-signet.sh`, le nœud repart sur la bonne chaîne sans recherche. Si ce volume n'existe pas, le script utilise le volume nommé `signet-bitcoin-data`.
|
||||
|
||||
- **Volume « chaîne complète »** (prioritaire si présent) : ID `4b5dca4d940b9f6e5db67b460f40f230a5ef1195a3769e5f91fa02be6edde649`
|
||||
- **Volume nommé** (sinon) : `-v signet-bitcoin-data:/root/.bitcoin`
|
||||
- **Montage host** : `-v /chemin/sur/hote/signet-data:/root/.bitcoin` (créer le répertoire avant)
|
||||
|
||||
**Sauvegarde prête à télécharger :** `backups/signet-datadir-latest.tar.gz` (symlink vers la dernière archive créée par `./save-signet-datadir-backup.sh`). À utiliser pour restaurer la chaîne sur une autre machine ou après perte du volume. Voir [Sauvegarde et Restauration](#sauvegarde-et-restauration).
|
||||
|
||||
Les commandes `docker run` ci-dessous utilisent le volume approprié (voir `update-signet.sh` pour la logique par défaut).
|
||||
|
||||
### Démarrage
|
||||
|
||||
```bash
|
||||
cd /home/ncantu/Bureau/code/bitcoin
|
||||
sudo docker run --env-file .env -d \
|
||||
--name bitcoin-signet-instance \
|
||||
-v signet-bitcoin-data:/root/.bitcoin \
|
||||
-p 38332:38332 \
|
||||
-p 38333:38333 \
|
||||
-p 28332:28332 \
|
||||
@ -256,6 +271,7 @@ sudo docker restart bitcoin-signet-instance
|
||||
sudo docker stop bitcoin-signet-instance
|
||||
sudo docker rm bitcoin-signet-instance
|
||||
sudo docker run --env-file .env -d --name bitcoin-signet-instance \
|
||||
-v signet-bitcoin-data:/root/.bitcoin \
|
||||
-p 38332:38332 -p 38333:38333 -p 28332:28332 -p 28333:28333 -p 28334:28334 \
|
||||
bitcoin-signet
|
||||
```
|
||||
@ -607,7 +623,7 @@ Le script `update-signet.sh` effectue automatiquement :
|
||||
4. **Sauvegarde automatique** : Données du conteneur et fichier `.env`
|
||||
5. **Mise à jour du Dockerfile** : Modification de la version Bitcoin Core
|
||||
6. **Reconstruction de l'image** : Build de la nouvelle image Docker
|
||||
7. **Redémarrage du conteneur** : Arrêt propre et redémarrage avec la nouvelle version
|
||||
7. **Redémarrage du conteneur** : Arrêt propre et redémarrage avec la nouvelle version (volume **signet-bitcoin-data** pour une seule chaîne Mempool/dashboard/APIs)
|
||||
8. **Vérification post-mise à jour** : Contrôle de l'état du nœud
|
||||
|
||||
#### Procédure Manuelle de Mise à Jour
|
||||
@ -671,9 +687,10 @@ sudo docker build -t bitcoin-signet .
|
||||
sudo docker stop bitcoin-signet-instance
|
||||
sudo docker rm bitcoin-signet-instance
|
||||
|
||||
# Démarrer avec la nouvelle image
|
||||
# Démarrer avec la nouvelle image (volume persistant pour conserver la chaîne)
|
||||
sudo docker run --env-file .env -d \
|
||||
--name bitcoin-signet-instance \
|
||||
-v signet-bitcoin-data:/root/.bitcoin \
|
||||
-p 38332:38332 \
|
||||
-p 38333:38333 \
|
||||
-p 28332:28332 \
|
||||
@ -736,8 +753,9 @@ sudo docker exec bitcoin-signet-instance tar xzf /tmp/signet-backup-YYYYMMDD-HHM
|
||||
# Reconstruire avec l'ancienne version
|
||||
sudo docker build -t bitcoin-signet .
|
||||
|
||||
# Redémarrer
|
||||
# Redémarrer (volume persistant pour conserver la chaîne)
|
||||
sudo docker run --env-file .env -d --name bitcoin-signet-instance \
|
||||
-v signet-bitcoin-data:/root/.bitcoin \
|
||||
-p 38332:38332 -p 38333:38333 -p 28332:28332 -p 28333:28333 -p 28334:28334 \
|
||||
bitcoin-signet
|
||||
```
|
||||
@ -850,8 +868,9 @@ sudo docker volume rm $(sudo docker volume ls -q | grep bitcoin)
|
||||
# Ou supprimer manuellement les données
|
||||
sudo rm -rf /var/lib/docker/volumes/*/bitcoin-signet-instance/_data
|
||||
|
||||
# Relancer avec un nouveau .env (clés seront régénérées)
|
||||
# Relancer avec un nouveau .env (clés seront régénérées ; utiliser un nouveau volume ou supprimer signet-bitcoin-data avant)
|
||||
sudo docker run --env-file .env -d --name bitcoin-signet-instance \
|
||||
-v signet-bitcoin-data:/root/.bitcoin \
|
||||
-p 38332:38332 -p 38333:38333 -p 28332:28332 -p 28333:28333 -p 28334:28334 \
|
||||
bitcoin-signet
|
||||
```
|
||||
@ -894,6 +913,39 @@ sudo docker run --env-file .env -d --name bitcoin-signet-instance \
|
||||
|
||||
### Sauvegarde
|
||||
|
||||
**Sauvegarde datadir (chaîne complète, prête à télécharger) :**
|
||||
|
||||
```bash
|
||||
# Créer une archive complète du datadir (blocs + chainstate + config)
|
||||
./save-signet-datadir-backup.sh
|
||||
# Crée backups/signet-datadir-YYYYMMDD-HHMMSS.tar.gz et met à jour le symlink :
|
||||
# backups/signet-datadir-latest.tar.gz -> dernière archive (prête à télécharger)
|
||||
```
|
||||
|
||||
Le symlink `backups/signet-datadir-latest.tar.gz` pointe toujours vers la dernière archive créée ; utile pour téléchargement ou restauration rapide : `./restore-signet-from-backup.sh backups/signet-datadir-latest.tar.gz`.
|
||||
|
||||
**Export wallet de minage (réimportable, pour retrouver les fonds sur cette chaîne) :**
|
||||
|
||||
```bash
|
||||
./export-mining-wallet.sh
|
||||
# Crée backups/mining-wallet-export-YYYYMMDD-HHMMSS.json et mining-wallet-export-latest.json
|
||||
# Contient : PRIVKEY, SIGNETCHALLENGE, wallet_name, descriptors avec clés privées, infos chaîne
|
||||
|
||||
# Réimporter sur un nœud avec la même chaîne :
|
||||
./import-mining-wallet.sh backups/mining-wallet-export-latest.json
|
||||
```
|
||||
|
||||
**Export quotidien vers git (2 versions conservées) :**
|
||||
|
||||
```bash
|
||||
# Tâche cron quotidienne : exporte signet-datadir et mining-wallet vers
|
||||
# https://git.4nkweb.com/4nk/backup (2 versions max de chaque)
|
||||
./data/backup-to-git-cron.sh
|
||||
# Voir features/backup-to-git-daily-cron.md pour configuration cron
|
||||
```
|
||||
|
||||
**Sauvegarde manuelle :**
|
||||
|
||||
```bash
|
||||
# Sauvegarder les données Bitcoin
|
||||
sudo docker exec bitcoin-signet-instance tar czf /tmp/bitcoin-backup.tar.gz /root/.bitcoin/
|
||||
@ -912,8 +964,9 @@ cp .env .env.backup-$(date +%Y%m%d)
|
||||
sudo docker stop bitcoin-signet-instance
|
||||
sudo docker rm bitcoin-signet-instance
|
||||
|
||||
# Créer un nouveau conteneur
|
||||
# Créer un nouveau conteneur (volume persistant pour que les données restaurées soient conservées)
|
||||
sudo docker run --env-file .env -d --name bitcoin-signet-instance \
|
||||
-v signet-bitcoin-data:/root/.bitcoin \
|
||||
-p 38332:38332 -p 38333:38333 -p 28332:28332 -p 28333:28333 -p 28334:28334 \
|
||||
bitcoin-signet
|
||||
|
||||
@ -941,6 +994,36 @@ Ces clés sont essentielles pour maintenir la cohérence du signet.
|
||||
|
||||
## Commandes Utiles
|
||||
|
||||
### Vérification de l'alignement Dashboard / Miner / Signet (chaîne ~11535)
|
||||
|
||||
Dashboard, miner et signet utilisent **une seule source de vérité** : le nœud Bitcoin Signet dans le conteneur `bitcoin-signet-instance` (RPC port 38332).
|
||||
|
||||
| Composant | Source de la hauteur |
|
||||
|-----------|----------------------|
|
||||
| **Signet (nœud)** | `bitcoind` dans le conteneur, datadir `/root/.bitcoin` |
|
||||
| **Dashboard** | RPC `getblockchaininfo` vers `127.0.0.1:38332` (même nœud) |
|
||||
| **Miner** | S'exécute dans le conteneur, appelle `bitcoin-cli -datadir=/root/.bitcoin` (même nœud) |
|
||||
| **Mempool** | Backend connecté au même nœud (CORE_RPC_PORT=38332) |
|
||||
|
||||
La chaîne attendue est d’environ **11535 blocs**. Pour vérifier que tout est aligné :
|
||||
|
||||
1. **Hauteur depuis le nœud** (référence) :
|
||||
```bash
|
||||
sudo docker exec bitcoin-signet-instance bitcoin-cli -datadir=/root/.bitcoin getblockchaininfo | grep -E '"chain"|"blocks"'
|
||||
```
|
||||
Attendu : `"chain": "signet"`, `"blocks":` proche de 11535.
|
||||
|
||||
2. **Hauteur depuis le Dashboard** (doit être identique) :
|
||||
```bash
|
||||
curl -s http://localhost:3020/api/blockchain/info | grep -o '"blocks":[0-9]*'
|
||||
```
|
||||
|
||||
3. **Miner** : les logs du conteneur affichent la hauteur à chaque bloc miné (`Mined ... at height N`). Cette hauteur est celle du nœud.
|
||||
|
||||
Si le Dashboard affiche une hauteur très différente (ex. 2) ou si l’API d’ancrage retourne « Insufficient Balance » (0 BTC), exécuter sur la machine bitcoin : `./fix-dashboard-anchor-chain.sh` (ou avec chemin de backup pour restaurer la chaîne). Voir [fixKnowledge/dashboard-anchor-wrong-chain-insufficient-balance.md](../fixKnowledge/dashboard-anchor-wrong-chain-insufficient-balance.md) et [fixKnowledge/signet-chain-lost-volume-persistent.md](../fixKnowledge/signet-chain-lost-volume-persistent.md).
|
||||
|
||||
**Script de test RPC (même nœud que Mempool) :** `./test-mempool-rpc-config.sh [HOST] [PORT]` (défaut 127.0.0.1 38332). **Script de vérification dashboard signet :** `./verify-dashboard-signet.sh` (machine bitcoin, pour que https://dashboard.certificator.4nkweb.com/ affiche le signet custom). **Script de vérification :** exécuter `./verify-chain-alignment.sh` à la racine du projet pour comparer la hauteur du nœud et du Dashboard et vérifier qu’elle est dans la plage attendue (~11535).
|
||||
|
||||
### Script de Vérification Rapide
|
||||
|
||||
```bash
|
||||
@ -980,4 +1063,4 @@ watch -n 5 'sudo docker exec bitcoin-signet-instance bitcoin-cli -datadir=/root/
|
||||
|
||||
---
|
||||
|
||||
**Dernière mise à jour** : 2026-01-24
|
||||
**Dernière mise à jour** : 2026-02-02
|
||||
|
||||
@ -6,7 +6,9 @@
|
||||
|
||||
## Vue d'Ensemble
|
||||
|
||||
Mempool est un explorateur de blockchain Bitcoin qui permet de visualiser et d'analyser la blockchain et la mempool du signet custom. Il fournit une interface web moderne pour explorer les blocs, transactions, adresses et statistiques du réseau.
|
||||
Mempool est un explorateur de blockchain Bitcoin qui permet de visualiser et d'analyser la blockchain et la mempool du signet custom.
|
||||
|
||||
**Emplacement dans le projet :** le chemin de base du projet est `/home/ncantu/Bureau/code/bitcoin/` ; Mempool se trouve dans le sous-répertoire `mempool/` (soit `.../bitcoin/mempool/`). Il fournit une interface web moderne pour explorer les blocs, transactions, adresses et statistiques du réseau.
|
||||
|
||||
## Caractéristiques
|
||||
|
||||
@ -26,19 +28,23 @@ Mempool est un explorateur de blockchain Bitcoin qui permet de visualiser et d'a
|
||||
|
||||
### Installation Rapide
|
||||
|
||||
Depuis la racine du projet (`/home/ncantu/Bureau/code/bitcoin/`) :
|
||||
|
||||
```bash
|
||||
cd /home/ncantu/Bureau/code/bitcoin/mempool
|
||||
cd mempool
|
||||
./start.sh
|
||||
```
|
||||
|
||||
Le script :
|
||||
1. Charge les variables d'environnement depuis `../.env`
|
||||
1. Charge les variables d'environnement depuis `../.env` (racine du projet)
|
||||
2. Vérifie la connexion au nœud Bitcoin
|
||||
3. Crée les répertoires nécessaires
|
||||
4. Lance les services Docker (frontend, backend, base de données)
|
||||
|
||||
### Installation Manuelle
|
||||
|
||||
Depuis la racine du projet :
|
||||
|
||||
```bash
|
||||
cd mempool
|
||||
docker-compose -f docker-compose.signet.yml up -d
|
||||
@ -121,7 +127,7 @@ Mempool utilise quatre services Docker :
|
||||
### Interface Web
|
||||
|
||||
- **URL locale** : http://localhost:3015
|
||||
- **URL production** : https://mempool1.4nkweb.com (via nginx proxy)
|
||||
- **URL production** : https://mempool.4nkweb.com (via nginx proxy, machine bitcoin 192.168.1.105)
|
||||
|
||||
### API Backend
|
||||
|
||||
@ -136,6 +142,8 @@ Mempool utilise quatre services Docker :
|
||||
|
||||
### Démarrage
|
||||
|
||||
Depuis la racine du projet (`/home/ncantu/Bureau/code/bitcoin/`) :
|
||||
|
||||
```bash
|
||||
cd mempool
|
||||
./start.sh
|
||||
@ -189,6 +197,8 @@ sudo docker-compose -f docker-compose.signet.yml down -v
|
||||
|
||||
### Mise à Jour
|
||||
|
||||
Depuis la racine du projet :
|
||||
|
||||
```bash
|
||||
cd mempool
|
||||
# Récupérer les dernières images
|
||||
@ -318,19 +328,19 @@ sudo docker-compose -f docker-compose.signet.yml logs -f db
|
||||
|
||||
### Configuration Nginx (sur proxy 192.168.1.100)
|
||||
|
||||
Pour exposer Mempool via le proxy nginx (mempool1.4nkweb.com), ajouter une configuration similaire aux autres services :
|
||||
Pour exposer Mempool via le proxy nginx (mempool.4nkweb.com), ajouter une configuration similaire aux autres services. Mempool est hébergé sur la machine bitcoin (192.168.1.105) uniquement :
|
||||
|
||||
```nginx
|
||||
# Mempool Bitcoin Signet Explorer
|
||||
server {
|
||||
listen 80;
|
||||
server_name mempool1.4nkweb.com;
|
||||
server_name mempool.4nkweb.com;
|
||||
|
||||
access_log /var/log/nginx/mempool1.4nkweb.com.access.log;
|
||||
error_log /var/log/nginx/mempool1.4nkweb.com.error.log;
|
||||
access_log /var/log/nginx/mempool.4nkweb.com.access.log;
|
||||
error_log /var/log/nginx/mempool.4nkweb.com.error.log;
|
||||
|
||||
location / {
|
||||
proxy_pass http://192.168.1.XXX:3015;
|
||||
proxy_pass http://192.168.1.105:3015;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
|
||||
@ -2,6 +2,8 @@
|
||||
|
||||
Ce dossier contient toute la documentation nécessaire pour la maintenance et l'utilisation du Bitcoin Signet custom.
|
||||
|
||||
**Structure du projet :** chemin de base `/home/ncantu/Bureau/code/bitcoin/` ; Mempool dans le sous-répertoire `mempool/`.
|
||||
|
||||
## Fichiers de Documentation
|
||||
|
||||
- **[MAINTENANCE.md](./MAINTENANCE.md)** : Documentation complète de maintenance
|
||||
@ -81,16 +83,35 @@ Ce dossier contient toute la documentation nécessaire pour la maintenance et l'
|
||||
cd /home/ncantu/Bureau/code/bitcoin
|
||||
sudo docker build -t bitcoin-signet .
|
||||
sudo docker run --env-file .env -d --name bitcoin-signet-instance \
|
||||
-v signet-bitcoin-data:/root/.bitcoin \
|
||||
-p 38332:38332 -p 38333:38333 -p 28332:28332 -p 28333:28333 -p 28334:28334 \
|
||||
bitcoin-signet
|
||||
```
|
||||
|
||||
Le volume `signet-bitcoin-data` conserve la chaîne Bitcoin ; sans lui, une recréation du conteneur repart d’une nouvelle chaîne (hauteur 0).
|
||||
|
||||
### Vérification
|
||||
|
||||
```bash
|
||||
# Alignement Dashboard / Miner / Signet (chaîne ~11535)
|
||||
./verify-chain-alignment.sh
|
||||
|
||||
# État du nœud
|
||||
sudo docker exec bitcoin-signet-instance bitcoin-cli -datadir=/root/.bitcoin getblockchaininfo
|
||||
```
|
||||
|
||||
### Correction Dashboard mauvaise chaîne / API ancrage Insufficient Balance
|
||||
|
||||
Sur la machine bitcoin, à la racine du projet :
|
||||
|
||||
```bash
|
||||
# Vérifier, redémarrer Dashboard et API d’ancrage
|
||||
./fix-dashboard-anchor-chain.sh
|
||||
|
||||
# Avec restauration de la chaîne depuis une sauvegarde
|
||||
./fix-dashboard-anchor-chain.sh backups/signet-datadir-YYYYMMDD-HHMMSS.tar.gz
|
||||
```
|
||||
|
||||
### Logs
|
||||
|
||||
```bash
|
||||
|
||||
40
docs/SIGNET-CUSTOM-CONFIG.md
Normal file
40
docs/SIGNET-CUSTOM-CONFIG.md
Normal file
@ -0,0 +1,40 @@
|
||||
# Configuration de référence – Signet custom
|
||||
|
||||
**Auteur** : Équipe 4NK
|
||||
**Date** : 2026-02-02
|
||||
|
||||
Configuration de référence du nœud Bitcoin Signet custom. Elle est générée par `gen-bitcoind-conf.sh` à partir du fichier `.env` à la racine du projet.
|
||||
|
||||
## bitcoin.conf (équivalent)
|
||||
|
||||
```ini
|
||||
signet=1
|
||||
txindex=1
|
||||
blockfilterindex=1
|
||||
peerblockfilters=1
|
||||
coinstatsindex=1
|
||||
dnsseed=0
|
||||
persistmempool=1
|
||||
uacomment=CustomSignet
|
||||
|
||||
[signet]
|
||||
daemon=1
|
||||
listen=1
|
||||
server=1
|
||||
discover=1
|
||||
signetchallenge=5121028b8d4cea1b3d8582babc8405bc618fbbb281c0f64e6561aa85968251931cd0a651ae
|
||||
rpcbind=0.0.0.0:38332
|
||||
rpcallowip=0.0.0.0/0
|
||||
whitelist=0.0.0.0/0
|
||||
fallbackfee=0.0002
|
||||
```
|
||||
|
||||
Pas de `addnode` nécessaire pour cette machine (nœud unique sur 192.168.1.105). Le nœud et les services (Dashboard, Anchorage, Faucet, Mempool) tournent sur cette machine.
|
||||
|
||||
## Variables .env correspondantes
|
||||
|
||||
- `SIGNETCHALLENGE`, `RPCUSER`, `RPCPASSWORD`, `UACOMMENT`
|
||||
- `RPCBIND=0.0.0.0:38332`, `RPCALLOWIP=0.0.0.0/0`, `WHITELIST=0.0.0.0/0`
|
||||
- ZMQ : `ZMQPUBRAWBLOCK`, `ZMQPUBRAWTX`, `ZMQPUBHASHBLOCK`
|
||||
|
||||
Voir `env.example` et `gen-bitcoind-conf.sh`.
|
||||
@ -77,6 +77,7 @@ sudo docker rm bitcoin-signet-instance
|
||||
sudo docker build -t bitcoin-signet .
|
||||
sudo docker run --env-file .env -d \
|
||||
--name bitcoin-signet-instance \
|
||||
-v signet-bitcoin-data:/root/.bitcoin \
|
||||
-p 38332:38332 -p 38333:38333 \
|
||||
-p 28332:28332 -p 28333:28333 -p 28334:28334 \
|
||||
bitcoin-signet
|
||||
|
||||
60
docs/USERWALLET_KEY_DERIVATION.md
Normal file
60
docs/USERWALLET_KEY_DERIVATION.md
Normal file
@ -0,0 +1,60 @@
|
||||
# UserWallet — Dérivation de clés et clés multiples par pair
|
||||
|
||||
**Author:** Équipe 4NK
|
||||
**Date:** 2026-02-02
|
||||
**Version:** 1.0
|
||||
|
||||
## Objectif
|
||||
|
||||
Permettre à un pair (identité locale ou appareil distant) d’avoir **plusieurs clés publiques** et de **dériver** de nouvelles paires de clés de façon déterministe à partir d’une clé privée. Vérifier rapidement si une clé publique donnée « appartient » à une clé privée (clé principale ou l’une des clés dérivées).
|
||||
|
||||
## Modèle de données
|
||||
|
||||
### PairConfig
|
||||
|
||||
- **`publicKey?: string`** — Clé publique principale (hex 66 caractères). Utilisée pour ECDH, pairing, etc.
|
||||
- **`publicKeys?: string[]`** — Liste optionnelle de clés publiques supplémentaires. Un pair « possède » toute clé dans `{ publicKey } ∪ publicKeys`.
|
||||
|
||||
Un pair peut donc avoir un nombre important de clés publiques différentes (principale + dérivées ou ajoutées).
|
||||
|
||||
## Dérivation déterministe (crypto)
|
||||
|
||||
Une seule clé privée permet d’obtenir plusieurs paires (clé privée enfant, clé publique enfant) sans stocker plusieurs secrets. La dérivation est **déterministe** : même index ⇒ même paire.
|
||||
|
||||
### Algorithme
|
||||
|
||||
- **Entrée** : clé privée parente (64 hex), index ≥ 0.
|
||||
- **Procédé** : HMAC-SHA256(clé_privée_parente, `"userwallet-derive-v1-"` + index) → 32 octets ; réduction modulo (ordre de la courbe secp256k1 − 1) + 1 pour obtenir un scalaire valide ; clé publique = multiplication du point de base par ce scalaire (format compressé).
|
||||
- **Sortie** : paire (clé privée enfant, clé publique enfant).
|
||||
|
||||
Courbe : secp256k1 (même que Bitcoin). La clé principale (index « aucun ») est la clé publique dérivée directement de la clé privée de l’identité.
|
||||
|
||||
### API (userwallet)
|
||||
|
||||
| Fonction | Rôle |
|
||||
|----------|------|
|
||||
| `deriveChildKeyPair(parentPrivateKeyHex, index)` | Retourne la paire (privée, publique) pour l’index donné. |
|
||||
| `getDerivedPublicKeys(parentPrivateKeyHex, count)` | Retourne [clé principale, dérivée(0), …, dérivée(count−1)]. Longueur = 1 + count. |
|
||||
| `publicKeyBelongsToIdentity(identityPrivateKeyHex, publicKeyHex, maxDerived?)` | Vrai si la clé publique est la clé principale ou l’une des dérivées d’indice 0..maxDerived−1. Par défaut maxDerived = 0 (seule la clé principale est testée). |
|
||||
|
||||
**Performance** : la vérification « cette clé appartient-elle à mon identité ? » est en O(1) pour la clé principale (une dérivation + comparaison). Pour les dérivées, au plus `maxDerived` dérivations + comparaisons ; en pratique on borne `maxDerived` pour rester rapide.
|
||||
|
||||
## Pairing (pairs et multi-clés)
|
||||
|
||||
| Fonction | Rôle |
|
||||
|----------|------|
|
||||
| `getPairPublicKeys(pair, identityPrivateKeyHex?, derivedCount?)` | Liste toutes les clés publiques du pair. Pour le pair local + clé privée fournie : clé principale + dérivées 0..derivedCount−1. Pour un pair distant : `publicKey` + `publicKeys`. |
|
||||
| `pairContainsPublicKey(pair, publicKeyHex, identityPrivateKeyHex?, maxDerived?)` | Vrai si le pair possède cette clé (locale : dérivation bornée ; distant : test d’appartenance à `publicKey` / `publicKeys`). |
|
||||
| `addPairPublicKey(pairUuid, publicKeyHex)` | Ajoute une clé à `publicKeys` du pair (sans doublon avec `publicKey`). |
|
||||
|
||||
## Fichiers concernés
|
||||
|
||||
- `userwallet/src/utils/crypto.ts` : `deriveChildKeyPair`, `getDerivedPublicKeys`, `publicKeyBelongsToIdentity`
|
||||
- `userwallet/src/utils/pairing.ts` : `getPairPublicKeys`, `pairContainsPublicKey`, `addPairPublicKey`
|
||||
- `userwallet/src/types/identity.ts` : `PairConfig.publicKeys`
|
||||
|
||||
## Usage typique
|
||||
|
||||
- **Obtenir N clés dérivées** : `getDerivedPublicKeys(identity.privateKey, N)`.
|
||||
- **Vérifier qu’une clé appartient à l’identité** (avec au plus 100 dérivées) : `publicKeyBelongsToIdentity(identity.privateKey, somePubKey, 100)`.
|
||||
- **Vérifier qu’un pair possède une clé** : `pairContainsPublicKey(pair, somePubKey, identity?.privateKey, 100)`.
|
||||
@ -23,5 +23,5 @@ ZMQPUBHASHBLOCK=tcp://0.0.0.0:28334
|
||||
RPCBIND=0.0.0.0:38332
|
||||
RPCALLOWIP=0.0.0.0/0
|
||||
WHITELIST=0.0.0.0/0
|
||||
ADDNODE=<Add node IP, if applicable>
|
||||
ADDNODE=<Add node IP:port, if applicable (not needed for single node on this machine)>
|
||||
EXTERNAL_IP=<Your external IP, if applicable>
|
||||
|
||||
@ -7,7 +7,8 @@
|
||||
set -e
|
||||
|
||||
CONTAINER_NAME="bitcoin-signet-instance"
|
||||
DATADIR="/root/.bitcoin"
|
||||
# Use BITCOIN_DIR from container so we talk to the same datadir as bitcoind
|
||||
DATADIR=""
|
||||
WALLET_NAME="custom_signet"
|
||||
BACKUP_DIR="./backups"
|
||||
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
|
||||
@ -27,6 +28,8 @@ if ! sudo docker ps --format "{{.Names}}" | grep -q "^${CONTAINER_NAME}$"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DATADIR=$(sudo docker exec "$CONTAINER_NAME" printenv BITCOIN_DIR 2>/dev/null || echo "/root/.bitcoin")
|
||||
|
||||
# Start backup file with header
|
||||
cat > "${BACKUP_FILE}" << EOF
|
||||
# Bitcoin Signet Backup
|
||||
|
||||
86
export-mining-wallet.sh
Executable file
86
export-mining-wallet.sh
Executable file
@ -0,0 +1,86 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Export the mining wallet (custom_signet) in a re-importable format.
|
||||
# Contains: PRIVKEY, SIGNETCHALLENGE, wallet name, descriptors with private keys,
|
||||
# and chain info — everything needed to recover funds on this Signet chain.
|
||||
#
|
||||
# Output: backups/mining-wallet-export-YYYYMMDD-HHMMSS.json
|
||||
# Symlink: backups/mining-wallet-export-latest.json (ready to download)
|
||||
#
|
||||
# Re-import: ./import-mining-wallet.sh backups/mining-wallet-export-latest.json
|
||||
#
|
||||
# Author: 4NK Team
|
||||
# Date: 2026-02-02
|
||||
|
||||
set -e
|
||||
|
||||
CONTAINER_NAME="bitcoin-signet-instance"
|
||||
WALLET_NAME="custom_signet"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
BACKUP_DIR="${1:-$SCRIPT_DIR/backups}"
|
||||
mkdir -p "$BACKUP_DIR"
|
||||
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
||||
OUTPUT_FILE="$BACKUP_DIR/mining-wallet-export-$TIMESTAMP.json"
|
||||
|
||||
if ! docker ps --format "{{.Names}}" | grep -q "^${CONTAINER_NAME}$"; then
|
||||
echo "Error: Container ${CONTAINER_NAME} is not running"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DATADIR=$(docker exec "$CONTAINER_NAME" printenv BITCOIN_DIR 2>/dev/null || echo "/root/.bitcoin")
|
||||
|
||||
echo "=== Export mining wallet (re-importable) ==="
|
||||
echo "Container: $CONTAINER_NAME"
|
||||
echo "Wallet: $WALLET_NAME"
|
||||
echo "Output: $OUTPUT_FILE"
|
||||
echo ""
|
||||
|
||||
PRIVKEY=$(sudo docker exec "$CONTAINER_NAME" cat "$DATADIR/PRIVKEY.txt" 2>/dev/null || echo "")
|
||||
SIGNETCHALLENGE=$(sudo docker exec "$CONTAINER_NAME" cat "$DATADIR/SIGNETCHALLENGE.txt" 2>/dev/null || echo "")
|
||||
BLOCKCHAIN_INFO=$(sudo docker exec "$CONTAINER_NAME" bitcoin-cli -datadir="$DATADIR" getblockchaininfo 2>/dev/null || echo "{}")
|
||||
DESCRIPTORS_RAW=$(sudo docker exec "$CONTAINER_NAME" bitcoin-cli -datadir="$DATADIR" -rpcwallet="$WALLET_NAME" listdescriptors true 2>/dev/null || echo "{\"descriptors\":[]}")
|
||||
|
||||
# Build importdescriptors-compatible array: each item {desc, timestamp, internal}
|
||||
DESCRIPTORS_IMPORT=$(echo "$DESCRIPTORS_RAW" | jq -c '
|
||||
.descriptors
|
||||
| map(select(.desc != null))
|
||||
| map({
|
||||
desc: .desc,
|
||||
timestamp: (if .timestamp != null then .timestamp else 0 end),
|
||||
internal: (if .internal == true then true elif .internal == false then false else false end)
|
||||
})
|
||||
')
|
||||
|
||||
# Single JSON document for re-import and recovery on this chain
|
||||
jq -n \
|
||||
--arg chain "signet" \
|
||||
--arg signet_challenge "${SIGNETCHALLENGE}" \
|
||||
--arg privkey "${PRIVKEY}" \
|
||||
--arg wallet_name "${WALLET_NAME}" \
|
||||
--argjson descriptors "${DESCRIPTORS_IMPORT}" \
|
||||
--argjson blockchain_info "${BLOCKCHAIN_INFO}" \
|
||||
'{
|
||||
export_version: "1.0",
|
||||
export_date: (now | strftime("%Y-%m-%dT%H:%M:%SZ")),
|
||||
chain: $chain,
|
||||
signet_challenge: $signet_challenge,
|
||||
privkey: $privkey,
|
||||
wallet_name: $wallet_name,
|
||||
descriptors: $descriptors,
|
||||
blockchain_at_export: $blockchain_info,
|
||||
reimport: "Run: ./import-mining-wallet.sh <this-file.json> on a node with the same SIGNETCHALLENGE"
|
||||
}' > "$OUTPUT_FILE"
|
||||
|
||||
ln -sf "$(basename "$OUTPUT_FILE")" "$BACKUP_DIR/mining-wallet-export-latest.json"
|
||||
|
||||
echo "Export saved: $OUTPUT_FILE"
|
||||
echo "Latest symlink: $BACKUP_DIR/mining-wallet-export-latest.json"
|
||||
echo "Size: $(du -h "$OUTPUT_FILE" | cut -f1)"
|
||||
echo ""
|
||||
echo "To re-import and recover funds on this chain:"
|
||||
echo " ./import-mining-wallet.sh $OUTPUT_FILE"
|
||||
echo " # or: ./import-mining-wallet.sh $BACKUP_DIR/mining-wallet-export-latest.json"
|
||||
echo ""
|
||||
echo "WARNING: This file contains private keys. Keep it secure and encrypted."
|
||||
echo ""
|
||||
73
features/backup-to-git-daily-cron.md
Normal file
73
features/backup-to-git-daily-cron.md
Normal file
@ -0,0 +1,73 @@
|
||||
# Cron quotidien : export backups Signet et mining wallet vers git
|
||||
|
||||
**Auteur** : Équipe 4NK
|
||||
**Date** : 2026-02-04
|
||||
|
||||
## Objectif
|
||||
|
||||
Exporter quotidiennement les sauvegardes du Signet (chaîne complète) et du wallet de minage vers le dépôt git https://git.4nkweb.com/4nk/backup, en ne conservant que 2 versions de chaque type.
|
||||
|
||||
## Impacts
|
||||
|
||||
- Sauvegardes Signet et mining wallet disponibles sur git pour récupération à distance.
|
||||
- Rotation automatique : au plus 2 versions de la chaîne complète (signet-datadir) et 2 versions du wallet.
|
||||
- Log : `data/backup-to-git.log`.
|
||||
|
||||
## Solution implémentée
|
||||
|
||||
### Script `data/backup-to-git-cron.sh`
|
||||
|
||||
1. Exécute `save-signet-datadir-backup.sh` → crée `backups/signet-datadir-YYYYMMDD-HHMMSS.tar.gz`
|
||||
2. Exécute `export-mining-wallet.sh` → crée `backups/mining-wallet-export-YYYYMMDD-HHMMSS.json`
|
||||
3. Clone ou pull le dépôt https://git.4nkweb.com/4nk/backup dans `$HOME/.4nk-backup-git` (ou `$BACKUP_GIT_WORKSPACE`)
|
||||
4. Copie les derniers fichiers dans `signet-datadir/` et `mining-wallet/`
|
||||
5. Garde seulement 2 versions de signet-datadir et 2 de mining-wallet (supprime les plus anciennes)
|
||||
6. Commit et push vers le dépôt
|
||||
|
||||
### Structure dans le dépôt backup
|
||||
|
||||
```
|
||||
4nk/backup/
|
||||
├── signet-datadir/
|
||||
│ ├── signet-datadir-YYYYMMDD-HHMMSS.tar.gz (max 2)
|
||||
│ └── signet-datadir-YYYYMMDD-HHMMSS.tar.gz
|
||||
└── mining-wallet/
|
||||
├── mining-wallet-export-YYYYMMDD-HHMMSS.json (max 2)
|
||||
└── mining-wallet-export-YYYYMMDD-HHMMSS.json
|
||||
```
|
||||
|
||||
### Prérequis
|
||||
|
||||
- Docker (conteneur `bitcoin-signet-instance` en cours d’exécution pour les scripts de backup)
|
||||
- Git configuré avec accès push sans mot de passe (clés SSH vers git@git.4nkweb.com:4nk/backup.git)
|
||||
- Dépôt backup créé au préalable sur https://git.4nkweb.com/4nk/backup (peut être vide)
|
||||
- **Sécurité** : le dépôt backup contient des clés privées (mining wallet). Le rendre privé sur git.4nkweb.com.
|
||||
|
||||
## Modifications
|
||||
|
||||
**Fichiers créés :**
|
||||
|
||||
- `data/backup-to-git-cron.sh` : script d’export quotidien
|
||||
- `features/backup-to-git-daily-cron.md` : cette documentation
|
||||
|
||||
## Modalités de déploiement
|
||||
|
||||
1. Créer le dépôt `4nk/backup` sur https://git.4nkweb.com/4nk/backup si nécessaire.
|
||||
2. Configurer l’accès git (credential helper, clé, token) pour push sans interaction.
|
||||
3. Rendre le script exécutable : `chmod +x data/backup-to-git-cron.sh`
|
||||
4. Ajouter une entrée cron quotidienne (ex. 5h00, après restart-services si applicable) :
|
||||
```text
|
||||
0 5 * * * /home/ncantu/Bureau/code/bitcoin/data/backup-to-git-cron.sh
|
||||
```
|
||||
5. Tester manuellement : `./data/backup-to-git-cron.sh`
|
||||
|
||||
### Variables d'environnement (optionnel)
|
||||
|
||||
- `BACKUP_GIT_WORKSPACE` : chemin du clone local du dépôt backup (défaut : `$HOME/.4nk-backup-git`)
|
||||
|
||||
## Modalités d’analyse
|
||||
|
||||
- Consulter `data/backup-to-git.log` pour les exécutions et erreurs.
|
||||
- Vérifier le dépôt https://git.4nkweb.com/4nk/backup pour les commits.
|
||||
- En cas d’erreur `git clone` : créer le dépôt sur git.4nkweb.com.
|
||||
- En cas d’erreur `git push` : vérifier credential helper / accès réseau.
|
||||
60
features/services-ecoute-ipv4-proxy.md
Normal file
60
features/services-ecoute-ipv4-proxy.md
Normal file
@ -0,0 +1,60 @@
|
||||
# Écoute des services : IPv4 uniquement, flux reçus du proxy 192.168.1.100
|
||||
|
||||
**Auteur** : Équipe 4NK
|
||||
**Date** : 2026-02-02
|
||||
**Version** : 1.0
|
||||
|
||||
## Objectif
|
||||
|
||||
Les services backend (APIs, dashboard, etc.) doivent :
|
||||
|
||||
1. **Écouter en IPv4 uniquement** : aucune écoute sur IPv6 (`[::]`).
|
||||
2. **Accepter les flux reçus de la machine proxy uniquement** : les connexions entrantes doivent provenir du proxy (192.168.1.100).
|
||||
|
||||
## Impacts
|
||||
|
||||
- **Sécurité** : réduction de la surface d’exposition (pas d’IPv6, pas d’accès direct depuis d’autres machines que le proxy).
|
||||
- **Cohérence** : tous les accès passent par le proxy (HTTPS, certificats, routage).
|
||||
- **Environnements concernés** : machine bitcoin (192.168.1.105), machine prod (192.168.1.103), et tout backend recevant du trafic du proxy.
|
||||
|
||||
## Règles d’écoute
|
||||
|
||||
### 1. Bind sur l’adresse IPv4 de la machine
|
||||
|
||||
Chaque service écoute sur l’adresse LAN IPv4 de la machine où il tourne, et non sur `0.0.0.0` :
|
||||
|
||||
- **Machine bitcoin (192.168.1.105)** : `DASHBOARD_HOST=192.168.1.105`, `API_HOST=192.168.1.105` (dashboard, api-anchorage, etc.).
|
||||
- **Machine prod (192.168.1.103)** : `FAUCET_API_HOST=192.168.1.103`, `WATERMARK_API_HOST=192.168.1.103`, `CLAMAV_API_HOST=192.168.1.103`, etc.
|
||||
|
||||
Cela garantit une écoute IPv4 uniquement (pas d’écoute sur `[::]`).
|
||||
|
||||
### 2. Restriction à la source proxy (192.168.1.100)
|
||||
|
||||
Si la variable d’environnement `ALLOWED_SOURCE_IP=192.168.1.100` est définie, le service rejette toute requête dont l’adresse source (après normalisation IPv6-mapped → IPv4) n’est pas 192.168.1.100.
|
||||
|
||||
- **Middleware** : chaque service applicatif (Node/Express) peut utiliser un middleware qui lit `req.socket.remoteAddress`, normalise (ex. `::ffff:192.168.1.100` → `192.168.1.100`) et compare à `ALLOWED_SOURCE_IP`. Si différent, réponse 403.
|
||||
- **Alternative** : firewall sur chaque machine (autoriser uniquement 192.168.1.100 sur les ports des services). À documenter côté infra.
|
||||
|
||||
## Modifications
|
||||
|
||||
- **Fichiers de service systemd** : `DASHBOARD_HOST`, `API_HOST`, `FAUCET_API_HOST`, `WATERMARK_API_HOST`, `CLAMAV_API_HOST` définis à l’IP LAN de la machine ; `ALLOWED_SOURCE_IP=192.168.1.100` ajouté.
|
||||
- **Code** : middleware optionnel (activé si `ALLOWED_SOURCE_IP` est défini) dans signet-dashboard, api-anchorage, api-faucet, api-filigrane, api-clamav pour rejeter les requêtes dont la source n’est pas le proxy.
|
||||
- **Documentation** : `docs/ENVIRONMENT.md`, `docs/DOMAINS_AND_PORTS.md` mis à jour pour décrire cette règle et les variables.
|
||||
|
||||
## Nginx / Mempool
|
||||
|
||||
- **Mempool** : si un Nginx local expose le frontend (ex. port 3015), les directives `listen` doivent être en IPv4 uniquement (ex. `listen 192.168.1.105:3015;` ou `listen 0.0.0.0:3015;` sans `listen [::]:3015`).
|
||||
- **Proxy (192.168.1.100)** : inchangé ; c’est lui qui envoie les flux vers les backends.
|
||||
|
||||
## Modalités de déploiement
|
||||
|
||||
1. Mettre à jour les fichiers `.service` avec les bonnes valeurs `*_HOST` et `ALLOWED_SOURCE_IP=192.168.1.100`.
|
||||
2. Redémarrer les services après déploiement des unités systemd.
|
||||
3. Vérifier que le proxy peut joindre les backends (192.168.1.105 et 192.168.1.103) sur les ports concernés.
|
||||
4. Ne pas définir `ALLOWED_SOURCE_IP` en dev local si les requêtes ne viennent pas du proxy.
|
||||
|
||||
## Modalités d’analyse
|
||||
|
||||
- Vérifier qu’aucun service n’écoute sur `[::]` : `ss -tlnp` / `netstat -tlnp` sur chaque machine.
|
||||
- Vérifier que les services écoutent sur l’IP LAN attendue : `ss -tlnp | grep <port>`.
|
||||
- Tester depuis le proxy : `curl http://192.168.1.105:3020/...` et depuis une autre machine : doit être refusé si firewall ou middleware est actif.
|
||||
91
fix-dashboard-anchor-chain.sh
Executable file
91
fix-dashboard-anchor-chain.sh
Executable file
@ -0,0 +1,91 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Fix dashboard wrong chain and anchor API "Insufficient Balance".
|
||||
# Run on the bitcoin machine (192.168.1.105) from the project root.
|
||||
#
|
||||
# Usage:
|
||||
# ./fix-dashboard-anchor-chain.sh
|
||||
# Verify, restart Dashboard and Anchor API, run alignment check.
|
||||
# ./fix-dashboard-anchor-chain.sh backups/signet-datadir-YYYYMMDD-HHMMSS.tar.gz
|
||||
# Restore chain from backup, then verify and restart services.
|
||||
#
|
||||
# Author: 4NK Team
|
||||
# Date: 2026-02-02
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
if [[ ! -f .env ]]; then
|
||||
echo "Error: .env not found. Run from project root: $SCRIPT_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BACKUP_PATH="${1:-}"
|
||||
|
||||
echo "=== Fix Dashboard / Anchor API chain and balance ==="
|
||||
echo "Project root: $SCRIPT_DIR"
|
||||
echo ""
|
||||
|
||||
echo ">>> Testing RPC config (same node as Mempool: 127.0.0.1:38332)..."
|
||||
if ! ./test-mempool-rpc-config.sh 127.0.0.1 38332; then
|
||||
echo "Error: Node at 127.0.0.1:38332 does not have ~11535 blocks."
|
||||
echo "Run this script on the machine where Mempool runs (bitcoin 192.168.1.105)."
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
|
||||
if [[ -n "$BACKUP_PATH" ]]; then
|
||||
if [[ ! -f "$BACKUP_PATH" ]]; then
|
||||
echo "Error: Backup file not found: $BACKUP_PATH"
|
||||
exit 1
|
||||
fi
|
||||
echo ">>> Restoring chain from backup: $BACKUP_PATH"
|
||||
./restore-signet-from-backup.sh "$BACKUP_PATH"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
echo ">>> Checking Bitcoin Signet container..."
|
||||
CONTAINER_NAME="bitcoin-signet-instance"
|
||||
if ! docker ps --format "{{.Names}}" | grep -q "^${CONTAINER_NAME}$"; then
|
||||
echo "Error: Container $CONTAINER_NAME is not running."
|
||||
echo "Start it with the persistent volume (see docs/MAINTENANCE.md)."
|
||||
exit 1
|
||||
fi
|
||||
echo "Container is running."
|
||||
echo ""
|
||||
|
||||
echo ">>> Restarting signet-dashboard and anchorage-api..."
|
||||
if systemctl is-active --quiet signet-dashboard 2>/dev/null; then
|
||||
sudo systemctl restart signet-dashboard
|
||||
echo "signet-dashboard restarted."
|
||||
else
|
||||
echo "signet-dashboard not managed by systemctl (skip restart)."
|
||||
fi
|
||||
if systemctl is-active --quiet anchorage-api 2>/dev/null; then
|
||||
sudo systemctl restart anchorage-api
|
||||
echo "anchorage-api restarted."
|
||||
else
|
||||
echo "anchorage-api not managed by systemctl (skip restart)."
|
||||
fi
|
||||
echo ""
|
||||
|
||||
echo ">>> Waiting 5s for services to start..."
|
||||
sleep 5
|
||||
echo ""
|
||||
|
||||
echo ">>> Running chain alignment check..."
|
||||
./verify-chain-alignment.sh
|
||||
echo ""
|
||||
|
||||
echo ">>> Node wallet info (check balance):"
|
||||
BITCOIN_DATADIR=$(docker exec "$CONTAINER_NAME" printenv BITCOIN_DIR 2>/dev/null || echo "/root/.bitcoin")
|
||||
sudo docker exec "$CONTAINER_NAME" bitcoin-cli -datadir="$BITCOIN_DATADIR" getwalletinfo 2>/dev/null | grep -E '"walletname"|"balance"|"immature_balance"' || echo "(could not get walletinfo)"
|
||||
echo ""
|
||||
|
||||
echo "Done. If the dashboard still shows wrong chain or anchor API returns Insufficient Balance:"
|
||||
echo " - Ensure Dashboard and Anchor API run on the bitcoin machine (192.168.1.105)."
|
||||
echo " - If chain was lost, restore from backup: ./fix-dashboard-anchor-chain.sh backups/signet-datadir-YYYYMMDD-HHMMSS.tar.gz"
|
||||
echo " - See fixKnowledge/dashboard-anchor-wrong-chain-insufficient-balance.md"
|
||||
echo ""
|
||||
82
fixKnowledge/api-anchorage-health-503-remediation.md
Normal file
82
fixKnowledge/api-anchorage-health-503-remediation.md
Normal file
@ -0,0 +1,82 @@
|
||||
# Correction: Health check anchor-api HTTP 503 – causes et remédiation
|
||||
|
||||
**Date:** 2026-01-28
|
||||
**Auteur:** Équipe 4NK
|
||||
|
||||
## Problème
|
||||
|
||||
Le health check de l’API d’ancrage renvoie HTTP 503, avec des messages du type « Health check a échoué (HTTP 503) » ou « L’API d’ancrage n’est pas accessible », sans indiquer la cause précise.
|
||||
|
||||
**Machine concernée :** 192.168.1.105 (bitcoin)
|
||||
**Domaine externe :** https://anchorage.certificator.4nkweb.com
|
||||
**Service :** anchorage-api (port 3010)
|
||||
|
||||
### Impact
|
||||
|
||||
- Monitoring ou dashboard marquent l’API comme indisponible sans distinguer « processus arrêté » et « pas prêt » (Bitcoin déconnecté, UTXOs verrouillés).
|
||||
- L’utilisateur ne voit pas la raison du 503 (Bitcoin, mutex, UTXOs stale) pour agir.
|
||||
|
||||
## Root cause
|
||||
|
||||
1. **Causes du 503 côté api-anchorage** (`src/routes/health.js`) :
|
||||
- **GET /health** : 503 si Bitcoin RPC non connecté ou si `checkConnection()` lève.
|
||||
- **GET /health/detailed** : 503 si Bitcoin non connecté, ou UTXOs verrouillés depuis > 10 min (`stale_locks > 0`), ou plus de 10 UTXOs verrouillés.
|
||||
|
||||
2. **Causes côté consommateur** :
|
||||
- Le dashboard proxy renvoyait 200 avec un body réduit au lieu de transmettre le 503 et le body complet → le client ne voyait pas la raison.
|
||||
- Aucun endpoint de liveness (processus vivant) → impossible de distinguer « service down » et « service up mais not ready ».
|
||||
|
||||
## Correctifs
|
||||
|
||||
### 1. api-anchorage : endpoint de liveness
|
||||
|
||||
- **GET /health/live** : retourne toujours 200 tant que le processus tourne (sans vérifier Bitcoin ni mutex).
|
||||
- Usage : monitoring / load-balancer pour ne pas considérer le service comme mort quand il renvoie 503 (readiness).
|
||||
- Exclusion de l’auth API Key pour `/health/live` dans `server.js`.
|
||||
|
||||
### 2. signet-dashboard : propagation du 503 et du body
|
||||
|
||||
- Option **preserveStatus** dans `makeHttpRequest` : retourne `{ statusCode, body }` pour préserver le code HTTP et le body JSON.
|
||||
- Route **GET /api/anchor/health/detailed** : appelle l’API d’ancrage avec `preserveStatus: true`, puis répond avec `res.status(result.statusCode).json(result.body)`.
|
||||
- Effet : en cas de 503, le client reçoit 503 et le détail (bitcoin.connected, mutex, utxos, stale_locks).
|
||||
|
||||
### 3. signet-dashboard (hash-list.html) : affichage de la raison
|
||||
|
||||
- Si la réponse est 503 et que le body n’a pas la structure health (mutex, utxos, bitcoin), affichage d’un message d’erreur incluant `health.error` ou `health.message` et le code 503.
|
||||
- Si la réponse est 503 mais que le body contient le health complet, affichage du panneau health normal avec mention « 503 - voir détails ci-dessous » pour l’état général.
|
||||
- En cas d’exception (réseau, parse), affichage de « L’API d’ancrage n’est pas accessible » avec le message d’erreur.
|
||||
|
||||
## Modifications
|
||||
|
||||
**Motivations :**
|
||||
- Rendre le 503 explicable (Bitcoin, mutex, UTXOs) et permettre une remédiation ciblée.
|
||||
- Séparer liveness (processus up) et readiness (prêt à ancrer).
|
||||
|
||||
**Root causes :**
|
||||
- 503 défini par l’API sans toujours être propagé avec son body ; pas de liveness.
|
||||
|
||||
**Correctifs :**
|
||||
- GET /health/live dans api-anchorage ; propagation 503 + body dans le dashboard ; affichage de la raison dans hash-list.
|
||||
|
||||
**Pages affectées :**
|
||||
- `api-anchorage/src/routes/health.js` (GET /health/live)
|
||||
- `api-anchorage/src/server.js` (exclusion auth /health/live)
|
||||
- `signet-dashboard/src/server.js` (preserveStatus, route health/detailed)
|
||||
- `signet-dashboard/public/hash-list.html` (affichage erreur 503 et raison)
|
||||
|
||||
## Modalités de remédiation en production
|
||||
|
||||
Quand le health check renvoie 503 :
|
||||
|
||||
1. **Vérifier liveness** : `curl -s https://anchorage.certificator.4nkweb.com/health/live` → 200 si le processus tourne.
|
||||
2. **Lire la cause** : `curl -s https://anchorage.certificator.4nkweb.com/health/detailed` (ou via le dashboard) et regarder :
|
||||
- `bitcoin.connected === false` → vérifier Bitcoin Core (RPC) sur 192.168.1.105.
|
||||
- `utxos.stale_locks > 0` ou `utxos.locked > 10` → déverrouiller les UTXOs (voir ci-dessous).
|
||||
3. **Service arrêté** : sur la machine 192.168.1.105, `systemctl status anchorage-api` puis `sudo systemctl restart anchorage-api` si besoin.
|
||||
4. **UTXOs verrouillés** : sur la machine hébergeant api-anchorage, exécuter `node unlock-utxos.mjs` dans le répertoire api-anchorage, ou utiliser le bouton « Déverrouiller les UTXOs » depuis le dashboard (hash-list) si l’API est de nouveau joignable.
|
||||
|
||||
## Modalités d’analyse
|
||||
|
||||
- Consulter les logs du service : `journalctl -u anchorage-api -n 100`.
|
||||
- Vérifier le RPC Bitcoin : `bitcoin-cli -rpcconnect=... getblockchaininfo`.
|
||||
- Appeler GET /health/detailed et interpréter `ok`, `bitcoin.connected`, `utxos.locked`, `utxos.stale_locks`.
|
||||
@ -0,0 +1,124 @@
|
||||
# Dashboard mauvaise chaîne / API d'ancrage "Insufficient Balance"
|
||||
|
||||
**Auteur** : Équipe 4NK
|
||||
**Date** : 2026-02-02
|
||||
**Version** : 1.0
|
||||
|
||||
## Symptômes
|
||||
|
||||
- **Dashboard** (https://dashboard.certificator.4nkweb.com/) : affiche une mauvaise chaîne (hauteur 0, "-", ou valeurs incohérentes) au lieu d’environ 11535 blocs.
|
||||
- **Clients de l’API d’ancrage** : reçoivent `{"error":"Insufficient Balance","message":"Insufficient balance. Required: 0.00001 BTC, Available: 0 BTC"}` et l’API d’ancrage ne fonctionne pas correctement.
|
||||
|
||||
## Impacts
|
||||
|
||||
- Les utilisateurs ne voient pas l’état réel de la blockchain.
|
||||
- L’ancrage de documents échoue (solde 0 côté nœud utilisé par l’API).
|
||||
|
||||
## Cause
|
||||
|
||||
Une seule cause racine couvre les deux symptômes : **le nœud Bitcoin Signet auquel se connectent le Dashboard et l’API d’ancrage n’a pas la bonne chaîne ou n’a pas de solde**.
|
||||
|
||||
Causes possibles :
|
||||
|
||||
1. **Chaîne perdue** : le conteneur `bitcoin-signet-instance` a été recréé **sans volume persistant** (`-v signet-bitcoin-data:/root/.bitcoin`). Le nœud repart sur une nouvelle chaîne (hauteur 0 ou très basse), sans historique de mining → solde 0. Voir [signet-chain-lost-volume-persistent.md](./signet-chain-lost-volume-persistent.md).
|
||||
2. **Mauvais déploiement** : le Dashboard et/ou l’API d’ancrage tournent sur une **autre machine** (ex. prod 192.168.1.103). Avec `BITCOIN_RPC_HOST=127.0.0.1`, ils appellent alors le RPC de cette machine, où il n’y a pas de nœud Signet (ou un nœud vide) → hauteur 0 ou erreur, solde 0.
|
||||
3. **Wallet par défaut** : le nœud a la bonne chaîne mais le **wallet par défaut** utilisé par l’API n’est pas celui qui reçoit les récompenses de minage (`custom_signet`) → `getBalance()` retourne 0.
|
||||
|
||||
## Correctifs
|
||||
|
||||
### 0. Script de correction (machine bitcoin)
|
||||
|
||||
Sur la machine bitcoin (192.168.1.105), à la racine du projet :
|
||||
|
||||
```bash
|
||||
cd /home/ncantu/Bureau/code/bitcoin
|
||||
|
||||
# Vérifier, redémarrer Dashboard et API d’ancrage, lancer la vérification d’alignement
|
||||
./fix-dashboard-anchor-chain.sh
|
||||
|
||||
# Si la chaîne a été perdue, restaurer depuis une sauvegarde puis redémarrer
|
||||
./fix-dashboard-anchor-chain.sh backups/signet-datadir-YYYYMMDD-HHMMSS.tar.gz
|
||||
```
|
||||
|
||||
Le script teste d'abord la config RPC (même nœud que Mempool) avec `./test-mempool-rpc-config.sh 127.0.0.1 38332`, puis redémarre `signet-dashboard` et `anchorage-api`, lance `verify-chain-alignment.sh`, et affiche le wallet du nœud (solde).
|
||||
|
||||
**Configuration unique (une seule chaîne pour Mempool, dashboard, APIs, miner) :** Un seul nœud : **bitcoin-signet-instance** sur **38332** (Mempool = host.docker.internal:38332, dashboard/APIs/miner = 127.0.0.1:38332). **Volume par défaut (chaîne complète) :** `update-signet.sh` utilise par défaut le volume contenant la chaîne Signet complète (~11530 blocs) s'il existe : volume Docker d'ID **4b5dca4d940b9f6e5db67b460f40f230a5ef1195a3769e5f91fa02be6edde649** (`SIGNET_VOLUME_FULL_CHAIN` dans le script). Sinon, volume nommé **signet-bitcoin-data**. **Sauvegarde prête à télécharger :** `backups/signet-datadir-latest.tar.gz` (symlink vers la dernière archive créée par `./save-signet-datadir-backup.sh`). **Alignement :** Même machine : `BITCOIN_RPC_HOST=127.0.0.1`, `BITCOIN_RPC_PORT=38332`. Le seul processus sur 38332 doit être le conteneur **bitcoin-signet-instance** (Mempool utilise `host.docker.internal:38332` = ce même conteneur). Vérifier qu’il ne s’agit pas d’un autre Docker : `ss -tlnp | grep 38332` et `docker ps --format '{{.Names}}' | grep bitcoin-signet-instance`. Le miner utilise `BITCOIN_RPC_HOST` / `BITCOIN_RPC_PORT` en env (défaut 127.0.0.1:38332). Tester : `./test-mempool-rpc-config.sh 127.0.0.1 38332`. Vérifier le dashboard : `./verify-dashboard-signet.sh`.
|
||||
|
||||
### 1. Vérifier où tournent le Dashboard et l’API d’ancrage
|
||||
|
||||
- **Dashboard** : doit être sur la **machine bitcoin (192.168.1.105)**. Vérifier le service `signet-dashboard` sur cette machine.
|
||||
- **API d’ancrage** (`anchorage.certificator.4nkweb.com`) : doit être sur la **machine bitcoin (192.168.1.105)**. Vérifier le service `anchorage-api` sur cette machine.
|
||||
|
||||
Les deux doivent utiliser `BITCOIN_RPC_HOST=127.0.0.1` et `BITCOIN_RPC_PORT=38332` pour parler au nœud local (conteneur sur la même machine).
|
||||
|
||||
### 2. Source de vérité : Mempool et utilisateur ncantu
|
||||
|
||||
**Mempool** (machine bitcoin 192.168.1.105, `/srv/4NK/mempool.4nkweb.com`) se connecte au nœud Signet du même hôte (`host.docker.internal:38332`). Si Mempool affiche la bonne chaîne (~11535 blocs), le nœud utilisé par Mempool sur cette machine a encore la chaîne complète.
|
||||
|
||||
**Où chercher la chaîne / les sauvegardes (utilisateur ncantu, machine bitcoin 105) :**
|
||||
|
||||
- **Sauvegarde prête à télécharger** : `backups/signet-datadir-latest.tar.gz` (dernière archive datadir, ~11530 blocs). Créée par `./save-signet-datadir-backup.sh` ; le script met à jour le symlink à chaque sauvegarde.
|
||||
- **Sauvegardes horodatées** : `backups/signet-datadir-YYYYMMDD-HHMMSS.tar.gz`.
|
||||
- **Volume Docker « chaîne complète »** : volume d'ID `4b5dca4d940b9f6e5db67b460f40f230a5ef1195a3769e5f91fa02be6edde649` ; `update-signet.sh` l'utilise par défaut s'il existe (voir docs/MAINTENANCE.md).
|
||||
- **Volume nommé** : après restauration via `restore-signet-from-backup.sh`, le conteneur utilise `signet-bitcoin-data`. Vérifier avec `docker inspect bitcoin-signet-instance` (Mounts).
|
||||
- **Datadir dans le conteneur** : si le conteneur sur 105 n’a jamais été recréé sans volume, les blocs sont dans le conteneur ; faire une sauvegarde avec `save-signet-datadir-backup.sh` ou `docker exec bitcoin-signet-instance tar czf /tmp/bitcoin-backup.tar.gz /root/.bitcoin/` puis `docker cp` vers l’hôte.
|
||||
|
||||
Pour corriger la machine dont le nœud n’a que quelques blocs (ex. 6) : soit **restaurer** depuis une archive issue de 105 ou de `backups/` sous ncantu, soit **pointer** le Dashboard / l’API d’ancrage vers le RPC du nœud sur 105 (ex. `BITCOIN_RPC_HOST=192.168.1.105`) si l’architecture le permet.
|
||||
|
||||
### 3. Restaurer la chaîne si elle a été perdue
|
||||
|
||||
Si le nœud a une hauteur très basse (ex. 0, 6) ou pas de volume persistant :
|
||||
|
||||
1. Sur la machine qui a encore la chaîne ~11535 (ex. machine bitcoin 105, ou là où Mempool affiche la bonne chaîne) : exécuter `./save-signet-datadir-backup.sh`, ou récupérer une archive depuis `/home/ncantu/Bureau/code/bitcoin/backups/` (utilisateur ncantu).
|
||||
2. Copier l’archive sur la machine à corriger si besoin.
|
||||
3. Sur la machine à corriger : exécuter `./restore-signet-from-backup.sh backups/signet-datadir-YYYYMMDD-HHMMSS.tar.gz`.
|
||||
4. Redémarrer le conteneur si nécessaire et vérifier : `sudo docker exec bitcoin-signet-instance bitcoin-cli -datadir=$(docker exec bitcoin-signet-instance printenv BITCOIN_DIR 2>/dev/null || echo /root/.bitcoin) getblockchaininfo`.
|
||||
|
||||
Voir [signet-chain-lost-volume-persistent.md](./signet-chain-lost-volume-persistent.md) et [MAINTENANCE.md](../docs/MAINTENANCE.md).
|
||||
|
||||
### 4. Vérifier l’alignement chaîne / solde
|
||||
|
||||
Sur la machine bitcoin :
|
||||
|
||||
```bash
|
||||
./verify-chain-alignment.sh
|
||||
sudo docker exec bitcoin-signet-instance bitcoin-cli -datadir=/root/.bitcoin getblockchaininfo | grep -E '"chain"|"blocks"'
|
||||
sudo docker exec bitcoin-signet-instance bitcoin-cli -datadir=/root/.bitcoin getwalletinfo
|
||||
```
|
||||
|
||||
- `chain` doit être `"signet"`, `blocks` proche de 11535.
|
||||
- Le wallet utilisé par défaut (ex. `custom_signet`) doit avoir un solde > 0 après mining.
|
||||
|
||||
### 5. Si la chaîne est bonne mais le solde reste 0
|
||||
|
||||
Vérifier que le wallet contenant les récompenses de minage est bien celui utilisé par l’API :
|
||||
|
||||
- Le miner utilise en général le wallet `custom_signet`.
|
||||
- L’API d’ancrage appelle `getBalance()` sans nom de wallet → utilise le **wallet par défaut** du nœud.
|
||||
- Si le nœud a plusieurs wallets, s’assurer que le wallet par défaut est celui qui a du solde (ou charger `custom_signet` au démarrage du nœud comme wallet par défaut).
|
||||
|
||||
## Modalités de déploiement
|
||||
|
||||
- Sur la machine bitcoin : exécuter `./fix-dashboard-anchor-chain.sh` (avec chemin de backup si la chaîne a été perdue). Le script redémarre `signet-dashboard` et `anchorage-api`.
|
||||
- Recréer le conteneur Bitcoin via `./update-signet.sh` : le script utilise par défaut le volume chaîne complète (ID `4b5dca4d940b9f6e5db67b460f40f230a5ef1195a3769e5f91fa02be6edde649`) s'il existe, sinon `signet-bitcoin-data`. Ne pas recréer manuellement sans volume persistant.
|
||||
- Sauvegarde prête à télécharger : `backups/signet-datadir-latest.tar.gz` (créée par `./save-signet-datadir-backup.sh`).
|
||||
|
||||
## Modalités d’analyse
|
||||
|
||||
- Consulter les logs du Dashboard : `sudo journalctl -u signet-dashboard -f` (erreurs RPC).
|
||||
- Consulter les logs de l’API d’ancrage : `sudo journalctl -u anchorage-api -f` (erreurs "Insufficient balance", connexion RPC).
|
||||
- Vérifier la hauteur et le wallet sur le nœud : commandes ci-dessus.
|
||||
|
||||
## Pages affectées
|
||||
|
||||
- fixKnowledge/dashboard-anchor-wrong-chain-insufficient-balance.md (ce fichier)
|
||||
- fixKnowledge/signet-chain-lost-volume-persistent.md (volume chaîne complète, sauvegarde latest)
|
||||
- update-signet.sh (SIGNET_VOLUME_FULL_CHAIN, utilisation par défaut du volume chaîne complète)
|
||||
- save-signet-datadir-backup.sh (symlink signet-datadir-latest.tar.gz, tolérance tar exit 1)
|
||||
- backups/README.md (sauvegarde prête à télécharger, volume par défaut)
|
||||
- docs/MAINTENANCE.md (volume chaîne complète, sauvegarde latest)
|
||||
- test-mempool-rpc-config.sh (test de la config RPC utilisée par Mempool)
|
||||
- verify-dashboard-signet.sh (vérification que le dashboard affiche le signet custom)
|
||||
- fix-dashboard-anchor-chain.sh (script de correction sur la machine bitcoin)
|
||||
- signet-dashboard.service, anchorage-api.service, faucet-api.service (alignement RPC sur le même nœud que Mempool)
|
||||
- signet-dashboard/public/app.js (vérification response.ok et alerte chaîne anormale)
|
||||
@ -56,7 +56,7 @@ healthcheck:
|
||||
1. Modifier le fichier `docker-compose.signet.yml`
|
||||
2. Recréer le conteneur pour appliquer la nouvelle configuration:
|
||||
```bash
|
||||
cd /srv/4NK/mempool1.4nkweb.com
|
||||
cd /srv/4NK/mempool.4nkweb.com
|
||||
docker-compose -f docker-compose.signet.yml up -d --force-recreate api
|
||||
```
|
||||
3. Vérifier que le healthcheck passe à "healthy" après le délai de démarrage (40s)
|
||||
|
||||
@ -26,7 +26,7 @@
|
||||
## Evolutions
|
||||
|
||||
- Création d'un script de diagnostic (`mempool/diagnose-mempool.sh`) pour vérifier l'état des services localement
|
||||
- Création d'un script de diagnostic et correction (`mempool/fix-mempool-websocket.sh`) pour diagnostiquer et corriger les problèmes sur le serveur services
|
||||
- Création d'un script de diagnostic et correction (`mempool/fix-mempool-websocket.sh`) pour diagnostiquer et corriger les problèmes sur la machine bitcoin
|
||||
- Amélioration de la configuration nginx pour le WebSocket (ajout des headers nécessaires dans `mempool/nginx-mempool.conf`)
|
||||
|
||||
## Pages affectées
|
||||
@ -37,7 +37,7 @@
|
||||
|
||||
## Modalités de déploiement
|
||||
|
||||
1. **Diagnostic local** :
|
||||
1. **Diagnostic local** (depuis la racine du projet `/home/ncantu/Bureau/code/bitcoin/`) :
|
||||
```bash
|
||||
cd mempool
|
||||
./diagnose-mempool.sh
|
||||
|
||||
77
fixKnowledge/signet-chain-lost-volume-persistent.md
Normal file
77
fixKnowledge/signet-chain-lost-volume-persistent.md
Normal file
@ -0,0 +1,77 @@
|
||||
# Chaîne perdue après recréation du conteneur (hauteur 2 au lieu de ~11535)
|
||||
|
||||
**Date** : 2026-02-02
|
||||
**Auteur** : Équipe 4NK
|
||||
|
||||
## Problème
|
||||
|
||||
- Le dashboard affiche une **hauteur de bloc 2** alors que la chaîne attendue est autour de **11535**.
|
||||
- La chaîne visible n’est pas la bonne : le nœud est sur une **nouvelle chaîne** (genèse récente), pas sur la chaîne signet existante.
|
||||
|
||||
## Cause racine
|
||||
|
||||
- Le conteneur Docker `bitcoin-signet-instance` a été **recréé** (`docker stop` + `docker rm` + `docker run`) **sans montage persistant** du datadir Bitcoin (`/root/.bitcoin`).
|
||||
- Le Dockerfile déclare `VOLUME $BITCOIN_DIR` ; en l’absence de `-v` dans `docker run`, Docker utilise un **volume anonyme** lié au conteneur.
|
||||
- À la suppression du conteneur (`docker rm`), ce volume anonyme peut être supprimé (ou n’est plus rattaché), donc les **données de la chaîne (blocs, wallet, etc.) sont perdues**.
|
||||
- Le nouveau conteneur repart avec un datadir vide : install/signet crée une **nouvelle chaîne** (hauteur 0), puis le miner produit quelques blocs (ex. hauteur 2).
|
||||
|
||||
## Correctifs
|
||||
|
||||
1. **Documentation** : Toutes les commandes `docker run` du projet ont été mises à jour pour utiliser un **volume nommé** :
|
||||
```bash
|
||||
-v signet-bitcoin-data:/root/.bitcoin
|
||||
```
|
||||
Ainsi, à chaque recréation du conteneur, les données restent dans le volume `signet-bitcoin-data`.
|
||||
|
||||
2. **Persistance** : Utiliser **toujours** soit :
|
||||
- le volume « chaîne complète » (s'il existe) : `update-signet.sh` utilise par défaut le volume d'ID **4b5dca4d940b9f6e5db67b460f40f230a5ef1195a3769e5f91fa02be6edde649** (~11530 blocs), soit
|
||||
- un volume nommé : `-v signet-bitcoin-data:/root/.bitcoin`, soit
|
||||
- un montage host : `-v /chemin/hote/signet-data:/root/.bitcoin`
|
||||
pour tout démarrage ou recréation du conteneur Bitcoin Signet. Voir docs/MAINTENANCE.md.
|
||||
|
||||
## Récupérer la chaîne ~11535 (reprendre sur la chaîne précédente)
|
||||
|
||||
1. **Obtenir une sauvegarde complète du datadir** (blocs + chainstate + config) :
|
||||
- **Sauvegarde prête à télécharger** : `backups/signet-datadir-latest.tar.gz` (symlink vers la dernière archive créée par `./save-signet-datadir-backup.sh`).
|
||||
- **Sur la machine qui a encore la chaîne** : exécuter `./save-signet-datadir-backup.sh` pour créer `backups/signet-datadir-YYYYMMDD-HHMMSS.tar.gz` et mettre à jour le symlink `signet-datadir-latest.tar.gz`.
|
||||
- Ou utiliser une archive existante (ex. `bitcoin-backup-*.tar.gz` créée avec `docker exec ... tar czf /tmp/bitcoin-backup.tar.gz /root/.bitcoin`).
|
||||
|
||||
2. **Sur la machine où reprendre la chaîne** : placer l’archive dans le projet (ex. `backups/`) puis lancer :
|
||||
```bash
|
||||
./restore-signet-from-backup.sh backups/signet-datadir-YYYYMMDD-HHMMSS.tar.gz
|
||||
```
|
||||
Le script arrête le conteneur actuel, remplit le volume nommé `signet-bitcoin-data` avec les données restaurées, puis redémarre le conteneur avec ce volume.
|
||||
|
||||
3. **Si aucune sauvegarde n’existe** : la chaîne à 11535 n’est plus récupérable sur ce nœud. Il faut qu’un autre nœud (ex. machine bitcoin) ait encore cette chaîne et qu’on en tire une sauvegarde avec `save-signet-datadir-backup.sh`, puis qu’on restaure avec `restore-signet-from-backup.sh`.
|
||||
|
||||
## Modifications
|
||||
|
||||
- **docs/MAINTENANCE.md** : section « Persistance du datadir », et ajout de `-v signet-bitcoin-data:/root/.bitcoin` à toutes les commandes `docker run`.
|
||||
- **docs/README.md** : idem + note sur le volume.
|
||||
- **docs/INTERFACES.md** : idem.
|
||||
- **docs/INSTALLATION_NEW_NODE.md** : idem (premier démarrage et recréation).
|
||||
- **docs/TROUBLESHOOTING_MINING.md** : idem.
|
||||
|
||||
## Modalités d’analyse
|
||||
|
||||
- Vérifier la hauteur et la chaîne :
|
||||
`sudo docker exec bitcoin-signet-instance bitcoin-cli -datadir=/root/.bitcoin getblockchaininfo`
|
||||
- Vérifier les volumes Docker :
|
||||
`docker volume ls` (présence de `signet-bitcoin-data` si déjà utilisé).
|
||||
- En cas de hauteur très basse (0, 1, 2…) après une recréation récente du conteneur : confirmer si un volume persistant était utilisé.
|
||||
|
||||
## Scripts ajoutés
|
||||
|
||||
- **save-signet-datadir-backup.sh** : crée une archive complète du datadir depuis le conteneur en cours. À exécuter sur la machine qui a encore la chaîne (ex. machine bitcoin).
|
||||
- **restore-signet-from-backup.sh** : restaure une archive datadir dans le volume `signet-bitcoin-data` et redémarre le conteneur. Usage : `./restore-signet-from-backup.sh <chemin-vers-archive.tar.gz>`.
|
||||
|
||||
## Pages affectées
|
||||
|
||||
- docs/MAINTENANCE.md
|
||||
- docs/README.md
|
||||
- docs/INTERFACES.md
|
||||
- docs/INSTALLATION_NEW_NODE.md
|
||||
- docs/TROUBLESHOOTING_MINING.md
|
||||
- save-signet-datadir-backup.sh (nouveau)
|
||||
- restore-signet-from-backup.sh (nouveau)
|
||||
- fixKnowledge/signet-chain-lost-volume-persistent.md (ce fichier)
|
||||
99
import-mining-wallet.sh
Executable file
99
import-mining-wallet.sh
Executable file
@ -0,0 +1,99 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Re-import a mining wallet export (created by export-mining-wallet.sh).
|
||||
# Use on a node that has the same Signet chain (same SIGNETCHALLENGE) to recover funds.
|
||||
#
|
||||
# Usage: ./import-mining-wallet.sh <path-to-export.json> [container_name]
|
||||
# Example: ./import-mining-wallet.sh backups/mining-wallet-export-latest.json
|
||||
#
|
||||
# Prerequisite: bitcoin-signet-instance (or given container) running with the same chain.
|
||||
#
|
||||
# Author: 4NK Team
|
||||
# Date: 2026-02-02
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
EXPORT_PATH="${1:?Usage: $0 <path-to-export.json> [container_name]}"
|
||||
CONTAINER_NAME="${2:-bitcoin-signet-instance}"
|
||||
|
||||
if [[ ! -f "$EXPORT_PATH" ]]; then
|
||||
echo "Error: Export file not found: $EXPORT_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker ps --format "{{.Names}}" | grep -q "^${CONTAINER_NAME}$"; then
|
||||
echo "Error: Container ${CONTAINER_NAME} is not running"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
DATADIR=$(docker exec "$CONTAINER_NAME" printenv BITCOIN_DIR 2>/dev/null || echo "/root/.bitcoin")
|
||||
|
||||
echo "=== Import mining wallet ==="
|
||||
echo "Export file: $EXPORT_PATH"
|
||||
echo "Container: $CONTAINER_NAME"
|
||||
echo ""
|
||||
|
||||
SIGNET_EXPORT=$(jq -r '.signet_challenge' "$EXPORT_PATH")
|
||||
WALLET_NAME=$(jq -r '.wallet_name' "$EXPORT_PATH")
|
||||
PRIVKEY_EXPORT=$(jq -r '.privkey' "$EXPORT_PATH")
|
||||
|
||||
if [[ -z "$WALLET_NAME" ]] || [[ "$WALLET_NAME" == "null" ]]; then
|
||||
echo "Error: Invalid export file (missing wallet_name)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Verify we are on the same chain (SIGNETCHALLENGE must match)
|
||||
SIGNET_NODE=$(sudo docker exec "$CONTAINER_NAME" cat "$DATADIR/SIGNETCHALLENGE.txt" 2>/dev/null || echo "")
|
||||
if [[ -n "$SIGNET_NODE" ]] && [[ -n "$SIGNET_EXPORT" ]] && [[ "$SIGNET_EXPORT" != "null" ]]; then
|
||||
if [[ "$SIGNET_NODE" != "$SIGNET_EXPORT" ]]; then
|
||||
echo "Error: SIGNETCHALLENGE mismatch. Export is for another chain."
|
||||
echo " Node: ${SIGNET_NODE:0:20}..."
|
||||
echo " Export: ${SIGNET_EXPORT:0:20}..."
|
||||
exit 1
|
||||
fi
|
||||
echo "SIGNETCHALLENGE matches (same chain)"
|
||||
fi
|
||||
|
||||
# Create wallet if it does not exist
|
||||
if ! sudo docker exec "$CONTAINER_NAME" bitcoin-cli -datadir="$DATADIR" listwallets 2>/dev/null | grep -q "\"$WALLET_NAME\""; then
|
||||
echo "Creating wallet: $WALLET_NAME"
|
||||
sudo docker exec "$CONTAINER_NAME" bitcoin-cli -datadir="$DATADIR" -named createwallet \
|
||||
wallet_name="$WALLET_NAME" load_on_startup=true descriptors=true 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Load wallet if not loaded
|
||||
if ! sudo docker exec "$CONTAINER_NAME" bitcoin-cli -datadir="$DATADIR" listwallets 2>/dev/null | grep -q "\"$WALLET_NAME\""; then
|
||||
echo "Loading wallet: $WALLET_NAME"
|
||||
sudo docker exec "$CONTAINER_NAME" bitcoin-cli -datadir="$DATADIR" loadwallet "$WALLET_NAME" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Import descriptors (with private keys). Pass JSON via temp file to avoid shell escaping issues.
|
||||
DESCRIPTORS_JSON=$(jq -c '.descriptors' "$EXPORT_PATH")
|
||||
if [[ -z "$DESCRIPTORS_JSON" ]] || [[ "$DESCRIPTORS_JSON" == "null" ]] || [[ "$DESCRIPTORS_JSON" == "[]" ]]; then
|
||||
echo "Warning: No descriptors in export. Importing PRIVKEY as pk() descriptor if present."
|
||||
if [[ -n "$PRIVKEY_EXPORT" ]] && [[ "$PRIVKEY_EXPORT" != "null" ]]; then
|
||||
DESCRIPTOR_INFO=$(sudo docker exec "$CONTAINER_NAME" bitcoin-cli -datadir="$DATADIR" -rpcwallet="$WALLET_NAME" getdescriptorinfo "pk($PRIVKEY_EXPORT)" 2>/dev/null || echo "{}")
|
||||
CHECKSUM=$(echo "$DESCRIPTOR_INFO" | jq -r '.checksum')
|
||||
if [[ -n "$CHECKSUM" ]] && [[ "$CHECKSUM" != "null" ]]; then
|
||||
sudo docker exec "$CONTAINER_NAME" bitcoin-cli -datadir="$DATADIR" -rpcwallet="$WALLET_NAME" importdescriptors \
|
||||
"[{\"desc\":\"pk($PRIVKEY_EXPORT)#$CHECKSUM\",\"timestamp\":0,\"internal\":false}]" 2>/dev/null || true
|
||||
echo "PRIVKEY imported as pk() descriptor"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "Importing descriptors..."
|
||||
TMP_JSON=$(mktemp)
|
||||
echo "$DESCRIPTORS_JSON" > "$TMP_JSON"
|
||||
sudo docker cp "$TMP_JSON" "$CONTAINER_NAME:/tmp/import-descriptors.json"
|
||||
sudo docker exec "$CONTAINER_NAME" sh -c "bitcoin-cli -datadir=$DATADIR -rpcwallet=$WALLET_NAME importdescriptors \"\$(cat /tmp/import-descriptors.json)\"" 2>/dev/null || true
|
||||
sudo docker exec "$CONTAINER_NAME" rm -f /tmp/import-descriptors.json 2>/dev/null || true
|
||||
rm -f "$TMP_JSON"
|
||||
echo "Descriptors imported"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Wallet $WALLET_NAME imported. Check balance:"
|
||||
sudo docker exec "$CONTAINER_NAME" bitcoin-cli -datadir="$DATADIR" -rpcwallet="$WALLET_NAME" getwalletinfo 2>/dev/null | jq '{walletname, balance, txcount}' || true
|
||||
echo ""
|
||||
4
mine.sh
4
mine.sh
@ -21,7 +21,7 @@ while true; do
|
||||
# Export PRIVKEY to ensure it's available to the miner process
|
||||
export PRIVKEY=${PRIVKEY:-$(cat ~/.bitcoin/PRIVKEY.txt 2>/dev/null || echo "")}
|
||||
# Get block template and pipe it to miner
|
||||
# Use bitcoin-cli with -datadir but without -rpcwallet for miner (descriptorprocesspsbt is node RPC, not wallet RPC)
|
||||
# Use bitcoin-cli with -datadir from BITCOIN_DIR (container env) but without -rpcwallet for miner (descriptorprocesspsbt is node RPC, not wallet RPC)
|
||||
bitcoin-cli -rpcwallet=$WALLET getblocktemplate '{"rules": ["segwit", "signet"]}' | \
|
||||
miner --cli="bitcoin-cli -datadir=/root/.bitcoin" generate --grind-cmd="bitcoin-util grind" --address=$ADDR --nbits=$NBITS --set-block-time=$(date +%s)
|
||||
miner --cli="bitcoin-cli -datadir=${BITCOIN_DIR:-/root/.bitcoin}" generate --grind-cmd="bitcoin-util grind" --address=$ADDR --nbits=$NBITS --set-block-time=$(date +%s)
|
||||
done
|
||||
11
miner
11
miner
@ -516,8 +516,10 @@ def do_generate(args):
|
||||
# Use JSON-RPC HTTP directly to avoid command line length limit
|
||||
import urllib.request
|
||||
import urllib.parse
|
||||
# Extract RPC credentials from bitcoin-cli config or use defaults
|
||||
rpc_url = "http://127.0.0.1:38332/" # Default signet RPC port
|
||||
# RPC = same node as Mempool (BITCOIN_RPC_HOST:BITCOIN_RPC_PORT, default 127.0.0.1:38332)
|
||||
rpc_host = os.environ.get("BITCOIN_RPC_HOST", "127.0.0.1")
|
||||
rpc_port = os.environ.get("BITCOIN_RPC_PORT", "38332")
|
||||
rpc_url = "http://%s:%s/" % (rpc_host, rpc_port)
|
||||
rpc_user = "bitcoin"
|
||||
rpc_pass = "bitcoin"
|
||||
# Try to get RPC credentials from environment or config
|
||||
@ -637,7 +639,10 @@ def do_calibrate(args):
|
||||
return 0
|
||||
|
||||
def bitcoin_cli(basecmd, args, **kwargs):
|
||||
cmd = basecmd + ["-signet"] + args
|
||||
# When --cli includes -datadir=, the datadir config already selects signet; adding -signet can cause connection failure
|
||||
if not any("-datadir=" in x for x in basecmd):
|
||||
basecmd = basecmd + ["-signet"]
|
||||
cmd = basecmd + args
|
||||
logging.debug("Calling bitcoin-cli: %r", cmd)
|
||||
out = subprocess.run(cmd, stdout=subprocess.PIPE, **kwargs, check=True).stdout
|
||||
if isinstance(out, bytes):
|
||||
|
||||
76
miner_imports/docs/README.md
Normal file
76
miner_imports/docs/README.md
Normal file
@ -0,0 +1,76 @@
|
||||
# Miner Bitcoin Signet – Documentation
|
||||
|
||||
**Auteur** : Équipe 4NK
|
||||
**Date** : 2026-02-02
|
||||
**Version** : 1.0
|
||||
|
||||
## Vue d’ensemble
|
||||
|
||||
Le **miner** est un script Python dérivé de `contrib/signet/miner.py` de Bitcoin Core. Il permet de miner des blocs sur une chaîne Bitcoin Signet custom en produisant des blocs signés avec la clé du signet (PRIVKEY) et en respectant le SIGNETCHALLENGE.
|
||||
|
||||
**Emplacement dans le projet :**
|
||||
|
||||
- Racine du projet : `/home/ncantu/Bureau/code/bitcoin/`
|
||||
- Script principal : `miner` (à la racine du projet)
|
||||
- Imports et framework de test : `miner_imports/` (ce répertoire et son contenu)
|
||||
- Script shell de boucle de minage : `mine.sh` (à la racine)
|
||||
- Point d’entrée conteneur : `run.sh` (démarre bitcoind, importe la clé, lance `mine.sh` si `MINERENABLED=1`)
|
||||
|
||||
## Rôle de `miner_imports/`
|
||||
|
||||
Le répertoire `miner_imports/` contient le **test_framework** utilisé par le script `miner` :
|
||||
|
||||
- **test_framework/** : modules Python (messages, blocktools, script, etc.) issus du framework de tests fonctionnels de Bitcoin Core, nécessaires pour construire et sérialiser blocs, transactions et PSBT signet.
|
||||
- Le script `miner` ajoute `miner_imports` au `sys.path` et importe depuis `test_framework` (blocktools, messages, script, etc.).
|
||||
|
||||
Aucun exécutable de minage n’est dans `miner_imports/` : l’exécutable est le fichier `miner` à la racine du projet.
|
||||
|
||||
## Invocation du miner
|
||||
|
||||
Dans le conteneur Docker (voir `mine.sh` et `run.sh`) :
|
||||
|
||||
```bash
|
||||
bitcoin-cli -rpcwallet=custom_signet getblocktemplate '{"rules": ["segwit", "signet"]}' | \
|
||||
miner --cli="bitcoin-cli -datadir=/root/.bitcoin" generate --grind-cmd="bitcoin-util grind" \
|
||||
--address=$ADDR --nbits=$NBITS --set-block-time=$(date +%s)
|
||||
```
|
||||
|
||||
- **--cli** : commande `bitcoin-cli` (avec `-datadir` dans le conteneur).
|
||||
- **generate** : sous-commande pour miner des blocs (boucle getblocktemplate → signer PSBT signet → grind → submitblock).
|
||||
- **--grind-cmd** : commande pour le proof-of-work (grind du header).
|
||||
- **--address** : adresse de récompense de bloc.
|
||||
- **--nbits** : difficulté cible (ex. `1e0377ae`).
|
||||
- **--set-block-time** : timestamp du bloc.
|
||||
|
||||
La variable d’environnement **PRIVKEY** doit être définie (exportée par `mine.sh` depuis `.env`) pour que le miner puisse signer le PSBT signet via `descriptorprocesspsbt` / `walletprocesspsbt`.
|
||||
|
||||
## Comportement de `bitcoin-cli` et option `-signet`
|
||||
|
||||
Le miner appelle Bitcoin RPC via une fonction interne qui construisait toujours la commande en ajoutant **-signet** aux arguments de `bitcoin-cli`. Lorsque `--cli="bitcoin-cli -datadir=/root/.bitcoin"` est utilisé, la config du datadir (ex. `bitcoin.conf` avec `signet=1`) définit déjà le réseau signet. Ajouter `-signet` en plus pouvait provoquer un échec de connexion RPC (exit 1).
|
||||
|
||||
**Correctif appliqué (dans le script `miner`)** : ne pas ajouter `-signet` lorsque la commande cli contient déjà `-datadir=`.
|
||||
|
||||
```python
|
||||
# Quand --cli inclut -datadir=, la config du datadir sélectionne déjà le signet
|
||||
if not any("-datadir=" in x for x in basecmd):
|
||||
basecmd = basecmd + ["-signet"]
|
||||
cmd = basecmd + args
|
||||
```
|
||||
|
||||
Référence : correctif documenté dans le dépôt (rechercher « bitcoin_cli » et « -signet » dans le fichier `miner`).
|
||||
|
||||
## Descriptor wallet et clé P2PK
|
||||
|
||||
Le miner signet doit signer des transactions vers le SIGNETCHALLENGE (script P2PK). La clé privée doit être importée dans le wallet comme descriptor **pk()** (P2PK), et non **wpkh()**, pour que `walletprocesspsbt` puisse signer. L’import est fait dans `run.sh` au démarrage du conteneur lorsque `MINERENABLED=1`.
|
||||
|
||||
Détails et dépannage : voir **docs/TROUBLESHOOTING_MINING.md** et **docs/SOLUTION_MINING.md** à la racine du projet.
|
||||
|
||||
## Références
|
||||
|
||||
- **Racine du projet** : `/home/ncantu/Bureau/code/bitcoin/`
|
||||
- **Script miner** : `miner` (racine)
|
||||
- **Boucle de minage** : `mine.sh` (racine)
|
||||
- **Démarrage conteneur** : `run.sh`, **docs/MAINTENANCE.md** (section Mining)
|
||||
- **Dépannage mining** : **docs/TROUBLESHOOTING_MINING.md**
|
||||
- **Solution PSBT / descriptor** : **docs/SOLUTION_MINING.md**
|
||||
- **Configuration** : **docs/ENVIRONMENT.md** (MINERENABLED, BLOCKPRODUCTIONDELAY, NBITS, PRIVKEY, etc.)
|
||||
65
restore-signet-from-backup.sh
Executable file
65
restore-signet-from-backup.sh
Executable file
@ -0,0 +1,65 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Restore the Bitcoin Signet chain from a full datadir backup (tar.gz).
|
||||
# Use this to resume on the previous chain (e.g. height ~11535) after data loss.
|
||||
#
|
||||
# Prerequisite: a backup created with save-signet-datadir-backup.sh
|
||||
# (or: docker exec bitcoin-signet-instance tar czf /tmp/bitcoin-backup.tar.gz /root/.bitcoin/
|
||||
# docker cp bitcoin-signet-instance:/tmp/bitcoin-backup.tar.gz ./backups/)
|
||||
#
|
||||
# Usage: ./restore-signet-from-backup.sh <path-to-backup.tar.gz>
|
||||
# Example: ./restore-signet-from-backup.sh backups/bitcoin-backup-20260124.tar.gz
|
||||
#
|
||||
# Author: 4NK Team
|
||||
# Date: 2026-02-02
|
||||
|
||||
set -e
|
||||
|
||||
CONTAINER_NAME="bitcoin-signet-instance"
|
||||
VOLUME_NAME="signet-bitcoin-data"
|
||||
BACKUP_PATH="${1:?Usage: $0 <path-to-backup.tar.gz>}"
|
||||
|
||||
if [[ ! -f "$BACKUP_PATH" ]]; then
|
||||
echo "Error: Backup file not found: $BACKUP_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BACKUP_PATH=$(realpath "$BACKUP_PATH")
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
echo "=== Restore Signet from backup ==="
|
||||
echo "Backup: $BACKUP_PATH"
|
||||
echo "Volume: $VOLUME_NAME"
|
||||
echo ""
|
||||
|
||||
if docker ps -a --format "{{.Names}}" | grep -q "^${CONTAINER_NAME}$"; then
|
||||
echo "Stopping and removing current container..."
|
||||
sudo docker stop "$CONTAINER_NAME" 2>/dev/null || true
|
||||
sudo docker rm "$CONTAINER_NAME" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
echo "Creating volume $VOLUME_NAME if needed..."
|
||||
sudo docker volume create "$VOLUME_NAME" 2>/dev/null || true
|
||||
|
||||
echo "Extracting backup into volume..."
|
||||
# Archive may contain root/.bitcoin/... (from tar czf -C / root/.bitcoin) or .bitcoin/...
|
||||
# Volume is mounted at /root/.bitcoin; extract to / then ensure content is under /root/.bitcoin
|
||||
sudo docker run --rm \
|
||||
-v "$VOLUME_NAME":/restore_target \
|
||||
-v "$BACKUP_PATH":/backup.tar.gz:ro \
|
||||
debian:bookworm-slim \
|
||||
sh -c "cd / && tar xzf /backup.tar.gz && if [ -d /root/.bitcoin ] && [ -n \"\$(ls -A /root/.bitcoin 2>/dev/null)\" ]; then cp -a /root/.bitcoin/. /restore_target/; elif [ -d /.bitcoin ]; then cp -a /.bitcoin/. /restore_target/; else echo 'Unexpected archive layout' && exit 1; fi"
|
||||
|
||||
echo "Starting container with restored data..."
|
||||
sudo docker run --env-file .env -d \
|
||||
--name "$CONTAINER_NAME" \
|
||||
-v "$VOLUME_NAME":/root/.bitcoin \
|
||||
-p 38332:38332 -p 38333:38333 \
|
||||
-p 28332:28332 -p 28333:28333 -p 28334:28334 \
|
||||
bitcoin-signet
|
||||
|
||||
echo ""
|
||||
echo "Container started. Check height with:"
|
||||
echo " sudo docker exec $CONTAINER_NAME bitcoin-cli -datadir=\$(docker exec $CONTAINER_NAME printenv BITCOIN_DIR 2>/dev/null || echo /root/.bitcoin) getblockchaininfo"
|
||||
echo ""
|
||||
59
save-signet-datadir-backup.sh
Executable file
59
save-signet-datadir-backup.sh
Executable file
@ -0,0 +1,59 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Create a full datadir backup (blocks + chainstate + config) from the running
|
||||
# Bitcoin Signet container. Use this on the machine that has the chain you want
|
||||
# to keep (e.g. height ~11535), then copy the archive and run restore-signet-from-backup.sh
|
||||
# on the target machine.
|
||||
#
|
||||
# Usage: ./save-signet-datadir-backup.sh [output_dir]
|
||||
# Default output: backups/signet-datadir-YYYYMMDD-HHMMSS.tar.gz
|
||||
#
|
||||
# Author: 4NK Team
|
||||
# Date: 2026-02-02
|
||||
|
||||
set -e
|
||||
|
||||
CONTAINER_NAME="bitcoin-signet-instance"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
OUTPUT_DIR="${1:-$SCRIPT_DIR/backups}"
|
||||
mkdir -p "$OUTPUT_DIR"
|
||||
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
|
||||
BACKUP_FILE="$OUTPUT_DIR/signet-datadir-$TIMESTAMP.tar.gz"
|
||||
|
||||
if ! docker ps --format "{{.Names}}" | grep -q "^${CONTAINER_NAME}$"; then
|
||||
echo "Error: Container $CONTAINER_NAME is not running."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Datadir inside container: use BITCOIN_DIR from container so backup matches actual bitcoind data
|
||||
DATADIR=$(docker exec "$CONTAINER_NAME" printenv BITCOIN_DIR 2>/dev/null || echo "/root/.bitcoin")
|
||||
DATADIR_REL="${DATADIR#/}"
|
||||
|
||||
echo "=== Full Signet datadir backup ==="
|
||||
echo "Container: $CONTAINER_NAME"
|
||||
echo "Datadir: $DATADIR"
|
||||
echo "Output: $BACKUP_FILE"
|
||||
echo ""
|
||||
|
||||
echo "Creating archive inside container..."
|
||||
# tar may exit 1 if a file changed during read (e.g. wallet.dat); archive is usually still usable
|
||||
sudo docker exec "$CONTAINER_NAME" tar czf /tmp/signet-datadir-backup.tar.gz -C / "$DATADIR_REL" || { e=$?; [ "$e" -eq 1 ] || exit "$e"; }
|
||||
|
||||
echo "Copying archive to host..."
|
||||
sudo docker cp "$CONTAINER_NAME:/tmp/signet-datadir-backup.tar.gz" "$BACKUP_FILE"
|
||||
|
||||
echo "Removing temp file in container..."
|
||||
sudo docker exec "$CONTAINER_NAME" rm -f /tmp/signet-datadir-backup.tar.gz
|
||||
|
||||
echo ""
|
||||
echo "Backup saved: $BACKUP_FILE"
|
||||
echo "Size: $(du -h "$BACKUP_FILE" | cut -f1)"
|
||||
# Symlink for easy download (signet-datadir-latest.tar.gz)
|
||||
ln -sf "$(basename "$BACKUP_FILE")" "$OUTPUT_DIR/signet-datadir-latest.tar.gz"
|
||||
echo "Latest symlink: $OUTPUT_DIR/signet-datadir-latest.tar.gz -> $(basename "$BACKUP_FILE")"
|
||||
echo ""
|
||||
echo "To restore on this or another machine:"
|
||||
echo " ./restore-signet-from-backup.sh $BACKUP_FILE"
|
||||
echo " # or: ./restore-signet-from-backup.sh $OUTPUT_DIR/signet-datadir-latest.tar.gz"
|
||||
echo ""
|
||||
@ -1,5 +1,5 @@
|
||||
# Bitcoin RPC Configuration
|
||||
BITCOIN_RPC_HOST=localhost
|
||||
# Bitcoin RPC Configuration (nœud local = même chaîne que Mempool sur la machine bitcoin)
|
||||
BITCOIN_RPC_HOST=127.0.0.1
|
||||
BITCOIN_RPC_PORT=38332
|
||||
BITCOIN_RPC_USER=bitcoin
|
||||
BITCOIN_RPC_PASSWORD=bitcoin
|
||||
|
||||
@ -1668,6 +1668,34 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Endpoint: Mining Status -->
|
||||
<section class="api-docs-section">
|
||||
<div class="endpoint-card">
|
||||
<div class="endpoint-header">
|
||||
<span class="method-badge method-get">GET</span>
|
||||
<span class="endpoint-path">/api/mining/status</span>
|
||||
</div>
|
||||
|
||||
<div class="endpoint-description">
|
||||
<p>État du miner : inféré depuis l’âge du dernier bloc. <code>active</code> si le dernier bloc a moins de 30 minutes, sinon <code>inactive</code>.</p>
|
||||
<p><strong>Base URL :</strong> <code>https://dashboard.certificator.4nkweb.com</code></p>
|
||||
</div>
|
||||
|
||||
<div class="response-example">
|
||||
<h4>Réponse (200 OK)</h4>
|
||||
<div class="code-block">
|
||||
<pre>{
|
||||
"status": "active",
|
||||
"blocks": 11535,
|
||||
"lastBlockTime": 1769730315,
|
||||
"lastBlockAgeSeconds": 120,
|
||||
"message": "Dernier bloc récent, miner probablement actif"
|
||||
}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Endpoint: Mining Avg Block Time -->
|
||||
<section class="api-docs-section">
|
||||
<div class="endpoint-card">
|
||||
|
||||
@ -63,6 +63,7 @@ function startBlockPolling() {
|
||||
loadAvgFee(),
|
||||
loadAvgTxAmount(),
|
||||
loadMiningDifficulty(),
|
||||
loadMinerStatus(),
|
||||
loadAvgBlockTime(),
|
||||
]);
|
||||
}
|
||||
@ -88,6 +89,7 @@ async function loadData() {
|
||||
loadAnchorCount(),
|
||||
loadNetworkPeers(),
|
||||
loadMiningDifficulty(),
|
||||
loadMinerStatus(),
|
||||
]);
|
||||
|
||||
updateLastUpdateTime();
|
||||
@ -111,18 +113,43 @@ async function loadData() {
|
||||
* Charge les informations de la blockchain
|
||||
*/
|
||||
async function loadBlockchainInfo() {
|
||||
const blockHeightEl = document.getElementById('block-height');
|
||||
const chainWarningEl = document.getElementById('chain-warning');
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/blockchain/info`);
|
||||
const data = await response.json();
|
||||
|
||||
document.getElementById('block-height').textContent = data.blocks || 0;
|
||||
// Initialiser lastBlockHeight si ce n'est pas déjà fait
|
||||
if (!response.ok) {
|
||||
blockHeightEl.textContent = 'Erreur';
|
||||
if (chainWarningEl) {
|
||||
chainWarningEl.textContent = data.error || `Erreur RPC (${response.status})`;
|
||||
chainWarningEl.style.display = 'block';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const blocks = data.blocks !== undefined ? data.blocks : 0;
|
||||
blockHeightEl.textContent = blocks;
|
||||
|
||||
if (chainWarningEl) {
|
||||
if (blocks > 0 && blocks < 10000) {
|
||||
chainWarningEl.textContent = 'Chaîne anormale (hauteur < 10000). Vérifier le nœud et le volume persistant sur la machine bitcoin.';
|
||||
chainWarningEl.style.display = 'block';
|
||||
} else {
|
||||
chainWarningEl.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
if (lastBlockHeight === null && data.blocks !== undefined) {
|
||||
lastBlockHeight = data.blocks;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading blockchain info:', error);
|
||||
document.getElementById('block-height').textContent = 'Erreur';
|
||||
blockHeightEl.textContent = 'Erreur';
|
||||
if (chainWarningEl) {
|
||||
chainWarningEl.textContent = 'Impossible de joindre le backend. Vérifier le nœud (127.0.0.1:38332) sur la machine bitcoin.';
|
||||
chainWarningEl.style.display = 'block';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -134,6 +161,12 @@ async function loadLatestBlock() {
|
||||
const response = await fetch(`${API_BASE_URL}/api/blockchain/latest-block`);
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
document.getElementById('last-block-time').textContent = 'Erreur';
|
||||
document.getElementById('last-block-tx-count').textContent = 'Erreur';
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.time) {
|
||||
const date = new Date(data.time * 1000);
|
||||
document.getElementById('last-block-time').textContent = date.toLocaleString('fr-FR');
|
||||
@ -141,7 +174,7 @@ async function loadLatestBlock() {
|
||||
document.getElementById('last-block-time').textContent = 'Aucun bloc';
|
||||
}
|
||||
|
||||
document.getElementById('last-block-tx-count').textContent = data.tx_count || 0;
|
||||
document.getElementById('last-block-tx-count').textContent = data.tx_count ?? 0;
|
||||
} catch (error) {
|
||||
console.error('Error loading latest block:', error);
|
||||
document.getElementById('last-block-time').textContent = 'Erreur';
|
||||
@ -157,8 +190,14 @@ async function loadWalletBalance() {
|
||||
const response = await fetch(`${API_BASE_URL}/api/wallet/balance`);
|
||||
const data = await response.json();
|
||||
|
||||
document.getElementById('balance-mature').textContent = formatBTC(data.mature || 0);
|
||||
document.getElementById('balance-immature').textContent = formatBTC(data.immature || 0);
|
||||
if (!response.ok) {
|
||||
document.getElementById('balance-mature').textContent = 'Erreur';
|
||||
document.getElementById('balance-immature').textContent = 'Erreur';
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('balance-mature').textContent = formatBTC(data.mature ?? 0);
|
||||
document.getElementById('balance-immature').textContent = formatBTC(data.immature ?? 0);
|
||||
} catch (error) {
|
||||
console.error('Error loading wallet balance:', error);
|
||||
document.getElementById('balance-mature').textContent = 'Erreur';
|
||||
@ -303,6 +342,37 @@ async function loadMiningDifficulty() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge l'état du miner (actif / inactif / inconnu, inféré depuis l'âge du dernier bloc).
|
||||
*/
|
||||
async function loadMinerStatus() {
|
||||
const el = document.getElementById('miner-status');
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/mining/status`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
if (data.status === 'active') {
|
||||
el.textContent = 'Actif';
|
||||
el.setAttribute('aria-label', data.message ?? 'Miner actif');
|
||||
} else if (data.status === 'inactive') {
|
||||
el.textContent = 'Inactif';
|
||||
el.setAttribute('aria-label', data.message ?? 'Miner inactif');
|
||||
} else {
|
||||
el.textContent = data.message ?? 'Inconnu';
|
||||
el.setAttribute('aria-label', data.message ?? 'État du miner inconnu');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading miner status:', error);
|
||||
el.textContent = 'Erreur';
|
||||
el.setAttribute('aria-label', 'Impossible de charger l\'état du miner');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Charge le temps moyen entre blocs
|
||||
*/
|
||||
|
||||
@ -432,19 +432,24 @@
|
||||
try {
|
||||
// Utiliser l'endpoint proxy du dashboard
|
||||
const response = await fetch(`${API_BASE_URL}/api/anchor/health/detailed`);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const health = await response.json();
|
||||
|
||||
// On 503 the API still returns full health JSON (ok, bitcoin, mutex, utxos) so we can show the reason
|
||||
if (!response.ok && (!health.mutex || !health.utxos || !health.bitcoin)) {
|
||||
const reason = health.error || health.message || `HTTP ${response.status}`;
|
||||
healthDiv.innerHTML = `<div class="error">✗ [anchor-api] Health check a échoué (HTTP ${response.status}). ${reason}</div>`;
|
||||
unlockButton.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '<div class="health-status">';
|
||||
|
||||
// État général
|
||||
// État général (on 503 we still render full panel so user sees Bitcoin/mutex/UTXOs reason)
|
||||
const statusClass = health.ok ? 'ok' : 'error';
|
||||
const statusText = health.ok ? '✓ OK' : (response.status === 503 ? '✗ Problème (503 - voir détails ci-dessous)' : '✗ Problème');
|
||||
html += `<div class="health-item">
|
||||
<label>État général</label>
|
||||
<div class="value ${statusClass}">${health.ok ? '✓ OK' : '✗ Problème'}</div>
|
||||
<div class="value ${statusClass}">${statusText}</div>
|
||||
</div>`;
|
||||
|
||||
// Mutex
|
||||
@ -510,7 +515,7 @@
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error loading health status:', error);
|
||||
healthDiv.innerHTML = `<div class="error">Erreur lors du chargement de l'état : ${error.message}</div>`;
|
||||
healthDiv.innerHTML = `<div class="error">✗ [anchor-api] L'API d'ancrage n'est pas accessible. Erreur : ${error.message}</div>`;
|
||||
unlockButton.style.display = 'none';
|
||||
} finally {
|
||||
refreshButton.disabled = false;
|
||||
|
||||
@ -34,6 +34,7 @@
|
||||
</header>
|
||||
|
||||
<main>
|
||||
<div id="chain-warning" class="chain-warning" style="display: none;" role="alert"></div>
|
||||
<!-- Section Supervision -->
|
||||
<section class="supervision-section">
|
||||
<h2>État de la Blockchain</h2>
|
||||
@ -80,6 +81,10 @@
|
||||
<h3>Difficulté de Minage</h3>
|
||||
<p class="value" id="mining-difficulty">-</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>État du Miner</h3>
|
||||
<p class="value" id="miner-status">-</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h3>Temps Moyen entre Blocs</h3>
|
||||
<p class="value" id="avg-block-time">
|
||||
|
||||
@ -58,60 +58,6 @@
|
||||
transform: scale(0.98);
|
||||
}
|
||||
|
||||
.payment-section {
|
||||
background: var(--card-background);
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
|
||||
margin-bottom: 30px;
|
||||
text-align: center;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.payment-section h2 {
|
||||
color: var(--primary-color);
|
||||
margin-bottom: 20px;
|
||||
font-size: 1.8em;
|
||||
}
|
||||
|
||||
.payment-info {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.payment-amount {
|
||||
font-size: 1.5em;
|
||||
font-weight: bold;
|
||||
color: var(--primary-color);
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.payment-address {
|
||||
font-family: 'Courier New', monospace;
|
||||
background: var(--card-background);
|
||||
color: var(--text-color);
|
||||
padding: 15px;
|
||||
border-radius: 5px;
|
||||
margin: 15px 0;
|
||||
word-break: break-all;
|
||||
font-size: 0.9em;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.nostr-profile-link {
|
||||
display: inline-block;
|
||||
margin-top: 15px;
|
||||
padding: 10px 20px;
|
||||
background: var(--primary-color);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
border-radius: 5px;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.nostr-profile-link:hover {
|
||||
background: #e0820d;
|
||||
}
|
||||
|
||||
.wallet-section {
|
||||
background: var(--card-background);
|
||||
padding: 30px;
|
||||
@ -127,28 +73,6 @@
|
||||
font-size: 1.8em;
|
||||
}
|
||||
|
||||
.wallet-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 20px 0;
|
||||
padding: 15px;
|
||||
background: var(--card-background);
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.wallet-checkbox input[type="checkbox"] {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
margin-right: 15px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wallet-checkbox label {
|
||||
cursor: pointer;
|
||||
font-size: 1.1em;
|
||||
}
|
||||
|
||||
.info-box {
|
||||
background: rgba(13, 202, 240, 0.15);
|
||||
border-left: 4px solid #0dcaf0;
|
||||
@ -162,16 +86,6 @@
|
||||
margin: 5px 0;
|
||||
}
|
||||
|
||||
.success-message {
|
||||
background: rgba(40, 167, 69, 0.2);
|
||||
border-left: 4px solid var(--success-color);
|
||||
padding: 15px;
|
||||
margin: 20px 0;
|
||||
border-radius: 5px;
|
||||
color: #90ee90;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.back-link {
|
||||
display: inline-block;
|
||||
margin-bottom: 20px;
|
||||
@ -244,80 +158,6 @@ addnode=anchorage.certificator.4nkweb.com:38333</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section Paiement Wallet de Mining -->
|
||||
<section class="join-section">
|
||||
<div class="payment-section">
|
||||
<h2>💳 Accès au Wallet de Mining</h2>
|
||||
<p>Pour recevoir le wallet de mining et les clés nécessaires pour miner sur le réseau, effectuez un paiement de :</p>
|
||||
|
||||
<div class="payment-amount">0,0065 BTC</div>
|
||||
|
||||
<p>Envoyez le paiement via Nostr à :</p>
|
||||
|
||||
<div class="payment-address" id="wallet-npub">npub18s03s39fa80ce2n3cmm0zme3jqehc82h6ld9sxq03uejqm3d05gsae0fuu</div>
|
||||
|
||||
<button class="copy-button" onclick="copyNpub('wallet-npub')">📋 Copier la npub</button>
|
||||
|
||||
<a href="https://yakihonne.com/profile/fancy-wallaby-90@rizful.com" target="_blank" rel="noopener noreferrer" class="nostr-profile-link">
|
||||
🔗 Voir le profil Nostr
|
||||
</a>
|
||||
|
||||
<div class="wallet-checkbox">
|
||||
<input type="checkbox" id="wallet-request" onchange="updatePaymentMessage('wallet')">
|
||||
<label for="wallet-request">Je souhaite recevoir le wallet de mining après le paiement</label>
|
||||
</div>
|
||||
|
||||
<div class="info-box" id="wallet-payment-info">
|
||||
<p><strong>Instructions :</strong></p>
|
||||
<p>1. Effectuez le paiement de 0,0065 BTC via Nostr à la npub ci-dessus</p>
|
||||
<p>2. Cochez la case ci-dessus si vous souhaitez recevoir le wallet de mining</p>
|
||||
<p>3. Après confirmation du paiement, vous recevrez le wallet de mining sur Nostr</p>
|
||||
</div>
|
||||
|
||||
<div class="success-message" id="wallet-payment-success">
|
||||
<p><strong>✅ Paiement reçu !</strong></p>
|
||||
<p>Votre demande a été enregistrée. Vous recevrez le wallet de mining sur Nostr sous peu.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section Paiement Clé API -->
|
||||
<section class="join-section">
|
||||
<div class="payment-section">
|
||||
<h2>🔑 Accès à une Clé API</h2>
|
||||
<p>Pour recevoir une clé API permettant d'utiliser les services d'ancrage et de filigrane, effectuez un paiement de :</p>
|
||||
|
||||
<div class="payment-amount">0,0065 BTC</div>
|
||||
|
||||
<p>Envoyez le paiement via Nostr à :</p>
|
||||
|
||||
<div class="payment-address" id="api-npub">npub18s03s39fa80ce2n3cmm0zme3jqehc82h6ld9sxq03uejqm3d05gsae0fuu</div>
|
||||
|
||||
<button class="copy-button" onclick="copyNpub('api-npub')">📋 Copier la npub</button>
|
||||
|
||||
<a href="https://yakihonne.com/profile/fancy-wallaby-90@rizful.com" target="_blank" rel="noopener noreferrer" class="nostr-profile-link">
|
||||
🔗 Voir le profil Nostr
|
||||
</a>
|
||||
|
||||
<div class="wallet-checkbox">
|
||||
<input type="checkbox" id="api-request" onchange="updatePaymentMessage('api')">
|
||||
<label for="api-request">Je souhaite recevoir la clé API après le paiement</label>
|
||||
</div>
|
||||
|
||||
<div class="info-box" id="api-payment-info">
|
||||
<p><strong>Instructions :</strong></p>
|
||||
<p>1. Effectuez le paiement de 0,0065 BTC via Nostr à la npub ci-dessus</p>
|
||||
<p>2. Cochez la case ci-dessus si vous souhaitez recevoir la clé API</p>
|
||||
<p>3. Après confirmation du paiement, vous recevrez la clé API sur Nostr</p>
|
||||
</div>
|
||||
|
||||
<div class="success-message" id="api-payment-success">
|
||||
<p><strong>✅ Paiement reçu !</strong></p>
|
||||
<p>Votre demande a été enregistrée. Vous recevrez la clé API sur Nostr sous peu.</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section Informations Supplémentaires -->
|
||||
<section class="join-section">
|
||||
<div class="wallet-section">
|
||||
@ -333,17 +173,6 @@ addnode=anchorage.certificator.4nkweb.com:38333</div>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p><strong>Que se passe-t-il après le paiement ?</strong></p>
|
||||
<p>Une fois le paiement confirmé, vous recevrez sur Nostr :</p>
|
||||
<ul style="margin-left: 20px; margin-top: 10px;">
|
||||
<li>Les fichiers de configuration complets</li>
|
||||
<li>La clé privée du signet (si vous avez coché la case pour le wallet de mining)</li>
|
||||
<li>La clé API (si vous avez coché la case pour la clé API)</li>
|
||||
<li>Les instructions détaillées pour démarrer votre nœud ou utiliser l'API</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="info-box">
|
||||
<p><strong>Besoin d'aide ?</strong></p>
|
||||
<p>Pour toute question, consultez la documentation complète dans le dépôt GitHub ou contactez l'équipe.</p>
|
||||
@ -363,10 +192,6 @@ addnode=anchorage.certificator.4nkweb.com:38333</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const NOSTR_NPUB = 'npub18s03s39fa80ce2n3cmm0zme3jqehc82h6ld9sxq03uejqm3d05gsae0fuu';
|
||||
const NOSTR_PROFILE_URL = 'https://yakihonne.com/profile/fancy-wallaby-90@rizful.com';
|
||||
const PAYMENT_AMOUNT = 0.0065;
|
||||
|
||||
function copyConfig() {
|
||||
const configText = document.getElementById('bitcoin-config')?.textContent || '';
|
||||
navigator.clipboard.writeText(configText).then(() => {
|
||||
@ -383,70 +208,6 @@ addnode=anchorage.certificator.4nkweb.com:38333</div>
|
||||
alert('Erreur lors de la copie. Veuillez sélectionner et copier manuellement.');
|
||||
});
|
||||
}
|
||||
|
||||
function copyNpub(elementId) {
|
||||
const npubElement = document.getElementById(elementId);
|
||||
const npub = npubElement?.textContent || NOSTR_NPUB;
|
||||
navigator.clipboard.writeText(npub).then(() => {
|
||||
const button = event?.target;
|
||||
if (button) {
|
||||
const originalText = button.textContent;
|
||||
button.textContent = '✅ Copié !';
|
||||
setTimeout(() => {
|
||||
button.textContent = originalText;
|
||||
}, 2000);
|
||||
}
|
||||
}).catch(err => {
|
||||
console.error('Erreur lors de la copie:', err);
|
||||
alert('Erreur lors de la copie. Veuillez sélectionner et copier manuellement.');
|
||||
});
|
||||
}
|
||||
|
||||
function updatePaymentMessage(type) {
|
||||
if (type === 'wallet') {
|
||||
const checkbox = document.getElementById('wallet-request');
|
||||
const paymentInfo = document.getElementById('wallet-payment-info');
|
||||
|
||||
if (checkbox && paymentInfo) {
|
||||
if (checkbox.checked) {
|
||||
paymentInfo.innerHTML = `
|
||||
<p><strong>Instructions :</strong></p>
|
||||
<p>1. Effectuez le paiement de 0,0065 BTC via Nostr à la npub ci-dessus</p>
|
||||
<p>2. ✅ Vous recevrez le wallet de mining après confirmation du paiement</p>
|
||||
<p>3. Le wallet vous sera envoyé sur Nostr</p>
|
||||
`;
|
||||
} else {
|
||||
paymentInfo.innerHTML = `
|
||||
<p><strong>Instructions :</strong></p>
|
||||
<p>1. Effectuez le paiement de 0,0065 BTC via Nostr à la npub ci-dessus</p>
|
||||
<p>2. Cochez la case ci-dessus si vous souhaitez recevoir le wallet de mining</p>
|
||||
<p>3. Après confirmation du paiement, vous recevrez le wallet de mining sur Nostr</p>
|
||||
`;
|
||||
}
|
||||
}
|
||||
} else if (type === 'api') {
|
||||
const checkbox = document.getElementById('api-request');
|
||||
const paymentInfo = document.getElementById('api-payment-info');
|
||||
|
||||
if (checkbox && paymentInfo) {
|
||||
if (checkbox.checked) {
|
||||
paymentInfo.innerHTML = `
|
||||
<p><strong>Instructions :</strong></p>
|
||||
<p>1. Effectuez le paiement de 0,0065 BTC via Nostr à la npub ci-dessus</p>
|
||||
<p>2. ✅ Vous recevrez la clé API après confirmation du paiement</p>
|
||||
<p>3. La clé API vous sera envoyée sur Nostr</p>
|
||||
`;
|
||||
} else {
|
||||
paymentInfo.innerHTML = `
|
||||
<p><strong>Instructions :</strong></p>
|
||||
<p>1. Effectuez le paiement de 0,0065 BTC via Nostr à la npub ci-dessus</p>
|
||||
<p>2. Cochez la case ci-dessus si vous souhaitez recevoir la clé API</p>
|
||||
<p>3. Après confirmation du paiement, vous recevrez la clé API sur Nostr</p>
|
||||
`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -390,6 +390,42 @@
|
||||
Le bloc est ajouté à la blockchain, le mineur reçoit la récompense
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Comment fonctionne le Miner ?</h3>
|
||||
<div class="concept-explanation">
|
||||
<p>Le <span class="highlight">miner</span> (mineur) est le logiciel ou le nœud qui produit effectivement les blocs. Il écoute le réseau, construit des blocs candidats et les diffuse une fois qu’ils sont valides.</p>
|
||||
<p><strong>Sur le réseau principal Bitcoin</strong>, le miner :</p>
|
||||
<ul>
|
||||
<li>Obtient un <strong>modèle de bloc</strong> (block template) depuis son nœud Bitcoin Core : en-tête du bloc, transactions du mempool, difficulté cible</li>
|
||||
<li>Sélectionne les transactions à inclure (souvent par ordre de frais par octet)</li>
|
||||
<li>Construit le bloc (transaction coinbase + liste de transactions)</li>
|
||||
<li>Cherche un <strong>nonce</strong> (et varie le coinbase si besoin) pour que le hash du bloc soit sous la cible de difficulté (proof-of-work)</li>
|
||||
<li>Diffuse le bloc miné au réseau ; les autres nœuds le valident et l’ajoutent à leur chaîne</li>
|
||||
</ul>
|
||||
<p><strong>Sur Bitcoin Signet</strong> (réseau de test comme celui de ce dashboard), le principe est le même, mais la « preuve » change : au lieu d’un proof-of-work coûteux, un bloc valide doit contenir une <strong>signature</strong> d’une clé autorisée (signet). Le miner Signet récupère un template, construit le bloc, le signe avec la clé du signet, puis le diffuse. La récompense par bloc est fixe (par ex. 50 000 sats sur ce Signet).</p>
|
||||
</div>
|
||||
|
||||
<div class="flow-diagram">
|
||||
<div class="flow-step">
|
||||
<strong>1. Modèle de bloc</strong><br>
|
||||
Le nœud (Bitcoin Core) fournit un template : bloc précédent, mempool, difficulté
|
||||
</div>
|
||||
<div class="flow-arrow">↓</div>
|
||||
<div class="flow-step">
|
||||
<strong>2. Construction du bloc</strong><br>
|
||||
Le miner ajoute la transaction coinbase (récompense vers son adresse) et des transactions du mempool
|
||||
</div>
|
||||
<div class="flow-arrow">↓</div>
|
||||
<div class="flow-step">
|
||||
<strong>3. Preuve</strong><br>
|
||||
Mainnet : recherche d’un nonce (proof-of-work). Signet : signature du bloc avec la clé autorisée
|
||||
</div>
|
||||
<div class="flow-arrow">↓</div>
|
||||
<div class="flow-step">
|
||||
<strong>4. Diffusion</strong><br>
|
||||
Le bloc valide est envoyé au réseau ; les nœuds le valident et l’ajoutent à la blockchain
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- Section: Entrée de Transaction -->
|
||||
|
||||
@ -75,6 +75,16 @@ main {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
.chain-warning {
|
||||
margin-bottom: 20px;
|
||||
padding: 14px 18px;
|
||||
background-color: rgba(220, 53, 69, 0.2);
|
||||
border: 1px solid var(--error-color);
|
||||
border-radius: 8px;
|
||||
color: var(--error-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
section {
|
||||
margin-bottom: 40px;
|
||||
}
|
||||
|
||||
@ -8,7 +8,12 @@ User=ncantu
|
||||
WorkingDirectory=/home/ncantu/Bureau/code/bitcoin/signet-dashboard
|
||||
Environment=NODE_ENV=production
|
||||
Environment=DASHBOARD_PORT=3020
|
||||
Environment=DASHBOARD_HOST=0.0.0.0
|
||||
# Bind IPv4 only: machine bitcoin (192.168.1.105). Accept only from proxy 192.168.1.100.
|
||||
Environment=DASHBOARD_HOST=192.168.1.105
|
||||
Environment=ALLOWED_SOURCE_IP=192.168.1.100
|
||||
# RPC = même machine : 127.0.0.1:38332 = host.docker.internal:38332 (Mempool) = bitcoin-signet-instance
|
||||
Environment=BITCOIN_RPC_HOST=127.0.0.1
|
||||
Environment=BITCOIN_RPC_PORT=38332
|
||||
ExecStart=/usr/bin/node /home/ncantu/Bureau/code/bitcoin/signet-dashboard/src/server.js
|
||||
Restart=always
|
||||
RestartSec=10
|
||||
|
||||
@ -63,14 +63,22 @@ function makeHttpRequest(baseUrl, path, options = {}) {
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const jsonData = JSON.parse(data);
|
||||
if (options.preserveStatus) {
|
||||
resolve({ statusCode: res.statusCode, body: jsonData });
|
||||
return;
|
||||
}
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
resolve(jsonData);
|
||||
} else {
|
||||
resolve({ error: jsonData.error || 'Request failed', message: jsonData.message || `HTTP ${res.statusCode}` });
|
||||
}
|
||||
} catch (e) {
|
||||
if (options.preserveStatus) {
|
||||
resolve({ statusCode: res.statusCode, body: { error: 'Invalid JSON response', message: data.substring(0, 100) } });
|
||||
} else {
|
||||
resolve({ error: 'Invalid JSON response', message: data.substring(0, 100) });
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@ -126,6 +134,34 @@ if (envResult.error) {
|
||||
const app = express();
|
||||
const PORT = process.env.DASHBOARD_PORT || 3020;
|
||||
const HOST = process.env.DASHBOARD_HOST || '0.0.0.0';
|
||||
const ALLOWED_SOURCE_IP = process.env.ALLOWED_SOURCE_IP ?? '';
|
||||
|
||||
/**
|
||||
* Normalize remote address: IPv6-mapped IPv4 (::ffff:192.168.1.100) -> 192.168.1.100
|
||||
* @param {string} addr - req.socket.remoteAddress
|
||||
* @returns {string}
|
||||
*/
|
||||
function normalizeRemoteAddress(addr) {
|
||||
if (!addr) return '';
|
||||
if (addr.startsWith('::ffff:')) return addr.slice(7);
|
||||
return addr;
|
||||
}
|
||||
|
||||
// Middleware: accept only requests from proxy when ALLOWED_SOURCE_IP is set (IPv4 only)
|
||||
app.use((req, res, next) => {
|
||||
if (!ALLOWED_SOURCE_IP) return next();
|
||||
const remote = normalizeRemoteAddress(req.socket.remoteAddress ?? '');
|
||||
if (remote !== ALLOWED_SOURCE_IP) {
|
||||
logger.warn('Request rejected: source not allowed', {
|
||||
remoteAddress: req.socket.remoteAddress,
|
||||
allowedSourceIp: ALLOWED_SOURCE_IP,
|
||||
path: req.path,
|
||||
});
|
||||
res.status(403).json({ error: 'Forbidden', message: 'Source not allowed' });
|
||||
return;
|
||||
}
|
||||
next();
|
||||
});
|
||||
|
||||
// Middleware
|
||||
app.use(cors());
|
||||
@ -595,6 +631,35 @@ app.get('/api/mining/difficulty', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
/** État du miner : inféré depuis l’âge du dernier bloc (actif si dernier bloc < 30 min). */
|
||||
const MINER_ACTIVE_THRESHOLD_SECONDS = 30 * 60;
|
||||
|
||||
app.get('/api/mining/status', async (req, res) => {
|
||||
try {
|
||||
const blockchainInfo = await bitcoinRPC.getBlockchainInfo();
|
||||
const latestBlock = await bitcoinRPC.getLatestBlock();
|
||||
const nowSeconds = Math.floor(Date.now() / 1000);
|
||||
const lastBlockTime = latestBlock.time;
|
||||
const ageSeconds = nowSeconds - lastBlockTime;
|
||||
const status = ageSeconds <= MINER_ACTIVE_THRESHOLD_SECONDS ? 'active' : 'inactive';
|
||||
res.json({
|
||||
status,
|
||||
blocks: blockchainInfo.blocks,
|
||||
lastBlockTime: lastBlockTime,
|
||||
lastBlockAgeSeconds: ageSeconds,
|
||||
message: status === 'active'
|
||||
? 'Dernier bloc récent, miner probablement actif'
|
||||
: 'Dernier bloc ancien, miner probablement inactif ou en attente du délai',
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('Error getting mining status', { error: error.message });
|
||||
res.status(500).json({
|
||||
status: 'unknown',
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/mining/avg-block-time', async (req, res) => {
|
||||
try {
|
||||
// Utiliser l'API mempool pour obtenir le temps moyen entre blocs
|
||||
@ -985,6 +1050,7 @@ app.post('/api/anchor/unlock-utxos', async (req, res) => {
|
||||
});
|
||||
|
||||
// Route pour obtenir l'état de santé détaillé (appelle l'API d'ancrage externe)
|
||||
// Forward status code and body so client sees 503 and exact reason (Bitcoin, mutex, UTXOs).
|
||||
app.get('/api/anchor/health/detailed', async (req, res) => {
|
||||
try {
|
||||
// Toujours utiliser l'URL publique HTTPS
|
||||
@ -998,9 +1064,10 @@ app.get('/api/anchor/health/detailed', async (req, res) => {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
preserveStatus: true,
|
||||
});
|
||||
|
||||
res.json(result);
|
||||
res.status(result.statusCode).json(result.body);
|
||||
} catch (error) {
|
||||
logger.error('Error getting detailed health', { error: error.message });
|
||||
res.status(500).json({ error: error.message });
|
||||
|
||||
72
test-mempool-rpc-config.sh
Executable file
72
test-mempool-rpc-config.sh
Executable file
@ -0,0 +1,72 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Test the RPC configuration that Mempool uses (host.docker.internal:38332
|
||||
# = host's 127.0.0.1:38332 when Mempool runs in Docker on that host).
|
||||
# Run on the machine where Mempool runs to verify the node has ~11535 blocks;
|
||||
# then align Dashboard, Anchorage and Faucet to the same host:port.
|
||||
#
|
||||
# Usage: ./test-mempool-rpc-config.sh [RPC_HOST] [RPC_PORT]
|
||||
# Default: 127.0.0.1 38332
|
||||
# Example (node on another machine): ./test-mempool-rpc-config.sh 192.168.1.105 38332
|
||||
#
|
||||
# Author: 4NK Team
|
||||
# Date: 2026-02-02
|
||||
|
||||
set -e
|
||||
|
||||
RPC_HOST="${1:-127.0.0.1}"
|
||||
RPC_PORT="${2:-38332}"
|
||||
RPC_USER="${BITCOIN_RPC_USER:-bitcoin}"
|
||||
RPC_PASS="${BITCOIN_RPC_PASSWORD:-bitcoin}"
|
||||
EXPECTED_MIN="${EXPECTED_HEIGHT_MIN:-11000}"
|
||||
EXPECTED_MAX="${EXPECTED_HEIGHT_MAX:-12000}"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
TMP_JSON="$SCRIPT_DIR/.test-mempool-rpc-$$.json"
|
||||
trap 'rm -f "$TMP_JSON"' EXIT
|
||||
|
||||
echo "=== Test Mempool RPC config (same node as Mempool uses) ==="
|
||||
echo "Host: $RPC_HOST Port: $RPC_PORT"
|
||||
echo ""
|
||||
|
||||
# JSON-RPC getblockchaininfo
|
||||
HTTP_CODE=$(curl -s -o "$TMP_JSON" -w "%{http_code}" \
|
||||
--user "$RPC_USER:$RPC_PASS" \
|
||||
--data-binary '{"jsonrpc":"1.0","id":"test","method":"getblockchaininfo","params":[]}' \
|
||||
-H "content-type: text/plain;" \
|
||||
"http://${RPC_HOST}:${RPC_PORT}/" 2>/dev/null || echo "000")
|
||||
|
||||
if [ "$HTTP_CODE" != "200" ]; then
|
||||
echo "FAIL: RPC returned HTTP $HTTP_CODE (node unreachable or auth failed)."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$TMP_JSON" ] || ! grep -q '"result"' "$TMP_JSON" 2>/dev/null; then
|
||||
echo "FAIL: RPC response missing result."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CHAIN=$(grep -o '"chain"[[:space:]]*:[[:space:]]*"[^"]*"' "$TMP_JSON" | sed 's/.*:[[:space:]]*"\([^"]*\)".*/\1/')
|
||||
BLOCKS=$(grep -o '"blocks"[[:space:]]*:[[:space:]]*[0-9]*' "$TMP_JSON" | head -1 | grep -o '[0-9]*$')
|
||||
|
||||
if [ -z "$BLOCKS" ]; then
|
||||
echo "FAIL: Could not parse 'blocks' from getblockchaininfo."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Node: chain=$CHAIN blocks=$BLOCKS"
|
||||
|
||||
if [ "$CHAIN" != "signet" ]; then
|
||||
echo "WARN: chain is not 'signet' (got '$CHAIN')."
|
||||
fi
|
||||
|
||||
if [ "$BLOCKS" -ge "$EXPECTED_MIN" ] && [ "$BLOCKS" -le "$EXPECTED_MAX" ]; then
|
||||
echo "OK: Height $BLOCKS is in expected range [$EXPECTED_MIN..$EXPECTED_MAX] (same as Mempool)."
|
||||
echo ""
|
||||
echo "Align Dashboard, Anchorage and Faucet to this node: BITCOIN_RPC_HOST=$RPC_HOST BITCOIN_RPC_PORT=$RPC_PORT"
|
||||
exit 0
|
||||
else
|
||||
echo "WARN: Height $BLOCKS is outside expected range [$EXPECTED_MIN..$EXPECTED_MAX]."
|
||||
echo " This may not be the node Mempool uses, or the chain is not synced."
|
||||
exit 1
|
||||
fi
|
||||
@ -130,6 +130,13 @@ rebuild_image() {
|
||||
fi
|
||||
}
|
||||
|
||||
# Volume persistant : même que restore-signet-from-backup.sh (une seule chaîne pour Mempool, dashboard, APIs)
|
||||
VOLUME_NAME="signet-bitcoin-data"
|
||||
|
||||
# Volume contenant la chaîne Signet complète (~11530 blocs). Utilisé par défaut s'il existe pour éviter de repartir sur une chaîne vide.
|
||||
# Voir docs/MAINTENANCE.md et fixKnowledge/dashboard-anchor-wrong-chain-insufficient-balance.md
|
||||
SIGNET_VOLUME_FULL_CHAIN="4b5dca4d940b9f6e5db67b460f40f230a5ef1195a3769e5f91fa02be6edde649"
|
||||
|
||||
# Redémarrage du conteneur
|
||||
restart_container() {
|
||||
info "Redémarrage du conteneur..."
|
||||
@ -141,10 +148,20 @@ restart_container() {
|
||||
sudo docker rm bitcoin-signet-instance
|
||||
fi
|
||||
|
||||
# Démarrer le nouveau conteneur
|
||||
info "Démarrage du nouveau conteneur..."
|
||||
# Utiliser le volume "chaîne complète" s'il existe, sinon le volume nommé (créé si besoin)
|
||||
local volume_to_use="$VOLUME_NAME"
|
||||
if sudo docker volume inspect "$SIGNET_VOLUME_FULL_CHAIN" &>/dev/null; then
|
||||
volume_to_use="$SIGNET_VOLUME_FULL_CHAIN"
|
||||
info "Utilisation du volume chaîne complète (signet ~11530 blocs)"
|
||||
else
|
||||
sudo docker volume create "$VOLUME_NAME" 2>/dev/null || true
|
||||
fi
|
||||
|
||||
# Démarrer le nouveau conteneur avec volume persistant (une seule chaîne pour tous les services)
|
||||
info "Démarrage du nouveau conteneur (volume $volume_to_use)..."
|
||||
sudo docker run --env-file .env -d \
|
||||
--name bitcoin-signet-instance \
|
||||
-v "$volume_to_use":/root/.bitcoin \
|
||||
-p 38332:38332 \
|
||||
-p 38333:38333 \
|
||||
-p 28332:28332 \
|
||||
@ -167,10 +184,12 @@ verify_update() {
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Vérifier la version de Bitcoin
|
||||
# Vérifier la version de Bitcoin (datadir = BITCOIN_DIR du conteneur)
|
||||
local bitcoin_datadir
|
||||
bitcoin_datadir=$(sudo docker exec bitcoin-signet-instance printenv BITCOIN_DIR 2>/dev/null || echo "/root/.bitcoin")
|
||||
local version_info
|
||||
version_info=$(sudo docker exec bitcoin-signet-instance bitcoin-cli -datadir=/root/.bitcoin -version 2>/dev/null || \
|
||||
sudo docker exec bitcoin-signet-instance bitcoin-cli -datadir=/root/.bitcoin getnetworkinfo 2>/dev/null | grep -oP '"subversion": "\K[^"]+' || echo "unknown")
|
||||
version_info=$(sudo docker exec bitcoin-signet-instance bitcoin-cli -datadir="$bitcoin_datadir" -version 2>/dev/null || \
|
||||
sudo docker exec bitcoin-signet-instance bitcoin-cli -datadir="$bitcoin_datadir" getnetworkinfo 2>/dev/null | grep -oP '"subversion": "\K[^"]+' || echo "unknown")
|
||||
|
||||
if [ "$version_info" != "unknown" ]; then
|
||||
success "Version Bitcoin détectée: $version_info"
|
||||
@ -180,7 +199,7 @@ verify_update() {
|
||||
|
||||
# Vérifier l'état de la blockchain
|
||||
local blockchain_info
|
||||
blockchain_info=$(sudo docker exec bitcoin-signet-instance bitcoin-cli -datadir=/root/.bitcoin getblockchaininfo 2>/dev/null | grep -oP '"chain": "\K[^"]+' || echo "unknown")
|
||||
blockchain_info=$(sudo docker exec bitcoin-signet-instance bitcoin-cli -datadir="$bitcoin_datadir" getblockchaininfo 2>/dev/null | grep -oP '"chain": "\K[^"]+' || echo "unknown")
|
||||
|
||||
if [ "$blockchain_info" = "signet" ]; then
|
||||
success "Blockchain signet opérationnelle"
|
||||
@ -252,7 +271,7 @@ main() {
|
||||
echo ""
|
||||
success "Mise à jour terminée avec succès!"
|
||||
info "Pour voir les logs: sudo docker logs -f bitcoin-signet-instance"
|
||||
info "Pour vérifier l'état: sudo docker exec bitcoin-signet-instance bitcoin-cli -datadir=/root/.bitcoin getblockchaininfo"
|
||||
info "Pour vérifier l'état: sudo docker exec bitcoin-signet-instance bitcoin-cli -datadir=\$(docker exec bitcoin-signet-instance printenv BITCOIN_DIR 2>/dev/null || echo /root/.bitcoin) getblockchaininfo"
|
||||
}
|
||||
|
||||
# Gestion des arguments
|
||||
|
||||
@ -61,6 +61,6 @@ const PORT = process.env.PORT ? parseInt(process.env.PORT, 10) : <nouveau_port>;
|
||||
Les ports suivants sont évités car potentiellement occupés :
|
||||
- **3007** : Utilisé par d'autres services
|
||||
- **8080** : Port commun, souvent occupé
|
||||
- **3015** : Occupé (mempool1.4nkweb.com)
|
||||
- **3015** : Occupé (mempool.4nkweb.com, machine bitcoin)
|
||||
- **3016** : Réservé (git1.4nkweb.com)
|
||||
- **3017** : Réservé (rocket1.4nkweb.com)
|
||||
|
||||
@ -183,7 +183,7 @@ export function GlobalActionBar(): JSX.Element {
|
||||
margin: '1rem 0',
|
||||
}}
|
||||
>
|
||||
{mots.join(' ')}
|
||||
{mots.map((mot, i) => `${i + 1}. ${mot}`).join(' ')}
|
||||
</p>
|
||||
) : (
|
||||
<p style={{ margin: '1rem 0' }}>
|
||||
|
||||
@ -1,11 +1,17 @@
|
||||
import { useEffect, useRef } from 'react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useIdentity } from '../hooks/useIdentity';
|
||||
import { isPairingSatisfied, hasRemotePair } from '../utils/pairing';
|
||||
import {
|
||||
isPairingSatisfied,
|
||||
hasRemotePair,
|
||||
getStoredPairs,
|
||||
removePair,
|
||||
updatePairLabel,
|
||||
} from '../utils/pairing';
|
||||
import { usePairingConnected } from '../hooks/usePairingConnected';
|
||||
import { getStoredRelays } from '../utils/relay';
|
||||
import { isInIframe, sendToChannel } from '../utils/iframeChannel';
|
||||
import type { LocalIdentity } from '../types/identity';
|
||||
import type { LocalIdentity, PairConfig } from '../types/identity';
|
||||
import { PairingSetupBlock } from './PairingSetupBlock';
|
||||
|
||||
function logHomeStatus(
|
||||
@ -42,12 +48,19 @@ export function HomeScreen(): JSX.Element {
|
||||
const { identity, isLoading } = useIdentity();
|
||||
const { connected: pairingConnected } = usePairingConnected();
|
||||
const pairingSatisfied = isPairingSatisfied();
|
||||
const showSetupBlock = !hasRemotePair();
|
||||
const hasRemote = hasRemotePair();
|
||||
const showSetupBlock = !hasRemote;
|
||||
const [pairs, setPairs] = useState<PairConfig[]>(() => getStoredPairs());
|
||||
const [addingDevice, setAddingDevice] = useState(false);
|
||||
const relays = getStoredRelays();
|
||||
const relayStatus = relays.length > 0 ? 'OK' : 'Non configuré';
|
||||
const pairingSetupRef = useRef<HTMLElement | null>(null);
|
||||
const hasScrolledAndLoggedRef = useRef(false);
|
||||
|
||||
const refreshPairs = (): void => {
|
||||
setPairs(getStoredPairs());
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (identity === null) {
|
||||
return;
|
||||
@ -88,6 +101,12 @@ export function HomeScreen(): JSX.Element {
|
||||
return () => window.clearTimeout(t);
|
||||
}, [showSetupBlock, identity]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!addingDevice) {
|
||||
refreshPairs();
|
||||
}
|
||||
}, [addingDevice]);
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div role="status" aria-live="polite" aria-busy="true">
|
||||
@ -112,22 +131,169 @@ export function HomeScreen(): JSX.Element {
|
||||
);
|
||||
}
|
||||
|
||||
const handleRemovePair = (pairUuid: string): void => {
|
||||
const ok = typeof window !== 'undefined' && window.confirm('Supprimer cet appareil du pairing ?');
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
removePair(pairUuid);
|
||||
setPairs(getStoredPairs());
|
||||
};
|
||||
|
||||
const handleLabelChange = (pairUuid: string, label: string): void => {
|
||||
updatePairLabel(pairUuid, label);
|
||||
refreshPairs();
|
||||
};
|
||||
|
||||
const getDeviceDisplayLabel = (pair: PairConfig): string => {
|
||||
if (pair.label !== undefined && pair.label.trim() !== '') {
|
||||
return pair.label.trim();
|
||||
}
|
||||
if (pair.is_local) {
|
||||
return 'Appareil local';
|
||||
}
|
||||
const remoteIndex = pairs.filter((p) => !p.is_local).indexOf(pair) + 1;
|
||||
return `Appareil distant ${remoteIndex}`;
|
||||
};
|
||||
|
||||
return (
|
||||
<main>
|
||||
<main style={{ width: '100%', maxWidth: '100%', minWidth: 0 }}>
|
||||
{pairingConnected && (
|
||||
<p role="status" style={{ fontWeight: 'bold', color: 'var(--color-ok, var(--color-success))' }}>
|
||||
Connecté
|
||||
</p>
|
||||
)}
|
||||
{showSetupBlock && (
|
||||
<section
|
||||
ref={pairingSetupRef}
|
||||
id="pairing-setup"
|
||||
aria-labelledby="pairing-setup-heading"
|
||||
aria-labelledby="pairing-section-heading"
|
||||
style={{ width: '100%', maxWidth: '100%', minWidth: 0, boxSizing: 'border-box' }}
|
||||
>
|
||||
<PairingSetupBlock />
|
||||
</section>
|
||||
<h2 id="pairing-section-heading" style={{ marginTop: 0 }}>
|
||||
Pairing
|
||||
</h2>
|
||||
<p>
|
||||
<strong>Statut pairing</strong>
|
||||
<br />
|
||||
Requis: Oui
|
||||
<br />
|
||||
Satisfait: {pairingSatisfied ? 'Oui' : 'Non'}
|
||||
</p>
|
||||
{showSetupBlock && !addingDevice && (
|
||||
<PairingSetupBlock onDone={refreshPairs} isAddingDevice={false} />
|
||||
)}
|
||||
{hasRemote && !addingDevice && (
|
||||
<>
|
||||
<h3 id="devices-paired-heading">Appareils appariés</h3>
|
||||
<ul
|
||||
aria-labelledby="devices-paired-heading"
|
||||
style={{ listStyle: 'none', padding: 0, marginTop: '0.5rem' }}
|
||||
>
|
||||
{pairs.map((pair) => (
|
||||
<li
|
||||
key={pair.uuid}
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '0.75rem',
|
||||
marginBottom: '0.5rem',
|
||||
padding: '0.5rem',
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: 'var(--color-background)',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
flex: '0 0 auto',
|
||||
minWidth: '8rem',
|
||||
fontSize: '0.875rem',
|
||||
color: 'var(--color-text-secondary)',
|
||||
}}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{getDeviceDisplayLabel(pair)}
|
||||
</span>
|
||||
<input
|
||||
id={`pair-label-${pair.uuid}`}
|
||||
type="text"
|
||||
value={pair.label ?? ''}
|
||||
onChange={(e) => handleLabelChange(pair.uuid, e.target.value)}
|
||||
onBlur={(e) => handleLabelChange(pair.uuid, e.target.value)}
|
||||
placeholder={getDeviceDisplayLabel(pair)}
|
||||
aria-label={
|
||||
pair.is_local ? 'Éditer le label appareil local' : 'Éditer le label appareil distant'
|
||||
}
|
||||
style={{
|
||||
flex: 1,
|
||||
minWidth: 0,
|
||||
padding: '0.25rem 0.5rem',
|
||||
fontSize: '1rem',
|
||||
border: '1px solid var(--color-border)',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: 'var(--color-surface)',
|
||||
color: 'var(--color-text)',
|
||||
}}
|
||||
/>
|
||||
{!pair.is_local && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
handleRemovePair(pair.uuid);
|
||||
}}
|
||||
aria-label={`Supprimer ${getDeviceDisplayLabel(pair)}`}
|
||||
style={{
|
||||
flexShrink: 0,
|
||||
width: 'auto',
|
||||
padding: '0.25rem 0.5rem',
|
||||
fontSize: '0.875rem',
|
||||
color: 'var(--color-error)',
|
||||
backgroundColor: 'transparent',
|
||||
border: '1px solid var(--color-error)',
|
||||
borderRadius: '4px',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
Supprimer
|
||||
</button>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<p style={{ marginTop: '0.75rem' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAddingDevice(true)}
|
||||
aria-label="Ajouter un appareil"
|
||||
>
|
||||
Ajouter un device
|
||||
</button>
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
{addingDevice && (
|
||||
<>
|
||||
<p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAddingDevice(false)}
|
||||
aria-label="Annuler l'ajout d'un appareil"
|
||||
>
|
||||
Annuler
|
||||
</button>
|
||||
</p>
|
||||
<PairingSetupBlock
|
||||
onDone={() => {
|
||||
setAddingDevice(false);
|
||||
refreshPairs();
|
||||
}}
|
||||
isAddingDevice
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import {
|
||||
ensureLocalPairForSetup,
|
||||
getStoredPairs,
|
||||
parseAndValidatePairingWords,
|
||||
removePair,
|
||||
} from '../utils/pairing';
|
||||
import { usePairingWordsContext } from '../contexts/PairingWordsContext';
|
||||
import { useIdentity } from '../hooks/useIdentity';
|
||||
@ -37,6 +38,15 @@ export function PairingDisplayScreen(): JSX.Element {
|
||||
});
|
||||
};
|
||||
|
||||
const handleGenerateAnotherKey = (): void => {
|
||||
const pairs = getStoredPairs();
|
||||
const local = pairs.find((p) => p.is_local);
|
||||
if (local !== undefined) {
|
||||
removePair(local.uuid);
|
||||
}
|
||||
createNewIdentity();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (identity !== null && identity.publicKey !== undefined) {
|
||||
const w = ensureLocalPairForSetup(identity.publicKey);
|
||||
@ -146,6 +156,15 @@ export function PairingDisplayScreen(): JSX.Element {
|
||||
<h2 id="words-2nd-heading" style={{ marginTop: 0, marginBottom: '0.5rem' }}>
|
||||
Mots du 2ᵉ appareil — à copier sur le 1ᵉʳ
|
||||
</h2>
|
||||
<p style={{ marginTop: 0, marginBottom: '0.5rem' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGenerateAnotherKey}
|
||||
aria-label="Générer une autre clé publique"
|
||||
>
|
||||
Générer une autre clé publique
|
||||
</button>
|
||||
</p>
|
||||
{words2nd.length > 0 ? (
|
||||
<div
|
||||
aria-label="Mots 2e appareil"
|
||||
|
||||
@ -6,6 +6,7 @@ import {
|
||||
addRemotePairFromWords,
|
||||
getStoredPairs,
|
||||
parseAndValidatePairingWords,
|
||||
removePair,
|
||||
} from '../utils/pairing';
|
||||
import { useIdentity } from '../hooks/useIdentity';
|
||||
import { getStoredRelays } from '../utils/relay';
|
||||
@ -19,9 +20,17 @@ function buildPairingDisplayUrl(): string {
|
||||
return `${window.location.origin}${PAIRING_DISPLAY_PATH}`;
|
||||
}
|
||||
|
||||
export function PairingSetupBlock(): JSX.Element {
|
||||
export interface PairingSetupBlockProps {
|
||||
onDone?: () => void;
|
||||
isAddingDevice?: boolean;
|
||||
}
|
||||
|
||||
export function PairingSetupBlock({
|
||||
onDone,
|
||||
isAddingDevice = false,
|
||||
}: PairingSetupBlockProps): JSX.Element {
|
||||
const navigate = useNavigate();
|
||||
const { identity } = useIdentity();
|
||||
const { identity, createNewIdentity } = useIdentity();
|
||||
const [words, setWords] = useState<string[]>([]);
|
||||
const [qrDataUrl, setQrDataUrl] = useState<string | null>(null);
|
||||
const [remoteWordsInput, setRemoteWordsInput] = useState<string[]>([]);
|
||||
@ -42,6 +51,15 @@ export function PairingSetupBlock(): JSX.Element {
|
||||
});
|
||||
};
|
||||
|
||||
const handleGenerateAnotherKey = (): void => {
|
||||
const pairs = getStoredPairs();
|
||||
const local = pairs.find((p) => p.is_local);
|
||||
if (local !== undefined) {
|
||||
removePair(local.uuid);
|
||||
}
|
||||
createNewIdentity();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (identity !== null && identity.publicKey !== undefined) {
|
||||
const w = ensureLocalPairForSetup(identity.publicKey);
|
||||
@ -71,22 +89,20 @@ export function PairingSetupBlock(): JSX.Element {
|
||||
setRemoteError('Mots invalides. 17 mots requis.');
|
||||
return;
|
||||
}
|
||||
const pair = addRemotePairFromWords(parsed, []);
|
||||
if (pair === null) {
|
||||
const newPair = addRemotePairFromWords(parsed, []);
|
||||
if (newPair === null) {
|
||||
setRemoteError('Mots invalides. Vérifiez la saisie.');
|
||||
return;
|
||||
}
|
||||
const pairs = getStoredPairs();
|
||||
const local = pairs.find((p) => p.is_local);
|
||||
const remote = pairs.find((p) => !p.is_local);
|
||||
if (
|
||||
identity === null ||
|
||||
local === undefined ||
|
||||
remote === undefined ||
|
||||
identity.privateKey === undefined
|
||||
) {
|
||||
if (identity === null || local === undefined || identity.privateKey === undefined) {
|
||||
setRemoteWordsInput([]);
|
||||
if (onDone !== undefined) {
|
||||
onDone();
|
||||
} else {
|
||||
navigate('/manage-pairs');
|
||||
}
|
||||
return;
|
||||
}
|
||||
const relays = getStoredRelays().filter((r) => r.enabled);
|
||||
@ -94,13 +110,12 @@ export function PairingSetupBlock(): JSX.Element {
|
||||
setRemoteError('Aucun relais activé. Configurez les relais pour finaliser le pairing.');
|
||||
return;
|
||||
}
|
||||
// Use pair's publicKey if available (will be updated from signatures if not)
|
||||
const remotePublicKey = remote.publicKey;
|
||||
const remotePublicKey = newPair.publicKey;
|
||||
setIsConfirming(true);
|
||||
try {
|
||||
await runDevice1Confirmation({
|
||||
pairLocal: local.uuid,
|
||||
pairRemote: remote.uuid,
|
||||
pairRemote: newPair.uuid,
|
||||
identity,
|
||||
relays,
|
||||
remotePublicKey,
|
||||
@ -115,12 +130,20 @@ export function PairingSetupBlock(): JSX.Element {
|
||||
}
|
||||
setIsConfirming(false);
|
||||
setRemoteWordsInput([]);
|
||||
if (onDone !== undefined) {
|
||||
onDone();
|
||||
} else {
|
||||
navigate('/manage-pairs');
|
||||
}
|
||||
};
|
||||
|
||||
const setupHeading = isAddingDevice
|
||||
? 'Ajouter un appareil'
|
||||
: 'Configurer le pairing avec un 2ᵉ appareil';
|
||||
|
||||
return (
|
||||
<div role="region" aria-labelledby="pairing-setup-heading">
|
||||
<h3 id="pairing-setup-heading">Configurer le pairing avec un 2ᵉ appareil</h3>
|
||||
<div role="region" aria-labelledby="pairing-setup-block-heading">
|
||||
<h3 id="pairing-setup-block-heading">{setupHeading}</h3>
|
||||
{words.length > 0 && (
|
||||
<>
|
||||
{!hasCopiedToSecondDevice ? (
|
||||
@ -128,6 +151,15 @@ export function PairingSetupBlock(): JSX.Element {
|
||||
<p>
|
||||
<strong>Mots du 1ᵉʳ appareil</strong> — à saisir sur le 2ᵉ (QR) :
|
||||
</p>
|
||||
<p style={{ marginTop: 0, marginBottom: '0.5rem' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleGenerateAnotherKey}
|
||||
aria-label="Générer une autre clé publique"
|
||||
>
|
||||
Générer une autre clé publique
|
||||
</button>
|
||||
</p>
|
||||
<div
|
||||
aria-label="Mots 1er appareil"
|
||||
style={{
|
||||
@ -199,13 +231,17 @@ export function PairingSetupBlock(): JSX.Element {
|
||||
type="button"
|
||||
onClick={() => setHasCopiedToSecondDevice(true)}
|
||||
>
|
||||
J'ai copié ces mots sur mon deuxième device
|
||||
{isAddingDevice
|
||||
? "J'ai copié ces mots sur le nouvel appareil"
|
||||
: "J'ai copié ces mots sur mon deuxième device"}
|
||||
</button>
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
<section aria-labelledby="remote-words-heading">
|
||||
<h4 id="remote-words-heading">Mots du 2ᵉ appareil</h4>
|
||||
<h4 id="remote-words-heading">
|
||||
{isAddingDevice ? 'Mots du nouvel appareil' : 'Mots du 2ᵉ appareil'}
|
||||
</h4>
|
||||
<form
|
||||
onSubmit={(ev) => void handleSubmitRemote(ev)}
|
||||
aria-label="Saisir les mots du 2e appareil"
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import { useState, useRef, useEffect, KeyboardEvent, ChangeEvent, FocusEvent } from 'react';
|
||||
import { BIP32_WORDLIST } from '../utils/bip32';
|
||||
import { BIP32_MAX_WORD_LENGTH, BIP32_WORDLIST } from '../utils/bip32';
|
||||
|
||||
interface WordInputGridProps {
|
||||
value: string[];
|
||||
@ -189,32 +189,69 @@ export function WordInputGrid({
|
||||
}
|
||||
};
|
||||
|
||||
const allVisible = visibleWords.size === WORD_COUNT;
|
||||
const toggleAllVisibility = (): void => {
|
||||
setVisibleWords(allVisible ? new Set() : new Set(Array.from({ length: WORD_COUNT }, (_, i) => i)));
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div style={{ marginBottom: '0.5rem' }}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleAllVisibility}
|
||||
aria-label={allVisible ? 'Masquer les mots' : 'Afficher les mots'}
|
||||
title={allVisible ? 'Masquer les mots' : 'Afficher les mots'}
|
||||
>
|
||||
{allVisible ? 'Masquer les mots' : 'Afficher les mots'}
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
role="group"
|
||||
aria-label={ariaLabel}
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(4, 1fr)',
|
||||
gridTemplateColumns: '1fr',
|
||||
gap: '0.5rem',
|
||||
marginBottom: focusedIndex !== null && suggestions.length > 0 ? '0.5rem' : '0',
|
||||
}}
|
||||
>
|
||||
{words.map((word, index) => (
|
||||
<div key={index} style={{ position: 'relative' }}>
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'stretch',
|
||||
gap: '0',
|
||||
border: '1px solid var(--color-border, #ccc)',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: 'var(--color-background)',
|
||||
}}
|
||||
>
|
||||
<span
|
||||
aria-hidden="true"
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
padding: '0.5rem 0.5rem 0.5rem 0.75rem',
|
||||
fontSize: '1rem',
|
||||
fontFamily: 'monospace',
|
||||
color: 'var(--color-text-secondary, #666)',
|
||||
borderRight: '1px solid var(--color-border, #ccc)',
|
||||
minWidth: '2.25rem',
|
||||
}}
|
||||
>
|
||||
{index + 1}.
|
||||
</span>
|
||||
<label
|
||||
htmlFor={`${id ?? 'word'}-${index}`}
|
||||
style={{
|
||||
flex: 1,
|
||||
margin: 0,
|
||||
minWidth: 0,
|
||||
display: 'block',
|
||||
fontSize: '0.875rem',
|
||||
marginBottom: '0.25rem',
|
||||
color: 'var(--color-text-secondary, #666)',
|
||||
}}
|
||||
>
|
||||
{index + 1}
|
||||
</label>
|
||||
<div style={{ position: 'relative' }}>
|
||||
<input
|
||||
ref={(el) => {
|
||||
inputRefs.current[index] = el;
|
||||
@ -234,55 +271,31 @@ export function WordInputGrid({
|
||||
onBlur={handleBlur}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
name={`bip32-w-${id ?? 'w'}-${index}`}
|
||||
data-lpignore="true"
|
||||
data-form-type="other"
|
||||
data-1p-ignore="true"
|
||||
aria-describedby={
|
||||
index === 0 && ariaDescribedBy !== undefined
|
||||
? ariaDescribedBy
|
||||
: undefined
|
||||
}
|
||||
size={BIP32_MAX_WORD_LENGTH}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '0.5rem',
|
||||
paddingRight: word.length > 0 ? '2.5rem' : '0.5rem',
|
||||
width: `${BIP32_MAX_WORD_LENGTH}ch`,
|
||||
minWidth: `${BIP32_MAX_WORD_LENGTH}ch`,
|
||||
maxWidth: `${BIP32_MAX_WORD_LENGTH}ch`,
|
||||
padding: '0.5rem 0.75rem',
|
||||
fontSize: '1rem',
|
||||
fontFamily: 'monospace',
|
||||
border: '1px solid var(--color-border, #ccc)',
|
||||
borderRadius: '4px',
|
||||
backgroundColor: 'var(--color-background)',
|
||||
border: 'none',
|
||||
borderRadius: 0,
|
||||
backgroundColor: 'transparent',
|
||||
color: 'var(--color-text)',
|
||||
boxSizing: 'content-box',
|
||||
}}
|
||||
/>
|
||||
{word.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setVisibleWords((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(index)) {
|
||||
next.delete(index);
|
||||
} else {
|
||||
next.add(index);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
}}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: '0.25rem',
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
cursor: 'pointer',
|
||||
padding: '0.25rem',
|
||||
fontSize: '0.875rem',
|
||||
color: 'var(--color-text-secondary, #666)',
|
||||
}}
|
||||
aria-label={`${visibleWords.has(index) ? 'Masquer' : 'Afficher'} le mot ${index + 1}`}
|
||||
title={`${visibleWords.has(index) ? 'Masquer' : 'Afficher'} le mot ${index + 1}`}
|
||||
>
|
||||
{visibleWords.has(index) ? '👁' : '👁🗨'}
|
||||
</button>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
{focusedIndex === index && suggestions.length > 0 && (
|
||||
<div
|
||||
|
||||
@ -284,7 +284,8 @@ a:focus-visible {
|
||||
}
|
||||
|
||||
/* Screen-reader only (e.g. login state machine status) */
|
||||
.sr-only {
|
||||
.sr-only,
|
||||
.visually-hidden {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
|
||||
@ -23,7 +23,10 @@ export interface RelayConfig {
|
||||
|
||||
/**
|
||||
* Pair configuration (local device or remote).
|
||||
* publicKey: remote device identity public key (hex), for ECDH encryption of pairing messages.
|
||||
* publicKey: primary public key (hex), for ECDH encryption of pairing messages.
|
||||
* publicKeys: optional list of additional public keys (e.g. derived from identity). When present,
|
||||
* the pair is considered to "own" any of these keys for ownership checks.
|
||||
* label: optional persistent user-defined label for display (e.g. "Mon téléphone").
|
||||
*/
|
||||
export interface PairConfig {
|
||||
uuid: string;
|
||||
@ -31,6 +34,9 @@ export interface PairConfig {
|
||||
is_local: boolean;
|
||||
can_sign: boolean;
|
||||
publicKey?: string;
|
||||
/** Additional public keys (e.g. derived). Ownership = publicKey or any of publicKeys. */
|
||||
publicKeys?: string[];
|
||||
label?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -265,6 +265,14 @@ export const BIP32_WORDLIST = [
|
||||
'young', 'youth', 'zebra', 'zero', 'zone', 'zoo',
|
||||
];
|
||||
|
||||
/**
|
||||
* Maximum character length of any word in BIP32_WORDLIST.
|
||||
* Used to size word input fields uniformly.
|
||||
*/
|
||||
export const BIP32_MAX_WORD_LENGTH: number = Math.max(
|
||||
...BIP32_WORDLIST.map((w) => w.length),
|
||||
);
|
||||
|
||||
/**
|
||||
* Convert UUID to BIP32 word list.
|
||||
* UUID is converted to bytes, then to BIP32 path, then to words.
|
||||
|
||||
@ -10,6 +10,10 @@ import { hmac } from '@noble/hashes/hmac';
|
||||
import { sha256 } from '@noble/hashes/sha256';
|
||||
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
|
||||
|
||||
/** secp256k1 curve order (number of points on the curve). */
|
||||
const SECP256K1_ORDER =
|
||||
0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141n;
|
||||
|
||||
// Require hmacSha256Sync for secp256k1 sign() (RFC6979). Set once at module load.
|
||||
if (secpEtc.hmacSha256Sync === undefined) {
|
||||
secpEtc.hmacSha256Sync = (k: Uint8Array, ...m: Uint8Array[]): Uint8Array =>
|
||||
@ -44,6 +48,67 @@ export function publicKeyFromPrivateKey(privateKeyHex: string): string {
|
||||
return bytesToHex(pub);
|
||||
}
|
||||
|
||||
const DERIVE_DOMAIN = 'userwallet-derive-v1';
|
||||
|
||||
/**
|
||||
* Derives a child key pair deterministically from a parent private key.
|
||||
* Index must be >= 0. Same index always yields the same key pair.
|
||||
* Uses HMAC-SHA256(parentKey, domain || index) then reduces to valid secp256k1 scalar.
|
||||
*/
|
||||
export function deriveChildKeyPair(
|
||||
parentPrivateKeyHex: string,
|
||||
index: number,
|
||||
): KeyPair {
|
||||
const parentBytes = hexToBytes(parentPrivateKeyHex);
|
||||
const indexBytes = new TextEncoder().encode(`${DERIVE_DOMAIN}-${index}`);
|
||||
const h = hmac(sha256, parentBytes, indexBytes);
|
||||
const num = BigInt('0x' + bytesToHex(h));
|
||||
const scalar = (num % (SECP256K1_ORDER - 1n)) + 1n;
|
||||
const privHex = scalar.toString(16).padStart(64, '0');
|
||||
const privateKeyBytes = hexToBytes(privHex);
|
||||
const publicKey = bytesToHex(getPublicKey(privateKeyBytes, true));
|
||||
return { privateKey: privHex, publicKey };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the main public key plus derived public keys for indices 0..count-1.
|
||||
* Index 0 is the first derived key, etc. Total length is 1 + count.
|
||||
*/
|
||||
export function getDerivedPublicKeys(
|
||||
parentPrivateKeyHex: string,
|
||||
count: number,
|
||||
): string[] {
|
||||
const main = publicKeyFromPrivateKey(parentPrivateKeyHex);
|
||||
const out: string[] = [main];
|
||||
for (let i = 0; i < count; i++) {
|
||||
const { publicKey } = deriveChildKeyPair(parentPrivateKeyHex, i);
|
||||
out.push(publicKey);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given public key is the main key or any derived key (indices 0..maxDerived-1).
|
||||
* Fast: main key check O(1), then at most maxDerived derivations. Pass maxDerived to limit search.
|
||||
*/
|
||||
export function publicKeyBelongsToIdentity(
|
||||
identityPrivateKeyHex: string,
|
||||
publicKeyHex: string,
|
||||
maxDerived: number = 0,
|
||||
): boolean {
|
||||
const main = publicKeyFromPrivateKey(identityPrivateKeyHex);
|
||||
if (main === publicKeyHex) {
|
||||
return true;
|
||||
}
|
||||
for (let i = 0; i < maxDerived; i++) {
|
||||
const { publicKey } = deriveChildKeyPair(identityPrivateKeyHex, i);
|
||||
if (publicKey === publicKeyHex) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Signs a message with a private key using secp256k1.
|
||||
* The message is hashed with SHA-256 before signing.
|
||||
|
||||
@ -4,6 +4,10 @@ import {
|
||||
publicKeyToBip32Words,
|
||||
bip32WordsToPublicKey,
|
||||
} from './bip32';
|
||||
import {
|
||||
getDerivedPublicKeys,
|
||||
publicKeyBelongsToIdentity,
|
||||
} from './crypto';
|
||||
import type { PairConfig } from '../types/identity';
|
||||
|
||||
const STORAGE_KEY_PAIRS = 'userwallet_pairs';
|
||||
@ -192,3 +196,84 @@ export function updatePairPublicKey(pairUuid: string, publicKey: string): void {
|
||||
pair.publicKey = publicKey;
|
||||
storePairs(pairs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a pair's persistent label (for display).
|
||||
*/
|
||||
export function updatePairLabel(pairUuid: string, label: string): void {
|
||||
const pairs = getStoredPairs();
|
||||
const pair = pairs.find((p) => p.uuid === pairUuid);
|
||||
if (pair === undefined) {
|
||||
return;
|
||||
}
|
||||
pair.label = label;
|
||||
storePairs(pairs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all public keys for a pair. For local pair, pass identity private key to include
|
||||
* derived keys (main + indices 0..derivedCount-1). For remote, returns publicKey + publicKeys.
|
||||
*/
|
||||
export function getPairPublicKeys(
|
||||
pair: PairConfig,
|
||||
identityPrivateKeyHex?: string,
|
||||
derivedCount: number = 0,
|
||||
): string[] {
|
||||
if (pair.is_local && identityPrivateKeyHex !== undefined) {
|
||||
return getDerivedPublicKeys(identityPrivateKeyHex, derivedCount);
|
||||
}
|
||||
const keys: string[] = [];
|
||||
if (pair.publicKey !== undefined) {
|
||||
keys.push(pair.publicKey);
|
||||
}
|
||||
if (pair.publicKeys !== undefined) {
|
||||
for (const k of pair.publicKeys) {
|
||||
if (k !== pair.publicKey && !keys.includes(k)) {
|
||||
keys.push(k);
|
||||
}
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the pair "owns" the given public key. Fast for local pair when identity
|
||||
* private key is passed (main key O(1), then at most maxDerived derivations).
|
||||
*/
|
||||
export function pairContainsPublicKey(
|
||||
pair: PairConfig,
|
||||
publicKeyHex: string,
|
||||
identityPrivateKeyHex?: string,
|
||||
maxDerived: number = 0,
|
||||
): boolean {
|
||||
if (pair.is_local && identityPrivateKeyHex !== undefined) {
|
||||
return publicKeyBelongsToIdentity(
|
||||
identityPrivateKeyHex,
|
||||
publicKeyHex,
|
||||
maxDerived,
|
||||
);
|
||||
}
|
||||
if (pair.publicKey === publicKeyHex) {
|
||||
return true;
|
||||
}
|
||||
return pair.publicKeys?.includes(publicKeyHex) ?? false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a derived public key to a pair (appends to publicKeys). Does not duplicate publicKey.
|
||||
*/
|
||||
export function addPairPublicKey(pairUuid: string, publicKeyHex: string): void {
|
||||
const pairs = getStoredPairs();
|
||||
const pair = pairs.find((p) => p.uuid === pairUuid);
|
||||
if (pair === undefined) {
|
||||
return;
|
||||
}
|
||||
const existing = pair.publicKey === publicKeyHex || pair.publicKeys?.includes(publicKeyHex);
|
||||
if (existing) {
|
||||
return;
|
||||
}
|
||||
const next = pair.publicKeys ?? [];
|
||||
next.push(publicKeyHex);
|
||||
pair.publicKeys = next;
|
||||
storePairs(pairs);
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ export default defineConfig({
|
||||
},
|
||||
server: {
|
||||
port: 3018,
|
||||
strictPort: false,
|
||||
strictPort: true,
|
||||
},
|
||||
preview: {
|
||||
port: 3018,
|
||||
|
||||
98
verify-chain-alignment.sh
Executable file
98
verify-chain-alignment.sh
Executable file
@ -0,0 +1,98 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Verify that Dashboard, Miner and Signet node are aligned on the same chain
|
||||
# (expected height ~11535). Run on the bitcoin machine (192.168.1.105).
|
||||
#
|
||||
# Usage: ./verify-chain-alignment.sh
|
||||
#
|
||||
# Author: 4NK Team
|
||||
# Date: 2026-02-02
|
||||
|
||||
set -e
|
||||
|
||||
CONTAINER_NAME="bitcoin-signet-instance"
|
||||
DASHBOARD_URL="${DASHBOARD_URL:-http://localhost:3020}"
|
||||
EXPECTED_HEIGHT_MIN="${EXPECTED_HEIGHT_MIN:-11000}"
|
||||
EXPECTED_HEIGHT_MAX="${EXPECTED_HEIGHT_MAX:-12000}"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
DASHBOARD_TMP="$SCRIPT_DIR/.verify-chain-dashboard-$$.json"
|
||||
trap 'rm -f "$DASHBOARD_TMP"' EXIT
|
||||
|
||||
echo "=== Chain alignment check (Dashboard / Miner / Signet) ==="
|
||||
echo ""
|
||||
|
||||
# 1. Node: getblockchaininfo (datadir = BITCOIN_DIR from container, default /root/.bitcoin)
|
||||
if ! docker ps --format "{{.Names}}" | grep -q "^${CONTAINER_NAME}$"; then
|
||||
echo "FAIL: Container $CONTAINER_NAME is not running."
|
||||
exit 1
|
||||
fi
|
||||
BITCOIN_DATADIR=$(docker exec "$CONTAINER_NAME" printenv BITCOIN_DIR 2>/dev/null || echo "/root/.bitcoin")
|
||||
|
||||
NODE_JSON=$(sudo docker exec "$CONTAINER_NAME" bitcoin-cli -datadir="$BITCOIN_DATADIR" getblockchaininfo 2>/dev/null || true)
|
||||
if [ -z "$NODE_JSON" ]; then
|
||||
echo "FAIL: Could not get getblockchaininfo from node."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
NODE_CHAIN=$(echo "$NODE_JSON" | grep -o '"chain"[[:space:]]*:[[:space:]]*"[^"]*"' | sed 's/.*:[[:space:]]*"\([^"]*\)".*/\1/')
|
||||
NODE_BLOCKS=$(echo "$NODE_JSON" | grep -o '"blocks"[[:space:]]*:[[:space:]]*[0-9]*' | grep -o '[0-9]*$')
|
||||
|
||||
if [ -z "$NODE_BLOCKS" ]; then
|
||||
echo "FAIL: Could not parse blocks from node getblockchaininfo."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Node (signet): chain=$NODE_CHAIN blocks=$NODE_BLOCKS"
|
||||
|
||||
if [ "$NODE_CHAIN" != "signet" ]; then
|
||||
echo "WARN: Node chain is not 'signet' (got '$NODE_CHAIN')."
|
||||
fi
|
||||
|
||||
# 2. Dashboard: /api/blockchain/info
|
||||
DASHBOARD_HTTP_CODE=$(curl -s -o "$DASHBOARD_TMP" -w "%{http_code}" "$DASHBOARD_URL/api/blockchain/info" 2>/dev/null || echo "000")
|
||||
DASHBOARD_JSON=""
|
||||
if [ -f "$DASHBOARD_TMP" ]; then
|
||||
DASHBOARD_JSON=$(cat "$DASHBOARD_TMP")
|
||||
fi
|
||||
|
||||
if [ "$DASHBOARD_HTTP_CODE" = "403" ]; then
|
||||
echo "Dashboard: (403 Forbidden - if ALLOWED_SOURCE_IP is set, use DASHBOARD_URL from proxy or allowed host)"
|
||||
elif [ -n "$DASHBOARD_JSON" ] && [ "$DASHBOARD_HTTP_CODE" = "200" ]; then
|
||||
DASHBOARD_BLOCKS=$(echo "$DASHBOARD_JSON" | grep -o '"blocks"[[:space:]]*:[[:space:]]*[0-9]*' | grep -o '[0-9]*$')
|
||||
if [ -n "$DASHBOARD_BLOCKS" ]; then
|
||||
echo "Dashboard: blocks=$DASHBOARD_BLOCKS"
|
||||
if [ "$DASHBOARD_BLOCKS" != "$NODE_BLOCKS" ]; then
|
||||
echo "WARN: Dashboard blocks ($DASHBOARD_BLOCKS) != Node blocks ($NODE_BLOCKS)."
|
||||
else
|
||||
echo "OK: Dashboard and Node show same height."
|
||||
fi
|
||||
else
|
||||
echo "Dashboard: (could not parse blocks)"
|
||||
fi
|
||||
else
|
||||
if [ "$DASHBOARD_HTTP_CODE" != "000" ]; then
|
||||
echo "Dashboard: (HTTP $DASHBOARD_HTTP_CODE at $DASHBOARD_URL)"
|
||||
else
|
||||
echo "Dashboard: (not reachable at $DASHBOARD_URL)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 3. Expected height range (~11535)
|
||||
echo ""
|
||||
if [ -n "$NODE_BLOCKS" ] && [ "$NODE_BLOCKS" -eq "$NODE_BLOCKS" ] 2>/dev/null; then
|
||||
if [ "$NODE_BLOCKS" -ge "$EXPECTED_HEIGHT_MIN" ] && [ "$NODE_BLOCKS" -le "$EXPECTED_HEIGHT_MAX" ]; then
|
||||
echo "OK: Node height $NODE_BLOCKS is in expected range [$EXPECTED_HEIGHT_MIN..$EXPECTED_HEIGHT_MAX]."
|
||||
else
|
||||
echo "WARN: Node height $NODE_BLOCKS is outside expected range [$EXPECTED_HEIGHT_MIN..$EXPECTED_HEIGHT_MAX]."
|
||||
echo " If the chain was lost (e.g. container recreated without persistent volume), see:"
|
||||
echo " fixKnowledge/signet-chain-lost-volume-persistent.md"
|
||||
echo " restore-signet-from-backup.sh"
|
||||
fi
|
||||
else
|
||||
echo "WARN: Could not validate node height (got '$NODE_BLOCKS')."
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "Done."
|
||||
117
verify-dashboard-signet.sh
Executable file
117
verify-dashboard-signet.sh
Executable file
@ -0,0 +1,117 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Verify that https://dashboard.certificator.4nkweb.com/ shows the custom signet
|
||||
# (~11535 blocks). Run on the machine bitcoin (192.168.1.105).
|
||||
#
|
||||
# Checks:
|
||||
# 1. Node RPC (127.0.0.1:38332) returns getblockchaininfo with ~11535 blocks
|
||||
# 2. Dashboard service (signet-dashboard) is running and uses same RPC
|
||||
# 3. Dashboard API /api/blockchain/info returns blocks
|
||||
#
|
||||
# Usage: ./verify-dashboard-signet.sh
|
||||
#
|
||||
# Author: 4NK Team
|
||||
# Date: 2026-02-02
|
||||
|
||||
set -e
|
||||
|
||||
RPC_HOST="${BITCOIN_RPC_HOST:-127.0.0.1}"
|
||||
RPC_PORT="${BITCOIN_RPC_PORT:-38332}"
|
||||
RPC_USER="${BITCOIN_RPC_USER:-bitcoin}"
|
||||
RPC_PASS="${BITCOIN_RPC_PASSWORD:-bitcoin}"
|
||||
DASHBOARD_URL="${DASHBOARD_URL:-http://localhost:3020}"
|
||||
EXPECTED_MIN="${EXPECTED_HEIGHT_MIN:-11000}"
|
||||
EXPECTED_MAX="${EXPECTED_HEIGHT_MAX:-12000}"
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
echo "=== Verify dashboard shows custom signet ==="
|
||||
echo "RPC: $RPC_HOST:$RPC_PORT Dashboard: $DASHBOARD_URL"
|
||||
echo ""
|
||||
|
||||
# 0. One node only: bitcoin-signet-instance on 38332 (same as Mempool host.docker.internal:38332)
|
||||
echo "0. Checking single node (bitcoin-signet-instance = Mempool host.docker.internal:$RPC_PORT)..."
|
||||
if command -v docker &>/dev/null && docker ps --format '{{.Names}}' 2>/dev/null | grep -q '^bitcoin-signet-instance$'; then
|
||||
if ss -tlnp 2>/dev/null | grep -q ":${RPC_PORT}"; then
|
||||
echo " OK: Port $RPC_PORT in use; bitcoin-signet-instance is running (same node as Mempool)."
|
||||
else
|
||||
echo " WARN: bitcoin-signet-instance running but port $RPC_PORT not found in ss (check mapping)."
|
||||
fi
|
||||
# Prefer named volume signet-bitcoin-data (same as restore-signet-from-backup.sh and update-signet.sh)
|
||||
if docker inspect bitcoin-signet-instance --format '{{range .Mounts}}{{.Name}} {{end}}' 2>/dev/null | grep -q 'signet-bitcoin-data'; then
|
||||
echo " OK: Container uses volume signet-bitcoin-data (persistent chain)."
|
||||
else
|
||||
echo " WARN: Container not using volume signet-bitcoin-data (chain may be lost on container replace). Run update-signet.sh or restore-signet-from-backup.sh for correct config."
|
||||
fi
|
||||
else
|
||||
echo " WARN: bitcoin-signet-instance not running; another process may be on $RPC_PORT (verify it is the signet node)."
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# 1. Test RPC (same node as Mempool / Dashboard)
|
||||
echo "1. Testing node RPC ($RPC_HOST:$RPC_PORT)..."
|
||||
RPC_JSON=$(curl -s --user "$RPC_USER:$RPC_PASS" \
|
||||
--data-binary '{"jsonrpc":"1.0","id":"test","method":"getblockchaininfo","params":[]}' \
|
||||
-H "content-type: text/plain;" "http://${RPC_HOST}:${RPC_PORT}/" 2>/dev/null || true)
|
||||
BLOCKS=$(echo "$RPC_JSON" | grep -o '"blocks"[[:space:]]*:[[:space:]]*[0-9]*' | head -1 | grep -o '[0-9]*$' || echo "")
|
||||
if [ -z "$BLOCKS" ]; then
|
||||
echo " FAIL: Cannot reach node at $RPC_HOST:$RPC_PORT (dashboard will show '-').
|
||||
Ensure: bitcoin-signet-instance or bitcoind is running and listening on 38332."
|
||||
exit 1
|
||||
fi
|
||||
echo " Node: blocks=$BLOCKS (expected ~11535)"
|
||||
if [ "$BLOCKS" -ge "$EXPECTED_MIN" ] && [ "$BLOCKS" -le "$EXPECTED_MAX" ]; then
|
||||
echo " OK: Height in expected range (custom signet, same as Mempool)."
|
||||
else
|
||||
echo " WARN: Height outside [$EXPECTED_MIN..$EXPECTED_MAX]. Dashboard will show this height."
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# 2. Dashboard service
|
||||
echo "2. Checking signet-dashboard service..."
|
||||
if systemctl is-active --quiet signet-dashboard 2>/dev/null; then
|
||||
echo " OK: signet-dashboard is running."
|
||||
else
|
||||
echo " FAIL: signet-dashboard is not running.
|
||||
Start: sudo systemctl start signet-dashboard"
|
||||
exit 1
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# 3. Dashboard API
|
||||
echo "3. Checking Dashboard API ($DASHBOARD_URL/api/blockchain/info)..."
|
||||
TMP_JSON="$SCRIPT_DIR/.verify-dashboard-$$.json"
|
||||
trap 'rm -f "$TMP_JSON"' EXIT
|
||||
DASH_HTTP=$(curl -s -o "$TMP_JSON" -w "%{http_code}" "$DASHBOARD_URL/api/blockchain/info" 2>/dev/null || echo "000")
|
||||
if [ "$DASH_HTTP" = "200" ]; then
|
||||
DASH_BLOCKS=$(grep -o '"blocks"[[:space:]]*:[[:space:]]*[0-9]*' "$TMP_JSON" | head -1 | grep -o '[0-9]*$' || echo "")
|
||||
if [ -n "$DASH_BLOCKS" ]; then
|
||||
echo " OK: Dashboard API returns blocks=$DASH_BLOCKS."
|
||||
if [ "$DASH_BLOCKS" -ge "$EXPECTED_MIN" ] && [ "$DASH_BLOCKS" -le "$EXPECTED_MAX" ]; then
|
||||
echo " OK: Height in expected range (custom signet)."
|
||||
else
|
||||
echo " WARN: Height $DASH_BLOCKS outside [$EXPECTED_MIN..$EXPECTED_MAX]."
|
||||
fi
|
||||
else
|
||||
echo " WARN: Could not parse blocks from Dashboard API."
|
||||
fi
|
||||
else
|
||||
if [ "$DASH_HTTP" = "403" ]; then
|
||||
echo " INFO: Dashboard returned 403 (ALLOWED_SOURCE_IP: call from proxy or allowed host).
|
||||
From proxy: curl https://dashboard.certificator.4nkweb.com/api/blockchain/info"
|
||||
else
|
||||
echo " FAIL: Dashboard API returned HTTP $DASH_HTTP.
|
||||
Check: sudo journalctl -u signet-dashboard -n 50"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
echo ""
|
||||
|
||||
echo "=== Summary ==="
|
||||
echo "For https://dashboard.certificator.4nkweb.com/ to show the custom signet:"
|
||||
echo " - Proxy nginx: dashboard.certificator.4nkweb.com -> 192.168.1.105:3020 (configure-nginx-proxy.sh)"
|
||||
echo " - Machine 105: signet-dashboard listening on 192.168.1.105:3020, BITCOIN_RPC_HOST=127.0.0.1, BITCOIN_RPC_PORT=38332"
|
||||
echo " - Node on 105: listening on 0.0.0.0:38332 (bitcoin.conf: signet=1, rpcbind=0.0.0.0:38332)"
|
||||
echo " - Run fix: ./fix-dashboard-anchor-chain.sh (tests RPC, restarts dashboard and anchorage)"
|
||||
echo ""
|
||||
@ -199,6 +199,51 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2>Dérivation de clés et clés multiples par pair</h2>
|
||||
<p>
|
||||
Chaque pair (appareil local ou distant) peut être associé à <strong>plusieurs clés publiques</strong> :
|
||||
une clé principale et, optionnellement, une liste de clés supplémentaires. Cela permet d’utiliser
|
||||
plusieurs clés pour un même pair sans multiplier les secrets à gérer.
|
||||
</p>
|
||||
<h3>Dérivation déterministe à partir d’une clé privée</h3>
|
||||
<p>
|
||||
À partir d’une seule clé privée (celle de l’identité), le système peut calculer d’autres paires
|
||||
clé privée / clé publique de façon <strong>déterministe</strong> : pour un index entier (0, 1, 2, …),
|
||||
le calcul produit toujours la même paire. Aucune donnée aléatoire n’est utilisée pour cette dérivation.
|
||||
</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 l’index)
|
||||
produit 32 octets, puis une réduction modulo l’ordre de la courbe secp256k1 donne un nombre
|
||||
valide comme clé privée sur la courbe ; la clé publique enfant est obtenue par multiplication
|
||||
du point de base de la courbe par ce nombre (format compressé, 66 caractères hex).</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 l’identité (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 qu’une 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 qu’une égalité est trouvée, la réponse est oui.</li>
|
||||
</ol>
|
||||
<p>
|
||||
Le coût est donc une seule opération pour la clé principale, puis au plus N opérations si on teste
|
||||
N clés dérivées. En limitant N (par exemple 100), la vérification reste rapide.
|
||||
</p>
|
||||
<p>
|
||||
Pour un pair distant (autre appareil), les clés sont celles enregistrées pour ce pair (clé principale
|
||||
et liste optionnelle) : la vérification consiste à tester si la clé donnée est dans cet ensemble,
|
||||
sans dérivation côté local.
|
||||
</p>
|
||||
|
||||
<h2>Le workflow complet</h2>
|
||||
<p>
|
||||
Voici ce qui se passe quand vous envoyez un message sécurisé, étape par étape :
|
||||
@ -414,12 +459,14 @@
|
||||
<h3>Fichiers source</h3>
|
||||
<ul>
|
||||
<li><code>userwallet/src/utils/encryption.ts</code> : encryptWithECDH, decryptWithECDH</li>
|
||||
<li><code>userwallet/src/utils/crypto.ts</code> : signMessage, verifySignature, generateChallenge</li>
|
||||
<li><code>userwallet/src/utils/crypto.ts</code> : signMessage, verifySignature, generateChallenge, deriveChildKeyPair, getDerivedPublicKeys, publicKeyBelongsToIdentity</li>
|
||||
<li><code>userwallet/src/utils/pairing.ts</code> : getPairPublicKeys, pairContainsPublicKey, addPairPublicKey</li>
|
||||
<li><code>userwallet/src/utils/relay.ts</code> : postMessageChiffre, postSignature, postKey</li>
|
||||
<li><code>userwallet/src/utils/collectSignatures.ts</code> : runCollectLoop, fetchSignaturesForHash</li>
|
||||
<li><code>userwallet/src/utils/loginValidation.ts</code> : hasEnoughSignatures, checkDependenciesSatisfied</li>
|
||||
<li><code>service-login-verify/</code> : verifyLoginProof (côté parent)</li>
|
||||
</ul>
|
||||
<p>Dérivation de clés et clés multiples par pair : <code>docs/USERWALLET_KEY_DERIVATION.md</code>.</p>
|
||||
|
||||
<h3>Collecte des signatures</h3>
|
||||
<table class="algo-table">
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user