**Motivations:** - Prevent bitcoind crashes caused by heavy RPC calls without timeout - Document bitcoind crash and wallet loading stuck issues - Clean up obsolete files (configure-nginx-proxy.sh, userwallet components, website-skeleton, old fixKnowledge docs) **Root causes:** - RPC calls without timeout causing bitcoind crashes - No pre-check of bitcoind health before heavy operations - Large wallet (315MB) causing long loading times and potential hangs - Missing retry mechanism for transient errors **Correctifs:** - Add timeouts on RPC calls (5 minutes for listunspent, 10 seconds for healthcheck) - Add bitcoind health check before synchronization - Implement retry with exponential backoff - Reduce maximumCount limit from 9999999 to 500000 UTXOs - Improve cron script with pre-checks and better error handling - Add container status verification before script execution **Evolutions:** - New check-services-status.sh script for service diagnostics - Documentation of crash issues in fixKnowledge - Improved logging with timestamps - Better error messages and handling **Pages affectées:** - data/sync-utxos-spent-status.mjs - data/sync-utxos-cron.sh - data/restart-services-cron.sh - data/check-services-status.sh (new) - fixKnowledge/sync-utxos-rpc-optimization.md (new) - fixKnowledge/signet-bitcoind-crash-mining-stopped.md (new) - fixKnowledge/signet-bitcoind-crash-wallet-loading-stuck.md (new) - Removed obsolete files: configure-nginx-proxy.sh, userwallet components, website-skeleton files, old fixKnowledge docs Co-authored-by: Cursor <cursoragent@cursor.com>
291 lines
9.6 KiB
JavaScript
Executable File
291 lines
9.6 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
||
|
||
/**
|
||
* Script de synchronisation des UTXOs dépensés
|
||
*
|
||
* Ce script vérifie tous les UTXOs marqués comme non dépensés dans la base de données
|
||
* et les compare avec listunspent pour mettre à jour leur statut is_spent_onchain.
|
||
*
|
||
* Usage: node data/sync-utxos-spent-status.mjs
|
||
*/
|
||
|
||
import { createRequire } from 'module';
|
||
import { fileURLToPath } from 'url';
|
||
import { dirname, join } from 'path';
|
||
import https from 'https';
|
||
import http from 'http';
|
||
|
||
const __filename = fileURLToPath(import.meta.url);
|
||
const __dirname = dirname(__filename);
|
||
|
||
// Utiliser better-sqlite3 depuis signet-dashboard
|
||
const signetDashboardPath = join(__dirname, '../signet-dashboard');
|
||
const require = createRequire(join(signetDashboardPath, 'package.json'));
|
||
const Database = require('better-sqlite3');
|
||
|
||
// Configuration RPC Bitcoin
|
||
const RPC_URL = process.env.BITCOIN_RPC_URL || 'http://localhost:38332';
|
||
const RPC_USER = process.env.BITCOIN_RPC_USER || 'bitcoin';
|
||
const RPC_PASS = process.env.BITCOIN_RPC_PASSWORD || 'bitcoin';
|
||
const RPC_WALLET = process.env.BITCOIN_RPC_WALLET || 'custom_signet';
|
||
|
||
// Chemin de la base de données
|
||
const DB_PATH = join(__dirname, 'signet.db');
|
||
|
||
/**
|
||
* Effectue un appel RPC Bitcoin avec timeout et retry
|
||
*/
|
||
function rpcCall(method, params = [], timeoutMs = 300000, maxRetries = 3) {
|
||
return new Promise(async (resolve, reject) => {
|
||
let lastError;
|
||
|
||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||
try {
|
||
const result = await rpcCallOnce(method, params, timeoutMs);
|
||
resolve(result);
|
||
return;
|
||
} catch (error) {
|
||
lastError = error;
|
||
const isTimeoutError = error.message.includes('timeout') ||
|
||
error.message.includes('ETIMEDOUT') ||
|
||
error.message.includes('ECONNRESET') ||
|
||
error.message.includes('socket hang up');
|
||
|
||
if (attempt < maxRetries - 1 && isTimeoutError) {
|
||
const delay = Math.min(1000 * Math.pow(2, attempt), 10000); // Backoff exponentiel, max 10s
|
||
console.log(` ⚠️ Tentative ${attempt + 1}/${maxRetries} échouée, nouvelle tentative dans ${delay}ms...`);
|
||
await new Promise(resolve => setTimeout(resolve, delay));
|
||
} else {
|
||
reject(error);
|
||
return;
|
||
}
|
||
}
|
||
}
|
||
|
||
reject(lastError);
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Effectue un appel RPC Bitcoin unique avec timeout
|
||
*/
|
||
function rpcCallOnce(method, params = [], timeoutMs = 300000) {
|
||
return new Promise((resolve, reject) => {
|
||
const url = new URL(RPC_URL);
|
||
const isHttps = url.protocol === 'https:';
|
||
const httpModule = isHttps ? https : http;
|
||
|
||
const auth = Buffer.from(`${RPC_USER}:${RPC_PASS}`).toString('base64');
|
||
|
||
const postData = JSON.stringify({
|
||
jsonrpc: '2.0',
|
||
id: 1,
|
||
method: method,
|
||
params: params,
|
||
});
|
||
|
||
const options = {
|
||
hostname: url.hostname,
|
||
port: url.port || (isHttps ? 443 : 80),
|
||
path: url.pathname,
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Authorization': `Basic ${auth}`,
|
||
'Content-Length': Buffer.byteLength(postData),
|
||
},
|
||
timeout: timeoutMs,
|
||
};
|
||
|
||
const req = httpModule.request(options, (res) => {
|
||
let body = '';
|
||
|
||
res.on('data', (chunk) => {
|
||
body += chunk;
|
||
});
|
||
|
||
res.on('end', () => {
|
||
try {
|
||
const json = JSON.parse(body);
|
||
if (json.error) {
|
||
reject(new Error(json.error.message || JSON.stringify(json.error)));
|
||
} else {
|
||
resolve(json.result);
|
||
}
|
||
} catch (e) {
|
||
reject(new Error(`Parse error: ${e.message}`));
|
||
}
|
||
});
|
||
});
|
||
|
||
req.on('error', (error) => {
|
||
reject(error);
|
||
});
|
||
|
||
req.on('timeout', () => {
|
||
req.destroy();
|
||
reject(new Error(`RPC timeout after ${timeoutMs}ms for method ${method}`));
|
||
});
|
||
|
||
req.write(postData);
|
||
req.end();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Vérifie que bitcoind est disponible et répond
|
||
*/
|
||
async function checkBitcoindHealth() {
|
||
try {
|
||
await rpcCall('getblockchaininfo', [], 10000, 1); // Timeout court pour le healthcheck
|
||
return true;
|
||
} catch (error) {
|
||
console.error(` ❌ Bitcoind non disponible: ${error.message}`);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Synchronise les UTXOs dépensés
|
||
*/
|
||
async function syncSpentUtxos() {
|
||
console.log('🔍 Démarrage de la synchronisation des UTXOs dépensés...\n');
|
||
|
||
// Vérifier que bitcoind est disponible avant de commencer
|
||
console.log('🔍 Vérification de la disponibilité de bitcoind...');
|
||
const bitcoindAvailable = await checkBitcoindHealth();
|
||
if (!bitcoindAvailable) {
|
||
console.error('❌ Bitcoind n\'est pas disponible. Arrêt de la synchronisation.');
|
||
process.exit(1);
|
||
}
|
||
console.log(' ✅ Bitcoind disponible\n');
|
||
|
||
// Ouvrir la base de données
|
||
const db = new Database(DB_PATH);
|
||
|
||
try {
|
||
// Compter les UTXOs à vérifier sans les charger en mémoire
|
||
const notSpentCount = db.prepare(`
|
||
SELECT COUNT(*) as count
|
||
FROM utxos
|
||
WHERE is_spent_onchain = 0
|
||
`).get().count;
|
||
|
||
console.log(`📊 UTXOs à vérifier: ${notSpentCount}`);
|
||
|
||
if (notSpentCount === 0) {
|
||
console.log('✅ Aucun UTXO à vérifier');
|
||
return;
|
||
}
|
||
|
||
// Récupérer tous les UTXOs disponibles depuis Bitcoin
|
||
// Note: listunspent ne supporte pas la pagination avec skip
|
||
// Limiter maximumCount pour éviter les problèmes de mémoire avec de gros wallets
|
||
// Timeout augmenté à 5 minutes pour les gros wallets
|
||
console.log('📡 Récupération des UTXOs depuis Bitcoin...');
|
||
console.log(' ⏳ Cela peut prendre plusieurs minutes avec un wallet volumineux...');
|
||
// Limite à 500000 UTXOs pour éviter les problèmes de mémoire
|
||
// Timeout de 5 minutes (300000ms) pour permettre le traitement des gros wallets
|
||
const unspent = await rpcCall('listunspent', [0, 9999999, [], false, {
|
||
minimumAmount: 0,
|
||
maximumCount: 500000, // Limite réduite de 9999999 à 500000 pour éviter les problèmes de mémoire
|
||
}], 300000, 2); // Timeout 5 minutes, max 2 retries
|
||
|
||
console.log(`📊 UTXOs disponibles dans Bitcoin: ${unspent.length}`);
|
||
|
||
// Utiliser une table temporaire pour éviter de charger tous les UTXOs en mémoire
|
||
console.log('💾 Création de la table temporaire...');
|
||
db.exec(`
|
||
CREATE TEMP TABLE IF NOT EXISTS temp_available_utxos (
|
||
txid TEXT,
|
||
vout INTEGER,
|
||
PRIMARY KEY (txid, vout)
|
||
)
|
||
`);
|
||
|
||
// Insérer les UTXOs disponibles par batch pour réduire la mémoire
|
||
const BATCH_SIZE = 1000;
|
||
const insertAvailableUtxo = db.prepare(`
|
||
INSERT OR IGNORE INTO temp_available_utxos (txid, vout)
|
||
VALUES (?, ?)
|
||
`);
|
||
|
||
const insertBatch = db.transaction((utxos) => {
|
||
for (const utxo of utxos) {
|
||
insertAvailableUtxo.run(utxo.txid, utxo.vout);
|
||
}
|
||
});
|
||
|
||
console.log('💾 Insertion des UTXOs disponibles par batch...');
|
||
for (let i = 0; i < unspent.length; i += BATCH_SIZE) {
|
||
const batch = unspent.slice(i, i + BATCH_SIZE);
|
||
insertBatch(batch);
|
||
|
||
if ((i + BATCH_SIZE) % 10000 === 0) {
|
||
console.log(` ⏳ Traitement: ${Math.min(i + BATCH_SIZE, unspent.length)}/${unspent.length} UTXOs insérés...`);
|
||
}
|
||
}
|
||
|
||
// Mettre à jour les UTXOs dépensés en une seule requête SQL (optimisé pour la mémoire)
|
||
console.log('💾 Mise à jour des UTXOs dépensés...');
|
||
const updateSpentStmt = db.prepare(`
|
||
UPDATE utxos
|
||
SET is_spent_onchain = 1, updated_at = CURRENT_TIMESTAMP
|
||
WHERE is_spent_onchain = 0
|
||
AND (txid || ':' || vout) NOT IN (
|
||
SELECT txid || ':' || vout FROM temp_available_utxos
|
||
)
|
||
`);
|
||
const updateResult = updateSpentStmt.run();
|
||
const updatedCount = updateResult.changes;
|
||
|
||
// Nettoyer la table temporaire
|
||
db.exec('DROP TABLE IF EXISTS temp_available_utxos');
|
||
|
||
const stillAvailableCount = notSpentCount - updatedCount;
|
||
|
||
console.log(`\n📊 Résumé:`);
|
||
console.log(` - UTXOs vérifiés: ${notSpentCount}`);
|
||
console.log(` - UTXOs toujours disponibles: ${stillAvailableCount}`);
|
||
console.log(` - UTXOs dépensés détectés: ${updatedCount}`);
|
||
|
||
// Afficher les statistiques finales
|
||
const finalStats = {
|
||
total: db.prepare('SELECT COUNT(*) as count FROM utxos').get().count,
|
||
spent: db.prepare('SELECT COUNT(*) as count FROM utxos WHERE is_spent_onchain = 1').get().count,
|
||
notSpent: db.prepare('SELECT COUNT(*) as count FROM utxos WHERE is_spent_onchain = 0').get().count,
|
||
};
|
||
|
||
console.log(`\n📈 Statistiques finales:`);
|
||
console.log(` - Total UTXOs: ${finalStats.total}`);
|
||
console.log(` - Dépensés: ${finalStats.spent}`);
|
||
console.log(` - Non dépensés: ${finalStats.notSpent}`);
|
||
|
||
} catch (error) {
|
||
console.error('❌ Erreur lors de la synchronisation:', error.message);
|
||
// Ne pas faire échouer le cron si c'est un problème temporaire de bitcoind
|
||
// Le script sera réexécuté à l'heure suivante
|
||
if (error.message.includes('timeout') ||
|
||
error.message.includes('ECONNRESET') ||
|
||
error.message.includes('socket hang up') ||
|
||
error.message.includes('Could not connect')) {
|
||
console.error(' ⚠️ Problème de connexion avec bitcoind (peut être temporaire)');
|
||
console.error(' ℹ️ Le script sera réexécuté à la prochaine heure');
|
||
}
|
||
process.exit(1);
|
||
} finally {
|
||
db.close();
|
||
}
|
||
}
|
||
|
||
// Exécuter la synchronisation
|
||
syncSpentUtxos()
|
||
.then(() => {
|
||
console.log('\n✅ Synchronisation terminée');
|
||
process.exit(0);
|
||
})
|
||
.catch((error) => {
|
||
console.error('❌ Erreur fatale:', error);
|
||
process.exit(1);
|
||
});
|