Fix UTXO selection and race condition in anchorage API
**Motivations:** - Resolve insufficient UTXO amount errors when wallet has many small UTXOs - Prevent race conditions when multiple anchor requests arrive simultaneously - Improve signet dashboard functionality and documentation **Root causes:** - API tried to find a single UTXO large enough instead of combining multiple UTXOs - No mutex mechanism to prevent concurrent transactions from using the same UTXOs - UTXOs in mempool still appear as available in listunspent before block confirmation **Correctifs:** - Implement coin selection algorithm to combine multiple UTXOs when needed - Add mutex-based locking mechanism to serialize UTXO access - Filter locked UTXOs during selection to prevent double spending - Properly handle change output when combining multiple UTXOs - Lock UTXOs during transaction creation and unlock after mempool broadcast **Evolutions:** - Enhance signet dashboard with improved Bitcoin RPC integration - Update mempool documentation - Add comprehensive fix documentation in fixKnowledge/ **Pages affectées:** - api-anchorage/src/bitcoin-rpc.js - signet-dashboard/src/bitcoin-rpc.js - signet-dashboard/src/server.js - signet-dashboard/public/app.js - signet-dashboard/public/index.html - signet-dashboard/public/styles.css - signet-dashboard/start.sh - docs/MEMPOOL.md - fixKnowledge/api-anchorage-insufficient-utxo.md (new) - fixKnowledge/api-anchorage-utxo-race-condition.md (new) - anchor_count.txt (new) - mempool (submodule update)
This commit is contained in:
parent
dde1ccbb07
commit
e34b6ee43a
1
anchor_count.txt
Normal file
1
anchor_count.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
2026-01-25T00:20:14.085Z;6982;00000004b2ea52142ffebb57483d6aa53b9b21334e3067f00e54b5df506bf039;9088
|
||||||
@ -16,6 +16,87 @@ class BitcoinRPC {
|
|||||||
password: process.env.BITCOIN_RPC_PASSWORD || 'bitcoin',
|
password: process.env.BITCOIN_RPC_PASSWORD || 'bitcoin',
|
||||||
timeout: parseInt(process.env.BITCOIN_RPC_TIMEOUT || '30000'),
|
timeout: parseInt(process.env.BITCOIN_RPC_TIMEOUT || '30000'),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Mutex pour gérer l'accès concurrent aux UTXOs
|
||||||
|
// Utilise une Promise-based queue pour sérialiser les accès
|
||||||
|
this.utxoMutexPromise = Promise.resolve();
|
||||||
|
|
||||||
|
// Liste des UTXOs en cours d'utilisation (format: "txid:vout")
|
||||||
|
this.lockedUtxos = new Set();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Acquiert le mutex pour l'accès aux UTXOs
|
||||||
|
* @returns {Promise<Function>} Fonction pour libérer le mutex
|
||||||
|
*/
|
||||||
|
async acquireUtxoMutex() {
|
||||||
|
// Attendre que le mutex précédent soit libéré
|
||||||
|
const previousMutex = this.utxoMutexPromise;
|
||||||
|
let releaseMutex;
|
||||||
|
|
||||||
|
// Créer une nouvelle Promise qui sera résolue quand le mutex est libéré
|
||||||
|
this.utxoMutexPromise = new Promise((resolve) => {
|
||||||
|
releaseMutex = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Attendre que le mutex précédent soit libéré
|
||||||
|
await previousMutex;
|
||||||
|
|
||||||
|
// Retourner la fonction pour libérer le mutex
|
||||||
|
return releaseMutex;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie si un UTXO est verrouillé
|
||||||
|
* @param {string} txid - ID de la transaction
|
||||||
|
* @param {number} vout - Index de l'output
|
||||||
|
* @returns {boolean} True si l'UTXO est verrouillé
|
||||||
|
*/
|
||||||
|
isUtxoLocked(txid, vout) {
|
||||||
|
const key = `${txid}:${vout}`;
|
||||||
|
return this.lockedUtxos.has(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verrouille un UTXO
|
||||||
|
* @param {string} txid - ID de la transaction
|
||||||
|
* @param {number} vout - Index de l'output
|
||||||
|
*/
|
||||||
|
lockUtxo(txid, vout) {
|
||||||
|
const key = `${txid}:${vout}`;
|
||||||
|
this.lockedUtxos.add(key);
|
||||||
|
logger.debug('UTXO locked', { txid: txid.substring(0, 16) + '...', vout });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verrouille plusieurs UTXOs
|
||||||
|
* @param {Array<Object>} utxos - Liste des UTXOs à verrouiller
|
||||||
|
*/
|
||||||
|
lockUtxos(utxos) {
|
||||||
|
for (const utxo of utxos) {
|
||||||
|
this.lockUtxo(utxo.txid, utxo.vout);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Déverrouille un UTXO
|
||||||
|
* @param {string} txid - ID de la transaction
|
||||||
|
* @param {number} vout - Index de l'output
|
||||||
|
*/
|
||||||
|
unlockUtxo(txid, vout) {
|
||||||
|
const key = `${txid}:${vout}`;
|
||||||
|
this.lockedUtxos.delete(key);
|
||||||
|
logger.debug('UTXO unlocked', { txid: txid.substring(0, 16) + '...', vout });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Déverrouille plusieurs UTXOs
|
||||||
|
* @param {Array<Object>} utxos - Liste des UTXOs à déverrouiller
|
||||||
|
*/
|
||||||
|
unlockUtxos(utxos) {
|
||||||
|
for (const utxo of utxos) {
|
||||||
|
this.unlockUtxo(utxo.txid, utxo.vout);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -77,6 +158,10 @@ class BitcoinRPC {
|
|||||||
* @returns {Promise<Object>} Transaction créée avec txid
|
* @returns {Promise<Object>} Transaction créée avec txid
|
||||||
*/
|
*/
|
||||||
async createAnchorTransaction(hash, recipientAddress = null) {
|
async createAnchorTransaction(hash, recipientAddress = null) {
|
||||||
|
// Acquérir le mutex pour l'accès aux UTXOs
|
||||||
|
const releaseMutex = await this.acquireUtxoMutex();
|
||||||
|
let selectedUtxos = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Vérifier que le hash est valide (64 caractères hex)
|
// Vérifier que le hash est valide (64 caractères hex)
|
||||||
if (!/^[0-9a-fA-F]{64}$/.test(hash)) {
|
if (!/^[0-9a-fA-F]{64}$/.test(hash)) {
|
||||||
@ -147,47 +232,133 @@ class BitcoinRPC {
|
|||||||
throw new Error('No unspent outputs available');
|
throw new Error('No unspent outputs available');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Log pour déboguer
|
// Filtrer les UTXOs verrouillés (en cours d'utilisation par d'autres transactions)
|
||||||
logger.info('Available UTXOs', {
|
const availableUtxos = unspent.filter(utxo => !this.isUtxoLocked(utxo.txid, utxo.vout));
|
||||||
count: unspent.length,
|
|
||||||
amounts: unspent.map(u => u.amount).slice(0, 10),
|
logger.info('Available UTXOs (after filtering locked)', {
|
||||||
largest: unspent.length > 0 ? Math.max(...unspent.map(u => u.amount)) : 0,
|
total: unspent.length,
|
||||||
|
available: availableUtxos.length,
|
||||||
|
locked: unspent.length - availableUtxos.length,
|
||||||
|
amounts: availableUtxos.map(u => u.amount).slice(0, 10),
|
||||||
|
largest: availableUtxos.length > 0 ? Math.max(...availableUtxos.map(u => u.amount)) : 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sélectionner un UTXO avec suffisamment de fonds
|
if (availableUtxos.length === 0) {
|
||||||
// Trier par montant décroissant pour prendre le plus grand UTXO disponible
|
throw new Error('No available UTXOs (all are locked or in use)');
|
||||||
const sortedUnspent = [...unspent].sort((a, b) => b.amount - a.amount);
|
}
|
||||||
|
|
||||||
|
// Sélectionner plusieurs UTXOs si nécessaire (coin selection)
|
||||||
|
// Stratégie : préférer les UTXOs qui sont juste assez grands, puis combiner plusieurs petits UTXOs
|
||||||
const amount = 0.00001; // Montant minimal pour la transaction
|
const amount = 0.00001; // Montant minimal pour la transaction
|
||||||
const estimatedFee = 0.00005; // Estimation des frais (conservateur)
|
const estimatedFeePerInput = 0.000001; // Estimation des frais par input (conservateur)
|
||||||
const totalNeeded = amount + estimatedFee;
|
const estimatedFeeBase = 0.00001; // Frais de base pour la transaction
|
||||||
|
const maxChangeRatio = 10; // Maximum 10x le montant requis pour éviter un change trop grand
|
||||||
|
|
||||||
// Trouver un UTXO avec suffisamment de fonds
|
// Sélectionner les UTXOs nécessaires pour couvrir le montant + frais
|
||||||
let utxo = sortedUnspent.find(u => u.amount >= totalNeeded);
|
const selectedUtxos = [];
|
||||||
if (!utxo) {
|
let totalSelected = 0;
|
||||||
// Si aucun UTXO n'est suffisant, utiliser le plus grand disponible
|
|
||||||
utxo = sortedUnspent[0];
|
// Estimer le nombre d'inputs nécessaires (itération pour ajuster les frais)
|
||||||
logger.warn('Using largest available UTXO', {
|
let estimatedInputs = 1;
|
||||||
required: totalNeeded,
|
let totalNeeded = amount + estimatedFeeBase;
|
||||||
available: utxo.amount,
|
|
||||||
allAmounts: sortedUnspent.map(u => u.amount).slice(0, 10),
|
// Itérer jusqu'à trouver une combinaison qui fonctionne
|
||||||
|
for (let iteration = 0; iteration < 10; iteration++) {
|
||||||
|
totalNeeded = amount + estimatedFeeBase + (estimatedInputs * estimatedFeePerInput);
|
||||||
|
selectedUtxos.length = 0;
|
||||||
|
totalSelected = 0;
|
||||||
|
|
||||||
|
// Trier les UTXOs : d'abord ceux qui sont juste assez grands, puis les plus petits
|
||||||
|
const sortedUnspent = [...availableUtxos].sort((a, b) => {
|
||||||
|
// Préférer les UTXOs qui sont juste assez grands (pas trop grands)
|
||||||
|
const aGood = a.amount >= totalNeeded && a.amount <= totalNeeded * maxChangeRatio;
|
||||||
|
const bGood = b.amount >= totalNeeded && b.amount <= totalNeeded * maxChangeRatio;
|
||||||
|
|
||||||
|
if (aGood && !bGood) return -1;
|
||||||
|
if (!aGood && bGood) return 1;
|
||||||
|
|
||||||
|
// Sinon, trier par montant croissant pour minimiser le change
|
||||||
|
return a.amount - b.amount;
|
||||||
});
|
});
|
||||||
if (utxo.amount < totalNeeded) {
|
|
||||||
throw new Error(`Insufficient UTXO amount. Required: ${totalNeeded} BTC, Largest available: ${utxo.amount} BTC. All UTXOs: ${sortedUnspent.map(u => u.amount).join(', ')}`);
|
// Sélectionner les UTXOs jusqu'à avoir suffisamment de fonds
|
||||||
|
for (const utxo of sortedUnspent) {
|
||||||
|
if (totalSelected >= totalNeeded) {
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Éviter les UTXOs trop grands qui créeraient un change énorme
|
||||||
|
// Sauf si c'est le seul UTXO disponible ou si on a déjà plusieurs UTXOs
|
||||||
|
if (selectedUtxos.length === 0 && utxo.amount > totalNeeded * maxChangeRatio) {
|
||||||
|
// Si c'est le premier UTXO et qu'il est trop grand, continuer à chercher
|
||||||
|
// Mais si c'est le seul disponible, l'utiliser quand même
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
selectedUtxos.push(utxo);
|
||||||
|
totalSelected += utxo.amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Si on a assez de fonds, sortir de la boucle
|
||||||
|
if (totalSelected >= totalNeeded) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sinon, réessayer avec plus d'inputs estimés
|
||||||
|
estimatedInputs = selectedUtxos.length + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérifier qu'on a assez de fonds
|
||||||
|
if (totalSelected < totalNeeded) {
|
||||||
|
throw new Error(`Insufficient UTXO amount. Required: ${totalNeeded} BTC, Available: ${totalSelected} BTC. Selected ${selectedUtxos.length} UTXOs from ${sortedUnspent.length} available.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
logger.info('Selected UTXOs for transaction', {
|
||||||
|
hash: hash,
|
||||||
|
date: now,
|
||||||
|
count: selectedUtxos.length,
|
||||||
|
totalAmount: totalSelected,
|
||||||
|
required: totalNeeded,
|
||||||
|
change: totalSelected - totalNeeded,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verrouiller les UTXOs sélectionnés pour éviter qu'ils soient utilisés par d'autres transactions
|
||||||
|
this.lockUtxos(selectedUtxos);
|
||||||
|
|
||||||
// Créer la transaction raw avec les inputs et outputs (sans fundrawtransaction)
|
// Créer la transaction raw avec les inputs et outputs (sans fundrawtransaction)
|
||||||
// Cela évite les erreurs de frais trop élevés avec la bibliothèque bitcoin-core
|
// Cela évite les erreurs de frais trop élevés avec la bibliothèque bitcoin-core
|
||||||
const inputs = [{
|
const inputs = selectedUtxos.map(utxo => ({
|
||||||
txid: utxo.txid,
|
txid: utxo.txid,
|
||||||
vout: utxo.vout,
|
vout: utxo.vout,
|
||||||
}];
|
}));
|
||||||
|
|
||||||
|
// Calculer le change (monnaie restante après avoir payé le montant)
|
||||||
|
// Estimation des frais : base + (nombre d'inputs * frais par input)
|
||||||
|
const estimatedFee = estimatedFeeBase + (selectedUtxos.length * estimatedFeePerInput);
|
||||||
|
let change = totalSelected - amount - estimatedFee;
|
||||||
|
|
||||||
|
// Arrondir le change à 8 décimales (précision Bitcoin standard)
|
||||||
|
change = Math.round(change * 100000000) / 100000000;
|
||||||
|
|
||||||
|
// Créer les outputs
|
||||||
const outputs = {
|
const outputs = {
|
||||||
[address]: amount, // Montant minimal pour la transaction
|
data: anchorData.toString('hex'), // OP_RETURN output (doit être en premier)
|
||||||
data: anchorData.toString('hex'), // OP_RETURN output
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Ajouter l'output de destination avec le montant minimal (arrondi à 8 décimales)
|
||||||
|
outputs[address] = Math.round(amount * 100000000) / 100000000;
|
||||||
|
|
||||||
|
// Si le change est significatif (> 0.00001 BTC pour éviter les problèmes de précision), l'envoyer à une adresse de change
|
||||||
|
// Sinon, il sera considéré comme frais (dust)
|
||||||
|
if (change > 0.00001) {
|
||||||
|
const changeAddress = await this.getNewAddress();
|
||||||
|
outputs[changeAddress] = change;
|
||||||
|
logger.info('Adding change output', { changeAddress, change });
|
||||||
|
} else if (change > 0) {
|
||||||
|
logger.info('Change too small, will be included in fees', { change });
|
||||||
|
}
|
||||||
|
|
||||||
const tx = await this.client.command('createrawtransaction', inputs, outputs);
|
const tx = await this.client.command('createrawtransaction', inputs, outputs);
|
||||||
|
|
||||||
// Signer la transaction
|
// Signer la transaction
|
||||||
@ -212,6 +383,13 @@ class BitcoinRPC {
|
|||||||
// Obtenir les informations de la transaction (dans le mempool)
|
// Obtenir les informations de la transaction (dans le mempool)
|
||||||
const txInfo = await this.getTransactionInfo(txid);
|
const txInfo = await this.getTransactionInfo(txid);
|
||||||
|
|
||||||
|
// Déverrouiller les UTXOs maintenant que la transaction est dans le mempool
|
||||||
|
// Les UTXOs seront automatiquement marqués comme dépensés par Bitcoin Core
|
||||||
|
this.unlockUtxos(selectedUtxos);
|
||||||
|
|
||||||
|
// Libérer le mutex
|
||||||
|
releaseMutex();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
txid,
|
txid,
|
||||||
status: 'confirmed', // Transaction dans le mempool
|
status: 'confirmed', // Transaction dans le mempool
|
||||||
@ -223,6 +401,13 @@ class BitcoinRPC {
|
|||||||
error: error.message,
|
error: error.message,
|
||||||
hash: hash?.substring(0, 16) + '...',
|
hash: hash?.substring(0, 16) + '...',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// En cas d'erreur, déverrouiller les UTXOs et libérer le mutex
|
||||||
|
if (selectedUtxos.length > 0) {
|
||||||
|
this.unlockUtxos(selectedUtxos);
|
||||||
|
}
|
||||||
|
releaseMutex();
|
||||||
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -67,6 +67,7 @@ Le fichier `docker-compose.signet.yml` contient toute la configuration nécessai
|
|||||||
- **Frontend** : Port 3015 (accessible depuis l'extérieur)
|
- **Frontend** : Port 3015 (accessible depuis l'extérieur)
|
||||||
- **Backend API** : Port 8999 (interne)
|
- **Backend API** : Port 8999 (interne)
|
||||||
- **Base de données** : MariaDB sur port 3306 (interne)
|
- **Base de données** : MariaDB sur port 3306 (interne)
|
||||||
|
- **Electrs** : Port 50002 (interne, pour les recherches d'adresses)
|
||||||
- **Réseau** : "signet" configuré
|
- **Réseau** : "signet" configuré
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
@ -79,15 +80,16 @@ Nginx Proxy (192.168.1.100:443)
|
|||||||
Mempool Frontend (localhost:3015)
|
Mempool Frontend (localhost:3015)
|
||||||
↓ HTTP (interne)
|
↓ HTTP (interne)
|
||||||
Mempool Backend (port 8999)
|
Mempool Backend (port 8999)
|
||||||
↓ RPC
|
├─→ RPC → Bitcoin Signet Node (localhost:38332)
|
||||||
Bitcoin Signet Node (localhost:38332)
|
└─→ Electrum Protocol → Electrs (port 50002)
|
||||||
|
└─→ RPC → Bitcoin Signet Node (localhost:38332)
|
||||||
↓ Base de données
|
↓ Base de données
|
||||||
MariaDB (cache et statistiques)
|
MariaDB (cache et statistiques)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Services
|
## Services
|
||||||
|
|
||||||
Mempool utilise trois services Docker :
|
Mempool utilise quatre services Docker :
|
||||||
|
|
||||||
1. **web** (frontend) : Interface utilisateur Angular
|
1. **web** (frontend) : Interface utilisateur Angular
|
||||||
- Image : `mempool/frontend:latest`
|
- Image : `mempool/frontend:latest`
|
||||||
@ -98,6 +100,7 @@ Mempool utilise trois services Docker :
|
|||||||
- Image : `mempool/backend:latest`
|
- Image : `mempool/backend:latest`
|
||||||
- Port : 8999 (interne)
|
- Port : 8999 (interne)
|
||||||
- Connexion RPC : `host.docker.internal:38332`
|
- Connexion RPC : `host.docker.internal:38332`
|
||||||
|
- Connexion Electrum : `electrs:50002`
|
||||||
- Healthcheck : Vérifie que l'API répond
|
- Healthcheck : Vérifie que l'API répond
|
||||||
|
|
||||||
3. **db** (database) : Base de données MariaDB
|
3. **db** (database) : Base de données MariaDB
|
||||||
@ -106,6 +109,13 @@ Mempool utilise trois services Docker :
|
|||||||
- Données : `./mysql/data`
|
- Données : `./mysql/data`
|
||||||
- Healthcheck : Vérifie que MySQL répond
|
- Healthcheck : Vérifie que MySQL répond
|
||||||
|
|
||||||
|
4. **electrs** (electrum server) : Serveur Electrum pour l'indexation
|
||||||
|
- Image : `ghcr.io/romanz/electrs:latest`
|
||||||
|
- Port : 50002 (interne, TCP)
|
||||||
|
- Connexion RPC : `host.docker.internal:38332`
|
||||||
|
- Données : `./electrs/data`
|
||||||
|
- Healthcheck : Vérifie que le port 50002 répond
|
||||||
|
|
||||||
## Accès
|
## Accès
|
||||||
|
|
||||||
### Interface Web
|
### Interface Web
|
||||||
@ -194,6 +204,7 @@ Les données importantes sont stockées dans :
|
|||||||
|
|
||||||
- **Cache** : `./data/` (peut être supprimé, sera régénéré)
|
- **Cache** : `./data/` (peut être supprimé, sera régénéré)
|
||||||
- **Base de données** : `./mysql/data/` (à sauvegarder pour conserver les statistiques)
|
- **Base de données** : `./mysql/data/` (à sauvegarder pour conserver les statistiques)
|
||||||
|
- **Index Electrs** : `./electrs/data/` (peut être supprimé, sera réindexé mais prend du temps)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Sauvegarder la base de données
|
# Sauvegarder la base de données
|
||||||
@ -397,22 +408,17 @@ GET /api/v1/mempool/txids
|
|||||||
|
|
||||||
Retourne les informations sur la mempool.
|
Retourne les informations sur la mempool.
|
||||||
|
|
||||||
## Limitations
|
## Fonctionnalités
|
||||||
|
|
||||||
### Recherche d'Adresses
|
### Recherche d'Adresses
|
||||||
|
|
||||||
La recherche d'adresses ne fonctionne pas sans Electrum Server. Pour activer cette fonctionnalité :
|
La recherche d'adresses est activée grâce au serveur Electrs intégré. Mempool peut maintenant :
|
||||||
|
- Rechercher des transactions par adresse
|
||||||
|
- Afficher l'historique complet des transactions d'une adresse
|
||||||
|
- Calculer les soldes par adresse
|
||||||
|
- Suivre les UTXOs (Unspent Transaction Outputs)
|
||||||
|
|
||||||
1. Installer et configurer un serveur Electrum (electrs ou Fulcrum)
|
Le serveur Electrs indexe la blockchain au démarrage. Pour un signet custom avec peu de blocs, l'indexation est rapide.
|
||||||
2. Modifier `docker-compose.signet.yml` :
|
|
||||||
```yaml
|
|
||||||
api:
|
|
||||||
environment:
|
|
||||||
MEMPOOL_BACKEND: "electrum"
|
|
||||||
ELECTRUM_HOST: "host.docker.internal"
|
|
||||||
ELECTRUM_PORT: "50002"
|
|
||||||
ELECTRUM_TLS_ENABLED: "false"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Pools de Minage
|
### Pools de Minage
|
||||||
|
|
||||||
|
|||||||
212
fixKnowledge/api-anchorage-insufficient-utxo.md
Normal file
212
fixKnowledge/api-anchorage-insufficient-utxo.md
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
# Correction : Erreur "Insufficient UTXO amount" sur l'API d'ancrage
|
||||||
|
|
||||||
|
**Auteur** : Équipe 4NK
|
||||||
|
**Date** : 2026-01-24
|
||||||
|
**Version** : 1.0
|
||||||
|
|
||||||
|
## Problème Identifié
|
||||||
|
|
||||||
|
L'API d'ancrage retournait une erreur "Insufficient UTXO amount. Required: 0.00006 BTC, Largest available: 0.00001 BTC" même lorsque le wallet avait suffisamment de fonds totaux.
|
||||||
|
|
||||||
|
### Symptômes
|
||||||
|
|
||||||
|
- Erreur : "Insufficient UTXO amount. Required: 0.00006 BTC, Largest available: 0.00001 BTC"
|
||||||
|
- Le wallet contient de nombreux UTXOs de 0.00001 BTC chacun
|
||||||
|
- Le solde total du wallet est suffisant, mais aucun UTXO individuel n'est assez grand
|
||||||
|
- L'API ne peut pas créer de transaction d'ancrage
|
||||||
|
|
||||||
|
## Cause Racine
|
||||||
|
|
||||||
|
L'API essayait de trouver un seul UTXO avec suffisamment de fonds pour couvrir le montant requis (0.00006 BTC = montant de sortie + frais estimés). Cependant, tous les UTXOs disponibles étaient de 0.00001 BTC chacun, ce qui est insuffisant individuellement.
|
||||||
|
|
||||||
|
**Problème technique** : L'algorithme de sélection d'UTXO ne combinait pas plusieurs UTXOs pour atteindre le montant requis. Il cherchait uniquement un UTXO unique assez grand.
|
||||||
|
|
||||||
|
## Correctifs Appliqués
|
||||||
|
|
||||||
|
### Modification de la sélection d'UTXOs dans `bitcoin-rpc.js`
|
||||||
|
|
||||||
|
**Fichier** : `api-anchorage/src/bitcoin-rpc.js`
|
||||||
|
|
||||||
|
**Avant** :
|
||||||
|
```javascript
|
||||||
|
// Sélectionner un UTXO avec suffisamment de fonds
|
||||||
|
const sortedUnspent = [...unspent].sort((a, b) => b.amount - a.amount);
|
||||||
|
const amount = 0.00001;
|
||||||
|
const estimatedFee = 0.00005;
|
||||||
|
const totalNeeded = amount + estimatedFee;
|
||||||
|
|
||||||
|
// Trouver un UTXO avec suffisamment de fonds
|
||||||
|
let utxo = sortedUnspent.find(u => u.amount >= totalNeeded);
|
||||||
|
if (!utxo) {
|
||||||
|
utxo = sortedUnspent[0];
|
||||||
|
if (utxo.amount < totalNeeded) {
|
||||||
|
throw new Error(`Insufficient UTXO amount...`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputs = [{
|
||||||
|
txid: utxo.txid,
|
||||||
|
vout: utxo.vout,
|
||||||
|
}];
|
||||||
|
```
|
||||||
|
|
||||||
|
**Après** :
|
||||||
|
```javascript
|
||||||
|
// Sélectionner plusieurs UTXOs si nécessaire (coin selection)
|
||||||
|
const sortedUnspent = [...unspent].sort((a, b) => b.amount - a.amount);
|
||||||
|
const amount = 0.00001;
|
||||||
|
const estimatedFeePerInput = 0.000001;
|
||||||
|
const estimatedFeeBase = 0.00001;
|
||||||
|
|
||||||
|
// Sélectionner les UTXOs nécessaires pour couvrir le montant + frais
|
||||||
|
const selectedUtxos = [];
|
||||||
|
let totalSelected = 0;
|
||||||
|
let estimatedInputs = 1;
|
||||||
|
let totalNeeded = amount + estimatedFeeBase;
|
||||||
|
|
||||||
|
// Itérer jusqu'à trouver une combinaison qui fonctionne
|
||||||
|
for (let iteration = 0; iteration < 10; iteration++) {
|
||||||
|
totalNeeded = amount + estimatedFeeBase + (estimatedInputs * estimatedFeePerInput);
|
||||||
|
selectedUtxos.length = 0;
|
||||||
|
totalSelected = 0;
|
||||||
|
|
||||||
|
for (const utxo of sortedUnspent) {
|
||||||
|
if (totalSelected >= totalNeeded) break;
|
||||||
|
selectedUtxos.push(utxo);
|
||||||
|
totalSelected += utxo.amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalSelected >= totalNeeded) break;
|
||||||
|
estimatedInputs = selectedUtxos.length + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const inputs = selectedUtxos.map(utxo => ({
|
||||||
|
txid: utxo.txid,
|
||||||
|
vout: utxo.vout,
|
||||||
|
}));
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact** : L'API peut maintenant combiner plusieurs UTXOs pour créer une transaction, même si aucun UTXO individuel n'est assez grand.
|
||||||
|
|
||||||
|
### Gestion du change (monnaie restante)
|
||||||
|
|
||||||
|
**Ajout** : Calcul et gestion du change lorsque plusieurs UTXOs sont utilisés :
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Calculer le change (monnaie restante après avoir payé le montant)
|
||||||
|
const estimatedFee = estimatedFeeBase + (selectedUtxos.length * estimatedFeePerInput);
|
||||||
|
const change = totalSelected - amount - estimatedFee;
|
||||||
|
|
||||||
|
const outputs = {
|
||||||
|
data: anchorData.toString('hex'), // OP_RETURN output
|
||||||
|
[address]: amount, // Montant minimal pour la transaction
|
||||||
|
};
|
||||||
|
|
||||||
|
// Si le change est significatif (> 0.000001 BTC), l'envoyer à une adresse de change
|
||||||
|
if (change > 0.000001) {
|
||||||
|
const changeAddress = await this.getNewAddress();
|
||||||
|
outputs[changeAddress] = change;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact** : Le change est correctement géré et renvoyé au wallet au lieu d'être perdu.
|
||||||
|
|
||||||
|
## Modifications
|
||||||
|
|
||||||
|
### Fichiers Modifiés
|
||||||
|
|
||||||
|
- `api-anchorage/src/bitcoin-rpc.js` : Implémentation de la sélection multiple d'UTXOs (coin selection) et gestion du change
|
||||||
|
|
||||||
|
### Fichiers Créés
|
||||||
|
|
||||||
|
- `fixKnowledge/api-anchorage-insufficient-utxo.md` : Cette documentation
|
||||||
|
|
||||||
|
## Modalités de Déploiement
|
||||||
|
|
||||||
|
### Redémarrage de l'API
|
||||||
|
|
||||||
|
1. **Arrêter l'API** :
|
||||||
|
```bash
|
||||||
|
ps aux | grep "node.*api-anchorage" | grep -v grep | awk '{print $2}' | xargs kill
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Redémarrer l'API** :
|
||||||
|
```bash
|
||||||
|
cd /home/ncantu/Bureau/code/bitcoin/api-anchorage
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vérification
|
||||||
|
|
||||||
|
1. **Tester l'ancrage** :
|
||||||
|
```bash
|
||||||
|
curl -X POST http://localhost:3010/api/anchor/document \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-H 'x-api-key: 770b9b33-8a15-4a6d-8f95-1cd2b36e7376' \
|
||||||
|
--data-raw '{"hash":"0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Vérifier les logs** :
|
||||||
|
```bash
|
||||||
|
tail -f /tmp/anchorage-api.log | grep -E "(Selected UTXOs|change)"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Modalités d'Analyse
|
||||||
|
|
||||||
|
### Vérification que la correction fonctionne
|
||||||
|
|
||||||
|
1. **Vérifier que la transaction est créée** :
|
||||||
|
- La réponse doit contenir un `txid` valide
|
||||||
|
- Pas d'erreur "Insufficient UTXO amount"
|
||||||
|
|
||||||
|
2. **Vérifier les logs** :
|
||||||
|
- Les logs doivent afficher "Selected UTXOs for transaction" avec le nombre d'UTXOs sélectionnés
|
||||||
|
- Les logs doivent afficher le montant total sélectionné et le change calculé
|
||||||
|
|
||||||
|
3. **Vérifier la transaction sur la blockchain** :
|
||||||
|
```bash
|
||||||
|
bitcoin-cli getrawtransaction <txid> true
|
||||||
|
```
|
||||||
|
- La transaction doit avoir plusieurs inputs (un par UTXO sélectionné)
|
||||||
|
- La transaction doit avoir un output de change si le change est significatif
|
||||||
|
|
||||||
|
### Cas limites
|
||||||
|
|
||||||
|
1. **Pas assez de fonds totaux** :
|
||||||
|
- L'erreur doit indiquer le montant total disponible
|
||||||
|
- L'erreur doit indiquer le nombre d'UTXOs disponibles
|
||||||
|
|
||||||
|
2. **Beaucoup de petits UTXOs** :
|
||||||
|
- L'API doit combiner plusieurs UTXOs jusqu'à avoir suffisamment de fonds
|
||||||
|
- Les frais doivent être correctement estimés en fonction du nombre d'inputs
|
||||||
|
|
||||||
|
3. **Change très petit** :
|
||||||
|
- Si le change est < 0.000001 BTC, il sera inclus dans les frais (dust)
|
||||||
|
- Si le change est >= 0.000001 BTC, il sera renvoyé au wallet
|
||||||
|
|
||||||
|
## Résultat
|
||||||
|
|
||||||
|
✅ **Problème résolu**
|
||||||
|
|
||||||
|
- L'API peut maintenant combiner plusieurs UTXOs pour créer une transaction
|
||||||
|
- Les transactions d'ancrage fonctionnent même avec de nombreux petits UTXOs
|
||||||
|
- Le change est correctement géré et renvoyé au wallet
|
||||||
|
- Les frais sont correctement estimés en fonction du nombre d'inputs
|
||||||
|
|
||||||
|
**Exemple de transaction réussie** :
|
||||||
|
- Transaction ID : `edacb5000f2f0520072f277e06406b1287f1d38531e7810d33939e5d9cbd598b`
|
||||||
|
- Plusieurs UTXOs de 0.00001 BTC combinés pour atteindre le montant requis
|
||||||
|
|
||||||
|
## Prévention
|
||||||
|
|
||||||
|
Pour éviter ce problème à l'avenir :
|
||||||
|
|
||||||
|
1. **Utiliser un algorithme de coin selection** : Toujours combiner plusieurs UTXOs si nécessaire
|
||||||
|
2. **Estimer correctement les frais** : Prendre en compte le nombre d'inputs dans l'estimation des frais
|
||||||
|
3. **Gérer le change** : Toujours renvoyer le change au wallet au lieu de le perdre
|
||||||
|
4. **Tester avec différents scénarios** : Tester avec des wallets contenant de nombreux petits UTXOs
|
||||||
|
|
||||||
|
## Pages Affectées
|
||||||
|
|
||||||
|
- `api-anchorage/src/bitcoin-rpc.js` : Implémentation de la sélection multiple d'UTXOs et gestion du change
|
||||||
|
- `fixKnowledge/api-anchorage-insufficient-utxo.md` : Documentation (nouveau)
|
||||||
227
fixKnowledge/api-anchorage-utxo-race-condition.md
Normal file
227
fixKnowledge/api-anchorage-utxo-race-condition.md
Normal file
@ -0,0 +1,227 @@
|
|||||||
|
# Correction : Race condition sur les UTXOs dans l'API d'ancrage
|
||||||
|
|
||||||
|
**Auteur** : Équipe 4NK
|
||||||
|
**Date** : 2026-01-24
|
||||||
|
**Version** : 1.0
|
||||||
|
|
||||||
|
## Problème Identifié
|
||||||
|
|
||||||
|
Lorsque plusieurs requêtes d'ancrage arrivent simultanément, elles peuvent toutes voir les mêmes UTXOs comme disponibles et essayer de les utiliser, causant des erreurs car les UTXOs sont déjà dépensés dans le mempool avant d'être confirmés dans les blocs.
|
||||||
|
|
||||||
|
### Symptômes
|
||||||
|
|
||||||
|
- Plusieurs transactions simultanées tentent d'utiliser les mêmes UTXOs
|
||||||
|
- Erreurs "Transaction already in block chain" ou "Missing inputs"
|
||||||
|
- Les UTXOs semblent disponibles alors qu'ils sont déjà dans le mempool
|
||||||
|
- Problème de race condition lors de requêtes concurrentes
|
||||||
|
|
||||||
|
## Cause Racine
|
||||||
|
|
||||||
|
Les validations se font dans le mempool avant la confirmation dans les blocs. Pendant ce temps, les UTXOs utilisés dans le mempool apparaissent toujours comme disponibles dans `listunspent`, car ils ne sont pas encore confirmés dans un bloc.
|
||||||
|
|
||||||
|
**Problème technique** : Aucun mécanisme de verrouillage (mutex) n'était en place pour empêcher plusieurs transactions simultanées d'utiliser les mêmes UTXOs.
|
||||||
|
|
||||||
|
## Correctifs Appliqués
|
||||||
|
|
||||||
|
### 1. Implémentation d'un mutex pour l'accès aux UTXOs
|
||||||
|
|
||||||
|
**Fichier** : `api-anchorage/src/bitcoin-rpc.js`
|
||||||
|
|
||||||
|
**Ajout dans le constructeur** :
|
||||||
|
```javascript
|
||||||
|
// Mutex pour gérer l'accès concurrent aux UTXOs
|
||||||
|
// Utilise une Promise-based queue pour sérialiser les accès
|
||||||
|
this.utxoMutexPromise = Promise.resolve();
|
||||||
|
|
||||||
|
// Liste des UTXOs en cours d'utilisation (format: "txid:vout")
|
||||||
|
this.lockedUtxos = new Set();
|
||||||
|
```
|
||||||
|
|
||||||
|
**Méthode `acquireUtxoMutex()`** :
|
||||||
|
```javascript
|
||||||
|
async acquireUtxoMutex() {
|
||||||
|
// Attendre que le mutex précédent soit libéré
|
||||||
|
const previousMutex = this.utxoMutexPromise;
|
||||||
|
let releaseMutex;
|
||||||
|
|
||||||
|
// Créer une nouvelle Promise qui sera résolue quand le mutex est libéré
|
||||||
|
this.utxoMutexPromise = new Promise((resolve) => {
|
||||||
|
releaseMutex = resolve;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Attendre que le mutex précédent soit libéré
|
||||||
|
await previousMutex;
|
||||||
|
|
||||||
|
// Retourner la fonction pour libérer le mutex
|
||||||
|
return releaseMutex;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact** : Les requêtes sont sérialisées, une à la fois, pour l'accès aux UTXOs.
|
||||||
|
|
||||||
|
### 2. Liste des UTXOs verrouillés
|
||||||
|
|
||||||
|
**Méthodes ajoutées** :
|
||||||
|
- `isUtxoLocked(txid, vout)` : Vérifie si un UTXO est verrouillé
|
||||||
|
- `lockUtxo(txid, vout)` : Verrouille un UTXO
|
||||||
|
- `lockUtxos(utxos)` : Verrouille plusieurs UTXOs
|
||||||
|
- `unlockUtxo(txid, vout)` : Déverrouille un UTXO
|
||||||
|
- `unlockUtxos(utxos)` : Déverrouille plusieurs UTXOs
|
||||||
|
|
||||||
|
**Impact** : Les UTXOs en cours d'utilisation sont marqués comme verrouillés et ne peuvent pas être utilisés par d'autres transactions.
|
||||||
|
|
||||||
|
### 3. Filtrage des UTXOs verrouillés
|
||||||
|
|
||||||
|
**Modification dans `createAnchorTransaction()`** :
|
||||||
|
```javascript
|
||||||
|
// Filtrer les UTXOs verrouillés (en cours d'utilisation par d'autres transactions)
|
||||||
|
const availableUtxos = unspent.filter(utxo => !this.isUtxoLocked(utxo.txid, utxo.vout));
|
||||||
|
|
||||||
|
logger.info('Available UTXOs (after filtering locked)', {
|
||||||
|
total: unspent.length,
|
||||||
|
available: availableUtxos.length,
|
||||||
|
locked: unspent.length - availableUtxos.length,
|
||||||
|
// ...
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact** : Seuls les UTXOs non verrouillés sont considérés pour la sélection.
|
||||||
|
|
||||||
|
### 4. Verrouillage et déverrouillage des UTXOs
|
||||||
|
|
||||||
|
**Dans `createAnchorTransaction()`** :
|
||||||
|
```javascript
|
||||||
|
// Acquérir le mutex pour l'accès aux UTXOs
|
||||||
|
const releaseMutex = await this.acquireUtxoMutex();
|
||||||
|
let selectedUtxos = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
// ... sélection des UTXOs ...
|
||||||
|
|
||||||
|
// Verrouiller les UTXOs sélectionnés
|
||||||
|
this.lockUtxos(selectedUtxos);
|
||||||
|
|
||||||
|
// ... création et envoi de la transaction ...
|
||||||
|
|
||||||
|
// Déverrouiller les UTXOs après l'envoi au mempool
|
||||||
|
this.unlockUtxos(selectedUtxos);
|
||||||
|
releaseMutex();
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// En cas d'erreur, déverrouiller les UTXOs et libérer le mutex
|
||||||
|
if (selectedUtxos.length > 0) {
|
||||||
|
this.unlockUtxos(selectedUtxos);
|
||||||
|
}
|
||||||
|
releaseMutex();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact** : Les UTXOs sont verrouillés pendant la création de la transaction et déverrouillés après l'envoi au mempool ou en cas d'erreur.
|
||||||
|
|
||||||
|
## Modifications
|
||||||
|
|
||||||
|
### Fichiers Modifiés
|
||||||
|
|
||||||
|
- `api-anchorage/src/bitcoin-rpc.js` :
|
||||||
|
- Ajout du mutex et de la liste des UTXOs verrouillés
|
||||||
|
- Filtrage des UTXOs verrouillés lors de la sélection
|
||||||
|
- Verrouillage/déverrouillage des UTXOs sélectionnés
|
||||||
|
|
||||||
|
### Fichiers Créés
|
||||||
|
|
||||||
|
- `fixKnowledge/api-anchorage-utxo-race-condition.md` : Cette documentation
|
||||||
|
|
||||||
|
## Modalités de Déploiement
|
||||||
|
|
||||||
|
### Redémarrage de l'API
|
||||||
|
|
||||||
|
1. **Arrêter l'API** :
|
||||||
|
```bash
|
||||||
|
ps aux | grep "node.*api-anchorage" | grep -v grep | awk '{print $2}' | xargs kill
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Redémarrer l'API** :
|
||||||
|
```bash
|
||||||
|
cd /home/ncantu/Bureau/code/bitcoin/api-anchorage
|
||||||
|
npm start
|
||||||
|
```
|
||||||
|
|
||||||
|
### Vérification
|
||||||
|
|
||||||
|
1. **Tester avec des requêtes simultanées** :
|
||||||
|
```bash
|
||||||
|
# Lancer plusieurs requêtes en parallèle
|
||||||
|
for i in {1..5}; do
|
||||||
|
curl -X POST http://localhost:3010/api/anchor/document \
|
||||||
|
-H 'Content-Type: application/json' \
|
||||||
|
-H 'x-api-key: 770b9b33-8a15-4a6d-8f95-1cd2b36e7376' \
|
||||||
|
--data-raw "{\"hash\":\"$(openssl rand -hex 32)\"}" &
|
||||||
|
done
|
||||||
|
wait
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Vérifier les logs** :
|
||||||
|
```bash
|
||||||
|
tail -f /tmp/anchorage-api.log | grep -E "(UTXO|locked|mutex|Selected)"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Modalités d'Analyse
|
||||||
|
|
||||||
|
### Vérification que la correction fonctionne
|
||||||
|
|
||||||
|
1. **Vérifier que les requêtes sont sérialisées** :
|
||||||
|
- Les logs doivent montrer que les requêtes sont traitées une à la fois
|
||||||
|
- Les UTXOs verrouillés ne doivent pas être sélectionnés
|
||||||
|
|
||||||
|
2. **Vérifier qu'il n'y a pas d'erreurs de double dépense** :
|
||||||
|
- Aucune erreur "Transaction already in block chain"
|
||||||
|
- Aucune erreur "Missing inputs"
|
||||||
|
- Toutes les transactions doivent être créées avec succès
|
||||||
|
|
||||||
|
3. **Vérifier les logs de verrouillage** :
|
||||||
|
- Les logs doivent afficher "Available UTXOs (after filtering locked)"
|
||||||
|
- Les logs doivent montrer le nombre d'UTXOs verrouillés
|
||||||
|
|
||||||
|
### Cas limites
|
||||||
|
|
||||||
|
1. **Tous les UTXOs sont verrouillés** :
|
||||||
|
- L'erreur doit indiquer "No available UTXOs (all are locked or in use)"
|
||||||
|
- Les requêtes doivent attendre que des UTXOs soient déverrouillés
|
||||||
|
|
||||||
|
2. **Requêtes simultanées** :
|
||||||
|
- Les requêtes doivent être traitées séquentiellement
|
||||||
|
- Chaque requête doit utiliser des UTXOs différents
|
||||||
|
|
||||||
|
3. **Erreur lors de la création de la transaction** :
|
||||||
|
- Les UTXOs doivent être déverrouillés même en cas d'erreur
|
||||||
|
- Le mutex doit être libéré même en cas d'erreur
|
||||||
|
|
||||||
|
## Résultat
|
||||||
|
|
||||||
|
✅ **Problème résolu**
|
||||||
|
|
||||||
|
- Les requêtes concurrentes sont sérialisées via un mutex
|
||||||
|
- Les UTXOs en cours d'utilisation sont verrouillés
|
||||||
|
- Les UTXOs verrouillés sont filtrés lors de la sélection
|
||||||
|
- Les UTXOs sont déverrouillés après l'envoi au mempool ou en cas d'erreur
|
||||||
|
- Plus de race condition sur les UTXOs
|
||||||
|
|
||||||
|
**Exemple de transaction réussie avec mutex** :
|
||||||
|
- Transaction ID : `ddcf585d703966ca206972b4ee662aa00ee820de9dc1a8a33789ac02cf578dfd`
|
||||||
|
- Les UTXOs sont correctement verrouillés et déverrouillés
|
||||||
|
|
||||||
|
## Prévention
|
||||||
|
|
||||||
|
Pour éviter ce problème à l'avenir :
|
||||||
|
|
||||||
|
1. **Toujours utiliser un mutex** pour les opérations critiques qui partagent des ressources
|
||||||
|
2. **Verrouiller les ressources** avant de les utiliser
|
||||||
|
3. **Déverrouiller les ressources** après utilisation ou en cas d'erreur
|
||||||
|
4. **Filtrer les ressources verrouillées** lors de la sélection
|
||||||
|
5. **Tester avec des requêtes simultanées** pour détecter les race conditions
|
||||||
|
|
||||||
|
## Pages Affectées
|
||||||
|
|
||||||
|
- `api-anchorage/src/bitcoin-rpc.js` : Implémentation du mutex et de la gestion des UTXOs verrouillés
|
||||||
|
- `fixKnowledge/api-anchorage-utxo-race-condition.md` : Documentation (nouveau)
|
||||||
@ -100,14 +100,45 @@ async function loadWalletBalance() {
|
|||||||
* Charge le nombre d'ancrages
|
* Charge le nombre d'ancrages
|
||||||
*/
|
*/
|
||||||
async function loadAnchorCount() {
|
async function loadAnchorCount() {
|
||||||
|
const anchorCountValue = document.getElementById('anchor-count-value');
|
||||||
|
const anchorCountSpinner = document.getElementById('anchor-count-spinner');
|
||||||
|
|
||||||
|
if (!anchorCountValue || !anchorCountSpinner) {
|
||||||
|
console.error('Elements anchor-count-value or anchor-count-spinner not found in DOM');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Afficher le spinner
|
||||||
|
anchorCountSpinner.style.display = 'inline';
|
||||||
|
anchorCountValue.textContent = '...';
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${API_BASE_URL}/api/anchor/count`);
|
const response = await fetch(`${API_BASE_URL}/api/anchor/count`);
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
document.getElementById('anchor-count').textContent = data.count || 0;
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
const count = data.count !== undefined ? data.count : 0;
|
||||||
|
|
||||||
|
// Masquer le spinner et mettre à jour la valeur
|
||||||
|
anchorCountSpinner.style.display = 'none';
|
||||||
|
if (count >= 0) {
|
||||||
|
anchorCountValue.textContent = count.toLocaleString();
|
||||||
|
} else {
|
||||||
|
anchorCountValue.textContent = '0';
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading anchor count:', error);
|
console.error('Error loading anchor count:', error);
|
||||||
document.getElementById('anchor-count').textContent = 'Erreur';
|
// Masquer le spinner
|
||||||
|
anchorCountSpinner.style.display = 'none';
|
||||||
|
// Ne pas réinitialiser à "Erreur" si on a déjà une valeur affichée
|
||||||
|
// Garder la dernière valeur valide ou afficher "0" si c'est la première erreur
|
||||||
|
const currentValue = anchorCountValue.textContent;
|
||||||
|
if (currentValue === '-' || currentValue === 'Erreur' || currentValue === '...') {
|
||||||
|
anchorCountValue.textContent = '0';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -130,9 +161,10 @@ async function loadNetworkPeers() {
|
|||||||
* Formate un montant en BTC
|
* Formate un montant en BTC
|
||||||
*/
|
*/
|
||||||
function formatBTC(btc) {
|
function formatBTC(btc) {
|
||||||
if (btc === 0) return '0 BTC';
|
if (btc === 0) return '0 🛡';
|
||||||
if (btc < 0.000001) return `${(btc * 100000000).toFixed(0)} sats`;
|
if (btc < 0.000001) return `${(btc * 100000000).toFixed(0)} sats`;
|
||||||
return `${btc.toFixed(8)} BTC`;
|
// Arrondir sans décimales pour les balances Mature et Immature
|
||||||
|
return `${Math.round(btc)} 🛡`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -265,6 +297,46 @@ async function generateHashFromFile() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifie le hash
|
||||||
|
*/
|
||||||
|
async function verifyHash() {
|
||||||
|
const hash = document.getElementById('anchor-hash').value.trim();
|
||||||
|
|
||||||
|
if (!hash || !/^[0-9a-fA-F]{64}$/.test(hash)) {
|
||||||
|
showResult('anchor-result', 'error', 'Veuillez entrer un hash valide (64 caractères hexadécimaux).');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
showResult('anchor-result', 'info', 'Vérification du hash en cours...');
|
||||||
|
|
||||||
|
const response = await fetch(`${API_BASE_URL}/api/anchor/verify`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
body: JSON.stringify({ hash }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (response.ok && data.anchor_info) {
|
||||||
|
const info = data.anchor_info;
|
||||||
|
showResult('anchor-result', 'success',
|
||||||
|
`Hash vérifié avec succès !<br>
|
||||||
|
<strong>TXID :</strong> ${info.transaction_id || 'N/A'}<br>
|
||||||
|
<strong>Hauteur du bloc :</strong> ${info.block_height !== null && info.block_height !== undefined ? info.block_height : 'Non confirmé'}<br>
|
||||||
|
<strong>Confirmations :</strong> ${info.confirmations || 0}<br>
|
||||||
|
<strong>Statut :</strong> ${info.confirmations > 0 ? 'Confirmé' : 'En attente'}`);
|
||||||
|
} else {
|
||||||
|
showResult('anchor-result', 'error', data.message || data.error || 'Hash non trouvé sur la blockchain.');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showResult('anchor-result', 'error', `Erreur : ${error.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ancre le document
|
* Ancre le document
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -40,7 +40,10 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3>Nombre d'Ancrages</h3>
|
<h3>Nombre d'Ancrages</h3>
|
||||||
<p class="value" id="anchor-count">-</p>
|
<p class="value" id="anchor-count">
|
||||||
|
<span id="anchor-count-value">-</span>
|
||||||
|
<span id="anchor-count-spinner" class="spinner" style="display: none;">⏳</span>
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3>Nombre de Pairs</h3>
|
<h3>Nombre de Pairs</h3>
|
||||||
@ -73,9 +76,12 @@
|
|||||||
|
|
||||||
<div class="hash-section">
|
<div class="hash-section">
|
||||||
<label for="anchor-hash">Hash SHA256 :</label>
|
<label for="anchor-hash">Hash SHA256 :</label>
|
||||||
<input type="text" id="anchor-hash" placeholder="Le hash sera généré automatiquement..." readonly>
|
<input type="text" id="anchor-hash" placeholder="Le hash sera généré automatiquement...">
|
||||||
|
<div class="hash-buttons">
|
||||||
|
<button onclick="verifyHash()">Vérifier le Hash</button>
|
||||||
<button onclick="anchorDocument()">Ancrer le Document</button>
|
<button onclick="anchorDocument()">Ancrer le Document</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="anchor-result" class="result"></div>
|
<div id="anchor-result" class="result"></div>
|
||||||
</div>
|
</div>
|
||||||
@ -100,6 +106,6 @@
|
|||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="app.js"></script>
|
<script src="app.js?v=20260124"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -187,6 +187,26 @@ button:disabled {
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hash-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hash-buttons button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner {
|
||||||
|
margin-left: 10px;
|
||||||
|
animation: spin 1s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
from { transform: rotate(0deg); }
|
||||||
|
to { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
|
||||||
.hash-section {
|
.hash-section {
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
padding-top: 20px;
|
padding-top: 20px;
|
||||||
|
|||||||
@ -6,6 +6,9 @@
|
|||||||
|
|
||||||
import Client from 'bitcoin-core';
|
import Client from 'bitcoin-core';
|
||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
|
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
||||||
|
import { join, dirname } from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
|
||||||
class BitcoinRPC {
|
class BitcoinRPC {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -71,8 +74,8 @@ class BitcoinRPC {
|
|||||||
// getbalances() retourne { "mine": { "trusted": ..., "untrusted_pending": ..., "immature": ... } }
|
// getbalances() retourne { "mine": { "trusted": ..., "untrusted_pending": ..., "immature": ... } }
|
||||||
const mine = balances.mine || {};
|
const mine = balances.mine || {};
|
||||||
return {
|
return {
|
||||||
mature: mine.trusted || 0,
|
mature: Math.round((mine.trusted || 0)),
|
||||||
immature: mine.immature || 0,
|
immature: Math.round((mine.immature || 0)),
|
||||||
unconfirmed: mine.untrusted_pending || 0,
|
unconfirmed: mine.untrusted_pending || 0,
|
||||||
total: (mine.trusted || 0) + (mine.immature || 0) + (mine.untrusted_pending || 0),
|
total: (mine.trusted || 0) + (mine.immature || 0) + (mine.untrusted_pending || 0),
|
||||||
};
|
};
|
||||||
@ -104,8 +107,8 @@ class BitcoinRPC {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
mature: balance,
|
mature: Math.round(balance),
|
||||||
immature: immatureBalance,
|
immature: Math.round(immatureBalance),
|
||||||
unconfirmed: unconfirmedBalance,
|
unconfirmed: unconfirmedBalance,
|
||||||
total: totalBalance + unconfirmedBalance,
|
total: totalBalance + unconfirmedBalance,
|
||||||
};
|
};
|
||||||
@ -141,19 +144,76 @@ class BitcoinRPC {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtient le nombre d'ancrages (approximatif en comptant les transactions OP_RETURN)
|
* Obtient le nombre d'ancrages en comptant tous les blocs depuis le début
|
||||||
|
* Utilise un fichier de cache anchor_count.txt pour éviter de tout recompter
|
||||||
|
* Format du cache: <date>;<hauteur du dernier bloc>;<hash du dernier bloc>;<count total des ancrages>
|
||||||
* @returns {Promise<number>} Nombre d'ancrages
|
* @returns {Promise<number>} Nombre d'ancrages
|
||||||
*/
|
*/
|
||||||
async getAnchorCount() {
|
async getAnchorCount() {
|
||||||
try {
|
try {
|
||||||
const blockchainInfo = await this.client.getBlockchainInfo();
|
const blockchainInfo = await this.client.getBlockchainInfo();
|
||||||
const currentHeight = blockchainInfo.blocks;
|
const currentHeight = blockchainInfo.blocks;
|
||||||
|
const currentBlockHash = blockchainInfo.bestblockhash;
|
||||||
|
|
||||||
// Rechercher dans les 1000 derniers blocs pour compter les ancrages
|
// Chemin du fichier de cache (à la racine du projet bitcoin)
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
// signet-dashboard/src/bitcoin-rpc.js -> signet-dashboard/src -> signet-dashboard -> bitcoin -> anchor_count.txt
|
||||||
|
// On remonte de 2 niveaux depuis signet-dashboard/src pour arriver à bitcoin/
|
||||||
|
const cachePath = join(__dirname, '../../anchor_count.txt');
|
||||||
|
|
||||||
|
let startHeight = 0;
|
||||||
let anchorCount = 0;
|
let anchorCount = 0;
|
||||||
const searchRange = Math.min(1000, currentHeight + 1);
|
let lastProcessedHash = null;
|
||||||
|
|
||||||
for (let height = currentHeight; height >= Math.max(0, currentHeight - searchRange); height--) {
|
// Lire le cache si il existe
|
||||||
|
if (existsSync(cachePath)) {
|
||||||
|
try {
|
||||||
|
const cacheContent = readFileSync(cachePath, 'utf8').trim();
|
||||||
|
const parts = cacheContent.split(';');
|
||||||
|
if (parts.length === 4) {
|
||||||
|
const cachedDate = parts[0];
|
||||||
|
const cachedHeight = parseInt(parts[1], 10);
|
||||||
|
const cachedHash = parts[2];
|
||||||
|
const cachedCount = parseInt(parts[3], 10);
|
||||||
|
|
||||||
|
// Vérifier que le hash du bloc en cache correspond toujours
|
||||||
|
if (cachedHeight >= 0 && cachedHeight <= currentHeight) {
|
||||||
|
try {
|
||||||
|
const cachedBlockHash = await this.client.getBlockHash(cachedHeight);
|
||||||
|
if (cachedBlockHash === cachedHash) {
|
||||||
|
startHeight = cachedHeight + 1;
|
||||||
|
anchorCount = cachedCount;
|
||||||
|
lastProcessedHash = cachedHash;
|
||||||
|
logger.info('Anchor count cache loaded', {
|
||||||
|
cachedDate,
|
||||||
|
cachedHeight,
|
||||||
|
cachedCount,
|
||||||
|
startHeight,
|
||||||
|
currentHeight
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
logger.warn('Anchor count cache invalid: block hash mismatch', {
|
||||||
|
cachedHeight,
|
||||||
|
cachedHash,
|
||||||
|
actualHash: cachedBlockHash
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Error verifying cached block hash', { error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Error reading anchor count cache', { error: error.message });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compter les ancrages depuis startHeight jusqu'à currentHeight
|
||||||
|
if (startHeight <= currentHeight) {
|
||||||
|
logger.info('Counting anchors from block', { startHeight, currentHeight });
|
||||||
|
|
||||||
|
for (let height = startHeight; height <= currentHeight; height++) {
|
||||||
try {
|
try {
|
||||||
const blockHash = await this.client.getBlockHash(height);
|
const blockHash = await this.client.getBlockHash(height);
|
||||||
const block = await this.client.getBlock(blockHash, 2);
|
const block = await this.client.getBlock(blockHash, 2);
|
||||||
@ -181,12 +241,27 @@ class BitcoinRPC {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mettre à jour le cache tous les 100 blocs pour éviter de perdre trop de travail
|
||||||
|
if (height % 100 === 0 || height === currentHeight) {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const cacheContent = `${now};${height};${blockHash};${anchorCount}`;
|
||||||
|
writeFileSync(cachePath, cacheContent, 'utf8');
|
||||||
|
logger.debug('Anchor count cache updated', { height, anchorCount });
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Continuer avec le bloc suivant
|
// Continuer avec le bloc suivant
|
||||||
logger.debug('Error checking block for anchors', { height, error: error.message });
|
logger.debug('Error checking block for anchors', { height, error: error.message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mettre à jour le cache final
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
const cacheContent = `${now};${currentHeight};${currentBlockHash};${anchorCount}`;
|
||||||
|
writeFileSync(cachePath, cacheContent, 'utf8');
|
||||||
|
logger.info('Anchor count cache saved', { currentHeight, anchorCount });
|
||||||
|
}
|
||||||
|
|
||||||
return anchorCount;
|
return anchorCount;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error getting anchor count', { error: error.message });
|
logger.error('Error getting anchor count', { error: error.message });
|
||||||
|
|||||||
@ -17,6 +17,9 @@ import dotenv from 'dotenv';
|
|||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { dirname, join } from 'path';
|
import { dirname, join } from 'path';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
|
import http from 'http';
|
||||||
|
import https from 'https';
|
||||||
|
import { URL } from 'url';
|
||||||
import { bitcoinRPC } from './bitcoin-rpc.js';
|
import { bitcoinRPC } from './bitcoin-rpc.js';
|
||||||
import { logger } from './logger.js';
|
import { logger } from './logger.js';
|
||||||
|
|
||||||
@ -24,10 +27,99 @@ import { logger } from './logger.js';
|
|||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = dirname(__filename);
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
// Charger les variables d'environnement depuis le répertoire racine du projet signet-dashboard
|
// Helper function pour faire des requêtes HTTP
|
||||||
|
function makeHttpRequest(baseUrl, path, options = {}) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const url = new URL(path, baseUrl);
|
||||||
|
const client = url.protocol === 'https:' ? https : http;
|
||||||
|
|
||||||
|
// Utiliser 127.0.0.1 au lieu de localhost pour éviter les problèmes de résolution DNS
|
||||||
|
const hostname = url.hostname === 'localhost' ? '127.0.0.1' : url.hostname;
|
||||||
|
|
||||||
|
// S'assurer que les headers sont bien passés (copie pour éviter les modifications)
|
||||||
|
const requestHeaders = { ...options.headers };
|
||||||
|
|
||||||
|
const requestOptions = {
|
||||||
|
hostname: hostname,
|
||||||
|
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
||||||
|
path: url.pathname,
|
||||||
|
method: options.method || 'GET',
|
||||||
|
headers: requestHeaders,
|
||||||
|
timeout: 30000,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Log des headers pour debug
|
||||||
|
if (requestHeaders['x-api-key']) {
|
||||||
|
logger.debug('HTTP request headers include x-api-key', {
|
||||||
|
headerLength: requestHeaders['x-api-key'].length,
|
||||||
|
headerValue: requestHeaders['x-api-key'].substring(0, 10) + '...'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const req = client.request(requestOptions, (res) => {
|
||||||
|
let data = '';
|
||||||
|
res.on('data', (chunk) => { data += chunk; });
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
const jsonData = JSON.parse(data);
|
||||||
|
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||||
|
resolve(jsonData);
|
||||||
|
} else {
|
||||||
|
resolve({ error: jsonData.error || 'Request failed', message: jsonData.message || `HTTP ${res.statusCode}` });
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
resolve({ error: 'Invalid JSON response', message: data.substring(0, 100) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('error', (error) => {
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('timeout', () => {
|
||||||
|
req.destroy();
|
||||||
|
reject(new Error('Request timeout'));
|
||||||
|
});
|
||||||
|
|
||||||
|
if (options.body) {
|
||||||
|
req.write(options.body);
|
||||||
|
}
|
||||||
|
req.end();
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Charger les variables d'environnement depuis le répertoire signet-dashboard
|
||||||
// Cela garantit que le .env est chargé même si le script est exécuté depuis un autre répertoire
|
// Cela garantit que le .env est chargé même si le script est exécuté depuis un autre répertoire
|
||||||
const envPath = join(__dirname, '../.env');
|
const envPath = join(__dirname, '../.env');
|
||||||
dotenv.config({ path: envPath });
|
const envResult = dotenv.config({ path: envPath });
|
||||||
|
if (envResult.error) {
|
||||||
|
console.warn('[ENV] Failed to load .env file', { path: envPath, error: envResult.error.message });
|
||||||
|
} else {
|
||||||
|
const parsedKeys = Object.keys(envResult.parsed || {});
|
||||||
|
const hasAnchorApiKey = !!(envResult.parsed?.ANCHOR_API_KEY);
|
||||||
|
console.log('[ENV] Loaded .env file', {
|
||||||
|
path: envPath,
|
||||||
|
keys: parsedKeys.length,
|
||||||
|
hasAnchorApiKey,
|
||||||
|
anchorApiKeyLength: envResult.parsed?.ANCHOR_API_KEY?.length || 0
|
||||||
|
});
|
||||||
|
// Forcer la mise à jour de process.env avec toutes les valeurs du .env
|
||||||
|
// Toujours utiliser les valeurs du .env, même si elles existent déjà dans process.env
|
||||||
|
if (envResult.parsed) {
|
||||||
|
for (const [key, value] of Object.entries(envResult.parsed)) {
|
||||||
|
process.env[key] = value;
|
||||||
|
}
|
||||||
|
console.log('[ENV] Forced update of process.env', {
|
||||||
|
ANCHOR_API_KEY: process.env.ANCHOR_API_KEY ? 'SET (' + process.env.ANCHOR_API_KEY.substring(0, 10) + '...)' : 'NOT SET',
|
||||||
|
ANCHOR_API_URL: process.env.ANCHOR_API_URL || 'NOT SET'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = process.env.DASHBOARD_PORT || 3020;
|
const PORT = process.env.DASHBOARD_PORT || 3020;
|
||||||
@ -125,6 +217,59 @@ app.post('/api/hash/generate', (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Route pour vérifier un hash (appelle l'API d'ancrage externe)
|
||||||
|
app.post('/api/anchor/verify', async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { hash, txid } = req.body;
|
||||||
|
|
||||||
|
if (!hash) {
|
||||||
|
return res.status(400).json({ error: 'hash is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!/^[0-9a-fA-F]{64}$/.test(hash)) {
|
||||||
|
return res.status(400).json({ error: 'hash must be a 64 character hexadecimal string' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utiliser l'URL HTTPS du sous-domaine si disponible, sinon localhost
|
||||||
|
// Par défaut, utiliser localhost:3010 pour l'environnement de développement
|
||||||
|
// Toujours forcer localhost si l'URL est HTTPS (pour éviter les problèmes de proxy/SSL)
|
||||||
|
let anchorApiUrl = process.env.ANCHOR_API_URL || 'http://localhost:3010';
|
||||||
|
if (anchorApiUrl.startsWith('https://')) {
|
||||||
|
anchorApiUrl = 'http://127.0.0.1:3010';
|
||||||
|
logger.info('Forcing localhost for anchor API', { originalUrl: process.env.ANCHOR_API_URL });
|
||||||
|
}
|
||||||
|
const anchorApiKey = process.env.ANCHOR_API_KEY || '';
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
};
|
||||||
|
if (anchorApiKey) {
|
||||||
|
headers['x-api-key'] = anchorApiKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = { hash };
|
||||||
|
if (txid) {
|
||||||
|
body.txid = txid;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await makeHttpRequest(anchorApiUrl, '/api/anchor/verify', {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Vérifier si le résultat contient une erreur
|
||||||
|
if (result.error) {
|
||||||
|
return res.status(400).json(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.json(result);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error verifying anchor', { error: error.message });
|
||||||
|
res.status(500).json({ error: error.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Route pour tester l'ancrage (appelle l'API d'ancrage externe)
|
// Route pour tester l'ancrage (appelle l'API d'ancrage externe)
|
||||||
app.post('/api/anchor/test', async (req, res) => {
|
app.post('/api/anchor/test', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@ -139,28 +284,61 @@ app.post('/api/anchor/test', async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Utiliser l'URL HTTPS du sous-domaine si disponible, sinon localhost
|
// Utiliser l'URL HTTPS du sous-domaine si disponible, sinon localhost
|
||||||
const anchorApiUrl = process.env.ANCHOR_API_URL ||
|
// Par défaut, utiliser localhost:3010 pour l'environnement de développement
|
||||||
(process.env.NODE_ENV === 'production'
|
// Si l'URL est HTTPS mais qu'on est en développement, forcer localhost
|
||||||
? 'https://anchorage.certificator.4nkweb.com'
|
let anchorApiUrl = process.env.ANCHOR_API_URL || 'http://localhost:3010';
|
||||||
: 'http://localhost:3010');
|
if (anchorApiUrl.startsWith('https://') && process.env.NODE_ENV !== 'production') {
|
||||||
|
anchorApiUrl = 'http://127.0.0.1:3010';
|
||||||
|
}
|
||||||
|
// Récupérer la clé API directement depuis process.env (forcé au démarrage)
|
||||||
const anchorApiKey = process.env.ANCHOR_API_KEY || '';
|
const anchorApiKey = process.env.ANCHOR_API_KEY || '';
|
||||||
|
|
||||||
const response = await fetch(`${anchorApiUrl}/api/anchor/document`, {
|
logger.info('Calling anchor API', {
|
||||||
method: 'POST',
|
url: anchorApiUrl,
|
||||||
headers: {
|
hasApiKey: !!anchorApiKey,
|
||||||
|
apiKeyLength: anchorApiKey ? anchorApiKey.length : 0,
|
||||||
|
nodeEnv: process.env.NODE_ENV,
|
||||||
|
anchorApiKeyEnv: process.env.ANCHOR_API_KEY ? 'SET (' + process.env.ANCHOR_API_KEY.substring(0, 10) + '...)' : 'NOT SET',
|
||||||
|
anchorApiKeyValue: anchorApiKey ? anchorApiKey.substring(0, 10) + '...' : 'EMPTY'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Utiliser http/https natifs pour une meilleure compatibilité
|
||||||
|
try {
|
||||||
|
const headers = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'x-api-key': anchorApiKey,
|
};
|
||||||
},
|
if (anchorApiKey && anchorApiKey.trim().length > 0) {
|
||||||
|
headers['x-api-key'] = anchorApiKey.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info('Making HTTP request to anchor API', {
|
||||||
|
url: anchorApiUrl,
|
||||||
|
path: '/api/anchor/document',
|
||||||
|
hasApiKeyHeader: !!headers['x-api-key'],
|
||||||
|
apiKeyHeaderLength: headers['x-api-key'] ? headers['x-api-key'].length : 0,
|
||||||
|
apiKeyHeaderValue: headers['x-api-key'] ? headers['x-api-key'].substring(0, 10) + '...' : 'MISSING'
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await makeHttpRequest(anchorApiUrl, '/api/anchor/document', {
|
||||||
|
method: 'POST',
|
||||||
|
headers,
|
||||||
body: JSON.stringify({ hash }),
|
body: JSON.stringify({ hash }),
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await response.json();
|
// Vérifier si le résultat contient une erreur
|
||||||
|
if (result.error) {
|
||||||
if (!response.ok) {
|
return res.status(400).json(result);
|
||||||
return res.status(response.status).json(result);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json(result);
|
res.json(result);
|
||||||
|
} catch (fetchError) {
|
||||||
|
logger.error('Error calling anchor API', {
|
||||||
|
error: fetchError.message,
|
||||||
|
url: anchorApiUrl,
|
||||||
|
stack: fetchError.stack
|
||||||
|
});
|
||||||
|
res.status(500).json({ error: 'Failed to connect to anchor API', message: fetchError.message });
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error testing anchor', { error: error.message });
|
logger.error('Error testing anchor', { error: error.message });
|
||||||
res.status(500).json({ error: error.message });
|
res.status(500).json({ error: error.message });
|
||||||
|
|||||||
@ -6,7 +6,10 @@ export BITCOIN_RPC_USER=${BITCOIN_RPC_USER:-bitcoin}
|
|||||||
export BITCOIN_RPC_PASSWORD=${BITCOIN_RPC_PASSWORD:-bitcoin}
|
export BITCOIN_RPC_PASSWORD=${BITCOIN_RPC_PASSWORD:-bitcoin}
|
||||||
export DASHBOARD_PORT=${DASHBOARD_PORT:-3020}
|
export DASHBOARD_PORT=${DASHBOARD_PORT:-3020}
|
||||||
export DASHBOARD_HOST=${DASHBOARD_HOST:-0.0.0.0}
|
export DASHBOARD_HOST=${DASHBOARD_HOST:-0.0.0.0}
|
||||||
export ANCHOR_API_URL=${ANCHOR_API_URL:-http://localhost:3010}
|
# Utiliser localhost:3010 si l'URL n'est pas explicitement définie ou si c'est une URL HTTPS non accessible
|
||||||
|
if [ -z "$ANCHOR_API_URL" ] || [[ "$ANCHOR_API_URL" == https://* ]]; then
|
||||||
|
export ANCHOR_API_URL="http://localhost:3010"
|
||||||
|
fi
|
||||||
export ANCHOR_API_KEY=${ANCHOR_API_KEY:-}
|
export ANCHOR_API_KEY=${ANCHOR_API_KEY:-}
|
||||||
export FAUCET_API_URL=${FAUCET_API_URL:-http://localhost:3021}
|
export FAUCET_API_URL=${FAUCET_API_URL:-http://localhost:3021}
|
||||||
export LOG_LEVEL=${LOG_LEVEL:-info}
|
export LOG_LEVEL=${LOG_LEVEL:-info}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user