anchorage_layer_simple/data/sync-utxos-spent-status.mjs
ncantu e0ce7a9d83 Optimize sync-utxos RPC calls and document bitcoind crash issues
**Motivations:**
- Prevent bitcoind crashes caused by heavy RPC calls without timeout
- Document bitcoind crash and wallet loading stuck issues
- Clean up obsolete files (configure-nginx-proxy.sh, userwallet components, website-skeleton, old fixKnowledge docs)

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

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

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

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

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-08 08:18:26 +01:00

291 lines
9.6 KiB
JavaScript
Executable File
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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);
});