anchorage_layer_simple/features/migration-fichiers-texte-vers-base-donnees.md
ncantu 0960e43a45 Optimisation mémoire api-anchorage avec base de données SQLite
**Motivations:**
- Réduction drastique de la consommation mémoire lors des ancrages
- Élimination du chargement de 173k+ UTXOs à chaque requête
- Stabilisation de la mémoire système sous charge élevée (50+ ancrages/minute)

**Root causes:**
- api-anchorage chargeait tous les UTXOs (173k+) via listunspent RPC à chaque ancrage
- Filtrage et tri de 173k+ objets en mémoire pour sélectionner un seul UTXO
- Croissance mémoire de ~16 MB toutes les 12 secondes avec 50 ancrages/minute
- Saturation mémoire système en quelques minutes

**Correctifs:**
- Création du module database.js pour gérer la base de données SQLite partagée
- Remplacement de listunspent RPC par requête SQL directe avec LIMIT 1
- Sélection directe d'un UTXO depuis la DB au lieu de charger/filtrer 173k+ objets
- Marquage des UTXOs comme dépensés dans la DB après utilisation
- Fermeture propre de la base de données lors de l'arrêt

**Evolutions:**
- Utilisation de la base de données SQLite partagée avec signet-dashboard
- Réduction mémoire de 99.999% (173k+ objets → 1 objet par requête)
- Amélioration des performances (requête SQL indexée vs filtrage en mémoire)
- Optimisation mémoire de signet-dashboard (chargement UTXOs seulement si nécessaire)
- Monitoring de lockedUtxos dans api-anchorage pour détecter les fuites
- Nettoyage des intervalles frontend pour éviter les fuites mémoire

**Pages affectées:**
- api-anchorage/src/database.js (nouveau)
- api-anchorage/src/bitcoin-rpc.js
- api-anchorage/src/server.js
- api-anchorage/package.json
- signet-dashboard/src/bitcoin-rpc.js
- signet-dashboard/public/app.js
- features/optimisation-memoire-applications.md (nouveau)
- features/api-anchorage-optimisation-base-donnees.md (nouveau)
2026-01-27 21:12:22 +01:00

12 KiB

Migration fichiers texte vers base de données

Date: 2026-01-27 Auteur: Équipe 4NK

Objectif

Évaluer la migration des fichiers texte (utxo_list.txt, hash_list.txt, fees_list.txt) vers une base de données pour améliorer les performances et la maintenabilité.

État actuel

Fichiers texte utilisés

Fichier Taille Lignes Format
utxo_list.txt 6.3 MB 68 397 category;txid;vout;amount;confirmations;isAnchorChange;blockTime
hash_list.txt 5.2 MB 32 718 hash;txid;blockHeight;confirmations;date
fees_list.txt 411 KB 2 666 txid;fee;fee_sats;blockHeight;blockTime;confirmations;changeAddress;changeAmount
Total 11.9 MB 103 781 -

Opérations actuelles

  1. Lecture complète : Chargement de tout le fichier en mémoire
  2. Parsing ligne par ligne : split(';') pour chaque ligne
  3. Recherche : Parsing de toutes les lignes (O(n))
  4. Mise à jour : Réécriture complète du fichier
  5. Comptage : Parsing de toutes les lignes

Problèmes identifiés

Performance

  • Lecture complète nécessaire : Même pour un seul élément, tout le fichier doit être lu
  • Pas d'indexation : Recherche linéaire O(n) pour trouver un élément
  • Parsing coûteux : Split et parsing de 68k+ lignes à chaque chargement
  • Mise à jour lourde : Réécriture complète du fichier pour une seule modification

Maintenabilité

  • Pas de validation de schéma : Format libre, erreurs possibles
  • Pas de transactions : Risque de corruption en cas d'interruption
  • Pas de concurrence : Accès concurrent non sécurisé
  • Pas de requêtes complexes : Impossible de faire des JOIN, GROUP BY, etc.

Exemples de lenteur

// Lecture complète de 68k lignes
const content = readFileSync(utxoListPath, 'utf8').trim();
const lines = content.split('\n');
for (const line of lines) {
  const parts = line.split(';'); // Parsing de chaque ligne
  // ...
}

// Recherche: parsing de toutes les lignes
for (const line of lines) {
  if (line.includes('ancrages')) { // Recherche linéaire
    // ...
  }
}

// Mise à jour: réécriture complète
writeFileSync(outputPath, allLines.join('\n')); // Réécriture de 6.3 MB

Solution proposée: Base de données

Choix de la base de données

Recommandation: SQLite

Avantages:

  • Pas de serveur séparé nécessaire (fichier local)
  • Très performant pour ce volume de données
  • Support SQL complet
  • Transactions ACID
  • Indexation native
  • Faible empreinte mémoire
  • Facile à migrer vers PostgreSQL/MySQL si nécessaire

Alternatives:

  • PostgreSQL : Si besoin de serveur centralisé, accès réseau
  • MySQL/MariaDB : Si déjà utilisé dans l'infrastructure
  • IndexedDB : Si besoin côté client (navigateur)

Schéma proposé

Table utxos

CREATE TABLE utxos (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  category TEXT NOT NULL, -- 'bloc_rewards', 'ancrages', 'changes'
  txid TEXT NOT NULL,
  vout INTEGER NOT NULL,
  address TEXT,
  amount REAL NOT NULL,
  confirmations INTEGER DEFAULT 0,
  is_anchor_change BOOLEAN DEFAULT FALSE,
  block_time INTEGER,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  UNIQUE(txid, vout)
);

CREATE INDEX idx_utxos_category ON utxos(category);
CREATE INDEX idx_utxos_txid_vout ON utxos(txid, vout);
CREATE INDEX idx_utxos_confirmations ON utxos(confirmations);
CREATE INDEX idx_utxos_amount ON utxos(amount);

Table anchors (hash_list.txt)

CREATE TABLE anchors (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  hash TEXT NOT NULL UNIQUE,
  txid TEXT NOT NULL,
  block_height INTEGER,
  confirmations INTEGER DEFAULT 0,
  date TIMESTAMP,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_anchors_hash ON anchors(hash);
CREATE INDEX idx_anchors_txid ON anchors(txid);
CREATE INDEX idx_anchors_block_height ON anchors(block_height);

Table fees

CREATE TABLE fees (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  txid TEXT NOT NULL UNIQUE,
  fee REAL NOT NULL,
  fee_sats INTEGER NOT NULL,
  block_height INTEGER,
  block_time INTEGER,
  confirmations INTEGER DEFAULT 0,
  change_address TEXT,
  change_amount REAL,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

CREATE INDEX idx_fees_txid ON fees(txid);
CREATE INDEX idx_fees_block_height ON fees(block_height);

Comparaison des performances

Opérations courantes

1. Chargement de la liste complète

Fichier texte:

// Temps: ~200-500ms pour 68k lignes
const content = readFileSync(path, 'utf8'); // ~50ms
const lines = content.split('\n'); // ~20ms
for (const line of lines) {
  const parts = line.split(';'); // ~200-300ms
  // ...
}

Base de données:

-- Temps: ~50-100ms avec index
SELECT * FROM utxos;

Gain: 2-5x plus rapide

2. Recherche par catégorie

Fichier texte:

// Temps: ~200-300ms (parsing de toutes les lignes)
const anchors = lines.filter(line => line.startsWith('ancrages;'));

Base de données:

-- Temps: ~5-10ms avec index
SELECT * FROM utxos WHERE category = 'ancrages';

Gain: 20-40x plus rapide

3. Recherche par txid

Fichier texte:

// Temps: ~200-300ms (recherche linéaire)
const utxo = lines.find(line => line.includes(`;${txid};`));

Base de données:

-- Temps: ~1-2ms avec index
SELECT * FROM utxos WHERE txid = ? AND vout = ?;

Gain: 100-200x plus rapide

4. Mise à jour d'un élément

Fichier texte:

// Temps: ~300-500ms (réécriture complète)
// 1. Lire tout le fichier
// 2. Modifier la ligne
// 3. Réécrire tout le fichier
writeFileSync(path, allLines.join('\n'));

Base de données:

-- Temps: ~2-5ms
UPDATE utxos SET confirmations = ? WHERE txid = ? AND vout = ?;

Gain: 60-250x plus rapide

5. Comptage

Fichier texte:

// Temps: ~100-200ms (parsing de toutes les lignes)
const count = lines.filter(line => line.startsWith('ancrages;')).length;

Base de données:

-- Temps: ~1-2ms (pas de parsing, index uniquement)
SELECT COUNT(*) FROM utxos WHERE category = 'ancrages';

Gain: 50-200x plus rapide

6. Requêtes complexes

Fichier texte:

// Impossible ou très lent
// Exemple: UTXOs disponibles pour ancrage avec confirmations >= 6
const available = lines
  .filter(line => line.startsWith('ancrages;'))
  .map(line => {
    const parts = line.split(';');
    return { amount: parseFloat(parts[3]), confirmations: parseInt(parts[4]) };
  })
  .filter(u => u.amount >= 0.00002 && u.confirmations >= 6);
// Temps: ~300-500ms

Base de données:

-- Temps: ~5-10ms
SELECT * FROM utxos 
WHERE category = 'ancrages' 
  AND amount >= 0.00002 
  AND confirmations >= 6;

Gain: 30-100x plus rapide

Résumé des gains de performance

Opération Fichier texte Base de données Gain
Chargement complet 200-500ms 50-100ms 2-5x
Recherche par catégorie 200-300ms 5-10ms 20-40x
Recherche par txid 200-300ms 1-2ms 100-200x
Mise à jour 300-500ms 2-5ms 60-250x
Comptage 100-200ms 1-2ms 50-200x
Requêtes complexes 300-500ms 5-10ms 30-100x

Avantages supplémentaires

1. Intégrité des données

  • Contraintes : UNIQUE, NOT NULL, CHECK
  • Transactions : Rollback en cas d'erreur
  • Validation : Types de données stricts

2. Accès concurrent

  • Verrous : Gestion automatique des accès concurrents
  • Isolation : Transactions isolées
  • Pas de corruption : Pas de risque de fichier partiellement écrit

3. Requêtes avancées

-- Exemple: Statistiques par catégorie
SELECT 
  category,
  COUNT(*) as count,
  SUM(amount) as total_amount,
  AVG(amount) as avg_amount
FROM utxos
GROUP BY category;

-- Exemple: Ancrages récents
SELECT a.*, f.fee_sats
FROM anchors a
LEFT JOIN fees f ON a.txid = f.txid
WHERE a.block_height > ?
ORDER BY a.block_height DESC
LIMIT 100;

4. Maintenance

  • Backup : Copie simple du fichier SQLite
  • Migration : Scripts SQL pour évolutions de schéma
  • Monitoring : Requêtes SQL pour diagnostics

Migration

Étape 1: Création de la base de données

// signet-dashboard/src/database.js
import Database from 'better-sqlite3';

const db = new Database('./data/signet.db');

// Créer les tables
db.exec(`
  CREATE TABLE IF NOT EXISTS utxos (...);
  CREATE TABLE IF NOT EXISTS anchors (...);
  CREATE TABLE IF NOT EXISTS fees (...);
`);

Étape 2: Migration des données existantes

// Script de migration unique
async function migrateFromTextFiles() {
  // 1. Lire utxo_list.txt
  const utxoLines = readFileSync('utxo_list.txt', 'utf8').split('\n');
  
  // 2. Insérer en batch
  const insert = db.prepare(`
    INSERT INTO utxos (category, txid, vout, amount, confirmations, ...)
    VALUES (?, ?, ?, ?, ?, ...)
  `);
  
  const insertMany = db.transaction((utxos) => {
    for (const utxo of utxos) {
      insert.run(...);
    }
  });
  
  insertMany(parsedUtxos);
  
  // Répéter pour hash_list.txt et fees_list.txt
}

Étape 3: Adaptation du code

// Avant (fichier texte)
async getUtxoList() {
  const content = readFileSync('utxo_list.txt', 'utf8');
  const lines = content.split('\n');
  // Parsing...
}

// Après (base de données)
async getUtxoList() {
  const utxos = db.prepare(`
    SELECT * FROM utxos
  `).all();
  
  return {
    blocRewards: utxos.filter(u => u.category === 'bloc_rewards'),
    anchors: utxos.filter(u => u.category === 'ancrages'),
    changes: utxos.filter(u => u.category === 'changes'),
  };
}

Étape 4: Mise à jour incrémentale

// Au lieu de réécrire tout le fichier
async function updateUtxo(txid, vout, updates) {
  db.prepare(`
    UPDATE utxos 
    SET confirmations = ?, updated_at = CURRENT_TIMESTAMP
    WHERE txid = ? AND vout = ?
  `).run(updates.confirmations, txid, vout);
}

Impact attendu

Performance

  • Chargement initial : 2-5x plus rapide
  • Recherches : 20-200x plus rapide
  • Mises à jour : 60-250x plus rapide
  • Requêtes complexes : 30-100x plus rapide

Maintenabilité

  • Code plus simple : Pas de parsing manuel
  • Moins de bugs : Validation automatique
  • Évolutivité : Facile d'ajouter de nouvelles tables/colonnes
  • Requêtes SQL : Plus expressives que le parsing manuel

Fiabilité

  • Pas de corruption : Transactions ACID
  • Accès concurrent : Gestion automatique
  • Backup : Copie simple du fichier

Recommandations

Priorité haute

  1. Migrer vers SQLite pour les gains de performance immédiats
  2. Créer les index pour optimiser les recherches
  3. Adapter le code pour utiliser la base de données

Priorité moyenne

  1. Scripts de migration pour les données existantes
  2. Tests de performance pour valider les gains
  3. Documentation des nouvelles requêtes SQL

Priorité basse

  1. Monitoring des performances de la base de données
  2. Optimisations supplémentaires si nécessaire
  3. Migration vers PostgreSQL si besoin de serveur centralisé

Conclusion

Oui, une base de données serait significativement plus performante pour ce cas d'usage :

  • Gains de performance : 2-200x selon l'opération
  • Meilleure maintenabilité : Code plus simple, moins de bugs
  • Plus de fonctionnalités : Requêtes complexes, transactions, etc.
  • Meilleure fiabilité : Pas de corruption, accès concurrent sécurisé

Recommandation: Migrer vers SQLite pour un gain immédiat avec un effort minimal (pas de serveur à configurer).