**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)
457 lines
12 KiB
Markdown
457 lines
12 KiB
Markdown
# 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
|
|
|
|
```javascript
|
|
// 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`
|
|
|
|
```sql
|
|
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)
|
|
|
|
```sql
|
|
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`
|
|
|
|
```sql
|
|
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:**
|
|
```javascript
|
|
// 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:**
|
|
```sql
|
|
-- Temps: ~50-100ms avec index
|
|
SELECT * FROM utxos;
|
|
```
|
|
|
|
**Gain:** 2-5x plus rapide
|
|
|
|
#### 2. Recherche par catégorie
|
|
|
|
**Fichier texte:**
|
|
```javascript
|
|
// Temps: ~200-300ms (parsing de toutes les lignes)
|
|
const anchors = lines.filter(line => line.startsWith('ancrages;'));
|
|
```
|
|
|
|
**Base de données:**
|
|
```sql
|
|
-- Temps: ~5-10ms avec index
|
|
SELECT * FROM utxos WHERE category = 'ancrages';
|
|
```
|
|
|
|
**Gain:** 20-40x plus rapide
|
|
|
|
#### 3. Recherche par txid
|
|
|
|
**Fichier texte:**
|
|
```javascript
|
|
// Temps: ~200-300ms (recherche linéaire)
|
|
const utxo = lines.find(line => line.includes(`;${txid};`));
|
|
```
|
|
|
|
**Base de données:**
|
|
```sql
|
|
-- 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:**
|
|
```javascript
|
|
// 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:**
|
|
```sql
|
|
-- Temps: ~2-5ms
|
|
UPDATE utxos SET confirmations = ? WHERE txid = ? AND vout = ?;
|
|
```
|
|
|
|
**Gain:** 60-250x plus rapide
|
|
|
|
#### 5. Comptage
|
|
|
|
**Fichier texte:**
|
|
```javascript
|
|
// Temps: ~100-200ms (parsing de toutes les lignes)
|
|
const count = lines.filter(line => line.startsWith('ancrages;')).length;
|
|
```
|
|
|
|
**Base de données:**
|
|
```sql
|
|
-- 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:**
|
|
```javascript
|
|
// 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:**
|
|
```sql
|
|
-- 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
|
|
|
|
```sql
|
|
-- 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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
```javascript
|
|
// 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
|
|
|
|
4. **Scripts de migration** pour les données existantes
|
|
5. **Tests de performance** pour valider les gains
|
|
6. **Documentation** des nouvelles requêtes SQL
|
|
|
|
### Priorité basse
|
|
|
|
7. **Monitoring** des performances de la base de données
|
|
8. **Optimisations** supplémentaires si nécessaire
|
|
9. **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).
|