diff --git a/ETAT_SYSTEME.md b/ETAT_SYSTEME.md deleted file mode 100644 index 62ff708..0000000 --- a/ETAT_SYSTEME.md +++ /dev/null @@ -1,87 +0,0 @@ -# État du Système Bitcoin Signet - -**Date** : 2026-01-23 - -## ✅ État de la Chaîne - -- **Chaîne** : Signet -- **Hauteur** : 0 (bloc genesis) -- **Hash du meilleur bloc** : `00000008819873e925422c1ff0f99f7cc9bbb232af63a077a480a3633bee1ef6` -- **Statut** : ✅ **UP** - Chaîne opérationnelle - -## ✅ État du Miner - -- **bitcoind** : ✅ **UP** - Processus actif (PID visible) -- **mine.sh** : ✅ **UP** - Script de mining actif -- **Statut** : ✅ **UP** - Miner opérationnel - -## 💰 Balance du Miner - -- **Balance mature** : 0.00000000 BTC -- **Balance immature** : 0.00000000 BTC -- **Total** : 0.00000000 BTC - -**Note** : La balance est à 0 car aucun bloc n'a encore été miné depuis le bloc genesis. Le prochain bloc miné apportera la récompense de minage. - -## ⚠️ API d'Ancrage - -- **Port** : 3010 -- **Statut** : ❌ **DOWN** - API non accessible -- **Action requise** : Lancer l'API d'ancrage - -## ⚠️ Dashboard et API Faucet - -- **Node.js** : ❌ **NON INSTALLÉ** sur cette machine -- **Dashboard (port 3020)** : ❌ **NON LANCÉ** - Nécessite Node.js -- **API Faucet (port 3021)** : ❌ **NON LANCÉ** - Nécessite Node.js - -## Actions Requises - -### Pour lancer le Dashboard et l'API Faucet - -1. **Installer Node.js** (version >= 18.0.0) : - ```bash - # Sur Ubuntu/Debian - curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - - sudo apt-get install -y nodejs - ``` - -2. **Installer les dépendances** : - ```bash - cd signet-dashboard && npm install - cd ../api-faucet && npm install - ``` - -3. **Configurer les fichiers .env** : - ```bash - cd signet-dashboard && cp .env.example .env - cd ../api-faucet && cp .env.example .env - ``` - -4. **Lancer les services** : - ```bash - # Dashboard - cd signet-dashboard && npm start & - - # API Faucet - cd api-faucet && npm start & - ``` - -### Pour lancer l'API d'Ancrage - -Vérifier si l'API d'ancrage est configurée et la lancer si nécessaire. - -## Vérification - -Une fois les services lancés, vérifier avec : - -```bash -# Dashboard -curl http://localhost:3020/api/blockchain/info - -# API Faucet -curl http://localhost:3021/health - -# API Anchor -curl http://localhost:3010/health -``` diff --git a/api-anchorage/README-MONITORING.md b/api-anchorage/README-MONITORING.md new file mode 100644 index 0000000..a80dc73 --- /dev/null +++ b/api-anchorage/README-MONITORING.md @@ -0,0 +1,282 @@ +# Monitoring et Maintenance de l'API d'Ancrage + +**Auteur:** Équipe 4NK +**Date:** 2026-01-28 + +## Vue d'ensemble + +Ce document décrit les outils de monitoring et de maintenance pour l'API d'ancrage Bitcoin Signet. + +## Endpoints de Monitoring + +### GET `/health` + +Vérifie l'état de base de l'API et de la connexion Bitcoin. + +**Exemple:** +```bash +curl https://anchorage.certificator.4nkweb.com/health +``` + +**Réponse:** +```json +{ + "ok": true, + "service": "anchor-api", + "bitcoin": { + "connected": true, + "blocks": 10833 + }, + "timestamp": "2026-01-28T11:45:00.000Z" +} +``` + +### GET `/health/detailed` + +Vérifie l'état détaillé de l'API, incluant l'état du mutex, des UTXOs verrouillés et de la connexion Bitcoin. + +**Exemple:** +```bash +curl https://anchorage.certificator.4nkweb.com/health/detailed +``` + +**Réponse (200 OK):** +```json +{ + "ok": true, + "service": "anchor-api", + "mutex": { + "locked": false, + "waiting": 0, + "timeout": 180000 + }, + "utxos": { + "locked": 0, + "locked_since": null, + "stale_locks": 0, + "stale_locks_details": [] + }, + "bitcoin": { + "connected": true, + "blocks": 10833, + "chain": "signet", + "rpc_timeout": 60000 + }, + "timestamp": "2026-01-28T11:45:00.000Z" +} +``` + +**Codes de statut:** +- `200` - Tout fonctionne correctement +- `200` - Warning si 5-10 UTXOs verrouillés (mais < 10 min) +- `503` - Service Unavailable si UTXOs verrouillés depuis > 10 min ou > 10 UTXOs verrouillés +- `503` - Service Unavailable si Bitcoin non connecté + +### GET `/api/anchor/locked-utxos` + +Liste tous les UTXOs actuellement verrouillés. + +**Exemple:** +```bash +curl https://anchorage.certificator.4nkweb.com/api/anchor/locked-utxos +``` + +**Réponse:** +```json +{ + "locked": [ + { + "txid": "abc123...", + "vout": 0 + } + ], + "count": 1 +} +``` + +## Scripts de Maintenance + +### 1. `unlock-utxos.mjs` + +Déverrouille tous les UTXOs verrouillés dans la base de données. + +**Utilisation:** +```bash +cd /home/ncantu/Bureau/code/bitcoin/api-anchorage +node unlock-utxos.mjs +``` + +**Sortie:** +``` +✅ UTXOs déverrouillés: 22 +``` + +### 2. `cleanup-stale-locks.mjs` + +Déverrouille automatiquement les UTXOs verrouillés depuis plus de 10 minutes. + +**Utilisation:** +```bash +cd /home/ncantu/Bureau/code/bitcoin/api-anchorage +node cleanup-stale-locks.mjs +``` + +**Sortie:** +``` +✅ UTXOs déverrouillés: 5 +``` + +**Cron job recommandé (toutes les 5 minutes):** +```bash +*/5 * * * * cd /home/ncantu/Bureau/code/bitcoin/api-anchorage && node cleanup-stale-locks.mjs >> /var/log/anchorage-cleanup.log 2>&1 +``` + +### 3. `diagnose.mjs` + +Affiche un diagnostic complet de l'état des UTXOs verrouillés. + +**Utilisation:** +```bash +cd /home/ncantu/Bureau/code/bitcoin/api-anchorage +node diagnose.mjs +``` + +**Sortie:** +``` +📊 UTXOs verrouillés: 3 + +Détails des UTXOs verrouillés: +🔒 [1] abc123... vout:0 amount:0.000025 locked_for:5.2min +⚠️ STALE [2] def456... vout:1 amount:0.000025 locked_for:15.5min + +⚠️ UTXOs verrouillés depuis plus de 10 minutes: 1 + Action recommandée: Exécuter cleanup-stale-locks.mjs + +📈 Statistiques UTXOs: + Total: 150 + Verrouillés: 3 + Dépensés: 50 + Disponibles: 97 +``` + +## Monitoring des Logs + +### Timeouts de mutex + +**Vérification:** +```bash +sudo journalctl -u anchorage-api --since "1 hour ago" | grep -c "Mutex acquisition timeout" +``` + +**Alerte recommandée:** Si > 1 timeout par heure + +### Erreurs RPC Bitcoin + +**Vérification:** +```bash +sudo journalctl -u anchorage-api --since "1 hour ago" | grep -c "ESOCKETTIMEDOUT\|ETIMEDOUT" +``` + +**Alerte recommandée:** Si > 1 erreur RPC par heure + +### Opérations longues + +**Vérification:** +```bash +sudo journalctl -u anchorage-api --since "1 hour ago" | grep "took too long" +``` + +**Alerte recommandée:** Si > 1 opération > 30s par heure + +## Alertes Recommandées + +### Niveau Warning + +- **UTXOs verrouillés > 5:** Plus de 5 UTXOs verrouillés +- **Timeout de mutex:** 1+ timeout par heure +- **Erreur RPC:** 1+ erreur RPC par heure + +### Niveau Critical + +- **UTXOs verrouillés > 10:** Plus de 10 UTXOs verrouillés +- **UTXOs stale:** UTXOs verrouillés depuis > 10 minutes +- **Timeout de mutex:** 5+ timeouts par heure +- **Erreur RPC:** 5+ erreurs RPC par heure + +## Configuration Cron + +### Nettoyage automatique (recommandé) + +Créer un fichier `/etc/cron.d/anchorage-cleanup`: + +```bash +# Nettoyage automatique des UTXOs verrouillés depuis plus de 10 minutes +# Toutes les 5 minutes +*/5 * * * * ncantu cd /home/ncantu/Bureau/code/bitcoin/api-anchorage && /usr/bin/node cleanup-stale-locks.mjs >> /var/log/anchorage-cleanup.log 2>&1 +``` + +### Diagnostic périodique (optionnel) + +Créer un fichier `/etc/cron.d/anchorage-diagnose`: + +```bash +# Diagnostic de l'état des UTXOs +# Toutes les heures +0 * * * * ncantu cd /home/ncantu/Bureau/code/bitcoin/api-anchorage && /usr/bin/node diagnose.mjs >> /var/log/anchorage-diagnose.log 2>&1 +``` + +## Procédure de Dépannage + +### 1. Vérifier l'état actuel + +```bash +# Health check détaillé +curl https://anchorage.certificator.4nkweb.com/health/detailed | jq . + +# UTXOs verrouillés +curl https://anchorage.certificator.4nkweb.com/api/anchor/locked-utxos | jq '.count' +``` + +### 2. Si UTXOs verrouillés + +```bash +# Diagnostic +cd /home/ncantu/Bureau/code/bitcoin/api-anchorage +node diagnose.mjs + +# Déverrouiller manuellement +node unlock-utxos.mjs + +# Ou nettoyer seulement les stale locks +node cleanup-stale-locks.mjs +``` + +### 3. Si mutex bloqué + +```bash +# Redémarrer le service +sudo systemctl restart anchorage-api + +# Vérifier les logs +sudo journalctl -u anchorage-api -n 50 --no-pager +``` + +### 4. Si erreurs RPC Bitcoin + +```bash +# Vérifier la connexion Bitcoin +curl -s --user bitcoin:bitcoin --data-binary '{"jsonrpc":"1.0","id":"test","method":"getblockchaininfo","params":[]}' -H 'content-type: text/plain;' http://localhost:38332/ + +# Vérifier les logs +sudo journalctl -u anchorage-api --since "10 minutes ago" | grep "ESOCKETTIMEDOUT" +``` + +## Pages Affectées + +- `api-anchorage/src/routes/health.js`: Endpoint `/health/detailed` +- `api-anchorage/src/bitcoin-rpc.js`: Monitoring de durée, timeout de sécurité, retry avec backoff +- `api-anchorage/src/server.js`: Déverrouillage automatique au démarrage +- `api-anchorage/unlock-utxos.mjs`: Script de déverrouillage (existant) +- `api-anchorage/cleanup-stale-locks.mjs`: Script de nettoyage automatique (nouveau) +- `api-anchorage/diagnose.mjs`: Script de diagnostic (nouveau) +- `api-anchorage/README-MONITORING.md`: Documentation (nouveau) diff --git a/api-anchorage/cleanup-stale-locks.mjs b/api-anchorage/cleanup-stale-locks.mjs new file mode 100755 index 0000000..bb8ce3e --- /dev/null +++ b/api-anchorage/cleanup-stale-locks.mjs @@ -0,0 +1,24 @@ +#!/usr/bin/env node + +/** + * Script pour déverrouiller automatiquement les UTXOs verrouillés depuis plus de 10 minutes + * À exécuter via cron job toutes les 5 minutes + */ + +import { getDatabase } from './src/database.js'; + +const db = getDatabase(); +const result = db.prepare(` + UPDATE utxos + SET is_locked_in_mutex = 0 + WHERE is_locked_in_mutex = 1 + AND updated_at < datetime('now', '-10 minutes') +`).run(); + +if (result.changes > 0) { + console.log(`✅ UTXOs déverrouillés: ${result.changes}`); +} else { + console.log('ℹ️ Aucun UTXO à déverrouiller'); +} + +db.close(); diff --git a/api-anchorage/diagnose.mjs b/api-anchorage/diagnose.mjs new file mode 100755 index 0000000..81d918b --- /dev/null +++ b/api-anchorage/diagnose.mjs @@ -0,0 +1,55 @@ +#!/usr/bin/env node + +/** + * Script de diagnostic pour l'API d'ancrage + * Affiche l'état des UTXOs verrouillés, leur durée de verrouillage, etc. + */ + +import { getDatabase } from './src/database.js'; + +const db = getDatabase(); + +// UTXOs verrouillés +const locked = db.prepare(` + SELECT txid, vout, address, amount, updated_at, + (julianday('now') - julianday(updated_at)) * 24 * 60 as minutes_locked + FROM utxos + WHERE is_locked_in_mutex = 1 + ORDER BY updated_at +`).all(); + +console.log(`\n📊 UTXOs verrouillés: ${locked.length}`); + +if (locked.length > 0) { + console.log('\nDétails des UTXOs verrouillés:'); + locked.forEach((u, index) => { + const minutes = Math.round(u.minutes_locked * 100) / 100; + const status = minutes > 10 ? '⚠️ STALE' : '🔒'; + console.log(`${status} [${index + 1}] ${u.txid.substring(0, 16)}... vout:${u.vout} amount:${u.amount} locked_for:${minutes}min`); + }); +} + +// UTXOs verrouillés depuis plus de 10 minutes +const stale = locked.filter(u => u.minutes_locked > 10); +if (stale.length > 0) { + console.log(`\n⚠️ UTXOs verrouillés depuis plus de 10 minutes: ${stale.length}`); + console.log(' Action recommandée: Exécuter cleanup-stale-locks.mjs'); +} + +// Statistiques générales +const stats = db.prepare(` + SELECT + COUNT(*) as total, + SUM(CASE WHEN is_locked_in_mutex = 1 THEN 1 ELSE 0 END) as locked, + SUM(CASE WHEN is_spent_onchain = 1 THEN 1 ELSE 0 END) as spent, + SUM(CASE WHEN is_spent_onchain = 0 AND is_locked_in_mutex = 0 AND confirmations > 0 THEN 1 ELSE 0 END) as available + FROM utxos +`).get(); + +console.log(`\n📈 Statistiques UTXOs:`); +console.log(` Total: ${stats.total}`); +console.log(` Verrouillés: ${stats.locked}`); +console.log(` Dépensés: ${stats.spent}`); +console.log(` Disponibles: ${stats.available}`); + +db.close(); diff --git a/api-anchorage/src/bitcoin-rpc.js b/api-anchorage/src/bitcoin-rpc.js index e54c66e..67de8f8 100644 --- a/api-anchorage/src/bitcoin-rpc.js +++ b/api-anchorage/src/bitcoin-rpc.js @@ -26,6 +26,8 @@ class BitcoinRPC { // Mutex pour gérer l'accès concurrent aux UTXOs // Utilise une Promise-based queue pour sérialiser les accès this.utxoMutexPromise = Promise.resolve(); + this.utxoMutexLocked = false; + this.utxoMutexWaiting = 0; // Timeout pour l'attente du mutex (180s = 3 minutes) // Si une requête prend plus de 180s, elle sera automatiquement libérée @@ -35,6 +37,17 @@ class BitcoinRPC { // via is_locked_in_mutex pour éviter la duplication et réduire la consommation mémoire } + /** + * Obtient l'état actuel du mutex + * @returns {Object} État du mutex + */ + getMutexState() { + return { + locked: this.utxoMutexLocked, + waiting: this.utxoMutexWaiting, + }; + } + /** * Acquiert le mutex pour l'accès aux UTXOs avec timeout * @returns {Promise} Fonction pour libérer le mutex @@ -45,9 +58,16 @@ class BitcoinRPC { let releaseMutex; let timeoutId; + // Incrémenter le compteur d'attente + this.utxoMutexWaiting++; + // Créer une nouvelle Promise qui sera résolue quand le mutex est libéré this.utxoMutexPromise = new Promise((resolve) => { - releaseMutex = resolve; + releaseMutex = () => { + this.utxoMutexLocked = false; + this.utxoMutexWaiting = Math.max(0, this.utxoMutexWaiting - 1); + resolve(); + }; }); // Créer une Promise avec timeout pour éviter les blocages indéfinis @@ -58,6 +78,7 @@ class BitcoinRPC { logger.warn('Mutex acquisition timeout, forcing release', { timeout: this.utxoMutexTimeout, }); + this.utxoMutexWaiting = Math.max(0, this.utxoMutexWaiting - 1); reject(new Error(`Mutex acquisition timeout after ${this.utxoMutexTimeout}ms`)); }, this.utxoMutexTimeout); }), @@ -65,6 +86,8 @@ class BitcoinRPC { try { await mutexWithTimeout; + this.utxoMutexLocked = true; + this.utxoMutexWaiting = Math.max(0, this.utxoMutexWaiting - 1); } finally { if (timeoutId) { clearTimeout(timeoutId); @@ -174,8 +197,8 @@ class BitcoinRPC { */ async checkConnection() { try { - const networkInfo = await this.client.getNetworkInfo(); - const blockchainInfo = await this.client.getBlockchainInfo(); + const networkInfo = await this.callRPCWithRetry('getNetworkInfo', []); + const blockchainInfo = await this.callRPCWithRetry('getBlockchainInfo', []); return { connected: true, @@ -193,13 +216,78 @@ class BitcoinRPC { } } + /** + * Appelle une méthode RPC avec retry et backoff exponentiel + * @param {string} method - Nom de la méthode RPC + * @param {Array} params - Paramètres de la méthode + * @param {number} maxRetries - Nombre maximum de tentatives (défaut: 3) + * @returns {Promise} Résultat de l'appel RPC + */ + async callRPCWithRetry(method, params = [], maxRetries = 3) { + for (let i = 0; i < maxRetries; i++) { + try { + return await this.client[method](...params); + } catch (error) { + const isTimeoutError = error.message.includes('ESOCKETTIMEDOUT') || + error.message.includes('ETIMEDOUT') || + error.message.includes('timeout'); + + if (i === maxRetries - 1 || !isTimeoutError) { + throw error; + } + + const delay = Math.min(1000 * Math.pow(2, i), 10000); // Backoff exponentiel, max 10s + logger.warn(`RPC call failed, retrying in ${delay}ms`, { + method, + attempt: i + 1, + maxRetries, + error: error.message, + }); + await new Promise((resolve) => setTimeout(resolve, delay)); + } + } + } + + /** + * Appelle une commande RPC avec retry et backoff exponentiel + * @param {string} command - Nom de la commande RPC + * @param {...any} args - Arguments de la commande + * @param {number} maxRetries - Nombre maximum de tentatives (défaut: 3) + * @returns {Promise} Résultat de l'appel RPC + */ + async callRPCCommandWithRetry(command, ...args) { + const maxRetries = 3; + for (let i = 0; i < maxRetries; i++) { + try { + return await this.client.command(command, ...args); + } catch (error) { + const isTimeoutError = error.message.includes('ESOCKETTIMEDOUT') || + error.message.includes('ETIMEDOUT') || + error.message.includes('timeout'); + + if (i === maxRetries - 1 || !isTimeoutError) { + throw error; + } + + const delay = Math.min(1000 * Math.pow(2, i), 10000); // Backoff exponentiel, max 10s + logger.warn(`RPC command failed, retrying in ${delay}ms`, { + command, + attempt: i + 1, + maxRetries, + error: error.message, + }); + await new Promise((resolve) => setTimeout(resolve, delay)); + } + } + } + /** * Obtient une nouvelle adresse depuis le wallet * @returns {Promise} Adresse Bitcoin */ async getNewAddress() { try { - return await this.client.getNewAddress(); + return await this.callRPCWithRetry('getNewAddress', []); } catch (error) { logger.error('Error getting new address', { error: error.message }); throw new Error(`Failed to get new address: ${error.message}`); @@ -212,7 +300,7 @@ class BitcoinRPC { */ async getBalance() { try { - return await this.client.getBalance(); + return await this.callRPCWithRetry('getBalance', []); } catch (error) { logger.error('Error getting balance', { error: error.message }); throw new Error(`Failed to get balance: ${error.message}`); @@ -228,10 +316,21 @@ class BitcoinRPC { * @returns {Promise} Transaction créée avec txid */ async createAnchorTransaction(hash, recipientAddress = null, provisioningAddresses = null, numberOfProvisioningUtxos = null, retryCount = 0) { + const startTime = Date.now(); // Acquérir le mutex pour l'accès aux UTXOs const releaseMutex = await this.acquireUtxoMutex(); let selectedUtxo = null; let selectedUtxos = []; + let mutexSafetyTimeout; + + // Timeout de sécurité: libérer le mutex après 5 minutes maximum + mutexSafetyTimeout = setTimeout(() => { + logger.error('Mutex held for too long, forcing release', { + hash: hash?.substring(0, 16) + '...', + duration: Date.now() - startTime, + }); + releaseMutex(); + }, 300000); // 5 minutes try { // Vérifier que le hash est valide (64 caractères hex) @@ -245,12 +344,21 @@ class BitcoinRPC { const provisioningCount = numberOfProvisioningUtxos ?? 7; const addressesNeeded = 1 + provisioningCount + 1; // principal + provisioning + change - // Générer toutes les adresses en parallèle + // Générer toutes les adresses en parallèle avec timeout const addressPromises = []; for (let i = 0; i < addressesNeeded; i++) { addressPromises.push(this.getNewAddress()); } - const allAddresses = await Promise.all(addressPromises); + + // Timeout de 30 secondes pour la génération d'adresses + const allAddresses = await Promise.race([ + Promise.all(addressPromises), + new Promise((_, reject) => { + setTimeout(() => { + reject(new Error('Address generation timeout after 30s')); + }, 30000); + }), + ]); // Utiliser l'adresse fournie ou la première générée const address = recipientAddress || allAddresses[0]; @@ -499,7 +607,7 @@ class BitcoinRPC { try { // Récupérer toutes les adresses uniques des UTXOs sélectionnés const uniqueAddresses = [...new Set(selectedUtxos.map(u => u.address))]; - const utxoCheck = await this.client.listunspent(0, 9999999, uniqueAddresses); + const utxoCheck = await this.callRPCWithRetry('listunspent', [0, 9999999, uniqueAddresses]); // Vérifier que tous les UTXOs sont toujours disponibles let allUtxosAvailable = true; @@ -563,7 +671,7 @@ class BitcoinRPC { let tx; try { - tx = await this.client.command('createrawtransaction', inputs, outputs); + tx = await this.callRPCCommandWithRetry('createrawtransaction', inputs, outputs); } catch (error) { logger.error('Error creating raw transaction', { error: error.message, @@ -591,7 +699,7 @@ class BitcoinRPC { // Signer la transaction // Utiliser command() directement pour éviter les problèmes avec la bibliothèque - const signedTx = await this.client.command('signrawtransactionwithwallet', tx); + const signedTx = await this.callRPCCommandWithRetry('signrawtransactionwithwallet', tx); if (!signedTx.complete) { const errorDetails = signedTx.errors || []; @@ -647,7 +755,7 @@ class BitcoinRPC { // Le test direct avec bitcoin-cli fonctionne avec cette syntaxe let txid; try { - txid = await this.client.command('sendrawtransaction', signedTx.hex, 0); + txid = await this.callRPCCommandWithRetry('sendrawtransaction', signedTx.hex, 0); } catch (sendError) { // Gérer l'erreur de remplacement RBF (Replace By Fee) // Si une transaction avec les mêmes inputs existe déjà, Bitcoin Core rejette la nouvelle @@ -675,7 +783,7 @@ class BitcoinRPC { // Vérifier si la transaction existe dans le mempool ou dans la blockchain try { - const mempoolEntry = await this.client.command('getmempoolentry', existingTxid); + const mempoolEntry = await this.callRPCCommandWithRetry('getmempoolentry', existingTxid); if (mempoolEntry) { // La transaction existe dans le mempool, utiliser cette transaction txid = existingTxid; @@ -696,7 +804,7 @@ class BitcoinRPC { if (errorMsg.includes('not in mempool') || errorMsg.includes('Transaction not in mempool')) { // La transaction n'est pas dans le mempool, vérifier si elle est confirmée try { - const txInfo = await this.client.getTransaction(existingTxid); + const txInfo = await this.callRPCWithRetry('getTransaction', [existingTxid]); if (txInfo && txInfo.txid) { // La transaction existe dans la blockchain (confirmée), utiliser cette transaction txid = existingTxid; @@ -753,7 +861,7 @@ class BitcoinRPC { const txInfo = await this.getTransactionInfo(txid); // Obtenir la transaction brute pour identifier les index des outputs - const rawTx = await this.client.getRawTransaction(txid, true); + const rawTx = await this.callRPCWithRetry('getRawTransaction', [txid, true]); // Calculer les frais réels de la transaction // Frais = somme des inputs - somme des outputs @@ -906,6 +1014,20 @@ class BitcoinRPC { // Le mutex sera libéré dans le bloc finally pour garantir la libération même en cas d'erreur non gérée throw error; } finally { + // Nettoyer le timeout de sécurité + if (mutexSafetyTimeout) { + clearTimeout(mutexSafetyTimeout); + } + + // Logger la durée de l'opération + const duration = Date.now() - startTime; + if (duration > 30000) { + logger.warn('Anchor transaction took too long', { + duration, + hash: hash?.substring(0, 16) + '...', + }); + } + // Garantir que le mutex est toujours libéré, même en cas d'erreur non gérée try { releaseMutex(); @@ -922,8 +1044,8 @@ class BitcoinRPC { */ async getTransactionInfo(txid) { try { - const tx = await this.client.getTransaction(txid); - const blockchainInfo = await this.client.getBlockchainInfo(); + const tx = await this.callRPCWithRetry('getTransaction', [txid]); + const blockchainInfo = await this.callRPCWithRetry('getBlockchainInfo', []); return { txid: tx.txid, @@ -956,8 +1078,8 @@ class BitcoinRPC { // Si un txid est fourni, vérifier directement cette transaction if (txid) { try { - const tx = await this.client.getTransaction(txid, true); - const rawTx = await this.client.getRawTransaction(txid, true); + const tx = await this.callRPCWithRetry('getTransaction', [txid, true]); + const rawTx = await this.callRPCWithRetry('getRawTransaction', [txid, true]); // Vérifier si le hash est dans les outputs OP_RETURN const hashFound = this.checkHashInTransaction(rawTx, hash); @@ -979,19 +1101,19 @@ class BitcoinRPC { } // Rechercher dans les blocs récents (derniers 100 blocs) - const blockchainInfo = await this.client.getBlockchainInfo(); + const blockchainInfo = await this.callRPCWithRetry('getBlockchainInfo', []); const currentHeight = blockchainInfo.blocks; const searchRange = 100; // Rechercher dans les 100 derniers blocs for (let height = currentHeight; height >= Math.max(0, currentHeight - searchRange); height--) { try { - const blockHash = await this.client.getBlockHash(height); - const block = await this.client.getBlock(blockHash, 2); // Verbose level 2 + const blockHash = await this.callRPCWithRetry('getBlockHash', [height]); + const block = await this.callRPCWithRetry('getBlock', [blockHash, 2]); // Verbose level 2 // Parcourir toutes les transactions du bloc for (const tx of block.tx || []) { try { - const rawTx = await this.client.getRawTransaction(tx.txid, true); + const rawTx = await this.callRPCWithRetry('getRawTransaction', [tx.txid, true]); const hashFound = this.checkHashInTransaction(rawTx, hash); if (hashFound) { diff --git a/api-anchorage/src/routes/anchor.js b/api-anchorage/src/routes/anchor.js index 6601da2..dfd8a39 100644 --- a/api-anchorage/src/routes/anchor.js +++ b/api-anchorage/src/routes/anchor.js @@ -41,6 +41,38 @@ anchorRouter.get('/locked-utxos', async (req, res) => { } }); +/** + * POST /api/anchor/unlock-utxos + * Déverrouille tous les UTXOs verrouillés dans la base de données + * + * Authentification : Requise (header `x-api-key`) + */ +anchorRouter.post('/unlock-utxos', async (req, res) => { + try { + const { getDatabase } = await import('../database.js'); + const db = getDatabase(); + const result = db.prepare(` + UPDATE utxos + SET is_locked_in_mutex = 0 + WHERE is_locked_in_mutex = 1 + `).run(); + + logger.info('UTXOs unlocked via API', { count: result.changes }); + + res.status(200).json({ + ok: true, + unlocked: result.changes, + message: `${result.changes} UTXO(s) déverrouillé(s)`, + }); + } catch (error) { + logger.error('Error unlocking UTXOs', { error: error.message }); + res.status(500).json({ + error: 'Internal Server Error', + message: error.message, + }); + } +}); + /** * POST /api/anchor/document * Ancre un document sur Bitcoin Signet @@ -145,6 +177,38 @@ anchorRouter.post('/document', async (req, res) => { } }); +/** + * POST /api/anchor/unlock-utxos + * Déverrouille tous les UTXOs verrouillés dans la base de données + * + * Authentification : Requise (header `x-api-key`) + */ +anchorRouter.post('/unlock-utxos', async (req, res) => { + try { + const { getDatabase } = await import('../database.js'); + const db = getDatabase(); + const result = db.prepare(` + UPDATE utxos + SET is_locked_in_mutex = 0 + WHERE is_locked_in_mutex = 1 + `).run(); + + logger.info('UTXOs unlocked via API', { count: result.changes }); + + res.status(200).json({ + ok: true, + unlocked: result.changes, + message: `${result.changes} UTXO(s) déverrouillé(s)`, + }); + } catch (error) { + logger.error('Error unlocking UTXOs', { error: error.message }); + res.status(500).json({ + error: 'Internal Server Error', + message: error.message, + }); + } +}); + /** * POST /api/anchor/verify * Vérifie si un hash est ancré sur Bitcoin Signet diff --git a/api-anchorage/src/routes/health.js b/api-anchorage/src/routes/health.js index af82da9..3b598cf 100644 --- a/api-anchorage/src/routes/health.js +++ b/api-anchorage/src/routes/health.js @@ -5,6 +5,7 @@ import express from 'express'; import { bitcoinRPC } from '../bitcoin-rpc.js'; import { logger } from '../logger.js'; +import { getDatabase } from '../database.js'; export const healthRouter = express.Router(); @@ -38,3 +39,77 @@ healthRouter.get('/', async (req, res) => { }); } }); + +/** + * GET /health/detailed + * Vérifie l'état détaillé de l'API, du mutex, des UTXOs et de la connexion Bitcoin + */ +healthRouter.get('/detailed', async (req, res) => { + try { + const bitcoinStatus = await bitcoinRPC.checkConnection(); + + // Vérifier l'état du mutex + const mutexState = bitcoinRPC.getMutexState(); + + // Vérifier les UTXOs verrouillés + const db = getDatabase(); + const lockedUtxos = db.prepare(` + SELECT txid, vout, address, amount, updated_at, + (julianday('now') - julianday(updated_at)) * 24 * 60 as minutes_locked + FROM utxos + WHERE is_locked_in_mutex = 1 + ORDER BY updated_at + `).all(); + + const staleLocks = lockedUtxos.filter(u => u.minutes_locked > 10); + + const health = { + ok: bitcoinStatus.connected && lockedUtxos.length === 0 && staleLocks.length === 0, + service: 'anchor-api', + mutex: { + locked: mutexState.locked, + waiting: mutexState.waiting, + timeout: 180000, // 3 minutes (180000ms) + }, + utxos: { + locked: lockedUtxos.length, + locked_since: lockedUtxos.length > 0 ? lockedUtxos[0].updated_at : null, + stale_locks: staleLocks.length, + stale_locks_details: staleLocks.map(u => ({ + txid: u.txid.substring(0, 16) + '...', + vout: u.vout, + minutes_locked: Math.round(u.minutes_locked * 100) / 100, + })), + }, + bitcoin: { + connected: bitcoinStatus.connected, + blocks: bitcoinStatus.blocks || 0, + chain: bitcoinStatus.chain || null, + rpc_timeout: parseInt(process.env.BITCOIN_RPC_TIMEOUT || '30000'), + }, + timestamp: new Date().toISOString(), + }; + + // Déterminer le code de statut + let statusCode = 200; + if (!bitcoinStatus.connected) { + statusCode = 503; + } else if (staleLocks.length > 0) { + statusCode = 503; // UTXOs verrouillés depuis trop longtemps + } else if (lockedUtxos.length > 10) { + statusCode = 503; // Trop d'UTXOs verrouillés + } else if (lockedUtxos.length > 5) { + statusCode = 200; // Warning mais OK + } + + res.status(statusCode).json(health); + } catch (error) { + logger.error('Detailed health check error', { error: error.message }); + res.status(503).json({ + ok: false, + service: 'anchor-api', + error: error.message, + timestamp: new Date().toISOString(), + }); + } +}); diff --git a/api-anchorage/src/server.js b/api-anchorage/src/server.js index 61ec628..f768c3b 100644 --- a/api-anchorage/src/server.js +++ b/api-anchorage/src/server.js @@ -51,8 +51,8 @@ app.use((req, res, next) => { // Middleware d'authentification API Key app.use((req, res, next) => { - // Exclure /health et /api/anchor/locked-utxos de l'authentification - if (req.path === '/health' || req.path === '/' || req.path.startsWith('/api/anchor/locked-utxos')) { + // 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')) { return next(); } @@ -83,8 +83,10 @@ app.get('/', (req, res) => { version: '1.0.0', endpoints: { health: '/health', + healthDetailed: '/health/detailed', anchor: '/api/anchor/document', verify: '/api/anchor/verify', + lockedUtxos: '/api/anchor/locked-utxos', }, }); }); @@ -106,6 +108,24 @@ app.use((req, res) => { }); }); +// Déverrouiller automatiquement les UTXOs verrouillés depuis plus de 10 minutes au démarrage +try { + const { getDatabase } = await import('./database.js'); + const db = getDatabase(); + const result = db.prepare(` + UPDATE utxos + SET is_locked_in_mutex = 0 + WHERE is_locked_in_mutex = 1 + AND updated_at < datetime('now', '-10 minutes') + `).run(); + + if (result.changes > 0) { + logger.info('Unlocked stale UTXOs on startup', { count: result.changes }); + } +} catch (error) { + logger.warn('Error unlocking stale UTXOs on startup', { error: error.message }); +} + // Démarrage du serveur const server = app.listen(PORT, HOST, () => { logger.info(`API d'ancrage Bitcoin Signet démarrée`, { diff --git a/api-anchorage/unlock-utxos.mjs b/api-anchorage/unlock-utxos.mjs new file mode 100755 index 0000000..79b30f0 --- /dev/null +++ b/api-anchorage/unlock-utxos.mjs @@ -0,0 +1,14 @@ +#!/usr/bin/env node + +/** + * Script pour déverrouiller tous les UTXOs verrouillés dans la base de données + */ + +import { getDatabase } from './src/database.js'; + +const db = getDatabase(); +const result = db.prepare('UPDATE utxos SET is_locked_in_mutex = 0 WHERE is_locked_in_mutex = 1').run(); + +console.log(`✅ UTXOs déverrouillés: ${result.changes}`); + +db.close(); diff --git a/check-anchor-api.sh b/check-anchor-api.sh new file mode 100755 index 0000000..ecefb5c --- /dev/null +++ b/check-anchor-api.sh @@ -0,0 +1,143 @@ +#!/bin/bash + +# Script de diagnostic pour l'API d'Ancrage +# Vérifie l'état du service, la connectivité et les logs + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +cd "$SCRIPT_DIR" + +echo "=== Diagnostic API d'Ancrage ===" +echo "" + +# Configuration +BITCOIN_SERVER="192.168.1.105" # Machine bitcoin avec API d'ancrage +PROD_SERVER="192.168.1.103" # Machine prod (pour référence) +PROD_USER="ncantu" +PROXY_SERVER="4nk.myftp.biz" +API_URL="https://anchorage.certificator.4nkweb.com" # Domaine externe de l'API d'ancrage Bitcoin +SERVICE_NAME="anchorage-api" +PORT=3010 + +# Fonction pour exécuter une commande sur le serveur bitcoin (API d'ancrage) +run_on_bitcoin() { + ssh -J "${PROD_USER}@${PROXY_SERVER}" "${PROD_USER}@${BITCOIN_SERVER}" "$@" +} + +echo "📡 Test de connectivité externe..." +echo "" + +# Test 1: Vérifier que l'API répond depuis l'extérieur +echo "1. Test de l'endpoint /health depuis l'extérieur..." +HEALTH_RESPONSE=$(curl -s --max-time 10 "${API_URL}/health" || echo "FAILED") +if [ "$HEALTH_RESPONSE" = "FAILED" ]; then + echo "❌ L'API ne répond pas depuis l'extérieur" + echo "" +else + echo "✅ L'API répond depuis l'extérieur" + echo " Réponse: $HEALTH_RESPONSE" + echo "" + + # Vérifier si la connexion Bitcoin fonctionne + if echo "$HEALTH_RESPONSE" | grep -q '"connected":true'; then + echo "✅ Connexion Bitcoin: OK" + BLOCKS=$(echo "$HEALTH_RESPONSE" | grep -o '"blocks":[0-9]*' | grep -o '[0-9]*' || echo "0") + echo " Blocs: $BLOCKS" + else + echo "⚠️ Connexion Bitcoin: PROBLÈME" + echo " L'API répond mais ne peut pas se connecter à Bitcoin Core" + fi + echo "" +fi + +# Test 2: Vérifier l'endpoint racine +echo "2. Test de l'endpoint racine..." +ROOT_RESPONSE=$(curl -s --max-time 10 "${API_URL}/" || echo "FAILED") +if [ "$ROOT_RESPONSE" = "FAILED" ]; then + echo "❌ L'endpoint racine ne répond pas" +else + echo "✅ L'endpoint racine répond" + echo " Réponse: $ROOT_RESPONSE" +fi +echo "" + +# Test 3: Vérifier l'état du service sur le serveur bitcoin +echo "3. État du service systemd sur le serveur bitcoin (192.168.1.105)..." +echo " (Connexion via SSH via proxy...)" +echo "" + +SERVICE_STATUS=$(run_on_bitcoin "sudo systemctl status ${SERVICE_NAME} --no-pager" 2>&1 || echo "FAILED") +if echo "$SERVICE_STATUS" | grep -q "Active: active (running)"; then + echo "✅ Service actif et en cours d'exécution" +elif echo "$SERVICE_STATUS" | grep -q "Active: inactive"; then + echo "❌ Service inactif" +elif echo "$SERVICE_STATUS" | grep -q "Active: failed"; then + echo "❌ Service en échec" +else + echo "⚠️ État du service indéterminé" +fi +echo "" + +# Test 4: Vérifier que le port est en écoute +echo "4. Vérification du port ${PORT}..." +PORT_CHECK=$(run_on_bitcoin "sudo ss -tlnp | grep :${PORT}" 2>&1 || echo "NOT_LISTENING") +if [ "$PORT_CHECK" = "NOT_LISTENING" ] || [ -z "$PORT_CHECK" ]; then + echo "❌ Le port ${PORT} n'est pas en écoute" +else + echo "✅ Le port ${PORT} est en écoute" + echo " $PORT_CHECK" +fi +echo "" + +# Test 5: Vérifier les logs récents (dernières 50 lignes) +echo "5. Logs récents du service (dernières 50 lignes)..." +echo "" +run_on_bitcoin "sudo journalctl -u ${SERVICE_NAME} -n 50 --no-pager" 2>&1 | tail -50 +echo "" + +# Test 6: Vérifier les logs d'erreur récents +echo "6. Logs d'erreur récents (dernières 20 lignes)..." +echo "" +run_on_bitcoin "sudo journalctl -u ${SERVICE_NAME} -p err -n 20 --no-pager" 2>&1 | tail -20 +echo "" + +# Test 7: Vérifier la connexion Bitcoin RPC depuis le serveur bitcoin +echo "7. Test de connexion Bitcoin RPC depuis le serveur bitcoin..." +echo "" +BITCOIN_RPC_TEST=$(run_on_bitcoin "curl -s --user bitcoin:bitcoin --data-binary '{\"jsonrpc\":\"1.0\",\"id\":\"test\",\"method\":\"getblockchaininfo\",\"params\":[]}' -H 'content-type: text/plain;' http://localhost:38332/" 2>&1 || echo "FAILED") +if echo "$BITCOIN_RPC_TEST" | grep -q '"result"'; then + echo "✅ Bitcoin RPC répond" + BLOCKS_RPC=$(echo "$BITCOIN_RPC_TEST" | grep -o '"blocks":[0-9]*' | grep -o '[0-9]*' || echo "0") + echo " Blocs: $BLOCKS_RPC" +else + echo "❌ Bitcoin RPC ne répond pas" + echo " Réponse: $BITCOIN_RPC_TEST" +fi +echo "" + +# Test 8: Vérifier l'état du conteneur Docker Bitcoin (si applicable) +echo "8. État du conteneur Docker Bitcoin..." +echo "" +DOCKER_STATUS=$(run_on_bitcoin "sudo docker ps --filter 'name=bitcoin' --format '{{.Names}} {{.Status}}'" 2>&1 || echo "NO_DOCKER") +if [ "$DOCKER_STATUS" = "NO_DOCKER" ] || [ -z "$DOCKER_STATUS" ]; then + echo "⚠️ Aucun conteneur Bitcoin trouvé ou Docker non disponible" +else + echo "✅ Conteneur Bitcoin:" + echo "$DOCKER_STATUS" | while read -r line; do + echo " $line" + done +fi +echo "" + +# Résumé +echo "=== Résumé ===" +echo "" +echo "Pour redémarrer le service:" +echo " ssh -J ${PROD_USER}@${PROXY_SERVER} ${PROD_USER}@${BITCOIN_SERVER} 'sudo systemctl restart ${SERVICE_NAME}'" +echo "" +echo "Pour voir les logs en temps réel:" +echo " ssh -J ${PROD_USER}@${PROXY_SERVER} ${PROD_USER}@${BITCOIN_SERVER} 'sudo journalctl -u ${SERVICE_NAME} -f'" +echo "" +echo "Note: L'API d'ancrage Bitcoin est sur la machine 192.168.1.105 (bitcoin), pas sur 192.168.1.103 (prod)" +echo "" diff --git a/data/sync-utxos.log b/data/sync-utxos.log index df2364d..b428712 100644 --- a/data/sync-utxos.log +++ b/data/sync-utxos.log @@ -1,100 +1,100 @@ - ⏳ Traitement: 200000/225882 UTXOs insérés... - ⏳ Traitement: 210000/225882 UTXOs insérés... - ⏳ Traitement: 220000/225882 UTXOs insérés... + ⏳ Traitement: 190000/223585 UTXOs insérés... + ⏳ Traitement: 200000/223585 UTXOs insérés... + ⏳ Traitement: 210000/223585 UTXOs insérés... + ⏳ Traitement: 220000/223585 UTXOs insérés... 💾 Mise à jour des UTXOs dépensés... 📊 Résumé: - - UTXOs vérifiés: 61565 - - UTXOs toujours disponibles: 61565 + - UTXOs vérifiés: 48651 + - UTXOs toujours disponibles: 48651 - UTXOs dépensés détectés: 0 📈 Statistiques finales: - Total UTXOs: 68398 - - Dépensés: 6888 - - Non dépensés: 61510 + - Dépensés: 19747 + - Non dépensés: 48651 ✅ Synchronisation terminée 🔍 Démarrage de la synchronisation des UTXOs dépensés... -📊 UTXOs à vérifier: 49190 +📊 UTXOs à vérifier: 37046 📡 Récupération des UTXOs depuis Bitcoin... -📊 UTXOs disponibles dans Bitcoin: 223652 +📊 UTXOs disponibles dans Bitcoin: 221494 💾 Création de la table temporaire... 💾 Insertion des UTXOs disponibles par batch... - ⏳ Traitement: 10000/223652 UTXOs insérés... - ⏳ Traitement: 20000/223652 UTXOs insérés... - ⏳ Traitement: 30000/223652 UTXOs insérés... - ⏳ Traitement: 40000/223652 UTXOs insérés... - ⏳ Traitement: 50000/223652 UTXOs insérés... - ⏳ Traitement: 60000/223652 UTXOs insérés... - ⏳ Traitement: 70000/223652 UTXOs insérés... - ⏳ Traitement: 80000/223652 UTXOs insérés... - ⏳ Traitement: 90000/223652 UTXOs insérés... - ⏳ Traitement: 100000/223652 UTXOs insérés... - ⏳ Traitement: 110000/223652 UTXOs insérés... - ⏳ Traitement: 120000/223652 UTXOs insérés... - ⏳ Traitement: 130000/223652 UTXOs insérés... - ⏳ Traitement: 140000/223652 UTXOs insérés... - ⏳ Traitement: 150000/223652 UTXOs insérés... - ⏳ Traitement: 160000/223652 UTXOs insérés... - ⏳ Traitement: 170000/223652 UTXOs insérés... - ⏳ Traitement: 180000/223652 UTXOs insérés... - ⏳ Traitement: 190000/223652 UTXOs insérés... - ⏳ Traitement: 200000/223652 UTXOs insérés... - ⏳ Traitement: 210000/223652 UTXOs insérés... - ⏳ Traitement: 220000/223652 UTXOs insérés... + ⏳ Traitement: 10000/221494 UTXOs insérés... + ⏳ Traitement: 20000/221494 UTXOs insérés... + ⏳ Traitement: 30000/221494 UTXOs insérés... + ⏳ Traitement: 40000/221494 UTXOs insérés... + ⏳ Traitement: 50000/221494 UTXOs insérés... + ⏳ Traitement: 60000/221494 UTXOs insérés... + ⏳ Traitement: 70000/221494 UTXOs insérés... + ⏳ Traitement: 80000/221494 UTXOs insérés... + ⏳ Traitement: 90000/221494 UTXOs insérés... + ⏳ Traitement: 100000/221494 UTXOs insérés... + ⏳ Traitement: 110000/221494 UTXOs insérés... + ⏳ Traitement: 120000/221494 UTXOs insérés... + ⏳ Traitement: 130000/221494 UTXOs insérés... + ⏳ Traitement: 140000/221494 UTXOs insérés... + ⏳ Traitement: 150000/221494 UTXOs insérés... + ⏳ Traitement: 160000/221494 UTXOs insérés... + ⏳ Traitement: 170000/221494 UTXOs insérés... + ⏳ Traitement: 180000/221494 UTXOs insérés... + ⏳ Traitement: 190000/221494 UTXOs insérés... + ⏳ Traitement: 200000/221494 UTXOs insérés... + ⏳ Traitement: 210000/221494 UTXOs insérés... + ⏳ Traitement: 220000/221494 UTXOs insérés... 💾 Mise à jour des UTXOs dépensés... 📊 Résumé: - - UTXOs vérifiés: 49190 - - UTXOs toujours disponibles: 49190 + - UTXOs vérifiés: 37046 + - UTXOs toujours disponibles: 37046 - UTXOs dépensés détectés: 0 📈 Statistiques finales: - Total UTXOs: 68398 - - Dépensés: 19208 - - Non dépensés: 49190 + - Dépensés: 31352 + - Non dépensés: 37046 ✅ Synchronisation terminée 🔍 Démarrage de la synchronisation des UTXOs dépensés... -📊 UTXOs à vérifier: 49190 +📊 UTXOs à vérifier: 5146 📡 Récupération des UTXOs depuis Bitcoin... -📊 UTXOs disponibles dans Bitcoin: 223667 +📊 UTXOs disponibles dans Bitcoin: 215703 💾 Création de la table temporaire... 💾 Insertion des UTXOs disponibles par batch... - ⏳ Traitement: 10000/223667 UTXOs insérés... - ⏳ Traitement: 20000/223667 UTXOs insérés... - ⏳ Traitement: 30000/223667 UTXOs insérés... - ⏳ Traitement: 40000/223667 UTXOs insérés... - ⏳ Traitement: 50000/223667 UTXOs insérés... - ⏳ Traitement: 60000/223667 UTXOs insérés... - ⏳ Traitement: 70000/223667 UTXOs insérés... - ⏳ Traitement: 80000/223667 UTXOs insérés... - ⏳ Traitement: 90000/223667 UTXOs insérés... - ⏳ Traitement: 100000/223667 UTXOs insérés... - ⏳ Traitement: 110000/223667 UTXOs insérés... - ⏳ Traitement: 120000/223667 UTXOs insérés... - ⏳ Traitement: 130000/223667 UTXOs insérés... - ⏳ Traitement: 140000/223667 UTXOs insérés... - ⏳ Traitement: 150000/223667 UTXOs insérés... - ⏳ Traitement: 160000/223667 UTXOs insérés... - ⏳ Traitement: 170000/223667 UTXOs insérés... - ⏳ Traitement: 180000/223667 UTXOs insérés... - ⏳ Traitement: 190000/223667 UTXOs insérés... - ⏳ Traitement: 200000/223667 UTXOs insérés... - ⏳ Traitement: 210000/223667 UTXOs insérés... - ⏳ Traitement: 220000/223667 UTXOs insérés... + ⏳ Traitement: 10000/215703 UTXOs insérés... + ⏳ Traitement: 20000/215703 UTXOs insérés... + ⏳ Traitement: 30000/215703 UTXOs insérés... + ⏳ Traitement: 40000/215703 UTXOs insérés... + ⏳ Traitement: 50000/215703 UTXOs insérés... + ⏳ Traitement: 60000/215703 UTXOs insérés... + ⏳ Traitement: 70000/215703 UTXOs insérés... + ⏳ Traitement: 80000/215703 UTXOs insérés... + ⏳ Traitement: 90000/215703 UTXOs insérés... + ⏳ Traitement: 100000/215703 UTXOs insérés... + ⏳ Traitement: 110000/215703 UTXOs insérés... + ⏳ Traitement: 120000/215703 UTXOs insérés... + ⏳ Traitement: 130000/215703 UTXOs insérés... + ⏳ Traitement: 140000/215703 UTXOs insérés... + ⏳ Traitement: 150000/215703 UTXOs insérés... + ⏳ Traitement: 160000/215703 UTXOs insérés... + ⏳ Traitement: 170000/215703 UTXOs insérés... + ⏳ Traitement: 180000/215703 UTXOs insérés... + ⏳ Traitement: 190000/215703 UTXOs insérés... + ⏳ Traitement: 200000/215703 UTXOs insérés... + ⏳ Traitement: 210000/215703 UTXOs insérés... 💾 Mise à jour des UTXOs dépensés... 📊 Résumé: - - UTXOs vérifiés: 49190 - - UTXOs toujours disponibles: 49168 - - UTXOs dépensés détectés: 22 + - UTXOs vérifiés: 5146 + - UTXOs toujours disponibles: 5146 + - UTXOs dépensés détectés: 0 📈 Statistiques finales: - Total UTXOs: 68398 - - Dépensés: 19230 - - Non dépensés: 49168 + - Dépensés: 63307 + - Non dépensés: 5091 ✅ Synchronisation terminée diff --git a/docs/DOMAINS_AND_PORTS.md b/docs/DOMAINS_AND_PORTS.md index f22b7a8..a90bdc7 100644 --- a/docs/DOMAINS_AND_PORTS.md +++ b/docs/DOMAINS_AND_PORTS.md @@ -14,7 +14,8 @@ Ce document liste tous les domaines, ports et services de l'infrastructure Certi | Domaine | Service | Port Local | Description | |---------|---------|------------|-------------| -| `certificator.4nkweb.com` | API d'Ancrage | 3010 | API REST pour ancrer des documents | +| `anchorage.certificator.4nkweb.com` | API d'Ancrage | 3010 | API REST pour ancrer des documents (machine bitcoin 192.168.1.105) | +| `certificator.4nkweb.com` | API LeCoffre Anchor | 3004 | API REST LeCoffre pour ancrer (machine prod 192.168.1.103) | | `watermark.certificator.4nkweb.com` | API Filigrane | 3022 | API REST pour ajouter des filigranes et ancrer | | `antivir.certificator.4nkweb.com` | API ClamAV | 3023 | API REST pour scanner les fichiers (antivirus) | | `dashboard.certificator.4nkweb.com` | Dashboard | 3020 | Interface web de supervision | @@ -109,7 +110,8 @@ Internet ├─→ 4nk.myftp.biz (DynDNS) │ └─→ 192.168.1.100 (proxy) - Point d'entrée unique │ │ - │ ├─→ certificator.4nkweb.com → 192.168.1.103:3010 (API Anchorage) + │ ├─→ anchorage.certificator.4nkweb.com → 192.168.1.105:3010 (API Anchorage Bitcoin) + │ ├─→ 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) @@ -141,6 +143,7 @@ curl http://localhost:3022/health curl http://localhost:3023/health # Tester depuis l'extérieur (via domaine) +curl https://anchorage.certificator.4nkweb.com/health curl https://certificator.4nkweb.com/health curl -s https://dashboard.certificator.4nkweb.com/api/blockchain/info | head -c 200 ``` @@ -173,6 +176,7 @@ Tous les domaines utilisent des enregistrements CNAME pointant vers `4nk.myftp.b **Exemple de configuration DNS (Gandi) :** ``` +anchorage.certificator.4nkweb.com. 3600 IN CNAME 4nk.myftp.biz. certificator.4nkweb.com. 3600 IN CNAME 4nk.myftp.biz. watermark.certificator.4nkweb.com. 3600 IN CNAME 4nk.myftp.biz. antivir.certificator.4nkweb.com. 3600 IN CNAME 4nk.myftp.biz. @@ -180,6 +184,31 @@ dashboard.certificator.4nkweb.com. 3600 IN CNAME 4nk.myftp.biz. faucet.certificator.4nkweb.com. 3600 IN CNAME 4nk.myftp.biz. ``` +## APIs d'Ancrage + +Il existe deux APIs d'ancrage distinctes : + +### 1. API d'Ancrage Bitcoin (`anchorage.certificator.4nkweb.com`) + +- **Domaine :** `anchorage.certificator.4nkweb.com` +- **Machine :** 192.168.1.105 (bitcoin) +- **Port :** 3010 +- **Service systemd :** `anchorage-api` +- **Répertoire :** `/home/ncantu/Bureau/code/bitcoin/api-anchorage` +- **Bitcoin RPC :** 127.0.0.1:38332 (Bitcoin Core dans Docker sur la même machine) +- **Description :** API principale pour ancrer des documents sur Bitcoin Signet + +### 2. API LeCoffre Anchor (`certificator.4nkweb.com`) + +- **Domaine :** `certificator.4nkweb.com` +- **Machine :** 192.168.1.103 (prod) +- **Port :** 3004 +- **Service :** Processus Node.js dans `/srv/4NK/certificator.4nkweb.com/lecoffre-anchor-api` +- **Bitcoin RPC :** localhost:18443 (Bitcoin Core sur machine distante) +- **Description :** API LeCoffre pour ancrer des documents (service séparé) + +**Important :** Pour l'API d'ancrage Bitcoin principale, utiliser `anchorage.certificator.4nkweb.com` qui pointe vers la machine bitcoin (192.168.1.105). + ## Notes Importantes 1. **Ports fixes** : Tous les ports des APIs sont fixes et définis dans les services systemd. Ne pas modifier sans mettre à jour les services. diff --git a/docs/INTERFACES.md b/docs/INTERFACES.md index 67ab1dd..7c96f47 100644 --- a/docs/INTERFACES.md +++ b/docs/INTERFACES.md @@ -10,17 +10,22 @@ Ce document liste toutes les interfaces et IHM (Interfaces Homme-Machine) dispon --- -## 1. API REST d'Ancrage +## 1. API REST d'Ancrage Bitcoin ### Description API REST HTTP/JSON pour ancrer des documents sur la blockchain Bitcoin Signet. ### Accès -- **URL** : `https://certificator.4nkweb.com` (via nginx proxy) +- **URL** : `https://anchorage.certificator.4nkweb.com` (via nginx proxy) +- **Machine** : 192.168.1.105 (bitcoin) - **Port local** : `3010` - **Protocole** : HTTPS (production) / HTTP (développement) - **Format** : JSON +**Note :** Il existe deux APIs d'ancrage distinctes : +- **API Bitcoin** : `https://anchorage.certificator.4nkweb.com` (machine bitcoin 192.168.1.105, port 3010) - API principale +- **API LeCoffre** : `https://certificator.4nkweb.com` (machine prod 192.168.1.103, port 3004) - API LeCoffre séparée + ### Endpoints #### GET `/` @@ -33,8 +38,10 @@ Informations sur l'API "version": "1.0.0", "endpoints": { "health": "/health", + "healthDetailed": "/health/detailed", "anchor": "/api/anchor/document", - "verify": "/api/anchor/verify" + "verify": "/api/anchor/verify", + "lockedUtxos": "/api/anchor/locked-utxos" } } ``` @@ -57,6 +64,69 @@ Vérifie l'état de l'API et de la connexion Bitcoin } ``` +#### GET `/health/detailed` +Vérifie l'état détaillé de l'API, incluant l'état du mutex, des UTXOs verrouillés et de la connexion Bitcoin + +**Authentification** : Non requise + +**Réponse (200 OK)** : +```json +{ + "ok": true, + "service": "anchor-api", + "mutex": { + "locked": false, + "waiting": 0, + "timeout": 180000 + }, + "utxos": { + "locked": 0, + "locked_since": null, + "stale_locks": 0, + "stale_locks_details": [] + }, + "bitcoin": { + "connected": true, + "blocks": 152321, + "chain": "signet", + "rpc_timeout": 60000 + }, + "timestamp": "2026-01-23T16:35:27.821Z" +} +``` + +**Réponse (503 Service Unavailable)** : Si UTXOs verrouillés depuis > 10 min ou > 10 UTXOs verrouillés +```json +{ + "ok": false, + "service": "anchor-api", + "mutex": { + "locked": false, + "waiting": 0, + "timeout": 180000 + }, + "utxos": { + "locked": 5, + "locked_since": "2026-01-23T16:25:00.000Z", + "stale_locks": 5, + "stale_locks_details": [ + { + "txid": "abc123...", + "vout": 0, + "minutes_locked": 15.5 + } + ] + }, + "bitcoin": { + "connected": true, + "blocks": 152321, + "chain": "signet", + "rpc_timeout": 60000 + }, + "timestamp": "2026-01-23T16:35:27.821Z" +} +``` + #### POST `/api/anchor/document` Ancre un document sur Bitcoin Signet diff --git a/features/api-anchorage-monitoring-and-prevention.md b/features/api-anchorage-monitoring-and-prevention.md new file mode 100644 index 0000000..ca826d0 --- /dev/null +++ b/features/api-anchorage-monitoring-and-prevention.md @@ -0,0 +1,190 @@ +# API d'Ancrage - Monitoring et Prévention des Blocages + +**Date:** 2026-01-28 +**Auteur:** Équipe 4NK + +## Objectif + +Implémenter des moyens de contrôle et de correction pour prévenir et résoudre les blocages du mutex et des UTXOs verrouillés dans l'API d'ancrage. + +## Motivations + +- **Prévention des blocages:** Éviter que le mutex reste bloqué indéfiniment +- **Détection précoce:** Identifier les problèmes avant qu'ils n'affectent les utilisateurs +- **Récupération automatique:** Déverrouiller automatiquement les UTXOs verrouillés depuis trop longtemps +- **Robustesse:** Gérer les timeouts RPC Bitcoin avec retry et backoff +- **Observabilité:** Fournir des endpoints de monitoring détaillés + +## Impacts + +### Fonctionnels + +- **Endpoint `/health/detailed`:** Fournit un état détaillé du mutex, des UTXOs et de la connexion Bitcoin +- **Déverrouillage automatique:** Les UTXOs verrouillés depuis > 10 minutes sont automatiquement déverrouillés au démarrage +- **Timeout de sécurité:** Le mutex est automatiquement libéré après 5 minutes maximum +- **Retry avec backoff:** Les appels RPC Bitcoin sont automatiquement réessayés en cas de timeout +- **Monitoring de durée:** Les opérations > 30 secondes sont loggées avec un warning + +### Techniques + +- **Timeout sur Promise.all():** La génération d'adresses a un timeout de 30 secondes +- **État du mutex:** Suivi en temps réel de l'état du mutex (locked, waiting) +- **Scripts de maintenance:** Scripts pour diagnostic et nettoyage automatique +- **Documentation:** Documentation complète du monitoring et de la maintenance + +## Modifications + +### Fichiers modifiés + +1. **`api-anchorage/src/routes/health.js`** + - Ajout de l'endpoint `/health/detailed` avec état du mutex et UTXOs + - Vérification des UTXOs verrouillés depuis > 10 minutes + - Codes de statut adaptatifs selon l'état + +2. **`api-anchorage/src/bitcoin-rpc.js`** + - Ajout de `getMutexState()` pour exposer l'état du mutex + - Suivi de l'état du mutex (`utxoMutexLocked`, `utxoMutexWaiting`) + - Timeout de sécurité de 5 minutes sur le mutex dans `createAnchorTransaction()` + - Timeout de 30 secondes sur `Promise.all()` pour la génération d'adresses + - Méthode `callRPCWithRetry()` pour retry avec backoff exponentiel + - Méthode `callRPCCommandWithRetry()` pour les commandes RPC + - Tous les appels RPC critiques utilisent maintenant le retry avec backoff + - Monitoring de la durée des opérations avec alerte si > 30s + +3. **`api-anchorage/src/server.js`** + - Déverrouillage automatique des UTXOs verrouillés depuis > 10 minutes au démarrage + - Exclusion de `/health/detailed` de l'authentification API Key + - Mise à jour de la liste des endpoints dans la route racine + +4. **`api-anchorage/cleanup-stale-locks.mjs`** (nouveau) + - Script pour déverrouiller les UTXOs verrouillés depuis > 10 minutes + - À exécuter via cron job toutes les 5 minutes + +5. **`api-anchorage/diagnose.mjs`** (nouveau) + - Script de diagnostic complet de l'état des UTXOs + - Affiche les UTXOs verrouillés avec leur durée de verrouillage + - Statistiques générales des UTXOs + +6. **`signet-dashboard/public/api-docs.html`** + - Ajout de la documentation de l'endpoint `/health/detailed` + - Mise à jour de la liste des endpoints publics + - Correction de l'URL de l'API d'ancrage (anchorage.certificator.4nkweb.com) + +7. **`docs/INTERFACES.md`** + - Ajout de la documentation de l'endpoint `/health/detailed` + - Mise à jour de la liste des endpoints + +8. **`api-anchorage/README-MONITORING.md`** (nouveau) + - Documentation complète du monitoring et de la maintenance + - Procédures de dépannage + - Configuration des cron jobs + +### Appels RPC mis à jour avec retry + +- `getNewAddress()` → `callRPCWithRetry('getNewAddress', [])` +- `getBalance()` → `callRPCWithRetry('getBalance', [])` +- `checkConnection()` → `callRPCWithRetry('getNetworkInfo', [])` et `callRPCWithRetry('getBlockchainInfo', [])` +- `listunspent()` → `callRPCWithRetry('listunspent', [...])` +- `createrawtransaction` → `callRPCCommandWithRetry('createrawtransaction', ...)` +- `signrawtransactionwithwallet` → `callRPCCommandWithRetry('signrawtransactionwithwallet', ...)` +- `sendrawtransaction` → `callRPCCommandWithRetry('sendrawtransaction', ...)` +- `getmempoolentry` → `callRPCCommandWithRetry('getmempoolentry', ...)` +- `getTransaction()` → `callRPCWithRetry('getTransaction', [...])` +- `getRawTransaction()` → `callRPCWithRetry('getRawTransaction', [...])` +- `getTransactionInfo()` → Utilise `callRPCWithRetry()` pour tous les appels + +## Modalités de déploiement + +### 1. Redémarrage du service + +```bash +sudo systemctl restart anchorage-api +``` + +### 2. Vérification + +```bash +# Health check détaillé +curl http://localhost:3010/health/detailed + +# Depuis l'extérieur +curl https://anchorage.certificator.4nkweb.com/health/detailed +``` + +### 3. Configuration du cron job (optionnel mais recommandé) + +Créer `/etc/cron.d/anchorage-cleanup`: + +```bash +# Nettoyage automatique des UTXOs verrouillés depuis plus de 10 minutes +# Toutes les 5 minutes +*/5 * * * * ncantu cd /home/ncantu/Bureau/code/bitcoin/api-anchorage && /usr/bin/node cleanup-stale-locks.mjs >> /var/log/anchorage-cleanup.log 2>&1 +``` + +### 4. Test des scripts + +```bash +# Diagnostic +cd /home/ncantu/Bureau/code/bitcoin/api-anchorage +node diagnose.mjs + +# Nettoyage +node cleanup-stale-locks.mjs + +# Déverrouillage manuel +node unlock-utxos.mjs +``` + +## Modalités d'analyse + +### Endpoints de monitoring + +1. **GET `/health/detailed`** + - Vérifier l'état du mutex (`locked`, `waiting`) + - Vérifier le nombre d'UTXOs verrouillés + - Vérifier les UTXOs verrouillés depuis > 10 minutes (`stale_locks`) + - Vérifier la connexion Bitcoin + +2. **GET `/api/anchor/locked-utxos`** + - Liste complète des UTXOs verrouillés + - Compteur total + +### Logs à surveiller + +1. **Timeouts de mutex:** + ```bash + sudo journalctl -u anchorage-api | grep "Mutex acquisition timeout" + ``` + +2. **Opérations longues:** + ```bash + sudo journalctl -u anchorage-api | grep "took too long" + ``` + +3. **Erreurs RPC Bitcoin:** + ```bash + sudo journalctl -u anchorage-api | grep "ESOCKETTIMEDOUT\|ETIMEDOUT" + ``` + +4. **Retry RPC:** + ```bash + sudo journalctl -u anchorage-api | grep "RPC call failed, retrying" + ``` + +### Scripts de diagnostic + +1. **`diagnose.mjs`:** Diagnostic complet de l'état des UTXOs +2. **`cleanup-stale-locks.mjs`:** Nettoyage automatique des UTXOs stale +3. **`unlock-utxos.mjs`:** Déverrouillage manuel de tous les UTXOs + +## Pages affectées + +- `api-anchorage/src/routes/health.js`: Endpoint `/health/detailed` +- `api-anchorage/src/bitcoin-rpc.js`: Monitoring, timeouts, retry avec backoff +- `api-anchorage/src/server.js`: Déverrouillage automatique au démarrage +- `api-anchorage/cleanup-stale-locks.mjs`: Script de nettoyage (nouveau) +- `api-anchorage/diagnose.mjs`: Script de diagnostic (nouveau) +- `api-anchorage/README-MONITORING.md`: Documentation (nouveau) +- `signet-dashboard/public/api-docs.html`: Documentation API mise à jour +- `docs/INTERFACES.md`: Documentation API mise à jour +- `features/api-anchorage-monitoring-and-prevention.md`: Documentation (nouveau) diff --git a/features/login-bout-en-bout-implémentations.md b/features/login-bout-en-bout-implémentations.md new file mode 100644 index 0000000..e638ded --- /dev/null +++ b/features/login-bout-en-bout-implémentations.md @@ -0,0 +1,207 @@ +# Login fonctionnel de bout en bout - Implémentations réalisées + +**Author:** Équipe 4NK +**Date:** 2026-01-28 + +## Vue d'ensemble + +Ce document décrit les implémentations réalisées pour compléter le login fonctionnel de bout en bout sur PC et mobile. + +--- + +## 1. userwallet - Notifications relais pour mobile + +**Note importante :** Le modèle est **pull-only** (HTTP REST uniquement). Les WebSockets ne sont pas souhaités selon la documentation (`userwallet/features/userwallet-notifications-relais.md`). + +### 1.1 Modèle pull-only (HTTP REST) + +**Fichier modifié :** `userwallet/src/services/relayNotificationService.ts` + +**Implémentations :** + +1. **Polling HTTP REST uniquement** + - Modèle pull-only : uniquement des appels GET HTTP REST + - Pas de WebSocket (non souhaité selon la documentation) + - Polling périodique des relais pour détecter nouveaux hashes + - Détection via `GET /keys?start=&end=` pour scanner les nouveaux messages + +2. **Gestion du polling** + - `startPolling(intervalMs)` : démarre le polling périodique + - `stopPolling()` : arrête le polling + - Fenêtre temporelle configurable pour le scan des clés + - Émission d'événements `RelayHashEvent` avec `source: 'polling'` + +### 1.2 Optimisations mobile (backoff, reconnexions) + +**Fichier modifié :** `userwallet/src/services/relayNotificationService.ts` + +**Implémentations :** + +1. **Stratégie de backoff exponentiel** + - Interface `BackoffState` pour gérer l'état de retry par relais + - `getBackoffDelay(endpoint)` : calcul du délai avec exponentiel (max 60s) + - `recordFailure(endpoint)` : enregistre un échec et incrémente les tentatives + - `recordSuccess(endpoint)` : réinitialise le backoff après succès + - `shouldRetry(endpoint)` : vérifie si un retry est autorisé (max 5 tentatives) + +2. **Gestion réseau mobile** + - `setupNetworkListeners()` : écoute des événements `online`/`offline` + - `resumeOperations()` : reprend les opérations après reconnexion + - `pauseOperations()` : pause les opérations en mode offline + - Vérification `isOnline` avant chaque opération réseau + +3. **Optimisation des appels réseau** + - Retry avec backoff pour `getMessageByHash`, `getSignatures`, `getKeys` + - Skip des relais en échec répétés + - Réduction des appels inutiles en mode offline + +### 1.3 Fetch automatique des données + +**Fichier modifié :** `userwallet/src/services/relayNotificationService.ts` + +**Implémentations :** + +1. **ProcessHash amélioré** + - Fetch automatique de message, signatures, clés quand un hash est connu + - Déchiffrement et mise à jour du graphe automatiques + - Gestion des erreurs avec backoff par relais + +2. **ProcessHashByType optimisé** + - Optimisation selon le type d'objet (signature, contrat, membre, pair, action, champ) + - Fetch sélectif selon le type (ex: pas besoin de clés pour une signature seule) + +3. **Intégration avec useRelayNotifications** + - Auto-processing des hashes détectés via polling + - Démarrage automatique du polling avec `startPolling()` + - Arrêt propre avec `stopPolling()` et `cleanup()` + +**Fichier modifié :** `userwallet/src/hooks/useRelayNotifications.ts` + +**Implémentations :** + +1. **Polling HTTP REST** + - `startPolling()` démarre le polling périodique + - `stopPolling()` arrête le polling + - Gestion du cycle de vie du polling + +--- + +## 2. userwallet - Responsive design mobile + +### 2.1 CSS responsive + +**Fichier modifié :** `userwallet/src/index.css` + +**Implémentations :** + +1. **Media queries** + - `@media (max-width: 768px)` : tablettes et petits écrans + - `@media (max-width: 480px)` : smartphones + - Adaptation des paddings, marges, tailles de police + +2. **Tailles tactiles** + - `min-height: 44px` pour tous les boutons (recommandation Apple/Google) + - `min-width: 44px` pour les éléments interactifs + - Boutons en `width: 100%` sur mobile + +3. **Gestion du clavier virtuel** + - `font-size: 16px` pour les inputs (évite le zoom automatique iOS) + - `scroll-margin-top: 100px` pour le focus des inputs + - Adaptation des hauteurs d'iframe selon la taille d'écran + +4. **Optimisations tactiles** + - `@media (hover: none) and (pointer: coarse)` : détection tactile + - Feedback visuel avec `opacity` et `transform` sur `:active` + - Espacement suffisant entre les éléments interactifs + +--- + +## 3. service-login-verify - Persistance NonceCache + +### 3.1 Amélioration PersistentNonceCache + +**Fichier modifié :** `service-login-verify/src/persistentNonceCache.ts` + +**Implémentations :** + +1. **Persistance IndexedDB en arrière-plan** + - `persistToIndexedDB(nonce, timestamp)` : persiste en IndexedDB de manière asynchrone + - Utilisation de localStorage comme stockage principal (synchrone, requis par l'interface) + - IndexedDB utilisé comme backup persistant + +2. **Nettoyage IndexedDB** + - `cleanupIndexedDB(now)` : supprime les entrées expirées depuis IndexedDB + - Utilisation de l'index `timestamp` pour les requêtes efficaces + - Nettoyage asynchrone non-bloquant + +3. **Double persistance** + - localStorage : accès synchrone (requis par `NonceCacheLike`) + - IndexedDB : persistance entre sessions (backup) + - Les deux sont maintenus en synchronisation + +**Note :** `PersistentNonceCache` est déjà disponible et peut être utilisé à la place de `NonceCache` dans `website-skeleton` si persistance entre redémarrages nécessaire. + +--- + +## Synthèse des modifications + +### Fichiers modifiés + +1. `userwallet/src/services/relayNotificationService.ts` : + - Modèle pull-only (HTTP REST) + - Optimisations mobile (backoff, reconnexions) + - Gestion réseau online/offline + +2. `userwallet/src/hooks/useRelayNotifications.ts` : + - Polling HTTP REST automatique + - Gestion du cycle de vie + +3. `userwallet/src/index.css` : + - Media queries responsive + - Tailles tactiles + - Gestion clavier virtuel + +4. `service-login-verify/src/persistentNonceCache.ts` : + - Persistance IndexedDB améliorée + +--- + +## Utilisation + +### userwallet - Activer les notifications + +Le hook `useRelayNotifications` active le polling HTTP REST : + +```typescript +const { startPolling, stopPolling } = useRelayNotifications(graphResolver, true); +// Démarrer polling HTTP REST (pull-only) +startPolling(60000); // 60 secondes +``` + +### service-login-verify - Utiliser PersistentNonceCache + +Dans `website-skeleton/src/main.ts` : + +```typescript +import { PersistentNonceCache } from 'service-login-verify'; + +const nonceCache = new PersistentNonceCache(3600000); +await nonceCache.init(); // Initialiser IndexedDB +``` + +--- + +## Notes importantes + +- **Modèle pull-only** : Le système utilise uniquement des appels HTTP REST (GET). Les WebSockets ne sont pas souhaités selon la documentation (`userwallet/features/userwallet-notifications-relais.md`). +- **Persistance NonceCache** : `PersistentNonceCache` utilise localStorage comme stockage principal (synchrone) et IndexedDB en arrière-plan (asynchrone). +- **Backoff** : Limité à 5 tentatives maximum avec délai exponentiel jusqu'à 60 secondes. +- **Mobile** : Les optimisations réduisent les appels réseau et gèrent les reconnexions automatiquement. + +--- + +## Références + +- `features/login-bout-en-bout-reste-a-faire.md` : Liste initiale des éléments à implémenter +- `userwallet/src/services/relayNotificationService.ts` : Service de notifications +- `service-login-verify/src/persistentNonceCache.ts` : Cache persistant diff --git a/features/login-bout-en-bout-reste-a-faire.md b/features/login-bout-en-bout-reste-a-faire.md new file mode 100644 index 0000000..5637e42 --- /dev/null +++ b/features/login-bout-en-bout-reste-a-faire.md @@ -0,0 +1,276 @@ +# Login fonctionnel de bout en bout PC et Mobile - Reste à faire + +**Author:** Équipe 4NK +**Date:** 2026-01-28 + +## Vue d'ensemble + +Ce document liste ce qui reste à implémenter pour avoir un login fonctionnel de bout en bout sur PC et mobile pour les projets : +- `website-skeleton` +- `userwallet` +- `service-login-verify` +- `api-relay` + +--- + +## 1. website-skeleton + +### 1.1 Envoi du contrat au UserWallet (Priorité : Haute) + +**Statut :** ✅ Implémenté +**Référence :** `website-skeleton/src/main.ts` + +**Fait :** +- ✅ Fonction `sendContractToIframe()` qui envoie le contrat stocké à l'iframe +- ✅ Stockage du contrat reçu pour envoi ultérieur (`storedContract`, `storedContratsFils`, `storedActions`) +- ✅ Envoi automatique du contrat au chargement de l'iframe (événement `load`) +- ✅ Envoi du contrat lors de la réception d'un nouveau contrat via `postMessage` +- ✅ Réception des messages `contract` depuis le parent (écoute `postMessage`) +- ✅ Extraction des validateurs depuis les contrats reçus +- ✅ Mise à jour des clés autorisées pour la vérification + +**Note :** L'implémentation est complète. Le contrat est envoyé automatiquement à l'iframe dès qu'il est disponible. + +### 1.2 Gestion de session utilisateur (Priorité : Haute) + +**Statut :** ✅ Implémenté +**Référence :** `website-skeleton/src/main.ts`, `website-skeleton/index.html` + +**Fait :** +- ✅ Stockage de session dans `sessionStorage` après acceptation de la preuve +- ✅ Fonctions `getSession()`, `setSession()`, `clearSession()`, `isLoggedIn()` +- ✅ Masquage de l'iframe après login réussi +- ✅ Affichage d'une interface "connecté" avec informations utilisateur (clés publiques) +- ✅ Bouton de déconnexion fonctionnel +- ✅ Vérification de session au chargement de la page +- ✅ Mise à jour automatique de l'interface selon l'état de connexion +- ✅ Vérification de la preuve de login avec `verifyLoginProof()` +- ✅ Affichage du statut (accepté/refusé) + +**Note :** L'implémentation est complète. La protection des routes/endpoints reste à implémenter si nécessaire selon les besoins spécifiques du service. + +### 1.3 Interface responsive mobile (Priorité : Moyenne) + +**Statut :** ✅ Implémenté (à tester) + +**Fait :** +- ✅ Meta viewport configuré (`width=device-width, initial-scale=1.0`) +- ✅ Styles CSS responsive avec media queries (768px, 480px) +- ✅ Adaptation de l'iframe pour mobile (hauteurs différentes selon taille d'écran) +- ✅ Boutons adaptés au tactile (min-height: 44px, width: 100% sur mobile) +- ✅ Layout flex adaptatif pour petits écrans +- ✅ Tailles de police adaptées + +**À tester :** +- Vérifier le comportement sur différents appareils mobiles +- Tester le clavier virtuel (focus, scroll) +- Valider les gestes tactiles si nécessaire + +### 1.4 Amélioration UX (Priorité : Moyenne) + +**Statut :** ✅ Implémenté (améliorations possibles) + +**Fait :** +- ✅ Bouton "Se connecter" clair et visible (style primary) +- ✅ Gestion de l'affichage/masquage de l'iframe selon l'état +- ✅ Indicateur de statut de connexion avec couleurs (accepted/rejected/pending) +- ✅ Messages d'erreur avec raison du refus +- ✅ Interface "connecté" avec informations utilisateur +- ✅ Styles améliorés (couleurs, transitions, bordures arrondies) + +**Améliorations possibles (optionnel) :** +- Loading states plus visibles pendant la vérification +- Guide utilisateur pour le premier login +- Messages d'aide contextuels + +--- + +## 2. userwallet + +### 2.1 Validation des écrans login (Priorité : Haute) + +**Statut :** À valider avant implémentation +**Référence :** `features/userwallet-ecrans-login-a-valider.md`, `RESTE_A_FAIRE.md` (§ 1.1) + +**Action requise :** Tester les écrans login existants et valider leur fonctionnement sur PC et mobile. + +**Écrans à valider :** +- Sélection service / sélection membre +- Construction du chemin login +- Message de login à valider +- Collecte signatures mFA +- Publication +- Vérification locale + résultat + +### 2.2 Notifications relais pour mobile (Priorité : Haute) + +**Statut :** Partiellement implémenté +**Référence :** `RESTE_A_FAIRE.md` (§ 1.2), `features/userwallet-contrat-login-reste-a-faire.md` (§ 3.2) + +**Fait :** +- Progression collecte signatures (X/Y) implémentée via `onProgress` dans `runCollectLoop` +- Affichage dans `LoginCollectShare` + +**Reste à faire :** + +1. **Notifications push (si extension mobile)** + - Réagir aux événements push du relais pour savoir quel hash fetch + - Implémenter un mécanisme de notification (Service Worker, Web Push, etc.) + +2. **Fetch automatique des données** + - Une fois le hash connu (via notification ou autre) : récupération sur le relai des signatures, contrats, membres, pairs, actions, champs + - Les notifications doivent piloter : quel hash fetch, puis fetch signatures/clés et mise à jour du graphe + +3. **Optimisation pour mobile** + - Réduire les appels réseau inutiles + - Gérer les reconnexions réseau + - Optimiser la collecte de signatures sur mobile (batterie, données) + +**Fichiers concernés :** +- `userwallet/src/services/relayNotificationService.ts` +- `userwallet/src/hooks/useRelayNotifications.ts` +- `userwallet/src/components/LoginCollectShare.tsx` + +### 2.3 Responsive design mobile (Priorité : Moyenne) + +**Statut :** À vérifier et améliorer + +**Ce qui manque :** + +1. **Interface responsive** + - Vérifier que tous les écrans login sont utilisables sur mobile + - Adapter les formulaires pour petits écrans + - Gérer le clavier virtuel (focus, scroll) + +2. **UX mobile** + - Gestes tactiles (swipe, etc.) + - Tailles de boutons adaptées au tactile + - Feedback visuel pour les actions + +3. **Performance mobile** + - Optimiser le chargement initial + - Réduire la consommation mémoire + - Gérer les limites de stockage IndexedDB sur mobile + +--- + +## 3. service-login-verify + +### 3.1 Intégration dans website-skeleton (Priorité : Basse) + +**Statut :** Déjà intégré et fonctionnel + +**Fait :** +- Package `service-login-verify` intégré dans `website-skeleton` +- Utilisation de `verifyLoginProof`, `NonceCache`, `buildAllowedPubkeysFromValidateurs` +- Instance de `NonceCache` créée et utilisée +- Construction des clés autorisées depuis les validateurs +- Vérification des preuves reçues + +**Note :** L'intégration est complète et fonctionnelle. Aucune action requise. + +### 3.2 Persistance du NonceCache (Priorité : Basse - Optionnel) + +**Statut :** Actuellement en mémoire uniquement +**Référence :** `RESTE_A_FAIRE.md` (§ 2.1) + +**Description :** Le `NonceCache` actuel est en mémoire avec TTL configurable. Une persistance optionnelle (IndexedDB, localStorage, etc.) peut être ajoutée si nécessaire pour éviter les rejeux après redémarrage du service. + +**Action requise :** Implémenter uniquement si besoin de persistance entre redémarrages du service. + +--- + +## 4. api-relay + +### 4.1 Support notifications push pour mobile (Priorité : Moyenne) + +**Statut :** À vérifier + +**Ce qui manque :** + +1. **Notifications push (optionnel)** + - Si support Web Push : endpoint pour s'abonner aux notifications + - Envoyer des notifications quand de nouveaux messages/signatures/clés sont disponibles + - Gérer les abonnements par hash ou par service + +2. **Optimisation pour mobile** + - Réduire la taille des réponses + - Support de la compression + - Pagination pour les grandes listes + +**Note :** Le modèle actuel est pull-only (GET). Les notifications push sont optionnelles mais amélioreraient l'expérience mobile. + +--- + +## Synthèse par priorité + +### ✅ Complété dans website-skeleton + +1. ✅ **website-skeleton** : Envoi du contrat au UserWallet (1.1) +2. ✅ **website-skeleton** : Gestion de session utilisateur (1.2) +3. ✅ **website-skeleton** : Interface responsive mobile (1.3) +4. ✅ **website-skeleton** : Amélioration UX (1.4) + +### Priorité Haute (Blocant pour login fonctionnel) + +1. **userwallet** : Validation des écrans login (2.1) - **À valider par tests** + +### Priorité Moyenne (Améliore l'expérience) + +2. **userwallet** : Notifications relais pour mobile (2.2) +3. **userwallet** : Responsive design mobile (2.3) - **À vérifier** +4. **api-relay** : Support notifications push (4.1) - **Optionnel** + +### Priorité Basse (Optionnel) + +5. **service-login-verify** : Persistance NonceCache (3.2) + +--- + +## Workflow de login complet (cible) + +### Sur PC + +1. Utilisateur ouvre website-skeleton +2. Clique sur "Se connecter" (ou iframe s'affiche automatiquement) +3. Iframe UserWallet s'affiche +4. website-skeleton envoie le contrat via `postMessage` à l'iframe +5. UserWallet reçoit le contrat et met à jour le graphe +6. Utilisateur sélectionne le service et le membre +7. UserWallet construit le challenge et collecte les signatures +8. UserWallet publie la preuve sur les relais +9. UserWallet envoie `login-proof` au parent via `postMessage` +10. website-skeleton vérifie la preuve avec `service-login-verify` +11. Si acceptée : session ouverte, iframe masquée, interface "connecté" affichée +12. Si refusée : erreur affichée, possibilité de réessayer + +### Sur Mobile + +1. Utilisateur ouvre website-skeleton (responsive) +2. Clique sur "Se connecter" +3. Iframe UserWallet s'affiche (adaptatif, responsive) +4. Même workflow que PC +5. **Différence :** Notifications push pour informer des nouvelles signatures disponibles (si implémenté) + +--- + +## Notes importantes + +- Les éléments marqués "À valider" nécessitent une validation explicite avant implémentation. +- `website-skeleton` a déjà une intégration de base fonctionnelle (iframe, vérification, écoute messages). +- Le package `service-login-verify` est déjà intégré et fonctionnel dans `website-skeleton`. +- Les points bloquants principaux sont : l'envoi du contrat à l'iframe et la gestion de session. +- Les écrans login de userwallet sont en place mais nécessitent validation. +- Les notifications push sont optionnelles mais amélioreraient l'expérience mobile. + +--- + +## Références + +- `features/userwallet-contrat-login-reste-a-faire.md` +- `features/userwallet-ecrans-login-a-valider.md` +- `features/service-login-verify.md` +- `RESTE_A_FAIRE.md` +- `website-skeleton/src/main.ts` : exemple d'intégration complète +- `userwallet/docs/specs.md` : spécifications complètes +- `service-login-verify/README.md` : documentation du package diff --git a/fixKnowledge/api-anchorage-mutex-blockage-analysis.md b/fixKnowledge/api-anchorage-mutex-blockage-analysis.md new file mode 100644 index 0000000..6374dc4 --- /dev/null +++ b/fixKnowledge/api-anchorage-mutex-blockage-analysis.md @@ -0,0 +1,476 @@ +# Analyse: Causes possibles du blocage du mutex et UTXOs verrouillés + +**Date:** 2026-01-28 +**Auteur:** Équipe 4NK + +## Vue d'ensemble + +Ce document analyse les causes possibles du blocage du mutex et des UTXOs verrouillés dans l'API d'ancrage, ainsi que les moyens de contrôle et de correction. + +## Causes possibles identifiées + +### 1. Timeout RPC Bitcoin (ESOCKETTIMEDOUT) + +**Cause:** +- Les appels RPC vers Bitcoin Core timeout (défaut: 30s, configuré: 60s) +- Si un appel RPC bloque indéfiniment, le code ne peut pas continuer +- Le mutex reste acquis et les UTXOs restent verrouillés + +**Scénarios:** +- Bitcoin Core surchargé ou non réactif +- Problème réseau entre l'API et Bitcoin Core +- Bitcoin Core en cours de synchronisation ou de traitement lourd +- Wallet Bitcoin verrouillé ou non accessible + +**Code concerné:** +- `bitcoin-rpc.js` ligne 23: `timeout: parseInt(process.env.BITCOIN_RPC_TIMEOUT || '30000')` +- `.env` ligne 6: `BITCOIN_RPC_TIMEOUT=60000` + +**Indicateurs:** +- Logs: `Error getting balance: ESOCKETTIMEDOUT` +- Logs: `Failed to get balance: ESOCKETTIMEDOUT` +- Mutex bloqué pendant plus de 60 secondes + +### 2. Promise.all() qui ne se résout jamais + +**Cause:** +- Ligne 253: `await Promise.all(addressPromises)` peut bloquer si une Promise ne se résout jamais +- Si `getNewAddress()` timeout ou échoue silencieusement, la Promise reste en attente +- Le mutex reste acquis + +**Scénarios:** +- Bitcoin RPC ne répond pas à `getNewAddress()` +- Timeout RPC trop court pour certaines opérations +- Erreur réseau non gérée + +**Code concerné:** +- `bitcoin-rpc.js` lignes 248-253: Génération des adresses en parallèle + +**Indicateurs:** +- Logs montrent "Anchor request received" mais pas de suite +- Pas d'erreur dans les logs, mais la requête ne se termine jamais + +### 3. Erreur non gérée avant le bloc finally + +**Cause:** +- Si une erreur se produit avant d'atteindre le bloc `finally` (ligne 908), le mutex peut ne pas être libéré +- Exemple: erreur fatale Node.js, OOM (Out of Memory), crash du processus + +**Scénarios:** +- Erreur fatale Node.js (segfault, assertion failed) +- Manque de mémoire (OOM killer) +- Arrêt brutal du processus (kill -9) +- Exception non catchée dans une fonction asynchrone + +**Code concerné:** +- `bitcoin-rpc.js` ligne 908-914: Bloc `finally` qui libère le mutex + +**Indicateurs:** +- Processus Node.js redémarré par systemd +- Logs montrent un arrêt brutal sans message d'erreur +- UTXOs verrouillés après redémarrage + +### 4. Erreur dans le bloc finally lui-même + +**Cause:** +- Si `releaseMutex()` échoue dans le bloc `finally`, le mutex reste bloqué +- Actuellement, l'erreur est seulement loggée en WARN, mais le mutex n'est pas libéré + +**Scénarios:** +- Exception lors de l'appel à `releaseMutex()` +- Problème avec la Promise du mutex + +**Code concerné:** +- `bitcoin-rpc.js` lignes 910-914: Gestion d'erreur dans le `finally` + +**Indicateurs:** +- Logs: `Error releasing mutex` +- Mutex reste bloqué malgré le `finally` + +### 5. Déverrouillage des UTXOs qui échoue silencieusement + +**Cause:** +- Ligne 903: `this.unlockUtxo()` peut échouer silencieusement (ligne 152-158) +- Si la mise à jour de la base de données échoue, l'UTXO reste verrouillé +- Le mutex est libéré, mais les UTXOs restent verrouillés en base + +**Scénarios:** +- Base de données verrouillée (WAL mode, autre transaction en cours) +- Erreur SQL (contrainte, table verrouillée) +- Problème de permissions sur la base de données + +**Code concerné:** +- `bitcoin-rpc.js` lignes 142-159: `unlockUtxo()` avec gestion d'erreur silencieuse +- `bitcoin-rpc.js` lignes 899-905: Déverrouillage en cas d'erreur + +**Indicateurs:** +- Mutex libéré (pas de timeout) +- UTXOs toujours verrouillés dans la base de données +- Logs: `Error updating UTXO unlock status in database` + +### 6. Transaction SQL qui échoue partiellement + +**Cause:** +- Si plusieurs UTXOs sont verrouillés mais que le déverrouillage échoue pour certains +- La boucle continue mais certains UTXOs restent verrouillés + +**Scénarios:** +- Erreur SQL pour un UTXO spécifique +- UTXO supprimé de la base pendant le déverrouillage +- Contrainte de base de données violée + +**Code concerné:** +- `bitcoin-rpc.js` lignes 900-904: Boucle de déverrouillage + +**Indicateurs:** +- Certains UTXOs déverrouillés, d'autres non +- Logs montrant des erreurs pour certains UTXOs + +### 7. Race condition lors de l'acquisition du mutex + +**Cause:** +- Si plusieurs requêtes arrivent simultanément, elles peuvent toutes acquérir le mutex +- Le timeout de 180s peut être atteint avant que toutes les requêtes ne se terminent + +**Scénarios:** +- Pic de charge avec de nombreuses requêtes simultanées +- Requêtes qui prennent plus de 180s chacune +- Accumulation de requêtes en attente + +**Code concerné:** +- `bitcoin-rpc.js` lignes 42-76: `acquireUtxoMutex()` avec timeout de 180s + +**Indicateurs:** +- Nombreux timeouts de mutex dans les logs +- Requêtes qui attendent plus de 180s + +### 8. Problème de connexion à la base de données + +**Cause:** +- Si la connexion à la base de données est perdue, les opérations de verrouillage/déverrouillage échouent +- Le mutex peut être libéré en mémoire, mais les UTXOs restent verrouillés en base + +**Scénarios:** +- Base de données corrompue +- Connexion SQLite fermée +- Problème de permissions + +**Code concerné:** +- `database.js`: Gestion de la connexion SQLite +- Toutes les opérations de verrouillage/déverrouillage + +**Indicateurs:** +- Erreurs SQL dans les logs +- Base de données inaccessible + +## Moyens de contrôle + +### 1. Monitoring des UTXOs verrouillés + +**Script de vérification:** +```bash +# Vérifier le nombre d'UTXOs verrouillés +curl http://localhost:3010/api/anchor/locked-utxos | jq '.count' + +# Si > 0, il y a un problème +``` + +**Alertes recommandées:** +- Warning si > 5 UTXOs verrouillés +- Critical si > 10 UTXOs verrouillés +- Critical si UTXOs verrouillés depuis > 10 minutes + +### 2. Monitoring des timeouts de mutex + +**Vérification dans les logs:** +```bash +# Compter les timeouts de mutex dans les dernières heures +sudo journalctl -u anchorage-api --since "1 hour ago" | grep -c "Mutex acquisition timeout" + +# Si > 0, il y a un problème +``` + +**Alertes recommandées:** +- Warning si > 1 timeout par heure +- Critical si > 5 timeouts par heure + +### 3. Monitoring des erreurs RPC Bitcoin + +**Vérification dans les logs:** +```bash +# Compter les erreurs RPC Bitcoin +sudo journalctl -u anchorage-api --since "1 hour ago" | grep -c "ESOCKETTIMEDOUT\|ETIMEDOUT\|ECONNRESET" + +# Si > 0, problème de connexion Bitcoin RPC +``` + +**Alertes recommandées:** +- Warning si > 1 erreur RPC par heure +- Critical si > 5 erreurs RPC par heure + +### 4. Monitoring de la durée des opérations + +**Vérification:** +- Logger le temps d'exécution de `createAnchorTransaction()` +- Alerter si > 30 secondes (normal: < 10 secondes) + +**Code à ajouter:** +```javascript +const startTime = Date.now(); +try { + // ... opération ... +} finally { + const duration = Date.now() - startTime; + if (duration > 30000) { + logger.warn('Anchor transaction took too long', { duration, hash }); + } +} +``` + +### 5. Health check amélioré + +**Endpoint à créer:** +```javascript +GET /health/detailed +{ + "ok": true, + "mutex": { + "locked": false, + "waiting": 0 + }, + "utxos": { + "locked": 0, + "locked_since": null + }, + "bitcoin": { + "connected": true, + "rpc_timeout": 60000 + } +} +``` + +### 6. Script de maintenance automatique + +**Script cron à créer:** +```bash +#!/bin/bash +# Déverrouiller les UTXOs verrouillés depuis plus de 10 minutes +LOCKED_COUNT=$(curl -s http://localhost:3010/api/anchor/locked-utxos | jq '.count') +if [ "$LOCKED_COUNT" -gt 0 ]; then + # Vérifier l'âge des verrouillages + # Si > 10 minutes, déverrouiller automatiquement + cd /home/ncantu/Bureau/code/bitcoin/api-anchorage + node unlock-utxos.mjs +fi +``` + +## Moyens de correction + +### 1. Correction immédiate (déjà implémentée) + +**Script de déverrouillage:** +- `api-anchorage/unlock-utxos.mjs`: Déverrouille tous les UTXOs verrouillés + +**Utilisation:** +```bash +cd /home/ncantu/Bureau/code/bitcoin/api-anchorage +node unlock-utxos.mjs +``` + +### 2. Améliorations préventives à implémenter + +#### A. Timeout de sécurité sur le mutex + +**Problème:** Le mutex peut rester acquis indéfiniment si l'opération se bloque. + +**Solution:** Ajouter un timeout de sécurité qui libère automatiquement le mutex après un délai maximum. + +**Code à ajouter:** +```javascript +async createAnchorTransaction(...) { + const releaseMutex = await this.acquireUtxoMutex(); + let mutexSafetyTimeout; + + // Timeout de sécurité: libérer le mutex après 5 minutes maximum + mutexSafetyTimeout = setTimeout(() => { + logger.error('Mutex held for too long, forcing release', { hash }); + releaseMutex(); + }, 300000); // 5 minutes + + try { + // ... opération ... + } finally { + clearTimeout(mutexSafetyTimeout); + releaseMutex(); + } +} +``` + +#### B. Timeout sur Promise.all() + +**Problème:** `Promise.all()` peut bloquer indéfiniment si une Promise ne se résout jamais. + +**Solution:** Ajouter un timeout sur `Promise.all()`. + +**Code à modifier:** +```javascript +// Ligne 253: Remplacer +const allAddresses = await Promise.all(addressPromises); + +// Par: +const allAddresses = await Promise.race([ + Promise.all(addressPromises), + new Promise((_, reject) => + setTimeout(() => reject(new Error('Address generation timeout')), 30000) + ) +]); +``` + +#### C. Déverrouillage automatique des UTXOs anciens + +**Problème:** Les UTXOs peuvent rester verrouillés si le processus crash. + +**Solution:** Déverrouiller automatiquement les UTXOs verrouillés depuis plus de 10 minutes au démarrage. + +**Code à ajouter dans `server.js`:** +```javascript +// Au démarrage du serveur +const db = getDatabase(); +const result = db.prepare(` + UPDATE utxos + SET is_locked_in_mutex = 0 + WHERE is_locked_in_mutex = 1 + AND updated_at < datetime('now', '-10 minutes') +`).run(); +if (result.changes > 0) { + logger.info('Unlocked stale UTXOs on startup', { count: result.changes }); +} +``` + +#### D. Retry avec backoff pour les appels RPC + +**Problème:** Les timeouts RPC peuvent être temporaires. + +**Solution:** Implémenter un retry avec backoff exponentiel. + +**Code à ajouter:** +```javascript +async callRPCWithRetry(method, params, maxRetries = 3) { + for (let i = 0; i < maxRetries; i++) { + try { + return await this.client[method](...params); + } catch (error) { + if (i === maxRetries - 1) throw error; + const delay = Math.min(1000 * Math.pow(2, i), 10000); + logger.warn(`RPC call failed, retrying in ${delay}ms`, { method, attempt: i + 1 }); + await new Promise(resolve => setTimeout(resolve, delay)); + } + } +} +``` + +#### E. Monitoring et alertes + +**Problème:** Les problèmes ne sont détectés qu'après coup. + +**Solution:** Implémenter un système de monitoring proactif. + +**À implémenter:** +- Endpoint `/health/detailed` avec état du mutex et UTXOs +- Script cron pour vérifier et déverrouiller automatiquement +- Alertes (email, webhook) en cas de problème + +### 3. Scripts de diagnostic + +#### A. Script de diagnostic complet + +**Créer:** `api-anchorage/diagnose.mjs` + +```javascript +#!/usr/bin/env node +import { getDatabase } from './src/database.js'; + +const db = getDatabase(); + +// UTXOs verrouillés +const locked = db.prepare(` + SELECT txid, vout, address, amount, updated_at, + (julianday('now') - julianday(updated_at)) * 24 * 60 as minutes_locked + FROM utxos + WHERE is_locked_in_mutex = 1 + ORDER BY updated_at +`).all(); + +console.log(`\n📊 UTXOs verrouillés: ${locked.length}`); +if (locked.length > 0) { + console.table(locked.map(u => ({ + txid: u.txid.substring(0, 16) + '...', + vout: u.vout, + amount: u.amount, + locked_for_minutes: Math.round(u.minutes_locked * 100) / 100 + }))); +} + +// UTXOs verrouillés depuis plus de 10 minutes +const stale = locked.filter(u => u.minutes_locked > 10); +if (stale.length > 0) { + console.log(`\n⚠️ UTXOs verrouillés depuis plus de 10 minutes: ${stale.length}`); +} + +db.close(); +``` + +#### B. Script de nettoyage automatique + +**Créer:** `api-anchorage/cleanup-stale-locks.mjs` + +```javascript +#!/usr/bin/env node +import { getDatabase } from './src/database.js'; + +const db = getDatabase(); +const result = db.prepare(` + UPDATE utxos + SET is_locked_in_mutex = 0 + WHERE is_locked_in_mutex = 1 + AND updated_at < datetime('now', '-10 minutes') +`).run(); + +console.log(`✅ UTXOs déverrouillés: ${result.changes}`); +db.close(); +``` + +**Cron job:** +```bash +# Toutes les 5 minutes +*/5 * * * * cd /home/ncantu/Bureau/code/bitcoin/api-anchorage && node cleanup-stale-locks.mjs +``` + +## Recommandations prioritaires + +### Priorité 1 (Critique - à implémenter immédiatement) + +1. **Déverrouillage automatique au démarrage** (solution C) +2. **Timeout de sécurité sur le mutex** (solution A) +3. **Script cron de nettoyage automatique** (solution B) + +### Priorité 2 (Important - à implémenter rapidement) + +4. **Health check amélioré** (solution 5) +5. **Monitoring des UTXOs verrouillés** (solution 1) +6. **Timeout sur Promise.all()** (solution B) + +### Priorité 3 (Amélioration - à planifier) + +7. **Retry avec backoff pour RPC** (solution D) +8. **Monitoring et alertes** (solution E) +9. **Scripts de diagnostic** (solution 3) + +## Pages affectées + +- `api-anchorage/src/bitcoin-rpc.js`: Améliorations du mutex et gestion d'erreurs +- `api-anchorage/src/server.js`: Déverrouillage au démarrage +- `api-anchorage/unlock-utxos.mjs`: Script de déverrouillage (existant) +- `api-anchorage/cleanup-stale-locks.mjs`: Script de nettoyage automatique (à créer) +- `api-anchorage/diagnose.mjs`: Script de diagnostic (à créer) +- `fixKnowledge/api-anchorage-mutex-blockage-analysis.md`: Documentation (nouveau) diff --git a/fixKnowledge/api-anchorage-mutex-timeout-blocked.md b/fixKnowledge/api-anchorage-mutex-timeout-blocked.md new file mode 100644 index 0000000..b45c699 --- /dev/null +++ b/fixKnowledge/api-anchorage-mutex-timeout-blocked.md @@ -0,0 +1,197 @@ +# Correction: Mutex bloqué causant des timeouts dans l'API d'ancrage + +**Date:** 2026-01-28 +**Auteur:** Équipe 4NK + +## Problème + +L'API d'ancrage Bitcoin ne répondait plus correctement. Toutes les requêtes d'ancrage échouaient avec des erreurs de timeout de mutex après 180 secondes (3 minutes). + +**Machine concernée :** 192.168.1.105 (bitcoin) +**Domaine externe :** https://anchorage.certificator.4nkweb.com +**Service :** anchorage-api (port 3010) + +### Symptômes + +- **Erreurs répétées:** Des centaines d'erreurs `Mutex acquisition timeout after 180000ms` dans les logs +- **UTXOs verrouillés:** 22 UTXOs restaient verrouillés dans la base de données (`is_locked_in_mutex = 1`) +- **API non fonctionnelle:** Les requêtes d'ancrage échouaient systématiquement +- **Blocage du mutex:** Le mutex était bloqué depuis une requête d'ancrage à 12:29:40 qui n'avait jamais libéré le mutex + +### Impact + +- **Indisponibilité:** L'API d'ancrage ne pouvait plus traiter de nouvelles requêtes +- **UTXOs inutilisables:** 22 UTXOs étaient verrouillés et non disponibles pour de nouvelles transactions +- **Requêtes en attente:** Toutes les nouvelles requêtes attendaient le timeout de 3 minutes avant d'échouer + +## Root cause + +Une requête d'ancrage reçue à 12:29:40 (`test-deploy-1769599785`) a acquis le mutex mais s'est bloquée avant d'atteindre le bloc `finally` qui libère le mutex. Le mutex est resté bloqué indéfiniment, empêchant toutes les requêtes suivantes de s'exécuter. + +**Causes possibles:** +1. **Appel RPC Bitcoin bloqué:** Un appel RPC vers Bitcoin Core n'a jamais répondu (timeout ou blocage réseau) +2. **Opération asynchrone non résolue:** Une Promise n'a jamais été résolue ou rejetée +3. **Erreur non gérée:** Une erreur s'est produite avant d'atteindre le bloc `finally` + +**Code concerné:** +- `api-anchorage/src/bitcoin-rpc.js` : Méthode `createAnchorTransaction()` ligne 230 +- Le mutex est acquis ligne 232 avec `await this.acquireUtxoMutex()` +- Le mutex devrait être libéré dans le bloc `finally` ligne 908-914, mais si l'opération se bloque avant, le `finally` n'est jamais atteint + +## Correctifs + +### Solution immédiate (appliquée) + +1. **Redémarrage du service:** Redémarrage forcé du service `anchorage-api` pour libérer le mutex en mémoire + ```bash + sudo systemctl restart anchorage-api + ``` + +2. **Déverrouillage des UTXOs dans la base de données:** Le redémarrage libère le mutex en mémoire, mais les UTXOs restent verrouillés dans la base de données. Il faut les déverrouiller manuellement : + ```bash + cd /home/ncantu/Bureau/code/bitcoin/api-anchorage + node unlock-utxos.mjs + ``` + + Ou directement avec SQL : + ```sql + UPDATE utxos SET is_locked_in_mutex = 0 WHERE is_locked_in_mutex = 1; + ``` + +3. **Vérification:** Après redémarrage et déverrouillage, l'API répond correctement et les UTXOs sont disponibles + +### Solutions préventives à implémenter + +1. **Timeout sur les appels RPC Bitcoin:** Ajouter des timeouts explicites sur tous les appels RPC pour éviter les blocages indéfinis +2. **Libération forcée du mutex:** Implémenter un mécanisme de libération automatique du mutex après un certain délai même si l'opération est en cours +3. **Monitoring des opérations longues:** Logger les opérations qui prennent plus de 30 secondes pour identifier les blocages potentiels +4. **Health check amélioré:** Vérifier l'état du mutex dans le health check et signaler si des UTXOs sont verrouillés depuis trop longtemps + +## Modifications + +### Fichiers concernés + +- `api-anchorage/src/bitcoin-rpc.js` : Méthode `createAnchorTransaction()` et gestion du mutex +- `api-anchorage/src/routes/anchor.js` : Gestion des erreurs et timeouts + +### Modifications recommandées (à implémenter) + +1. **Ajouter des timeouts sur les appels RPC:** + ```javascript + // Dans bitcoin-rpc.js, ajouter des timeouts sur tous les appels RPC + const rpcCallWithTimeout = async (method, params, timeoutMs = 30000) => { + return Promise.race([ + this.client[method](...params), + new Promise((_, reject) => + setTimeout(() => reject(new Error(`RPC timeout: ${method}`)), timeoutMs) + ) + ]); + }; + ``` + +2. **Libération automatique du mutex:** + ```javascript + // Dans createAnchorTransaction, ajouter un timer de sécurité + const mutexTimeout = setTimeout(() => { + logger.error('Mutex held for too long, forcing release', { hash }); + releaseMutex(); + }, 300000); // 5 minutes max + + try { + // ... opération ... + } finally { + clearTimeout(mutexTimeout); + releaseMutex(); + } + ``` + +3. **Déverrouillage automatique des UTXOs:** + ```javascript + // Script de maintenance pour déverrouiller les UTXOs verrouillés depuis plus de 10 minutes + // À exécuter périodiquement via cron + ``` + +## Modalités de déploiement + +### Vérification de l'état actuel + +```bash +# Vérifier que le service est actif +sudo systemctl status anchorage-api + +# Vérifier que l'API répond +curl http://localhost:3010/health + +# Vérifier les UTXOs verrouillés +curl http://localhost:3010/api/anchor/locked-utxos + +# Vérifier les logs récents +sudo journalctl -u anchorage-api -n 50 --no-pager +``` + +### En cas de nouveau blocage + +1. **Identifier le problème:** + ```bash + # Vérifier les UTXOs verrouillés + curl http://localhost:3010/api/anchor/locked-utxos + + # Vérifier les logs pour identifier la requête bloquée + sudo journalctl -u anchorage-api --since "10 minutes ago" | grep "Anchor request" + ``` + +2. **Redémarrer le service:** + ```bash + sudo systemctl restart anchorage-api + ``` + +3. **Déverrouiller les UTXOs dans la base de données:** + ```bash + cd /home/ncantu/Bureau/code/bitcoin/api-anchorage + node unlock-utxos.mjs + ``` + +4. **Vérifier la récupération:** + ```bash + sleep 3 + curl http://localhost:3010/health + curl http://localhost:3010/api/anchor/locked-utxos + # Le count doit être 0 + ``` + +## Modalités d'analyse + +### Logs à surveiller + +1. **Erreurs de timeout de mutex:** + ```bash + sudo journalctl -u anchorage-api | grep "Mutex acquisition timeout" + ``` + +2. **Requêtes d'ancrage:** + ```bash + sudo journalctl -u anchorage-api | grep "Anchor request received" + ``` + +3. **UTXOs verrouillés:** + ```bash + curl http://localhost:3010/api/anchor/locked-utxos | jq '.count' + ``` + +### Métriques à surveiller + +- **Nombre d'UTXOs verrouillés:** Doit être 0 en temps normal +- **Durée des opérations d'ancrage:** Ne devrait pas dépasser 30 secondes +- **Erreurs de timeout:** Ne devrait pas se produire en fonctionnement normal + +### Alertes recommandées + +- **UTXOs verrouillés > 5:** Alerte warning +- **UTXOs verrouillés > 10:** Alerte critique +- **Timeout de mutex:** Alerte critique immédiate + +## Pages affectées + +- `api-anchorage/src/bitcoin-rpc.js` : Gestion du mutex et des timeouts +- `api-anchorage/src/routes/anchor.js` : Gestion des erreurs +- `fixKnowledge/api-anchorage-mutex-timeout-blocked.md` : Documentation (nouveau) diff --git a/service-login-verify/dist/persistentNonceCache.d.ts b/service-login-verify/dist/persistentNonceCache.d.ts index 2324551..9dcc2e2 100644 --- a/service-login-verify/dist/persistentNonceCache.d.ts +++ b/service-login-verify/dist/persistentNonceCache.d.ts @@ -15,19 +15,26 @@ export declare class PersistentNonceCache implements NonceCacheLike { init(): Promise; /** * Check if nonce is valid (not seen within TTL). Records nonce on success. - * Note: IndexedDB operations are async, but NonceCacheLike interface requires sync. - * This implementation uses localStorage for synchronous access. - * For true IndexedDB persistence, consider making the interface async. + * Uses localStorage for synchronous access (required by NonceCacheLike interface). + * Also persists to IndexedDB in background if available. */ isValid(nonce: string, timestamp: number): boolean; /** - * Synchronous validation using localStorage (fallback). + * Synchronous validation using localStorage (primary storage). */ private isValidSync; /** - * Cleanup expired entries (localStorage). + * Persist nonce to IndexedDB in background (async, non-blocking). + */ + private persistToIndexedDB; + /** + * Cleanup expired entries (localStorage and IndexedDB). */ private cleanupSync; + /** + * Cleanup expired entries from IndexedDB (async, non-blocking). + */ + private cleanupIndexedDB; /** * Clear all entries. */ diff --git a/service-login-verify/dist/persistentNonceCache.d.ts.map b/service-login-verify/dist/persistentNonceCache.d.ts.map index 5fb59fa..b6829ce 100644 --- a/service-login-verify/dist/persistentNonceCache.d.ts.map +++ b/service-login-verify/dist/persistentNonceCache.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"persistentNonceCache.d.ts","sourceRoot":"","sources":["../src/persistentNonceCache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD;;;GAGG;AACH,qBAAa,oBAAqB,YAAW,cAAc;IACzD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAU;IACvC,OAAO,CAAC,EAAE,CAA4B;gBAE1B,KAAK,GAAE,MAAgB,EAAE,UAAU,GAAE,MAAsB;IAMvE;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA2B3B;;;;;OAKG;IACH,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO;IAIlD;;OAEG;IACH,OAAO,CAAC,WAAW;IAiBnB;;OAEG;IACH,OAAO,CAAC,WAAW;IAoBnB;;OAEG;IACH,KAAK,IAAI,IAAI;CAkBd"} \ No newline at end of file +{"version":3,"file":"persistentNonceCache.d.ts","sourceRoot":"","sources":["../src/persistentNonceCache.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEjD;;;GAGG;AACH,qBAAa,oBAAqB,YAAW,cAAc;IACzD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAS;IAC/B,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAS;IACpC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAU;IACvC,OAAO,CAAC,EAAE,CAA4B;gBAE1B,KAAK,GAAE,MAAgB,EAAE,UAAU,GAAE,MAAsB;IAMvE;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IA2B3B;;;;OAIG;IACH,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,OAAO;IASlD;;OAEG;IACH,OAAO,CAAC,WAAW;IAiBnB;;OAEG;YACW,kBAAkB;IAuBhC;;OAEG;IACH,OAAO,CAAC,WAAW;IAyBnB;;OAEG;YACW,gBAAgB;IAgC9B;;OAEG;IACH,KAAK,IAAI,IAAI;CAkBd"} \ No newline at end of file diff --git a/service-login-verify/dist/persistentNonceCache.js b/service-login-verify/dist/persistentNonceCache.js index 94605c5..c439419 100644 --- a/service-login-verify/dist/persistentNonceCache.js +++ b/service-login-verify/dist/persistentNonceCache.js @@ -39,15 +39,19 @@ export class PersistentNonceCache { } /** * Check if nonce is valid (not seen within TTL). Records nonce on success. - * Note: IndexedDB operations are async, but NonceCacheLike interface requires sync. - * This implementation uses localStorage for synchronous access. - * For true IndexedDB persistence, consider making the interface async. + * Uses localStorage for synchronous access (required by NonceCacheLike interface). + * Also persists to IndexedDB in background if available. */ isValid(nonce, timestamp) { - return this.isValidSync(nonce, timestamp); + const result = this.isValidSync(nonce, timestamp); + // Persist to IndexedDB in background if available + if (this.useIndexedDB && this.db !== null) { + void this.persistToIndexedDB(nonce, timestamp); + } + return result; } /** - * Synchronous validation using localStorage (fallback). + * Synchronous validation using localStorage (primary storage). */ isValidSync(nonce, timestamp) { const now = Date.now(); @@ -64,7 +68,32 @@ export class PersistentNonceCache { return true; } /** - * Cleanup expired entries (localStorage). + * Persist nonce to IndexedDB in background (async, non-blocking). + */ + async persistToIndexedDB(nonce, timestamp) { + if (this.db === null) { + return; + } + try { + const transaction = this.db.transaction(['nonces'], 'readwrite'); + const store = transaction.objectStore('nonces'); + await new Promise((resolve, reject) => { + const request = store.put({ nonce, timestamp }); + request.onsuccess = () => { + resolve(); + }; + request.onerror = () => { + reject(request.error); + }; + }); + } + catch (error) { + // IndexedDB errors are non-critical, localStorage is the primary storage + console.warn('Failed to persist nonce to IndexedDB:', error); + } + } + /** + * Cleanup expired entries (localStorage and IndexedDB). */ cleanupSync(now) { const keys = []; @@ -83,6 +112,44 @@ export class PersistentNonceCache { } } } + // Cleanup IndexedDB in background + if (this.useIndexedDB && this.db !== null) { + void this.cleanupIndexedDB(now); + } + } + /** + * Cleanup expired entries from IndexedDB (async, non-blocking). + */ + async cleanupIndexedDB(now) { + if (this.db === null) { + return; + } + try { + const transaction = this.db.transaction(['nonces'], 'readwrite'); + const store = transaction.objectStore('nonces'); + const index = store.index('timestamp'); + const range = IDBKeyRange.upperBound(now - this.ttlMs); + await new Promise((resolve, reject) => { + const request = index.openCursor(range); + request.onsuccess = (event) => { + const cursor = event.target.result; + if (cursor !== null && cursor !== undefined) { + cursor.delete(); + cursor.continue(); + } + else { + resolve(); + } + }; + request.onerror = () => { + reject(request.error); + }; + }); + } + catch (error) { + // IndexedDB errors are non-critical + console.warn('Failed to cleanup IndexedDB:', error); + } } /** * Clear all entries. diff --git a/service-login-verify/src/persistentNonceCache.ts b/service-login-verify/src/persistentNonceCache.ts index 87d2b90..91f1373 100644 --- a/service-login-verify/src/persistentNonceCache.ts +++ b/service-login-verify/src/persistentNonceCache.ts @@ -1,7 +1,7 @@ import type { NonceCacheLike } from './types.js'; /** - * Persistent nonce cache using IndexedDB (browser) or localStorage (fallback). + * Persistent nonce cache using IndexedDB (browser) and localStorage. * Implements NonceCacheLike interface for use with verifyLoginProof. */ export class PersistentNonceCache implements NonceCacheLike { @@ -48,16 +48,20 @@ export class PersistentNonceCache implements NonceCacheLike { /** * Check if nonce is valid (not seen within TTL). Records nonce on success. - * Note: IndexedDB operations are async, but NonceCacheLike interface requires sync. - * This implementation uses localStorage for synchronous access. - * For true IndexedDB persistence, consider making the interface async. + * Uses localStorage for synchronous access (required by NonceCacheLike interface). + * Also persists to IndexedDB in background if available. */ isValid(nonce: string, timestamp: number): boolean { - return this.isValidSync(nonce, timestamp); + const result = this.isValidSync(nonce, timestamp); + // Persist to IndexedDB in background if available + if (this.useIndexedDB && this.db !== null) { + void this.persistToIndexedDB(nonce, timestamp); + } + return result; } /** - * Synchronous validation using localStorage (fallback). + * Synchronous validation using localStorage (primary storage). */ private isValidSync(nonce: string, timestamp: number): boolean { const now = Date.now(); @@ -77,7 +81,33 @@ export class PersistentNonceCache implements NonceCacheLike { } /** - * Cleanup expired entries (localStorage). + * Persist nonce to IndexedDB in background (async, non-blocking). + */ + private async persistToIndexedDB(nonce: string, timestamp: number): Promise { + if (this.db === null) { + return; + } + + try { + const transaction = this.db.transaction(['nonces'], 'readwrite'); + const store = transaction.objectStore('nonces'); + await new Promise((resolve, reject) => { + const request = store.put({ nonce, timestamp }); + request.onsuccess = (): void => { + resolve(); + }; + request.onerror = (): void => { + reject(request.error); + }; + }); + } catch (error) { + // IndexedDB errors are non-critical, localStorage is the primary storage + console.warn('Failed to persist nonce to IndexedDB:', error); + } + } + + /** + * Cleanup expired entries (localStorage and IndexedDB). */ private cleanupSync(now: number): void { const keys: string[] = []; @@ -97,6 +127,46 @@ export class PersistentNonceCache implements NonceCacheLike { } } } + + // Cleanup IndexedDB in background + if (this.useIndexedDB && this.db !== null) { + void this.cleanupIndexedDB(now); + } + } + + /** + * Cleanup expired entries from IndexedDB (async, non-blocking). + */ + private async cleanupIndexedDB(now: number): Promise { + if (this.db === null) { + return; + } + + try { + const transaction = this.db.transaction(['nonces'], 'readwrite'); + const store = transaction.objectStore('nonces'); + const index = store.index('timestamp'); + const range = IDBKeyRange.upperBound(now - this.ttlMs); + + await new Promise((resolve, reject) => { + const request = index.openCursor(range); + request.onsuccess = (event: Event): void => { + const cursor = (event.target as IDBRequest).result; + if (cursor !== null && cursor !== undefined) { + cursor.delete(); + cursor.continue(); + } else { + resolve(); + } + }; + request.onerror = (): void => { + reject(request.error); + }; + }); + } catch (error) { + // IndexedDB errors are non-critical + console.warn('Failed to cleanup IndexedDB:', error); + } } /** diff --git a/signet-dashboard/public/api-docs.html b/signet-dashboard/public/api-docs.html index fb8c048..692e0e2 100644 --- a/signet-dashboard/public/api-docs.html +++ b/signet-dashboard/public/api-docs.html @@ -245,6 +245,7 @@

Endpoints publics (sans authentification) :

  • GET /health - Vérification de santé (API d'ancrage)
  • +
  • GET /health/detailed - Vérification de santé détaillée avec état mutex et UTXOs
  • GET /api/anchor/locked-utxos - Liste des UTXO verrouillés
  • GET /health - Vérification de santé (API faucet)
@@ -304,6 +305,120 @@ + +
+
+
+ GET + /health/detailed +
+ +
+

Vérifie l'état détaillé de l'API, incluant l'état du mutex, des UTXOs verrouillés et de la connexion Bitcoin. Cet endpoint est public et ne nécessite pas d'authentification.

+
+ +
+

Réponse (200 OK) - Tout fonctionne

+
+
{
+  "ok": true,
+  "service": "anchor-api",
+  "mutex": {
+    "locked": false,
+    "waiting": 0,
+    "timeout": 180000
+  },
+  "utxos": {
+    "locked": 0,
+    "locked_since": null,
+    "stale_locks": 0,
+    "stale_locks_details": []
+  },
+  "bitcoin": {
+    "connected": true,
+    "blocks": 12345,
+    "chain": "signet",
+    "rpc_timeout": 60000
+  },
+  "timestamp": "2026-01-25T12:00:00.000Z"
+}
+
+
+ +
+

Réponse (200 OK) - Warning (UTXOs verrouillés mais < 10 min)

+
+
{
+  "ok": true,
+  "service": "anchor-api",
+  "mutex": {
+    "locked": false,
+    "waiting": 0,
+    "timeout": 180000
+  },
+  "utxos": {
+    "locked": 3,
+    "locked_since": "2026-01-25T11:55:00.000Z",
+    "stale_locks": 0,
+    "stale_locks_details": []
+  },
+  "bitcoin": {
+    "connected": true,
+    "blocks": 12345,
+    "chain": "signet",
+    "rpc_timeout": 60000
+  },
+  "timestamp": "2026-01-25T12:00:00.000Z"
+}
+
+
+ +
+

Réponse (503 Service Unavailable) - UTXOs verrouillés depuis > 10 min

+
+
{
+  "ok": false,
+  "service": "anchor-api",
+  "mutex": {
+    "locked": false,
+    "waiting": 0,
+    "timeout": 180000
+  },
+  "utxos": {
+    "locked": 5,
+    "locked_since": "2026-01-25T11:45:00.000Z",
+    "stale_locks": 5,
+    "stale_locks_details": [
+      {
+        "txid": "abc123...",
+        "vout": 0,
+        "minutes_locked": 15.5
+      }
+    ]
+  },
+  "bitcoin": {
+    "connected": true,
+    "blocks": 12345,
+    "chain": "signet",
+    "rpc_timeout": 60000
+  },
+  "timestamp": "2026-01-25T12:00:00.000Z"
+}
+
+
+ +
+

Codes de statut :

+
    +
  • 200 - Tout fonctionne correctement
  • +
  • 200 - Warning si 5-10 UTXOs verrouillés (mais < 10 min)
  • +
  • 503 - Service Unavailable si UTXOs verrouillés depuis > 10 min ou > 10 UTXOs verrouillés
  • +
  • 503 - Service Unavailable si Bitcoin non connecté
  • +
+
+
+
+
@@ -1684,7 +1799,7 @@ Format: hexadécimal (0-9, a-f, A-F)

Les APIs sont accessibles aux adresses suivantes :

Dashboard : https://dashboard.certificator.4nkweb.com  (port 3020)
-API d'Ancrage : https://certificator.4nkweb.com       (port 3010)
+API d'Ancrage : https://anchorage.certificator.4nkweb.com (port 3010, machine bitcoin 192.168.1.105)
 API Faucet : https://faucet.certificator.4nkweb.com  (port 3021)
 API Filigrane : https://watermark.certificator.4nkweb.com (port 3022)
 API ClamAV : https://antivir.certificator.4nkweb.com (port 3023)
diff --git a/signet-dashboard/public/app.js b/signet-dashboard/public/app.js index 99674f0..0b01572 100644 --- a/signet-dashboard/public/app.js +++ b/signet-dashboard/public/app.js @@ -15,6 +15,8 @@ if (window.location.hostname.includes('dashboard.certificator.4nkweb.com')) { } let selectedFile = null; +let selectedClamavFile = null; +let selectedFiligraneFile = null; let lastBlockHeight = null; let blockPollingInterval = null; let dataRefreshInterval = null; @@ -722,15 +724,6 @@ async function verifyHash() { } } -/** - * Affiche/masque les options de filigrane - */ -function toggleWatermarkOptions() { - const enabled = document.getElementById('watermark-enabled').checked; - const optionsDiv = document.getElementById('watermark-options'); - optionsDiv.style.display = enabled ? 'block' : 'none'; -} - /** * Ancre le document */ @@ -743,7 +736,6 @@ async function anchorDocument() { const apiKey = apiKeyElement.value.trim(); const hash = document.getElementById('anchor-hash').value; - const watermarkEnabled = document.getElementById('watermark-enabled').checked; if (!apiKey || apiKey.length === 0) { showResult('anchor-result', 'error', 'Veuillez entrer une clé API.'); @@ -760,224 +752,30 @@ async function anchorDocument() { try { showResult('anchor-result', 'info', 'Ancrage en cours...'); - // Si le filigrane est activé, utiliser l'API filigrane - if (watermarkEnabled) { - await anchorWithWatermark(apiKey); - } else { - // Sinon, utiliser l'API d'ancrage classique - const response = await fetch(`${API_BASE_URL}/api/anchor/test`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ hash, apiKey }), - }); - - const data = await response.json(); - - if (response.ok && data.txid) { - showResult('anchor-result', 'success', - `Document ancré avec succès !
- TXID : ${data.txid}
- Statut : ${data.status}
- Confirmations : ${data.confirmations || 0}`); - - // Recharger le nombre d'ancrages après un court délai - setTimeout(loadAnchorCount, 2000); - } else { - showResult('anchor-result', 'error', data.message || data.error || 'Erreur lors de l\'ancrage.'); - } - } - } catch (error) { - showResult('anchor-result', 'error', `Erreur : ${error.message}`); - } -} - -/** - * Ancre le document avec filigrane - */ -async function anchorWithWatermark(apiKey) { - // Vérifier que la clé API est bien fournie - if (!apiKey || apiKey.trim().length === 0) { - showResult('anchor-result', 'error', 'Veuillez entrer une clé API.'); - return; - } - - // Utiliser le backend du dashboard comme proxy vers l'API filigrane - const watermarkApiUrl = API_BASE_URL; - - const textContent = document.getElementById('anchor-text').value; - const fileInput = document.getElementById('anchor-file'); - const currentSelectedFile = selectedFile || (fileInput.files.length > 0 ? fileInput.files[0] : null); - - // Préparer les données du fichier - let fileData = null; - let fileName = null; - let mimeType = null; - - if (currentSelectedFile) { - const reader = new FileReader(); - await new Promise((resolve, reject) => { - reader.onload = (e) => { - const arrayBuffer = e.target.result; - const base64 = btoa(String.fromCharCode(...new Uint8Array(arrayBuffer))); - fileData = base64; - fileName = currentSelectedFile.name; - mimeType = currentSelectedFile.type; - resolve(); - }; - reader.onerror = reject; - reader.readAsArrayBuffer(currentSelectedFile); - }); - } - - // Préparer les options de filigrane - const watermarkOptionsRaw = { - enabled: true, - text: document.getElementById('watermark-text').value.trim() || undefined, - signature: document.getElementById('watermark-signature').value.trim() || undefined, - depositor: document.getElementById('watermark-depositor').value.trim() || undefined, - watermarkedFileName: document.getElementById('watermarked-filename').value.trim() || undefined, - originalFileName: document.getElementById('original-filename').value.trim() || undefined, - dateUTC: document.getElementById('watermark-date-utc').checked, - dateLocal: document.getElementById('watermark-date-local').checked, - blockNumber: document.getElementById('watermark-block-number').checked, - blockHash: document.getElementById('watermark-block-hash').checked, - documentHash: document.getElementById('watermark-document-hash').checked, - }; - - // Nettoyer l'objet en supprimant les valeurs undefined - const watermarkOptions = Object.fromEntries( - Object.entries(watermarkOptionsRaw).filter(([_, value]) => value !== undefined) - ); - - // S'assurer que enabled est toujours présent et true - watermarkOptions.enabled = true; - - const requestBody = { - apiKey, - watermarkOptions, - }; - - if (textContent) { - requestBody.textContent = textContent; - } else if (fileData) { - requestBody.fileData = fileData; - requestBody.fileName = fileName; - requestBody.mimeType = mimeType; - } else { - showResult('anchor-result', 'error', 'Veuillez saisir un texte ou sélectionner un fichier.'); - return; - } - - try { - const response = await fetch(`${watermarkApiUrl}/api/watermark/document`, { + const response = await fetch(`${API_BASE_URL}/api/anchor/test`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify(requestBody), + body: JSON.stringify({ hash, apiKey }), }); - let data; - try { - data = await response.json(); - } catch (jsonError) { - const text = await response.text(); - showResult('anchor-result', 'error', `Erreur de parsing de la réponse: ${text}`); - return; + const data = await response.json(); + + if (response.ok && data.txid) { + showResult('anchor-result', 'success', + `Document ancré avec succès !
+ TXID : ${data.txid}
+ Statut : ${data.status}
+ Confirmations : ${data.confirmations || 0}`); + + // Recharger le nombre d'ancrages après un court délai + setTimeout(loadAnchorCount, 2000); + } else { + showResult('anchor-result', 'error', data.message || data.error || 'Erreur lors de l\'ancrage.'); } - - if (!response.ok) { - const errorMessage = data.message || data.error || `Erreur ${response.status}: ${response.statusText}`; - console.error('Watermark API error:', { - status: response.status, - statusText: response.statusText, - data: data, - requestBody: { - hasApiKey: !!requestBody.apiKey, - hasWatermarkOptions: !!requestBody.watermarkOptions, - watermarkEnabled: requestBody.watermarkOptions?.enabled, - hasFileData: !!requestBody.fileData, - hasTextContent: !!requestBody.textContent, - }, - }); - showResult('anchor-result', 'error', errorMessage); - return; - } - - if (data.success) { - const mempoolBaseUrl = 'https://mempool.4nkweb.com/fr'; - const originalTxidLink = data.original.txid - ? `${data.original.txid}` - : 'N/A'; - const watermarkedTxidLink = data.watermarked.txid - ? `${data.watermarked.txid}` - : 'N/A'; - - let resultHtml = ` - ✅ Documents ancrés avec succès !

- Document original :
- Hash SHA256 : ${data.original.hash || 'N/A'}
- TXID : ${originalTxidLink}
- Statut : ${data.original.status}
- Confirmations : ${data.original.confirmations || 0}
- Fichier : ${data.original.file.name}

- Document filigrané :
- Hash SHA256 : ${data.watermarked.hash || 'N/A'}
- TXID : ${watermarkedTxidLink}
- Statut : ${data.watermarked.status}
- Confirmations : ${data.watermarked.confirmations || 0}
- Fichier : ${data.watermarked.file.name}

- `; - - if (data.certificate && data.certificate.data) { - resultHtml += ` - Certificat :
- Fichier : ${data.certificate.name}

- `; - } - - if (data.merged && data.merged.data) { - resultHtml += ` - Document fusionné (filigrané + certificat) :
- Fichier : ${data.merged.name}

- `; - } - - resultHtml += `

📥 Téléchargement automatique en cours...

`; - - showResult('anchor-result', 'success', resultHtml); - - // Télécharger automatiquement les 4 documents avec un délai entre chaque - setTimeout(() => { - downloadFile(data.original.file.name, data.original.file.data); - }, 500); - - setTimeout(() => { - downloadFile(data.watermarked.file.name, data.watermarked.file.data); - }, 1000); - - if (data.certificate && data.certificate.data) { - setTimeout(() => { - downloadFile(data.certificate.name, data.certificate.data); - }, 1500); - } - - if (data.merged && data.merged.data) { - setTimeout(() => { - downloadFile(data.merged.name, data.merged.data); - }, 2000); - } - - // Recharger le nombre d'ancrages après un court délai - setTimeout(loadAnchorCount, 2000); - } else { - showResult('anchor-result', 'error', data.message || data.error || 'Erreur lors de l\'ancrage avec filigrane.'); - } } catch (error) { - console.error('Error in anchorWithWatermark:', error); - showResult('anchor-result', 'error', `Erreur lors de l'ancrage avec filigrane: ${error.message}`); + showResult('anchor-result', 'error', `Erreur : ${error.message}`); } } @@ -1062,3 +860,350 @@ function showResult(elementId, type, message) { element.className = `result ${type}`; element.innerHTML = message; } + +/** + * Gère la sélection de fichier pour ClamAV + */ +function handleClamavFileSelect(event) { + const file = event.target.files[0]; + if (file) { + selectedClamavFile = file; + const fileInfo = document.getElementById('clamav-file-info'); + const maxSize = 100 * 1024 * 1024; // 100 MB en bytes + const fileSize = file.size; + const isOverLimit = fileSize > maxSize; + + let infoHtml = ` + Fichier sélectionné : ${file.name}
+ Taille : ${formatFileSize(file.size)}
+ Type : ${file.type || 'Non spécifié'} + `; + + if (isOverLimit) { + infoHtml += `
⚠️ Fichier trop volumineux (limite : 100 MB)`; + } + + fileInfo.innerHTML = infoHtml; + } +} + +/** + * Scanne un fichier avec ClamAV + */ +async function scanWithClamav() { + if (!selectedClamavFile) { + showResult('clamav-result', 'error', 'Veuillez sélectionner un fichier.'); + return; + } + + // Vérifier la taille du fichier (limite : 100 MB) + const maxSize = 100 * 1024 * 1024; // 100 MB en bytes + if (selectedClamavFile.size > maxSize) { + showResult('clamav-result', 'error', `Le fichier est trop volumineux (${formatFileSize(selectedClamavFile.size)}). La limite est de 100 MB.`); + return; + } + + try { + showResult('clamav-result', 'info', 'Scan en cours...'); + + const reader = new FileReader(); + + await new Promise((resolve, reject) => { + reader.onload = async (e) => { + try { + const arrayBuffer = e.target.result; + const uint8Array = new Uint8Array(arrayBuffer); + let binaryString = ''; + for (let i = 0; i < uint8Array.length; i++) { + binaryString += String.fromCharCode(uint8Array[i]); + } + const base64 = btoa(binaryString); + + const response = await fetch(`${API_BASE_URL}/api/clamav/scan`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + data: base64, + filename: selectedClamavFile.name, + }), + }); + + if (!response.ok) { + const errorData = await response.json().catch(() => ({ error: `HTTP ${response.status}: ${response.statusText}` })); + throw new Error(errorData.error || errorData.message || `HTTP ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + + if (data.infected) { + showResult('clamav-result', 'error', + `⚠️ Fichier infecté détecté !
+ Fichier : ${data.filename || selectedClamavFile.name}
+ Taille : ${formatFileSize(data.size || selectedClamavFile.size)}
+ Virus détectés : ${data.viruses ? data.viruses.join(', ') : 'Inconnu'}`); + } else { + showResult('clamav-result', 'success', + `✅ Fichier propre !
+ Fichier : ${data.filename || selectedClamavFile.name}
+ Taille : ${formatFileSize(data.size || selectedClamavFile.size)}
+ Statut : Aucun virus détecté`); + } + resolve(); + } catch (error) { + showResult('clamav-result', 'error', `Erreur : ${error.message}`); + reject(error); + } + }; + + reader.onerror = (error) => { + showResult('clamav-result', 'error', `Erreur lors de la lecture du fichier : ${error.message || 'Erreur inconnue'}`); + reject(error); + }; + + reader.readAsArrayBuffer(selectedClamavFile); + }); + } catch (error) { + showResult('clamav-result', 'error', `Erreur lors du scan : ${error.message}`); + } +} + +/** + * Change d'onglet pour le formulaire filigrane + */ +function switchTabFiligrane(tab, buttonElement) { + // Désactiver tous les onglets filigrane + document.querySelectorAll('#filigrane-text-tab, #filigrane-file-tab').forEach(content => { + content.classList.remove('active'); + }); + // Désactiver tous les boutons d'onglet dans la section filigrane + const filigraneSection = document.querySelector('.filigrane-section'); + if (filigraneSection) { + filigraneSection.querySelectorAll('.tab-button').forEach(button => { + button.classList.remove('active'); + }); + } + + // Activer l'onglet sélectionné + document.getElementById(`filigrane-${tab}-tab`).classList.add('active'); + // Activer le bouton correspondant + if (buttonElement) { + buttonElement.classList.add('active'); + } +} + +/** + * Gère la sélection de fichier pour Filigrane + */ +function handleFiligraneFileSelect(event) { + const file = event.target.files[0]; + if (file) { + selectedFiligraneFile = file; + const fileInfo = document.getElementById('filigrane-file-info'); + const maxSize = 100 * 1024 * 1024; // 100 MB en bytes + const fileSize = file.size; + const isOverLimit = fileSize > maxSize; + + let infoHtml = ` + Fichier sélectionné : ${file.name}
+ Taille : ${formatFileSize(file.size)}
+ Type : ${file.type || 'Non spécifié'} + `; + + if (isOverLimit) { + infoHtml += `
⚠️ Fichier trop volumineux (limite : 100 MB)`; + } + + fileInfo.innerHTML = infoHtml; + } +} + +/** + * Teste l'API Filigrane + */ +async function testFiligrane() { + const apiKey = document.getElementById('filigrane-api-key').value.trim(); + + if (!apiKey) { + showResult('filigrane-result', 'error', 'Veuillez entrer une clé API.'); + return; + } + + const textContent = document.getElementById('filigrane-text').value; + const fileInput = document.getElementById('filigrane-file'); + const currentSelectedFile = selectedFiligraneFile || (fileInput.files.length > 0 ? fileInput.files[0] : null); + + if (!textContent && !currentSelectedFile) { + showResult('filigrane-result', 'error', 'Veuillez saisir un texte ou sélectionner un fichier.'); + return; + } + + // Vérifier la taille du fichier si un fichier est sélectionné + if (currentSelectedFile) { + const maxSize = 100 * 1024 * 1024; // 100 MB en bytes + if (currentSelectedFile.size > maxSize) { + showResult('filigrane-result', 'error', `Le fichier est trop volumineux (${formatFileSize(currentSelectedFile.size)}). La limite est de 100 MB.`); + return; + } + } + + try { + showResult('filigrane-result', 'info', 'Traitement en cours...'); + + // Préparer les données du fichier + let fileData = null; + let fileName = null; + let mimeType = null; + + if (currentSelectedFile) { + const reader = new FileReader(); + await new Promise((resolve, reject) => { + reader.onload = (e) => { + const arrayBuffer = e.target.result; + const uint8Array = new Uint8Array(arrayBuffer); + let binaryString = ''; + for (let i = 0; i < uint8Array.length; i++) { + binaryString += String.fromCharCode(uint8Array[i]); + } + const base64 = btoa(binaryString); + fileData = base64; + fileName = currentSelectedFile.name; + mimeType = currentSelectedFile.type; + resolve(); + }; + reader.onerror = reject; + reader.readAsArrayBuffer(currentSelectedFile); + }); + } + + // Préparer les options de filigrane + const watermarkOptions = { + enabled: true, + text: document.getElementById('filigrane-watermark-text').value.trim() || undefined, + signature: document.getElementById('filigrane-watermark-signature').value.trim() || undefined, + depositor: document.getElementById('filigrane-watermark-depositor').value.trim() || undefined, + watermarkedFileName: document.getElementById('filigrane-watermarked-filename').value.trim() || undefined, + originalFileName: document.getElementById('filigrane-original-filename').value.trim() || undefined, + dateUTC: document.getElementById('filigrane-watermark-date-utc').checked, + dateLocal: document.getElementById('filigrane-watermark-date-local').checked, + blockNumber: document.getElementById('filigrane-watermark-block-number').checked, + blockHash: document.getElementById('filigrane-watermark-block-hash').checked, + documentHash: document.getElementById('filigrane-watermark-document-hash').checked, + }; + + // Nettoyer l'objet en supprimant les valeurs undefined + const cleanedWatermarkOptions = Object.fromEntries( + Object.entries(watermarkOptions).filter(([_, value]) => value !== undefined) + ); + + const requestBody = { + apiKey, + watermarkOptions: cleanedWatermarkOptions, + }; + + if (textContent) { + requestBody.textContent = textContent; + } else if (fileData) { + requestBody.fileData = fileData; + requestBody.fileName = fileName; + requestBody.mimeType = mimeType; + } + + const response = await fetch(`${API_BASE_URL}/api/watermark/document`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(requestBody), + }); + + let data; + try { + data = await response.json(); + } catch (jsonError) { + const text = await response.text(); + showResult('filigrane-result', 'error', `Erreur de parsing de la réponse: ${text}`); + return; + } + + if (!response.ok) { + const errorMessage = data.message || data.error || `Erreur ${response.status}: ${response.statusText}`; + showResult('filigrane-result', 'error', errorMessage); + return; + } + + if (data.success) { + const mempoolBaseUrl = 'https://mempool.4nkweb.com/fr'; + const originalTxidLink = data.original.txid + ? `${data.original.txid}` + : 'N/A'; + const watermarkedTxidLink = data.watermarked.txid + ? `${data.watermarked.txid}` + : 'N/A'; + + let resultHtml = ` + ✅ Documents ancrés avec succès !

+ Document original :
+ Hash SHA256 : ${data.original.hash || 'N/A'}
+ TXID : ${originalTxidLink}
+ Statut : ${data.original.status}
+ Confirmations : ${data.original.confirmations || 0}
+ Fichier : ${data.original.file.name}

+ Document filigrané :
+ Hash SHA256 : ${data.watermarked.hash || 'N/A'}
+ TXID : ${watermarkedTxidLink}
+ Statut : ${data.watermarked.status}
+ Confirmations : ${data.watermarked.confirmations || 0}
+ Fichier : ${data.watermarked.file.name}

+ `; + + if (data.certificate && data.certificate.data) { + resultHtml += ` + Certificat :
+ Fichier : ${data.certificate.name}

+ `; + } + + if (data.merged && data.merged.data) { + resultHtml += ` + Document fusionné (filigrané + certificat) :
+ Fichier : ${data.merged.name}

+ `; + } + + resultHtml += `

📥 Téléchargement automatique en cours...

`; + + showResult('filigrane-result', 'success', resultHtml); + + // Télécharger automatiquement les documents avec un délai entre chaque + setTimeout(() => { + downloadFile(data.original.file.name, data.original.file.data); + }, 500); + + setTimeout(() => { + downloadFile(data.watermarked.file.name, data.watermarked.file.data); + }, 1000); + + if (data.certificate && data.certificate.data) { + setTimeout(() => { + downloadFile(data.certificate.name, data.certificate.data); + }, 1500); + } + + if (data.merged && data.merged.data) { + setTimeout(() => { + downloadFile(data.merged.name, data.merged.data); + }, 2000); + } + + // Recharger le nombre d'ancrages après un court délai + setTimeout(loadAnchorCount, 2000); + } else { + showResult('filigrane-result', 'error', data.message || data.error || 'Erreur lors du test de l\'API filigrane.'); + } + } catch (error) { + console.error('Error in testFiligrane:', error); + showResult('filigrane-result', 'error', `Erreur : ${error.message}`); + } +} diff --git a/signet-dashboard/public/hash-list.html b/signet-dashboard/public/hash-list.html index 64cdf76..a6a4e1e 100644 --- a/signet-dashboard/public/hash-list.html +++ b/signet-dashboard/public/hash-list.html @@ -164,6 +164,75 @@ .confirmed-check.no { color: #dc3545; } + .health-section { + background: var(--card-background); + padding: 15px; + border-radius: 5px; + margin-bottom: 20px; + border: 1px solid var(--border-color); + } + .health-status { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 15px; + margin-bottom: 15px; + } + .health-item { + padding: 10px; + background: rgba(255, 255, 255, 0.05); + border-radius: 5px; + border: 1px solid var(--border-color); + } + .health-item label { + display: block; + font-weight: bold; + margin-bottom: 5px; + color: var(--text-color); + } + .health-item .value { + font-size: 1.1em; + } + .health-item .value.ok { + color: #28a745; + } + .health-item .value.warning { + color: #ffc107; + } + .health-item .value.error { + color: #dc3545; + } + .unlock-button { + background: #dc3545; + color: white; + border: none; + padding: 10px 20px; + border-radius: 5px; + cursor: pointer; + font-size: 1em; + } + .unlock-button:hover { + background: #c82333; + } + .unlock-button:disabled { + background: #6c757d; + cursor: not-allowed; + } + .stale-locks-list { + margin-top: 10px; + padding: 10px; + background: rgba(220, 53, 69, 0.1); + border-radius: 5px; + border: 1px solid #dc3545; + } + .stale-locks-list h4 { + margin-top: 0; + color: #dc3545; + } + .stale-lock-item { + padding: 5px; + font-family: monospace; + font-size: 0.9em; + } @@ -184,6 +253,15 @@
+
+

État de l'API d'Ancrage

+
Chargement de l'état...
+
+ + +
+
+
Chargement des hash...
@@ -202,6 +280,7 @@ // Charger la liste au chargement de la page document.addEventListener('DOMContentLoaded', () => { loadHashList(); + loadHealthStatus(); }); async function loadHashList() { @@ -341,6 +420,141 @@ const now = new Date(); document.getElementById('last-update').textContent = now.toLocaleString('fr-FR'); } + + async function loadHealthStatus() { + const healthDiv = document.getElementById('health-status'); + const unlockButton = document.getElementById('unlock-utxos-button'); + const refreshButton = document.getElementById('refresh-health-button'); + + refreshButton.disabled = true; + healthDiv.innerHTML = '
Chargement de l\'état...
'; + + 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(); + + let html = '
'; + + // État général + const statusClass = health.ok ? 'ok' : 'error'; + html += `
+ +
${health.ok ? '✓ OK' : '✗ Problème'}
+
`; + + // Mutex + const mutexClass = health.mutex.locked ? 'warning' : 'ok'; + html += `
+ +
+ ${health.mutex.locked ? '🔒 Verrouillé' : '✓ Libre'} + ${health.mutex.waiting > 0 ? `(${health.mutex.waiting} en attente)` : ''} +
+
`; + + // UTXOs verrouillés + const utxosClass = health.utxos.locked === 0 ? 'ok' : (health.utxos.locked > 10 ? 'error' : 'warning'); + html += `
+ +
${health.utxos.locked}
+
`; + + // UTXOs stale + if (health.utxos.stale_locks > 0) { + html += `
+ +
${health.utxos.stale_locks}
+
`; + } + + // Bitcoin + const bitcoinClass = health.bitcoin.connected ? 'ok' : 'error'; + html += `
+ +
+ ${health.bitcoin.connected ? '✓ Connecté' : '✗ Déconnecté'} + ${health.bitcoin.blocks > 0 ? ` (${health.bitcoin.blocks.toLocaleString('fr-FR')} blocs)` : ''} +
+
`; + + html += '
'; + + // Afficher les détails des UTXOs stale + if (health.utxos.stale_locks > 0 && health.utxos.stale_locks_details.length > 0) { + html += '
'; + html += '

UTXOs verrouillés depuis plus de 10 minutes :

'; + health.utxos.stale_locks_details.forEach(utxo => { + html += `
${utxo.txid} vout:${utxo.vout} (${utxo.minutes_locked.toFixed(1)} min)
`; + }); + html += '
'; + } + + // Afficher le timestamp + html += `
+ Dernière mise à jour : ${new Date(health.timestamp).toLocaleString('fr-FR')} +
`; + + healthDiv.innerHTML = html; + + // Afficher le bouton de déverrouillage si des UTXOs sont verrouillés + if (health.utxos.locked > 0) { + unlockButton.style.display = 'inline-block'; + } else { + unlockButton.style.display = 'none'; + } + + } catch (error) { + console.error('Error loading health status:', error); + healthDiv.innerHTML = `
Erreur lors du chargement de l'état : ${error.message}
`; + unlockButton.style.display = 'none'; + } finally { + refreshButton.disabled = false; + } + } + + async function unlockUtxos() { + const unlockButton = document.getElementById('unlock-utxos-button'); + const originalText = unlockButton.textContent; + + if (!confirm('Êtes-vous sûr de vouloir déverrouiller tous les UTXOs ? Cette action libère le mutex et peut affecter les opérations en cours.')) { + return; + } + + unlockButton.disabled = true; + unlockButton.textContent = 'Déverrouillage en cours...'; + + try { + const response = await fetch(`${API_BASE_URL}/api/anchor/unlock-utxos`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + const error = await response.json(); + throw new Error(error.message || `HTTP error! status: ${response.status}`); + } + + const result = await response.json(); + + alert(`✅ ${result.unlocked || 0} UTXO(s) déverrouillé(s) avec succès.`); + + // Recharger l'état de santé + loadHealthStatus(); + } catch (error) { + console.error('Error unlocking UTXOs:', error); + alert(`❌ Erreur lors du déverrouillage : ${error.message}`); + } finally { + unlockButton.disabled = false; + unlockButton.textContent = originalText; + } + }

Bitcoin Ancrage Dashboard - Équipe 4NK

diff --git a/signet-dashboard/public/index.html b/signet-dashboard/public/index.html index 06e6775..ab66a80 100644 --- a/signet-dashboard/public/index.html +++ b/signet-dashboard/public/index.html @@ -104,6 +104,18 @@
+ +
+
+ ⚠️ Avertissement de sécurité +

+ Le serveur ne stocke pas vos documents. Cependant, soyez vigilant : + HTTPS n'est pas une protection parfaite et les navigateurs sont très imparfaits. + Évitez d'envoyer des documents contenant des informations sensibles ou confidentielles. +

+
+
+

Test de l'API d'Ancrage

@@ -142,61 +154,6 @@
- -
-
- - -
- - -
@@ -213,6 +170,93 @@
+ + +
+

Test de l'API ClamAV

+
+

Scannez un fichier pour détecter les virus avec ClamAV

+ + +

+ Limite de taille : 100 MB maximum +

+
+ +
+
+
+ + +
+

Test de l'API Filigrane

+
+ + + +
+ + +
+ +
+ + +
+ +
+ + +

+ Limite de taille : 100 MB maximum +

+
+
+ +
+ + + + + + + + + + + + + + + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+
+