Optimize UTXO list loading using text file cache
**Motivations:** - Le chargement des UTXOs est très long car il recalcule tout à chaque fois - Chaque UTXO nécessite plusieurs appels RPC (getrawtransaction, gettxout, getTransaction) - Avec des milliers d'UTXOs, cela prend beaucoup de temps **Root causes:** - Le code ne lit jamais le fichier utxo_list.txt pour éviter de recalculer - Tous les UTXOs sont recalculés à chaque appel, même s'ils n'ont pas changé - Le fichier de cache utxo_list_cache.txt n'est utilisé que pour stocker une date, pas pour éviter les recalculs **Correctifs:** - Chargement des UTXOs existants depuis utxo_list.txt au démarrage - Comparaison avec les UTXOs actuels de listunspent pour identifier les nouveaux/modifiés - Recalcul uniquement des UTXOs nouveaux ou modifiés - Mise à jour des informations dynamiques (isSpentOnchain, isLockedInMutex, confirmations) en parallèle - Conservation des UTXOs dépensés dans le fichier pour l'historique (mais exclus des listes actives) **Evolutions:** - Utilisation du fichier texte utxo_list.txt comme cache (comme getHashList() utilise hash_list.txt) - Format du fichier étendu : category;txid;vout;address;amount;confirmations;isAnchorChange - Vérifications en parallèle pour les UTXOs existants (Promise.all) - Logs indiquant le nombre d'UTXOs chargés depuis le fichier vs recalculés - Les UTXOs dépensés sont conservés dans le fichier mais exclus des listes actives **Pages affectées:** - signet-dashboard/src/bitcoin-rpc.js: Méthode getUtxoList() optimisée pour utiliser le fichier texte comme cache
This commit is contained in:
parent
64cba050a8
commit
300c728ce8
@ -359,12 +359,79 @@ class BitcoinRPC {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const unspent = rpcResult.result || [];
|
const unspent = rpcResult.result || [];
|
||||||
|
|
||||||
|
// Charger les UTXOs existants depuis le fichier texte
|
||||||
|
const existingUtxosMap = new Map(); // Clé: "txid:vout", Valeur: utxoItem
|
||||||
const blocRewards = [];
|
const blocRewards = [];
|
||||||
const anchors = [];
|
const anchors = [];
|
||||||
const changes = [];
|
const changes = [];
|
||||||
const fees = []; // Liste des transactions avec leurs frais onchain
|
const fees = []; // Liste des transactions avec leurs frais onchain
|
||||||
|
|
||||||
logger.info('Categorizing UTXOs', { total: unspent.length });
|
if (existsSync(outputPath)) {
|
||||||
|
try {
|
||||||
|
const existingContent = readFileSync(outputPath, 'utf8').trim();
|
||||||
|
const lines = existingContent.split('\n');
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.trim()) {
|
||||||
|
const parts = line.split(';');
|
||||||
|
if (parts.length >= 6) {
|
||||||
|
const [category, txid, vout, address, amount, confirmations] = parts;
|
||||||
|
const utxoKey = `${txid}:${vout}`;
|
||||||
|
const utxoItem = {
|
||||||
|
txid,
|
||||||
|
vout: parseInt(vout, 10),
|
||||||
|
address: address || '',
|
||||||
|
amount: parseFloat(amount),
|
||||||
|
confirmations: parseInt(confirmations, 10) || 0,
|
||||||
|
category,
|
||||||
|
// Ces champs seront mis à jour si nécessaire
|
||||||
|
isSpentOnchain: false,
|
||||||
|
isLockedInMutex: false,
|
||||||
|
blockHeight: null,
|
||||||
|
blockTime: null,
|
||||||
|
isAnchorChange: category === 'changes' && parts.length > 6 ? parts[6] === 'true' : false,
|
||||||
|
};
|
||||||
|
existingUtxosMap.set(utxoKey, utxoItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
logger.info('Loaded existing UTXOs from file', { count: existingUtxosMap.size });
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Error reading existing UTXO file', { error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Créer un Set des UTXOs actuels pour identifier les nouveaux
|
||||||
|
const currentUtxosSet = new Set();
|
||||||
|
for (const utxo of unspent) {
|
||||||
|
currentUtxosSet.add(`${utxo.txid}:${utxo.vout}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Identifier les UTXOs à recalculer (nouveaux ou modifiés)
|
||||||
|
const utxosToRecalculate = [];
|
||||||
|
const utxosToKeep = [];
|
||||||
|
|
||||||
|
for (const utxo of unspent) {
|
||||||
|
const utxoKey = `${utxo.txid}:${utxo.vout}`;
|
||||||
|
const existing = existingUtxosMap.get(utxoKey);
|
||||||
|
|
||||||
|
// Vérifier si l'UTXO existe et si les données de base sont identiques
|
||||||
|
if (existing &&
|
||||||
|
Math.abs(existing.amount - utxo.amount) < 0.00000001 &&
|
||||||
|
existing.confirmations === (utxo.confirmations || 0)) {
|
||||||
|
// UTXO existant avec données identiques, utiliser les données du fichier
|
||||||
|
utxosToKeep.push(existing);
|
||||||
|
} else {
|
||||||
|
// Nouvel UTXO ou UTXO modifié, doit être recalculé
|
||||||
|
utxosToRecalculate.push(utxo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('UTXO processing', {
|
||||||
|
total: unspent.length,
|
||||||
|
fromFile: utxosToKeep.length,
|
||||||
|
toRecalculate: utxosToRecalculate.length,
|
||||||
|
});
|
||||||
|
|
||||||
// Récupérer les UTXO verrouillés depuis l'API d'ancrage
|
// Récupérer les UTXO verrouillés depuis l'API d'ancrage
|
||||||
let lockedUtxos = new Set();
|
let lockedUtxos = new Set();
|
||||||
@ -395,8 +462,69 @@ class BitcoinRPC {
|
|||||||
logger.debug('Error getting locked UTXOs from anchor API', { error: error.message });
|
logger.debug('Error getting locked UTXOs from anchor API', { error: error.message });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Catégoriser chaque UTXO
|
// Mettre à jour les informations dynamiques pour les UTXOs existants
|
||||||
for (const utxo of unspent) {
|
// (isSpentOnchain, isLockedInMutex, confirmations)
|
||||||
|
// Faire les vérifications en parallèle pour accélérer
|
||||||
|
const updatePromises = utxosToKeep.map(async (existingUtxo) => {
|
||||||
|
// Mettre à jour les confirmations depuis listunspent
|
||||||
|
const currentUtxo = unspent.find(u => u.txid === existingUtxo.txid && u.vout === existingUtxo.vout);
|
||||||
|
if (currentUtxo) {
|
||||||
|
existingUtxo.confirmations = currentUtxo.confirmations || 0;
|
||||||
|
existingUtxo.amount = currentUtxo.amount; // Mettre à jour le montant au cas où
|
||||||
|
|
||||||
|
// Vérifier si l'UTXO est verrouillé
|
||||||
|
const utxoKey = `${existingUtxo.txid}:${existingUtxo.vout}`;
|
||||||
|
existingUtxo.isLockedInMutex = lockedUtxos.has(utxoKey);
|
||||||
|
|
||||||
|
// Vérifier si dépensé (gettxout retourne null)
|
||||||
|
try {
|
||||||
|
const txOutResponse = await fetch(rpcUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Authorization': `Basic ${auth}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
jsonrpc: '1.0',
|
||||||
|
id: 'gettxout',
|
||||||
|
method: 'gettxout',
|
||||||
|
params: [existingUtxo.txid, existingUtxo.vout],
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (txOutResponse.ok) {
|
||||||
|
const txOutResult = await txOutResponse.json();
|
||||||
|
existingUtxo.isSpentOnchain = txOutResult.result === null;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.debug('Error checking if existing UTXO is spent', { txid: existingUtxo.txid, vout: existingUtxo.vout, error: error.message });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// UTXO n'est plus dans listunspent, il a été dépensé
|
||||||
|
existingUtxo.isSpentOnchain = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(updatePromises);
|
||||||
|
|
||||||
|
// Ajouter les UTXOs existants aux listes appropriées (seulement s'ils ne sont pas dépensés)
|
||||||
|
for (const existingUtxo of utxosToKeep) {
|
||||||
|
// Ne pas ajouter les UTXOs dépensés aux listes actives
|
||||||
|
if (existingUtxo.isSpentOnchain) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existingUtxo.category === 'bloc_rewards') {
|
||||||
|
blocRewards.push(existingUtxo);
|
||||||
|
} else if (existingUtxo.category === 'ancrages') {
|
||||||
|
anchors.push(existingUtxo);
|
||||||
|
} else if (existingUtxo.category === 'changes') {
|
||||||
|
changes.push(existingUtxo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Catégoriser uniquement les nouveaux UTXOs ou ceux modifiés
|
||||||
|
for (const utxo of utxosToRecalculate) {
|
||||||
try {
|
try {
|
||||||
// Obtenir la transaction source pour déterminer sa catégorie
|
// Obtenir la transaction source pour déterminer sa catégorie
|
||||||
const rawTxResponse = await fetch(rpcUrl, {
|
const rawTxResponse = await fetch(rpcUrl, {
|
||||||
@ -473,6 +601,7 @@ class BitcoinRPC {
|
|||||||
if (isCoinbase) {
|
if (isCoinbase) {
|
||||||
utxoItem.category = 'bloc_rewards';
|
utxoItem.category = 'bloc_rewards';
|
||||||
blocRewards.push(utxoItem);
|
blocRewards.push(utxoItem);
|
||||||
|
existingUtxosMap.set(`${utxo.txid}:${utxo.vout}`, utxoItem);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -619,6 +748,7 @@ class BitcoinRPC {
|
|||||||
if (isAnchorOutput) {
|
if (isAnchorOutput) {
|
||||||
utxoItem.category = 'ancrages';
|
utxoItem.category = 'ancrages';
|
||||||
anchors.push(utxoItem);
|
anchors.push(utxoItem);
|
||||||
|
existingUtxosMap.set(`${utxo.txid}:${utxo.vout}`, utxoItem);
|
||||||
} else if (isChangeOutput) {
|
} else if (isChangeOutput) {
|
||||||
// C'est le change de la transaction d'ancrage
|
// C'est le change de la transaction d'ancrage
|
||||||
utxoItem.category = 'changes';
|
utxoItem.category = 'changes';
|
||||||
@ -632,12 +762,14 @@ class BitcoinRPC {
|
|||||||
utxoItem.onchainFeeAmount = onchainFeeAmount;
|
utxoItem.onchainFeeAmount = onchainFeeAmount;
|
||||||
}
|
}
|
||||||
changes.push(utxoItem);
|
changes.push(utxoItem);
|
||||||
|
existingUtxosMap.set(`${utxo.txid}:${utxo.vout}`, utxoItem);
|
||||||
} else {
|
} else {
|
||||||
// Montant très petit (< 1000 sats), probablement du dust
|
// Montant très petit (< 1000 sats), probablement du dust
|
||||||
// Classer comme change quand même (peu probable dans une transaction d'ancrage)
|
// Classer comme change quand même (peu probable dans une transaction d'ancrage)
|
||||||
utxoItem.category = 'changes';
|
utxoItem.category = 'changes';
|
||||||
utxoItem.isAnchorChange = true;
|
utxoItem.isAnchorChange = true;
|
||||||
changes.push(utxoItem);
|
changes.push(utxoItem);
|
||||||
|
existingUtxosMap.set(`${utxo.txid}:${utxo.vout}`, utxoItem);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Transaction normale (non-ancrage, non-coinbase) = change
|
// Transaction normale (non-ancrage, non-coinbase) = change
|
||||||
@ -646,18 +778,33 @@ class BitcoinRPC {
|
|||||||
utxoItem.category = 'changes';
|
utxoItem.category = 'changes';
|
||||||
utxoItem.isAnchorChange = false; // Change d'une transaction normale
|
utxoItem.isAnchorChange = false; // Change d'une transaction normale
|
||||||
changes.push(utxoItem);
|
changes.push(utxoItem);
|
||||||
|
existingUtxosMap.set(`${utxo.txid}:${utxo.vout}`, utxoItem);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// En cas d'erreur, classer comme change par défaut
|
// En cas d'erreur, classer comme change par défaut
|
||||||
logger.debug('Error categorizing UTXO', { txid: utxo.txid, error: error.message });
|
logger.debug('Error categorizing UTXO', { txid: utxo.txid, error: error.message });
|
||||||
changes.push({
|
const errorUtxoItem = {
|
||||||
txid: utxo.txid,
|
txid: utxo.txid,
|
||||||
vout: utxo.vout,
|
vout: utxo.vout,
|
||||||
address: utxo.address || '',
|
address: utxo.address || '',
|
||||||
amount: utxo.amount,
|
amount: utxo.amount,
|
||||||
confirmations: utxo.confirmations || 0,
|
confirmations: utxo.confirmations || 0,
|
||||||
category: 'changes',
|
category: 'changes',
|
||||||
});
|
isSpentOnchain: false,
|
||||||
|
isLockedInMutex: false,
|
||||||
|
};
|
||||||
|
changes.push(errorUtxoItem);
|
||||||
|
existingUtxosMap.set(`${utxo.txid}:${utxo.vout}`, errorUtxoItem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier les UTXOs dépensés (ceux qui étaient dans le fichier mais plus dans listunspent)
|
||||||
|
// Ces UTXOs sont marqués comme dépensés mais conservés dans le fichier pour l'historique
|
||||||
|
for (const [utxoKey, existingUtxo] of existingUtxosMap.entries()) {
|
||||||
|
if (!currentUtxosSet.has(utxoKey)) {
|
||||||
|
// UTXO n'est plus dans listunspent, il a été dépensé
|
||||||
|
existingUtxo.isSpentOnchain = true;
|
||||||
|
// Ne pas l'ajouter aux listes actives (déjà ajouté dans utxosToKeep si présent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -695,10 +842,12 @@ class BitcoinRPC {
|
|||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
writeFileSync(cachePath, now, 'utf8');
|
writeFileSync(cachePath, now, 'utf8');
|
||||||
|
|
||||||
// Écrire le fichier de sortie avec toutes les catégories
|
// Écrire le fichier de sortie avec toutes les catégories (incluant les UTXOs dépensés pour historique)
|
||||||
const outputLines = allUtxos.map((item) =>
|
// Format: category;txid;vout;address;amount;confirmations;isAnchorChange
|
||||||
`${item.category};${item.txid};${item.vout};${item.address};${item.amount};${item.confirmations}`
|
const outputLines = Array.from(existingUtxosMap.values()).map((item) => {
|
||||||
);
|
const isAnchorChange = item.isAnchorChange ? 'true' : 'false';
|
||||||
|
return `${item.category};${item.txid};${item.vout};${item.address};${item.amount};${item.confirmations};${isAnchorChange}`;
|
||||||
|
});
|
||||||
writeFileSync(outputPath, outputLines.join('\n'), 'utf8');
|
writeFileSync(outputPath, outputLines.join('\n'), 'utf8');
|
||||||
|
|
||||||
// Analyser la distribution pour comprendre pourquoi il y a si peu de changes
|
// Analyser la distribution pour comprendre pourquoi il y a si peu de changes
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user