Add skipIfExists parameter to anchor API endpoint

**Motivations:**
- Avoid unnecessary re-anchoring of already anchored hashes
- Improve performance by using database as source of truth
- Return consistent information without additional RPC calls

**Evolutions:**
- Added optional skipIfExists parameter to /api/anchor/document endpoint
- Check database for existing anchors before creating new transaction
- Automatically store anchors in database after transaction creation
- Return old: true/false flag and ok: true in all responses
- Enrich anchors table with all necessary information for retrieval without RPC calls

**Pages affectées:**
- api-anchorage/src/routes/anchor.js: Added skipIfExists parameter and database check
- api-anchorage/src/bitcoin-rpc.js: Store anchor in database after transaction creation
- api-anchorage/README.md: Updated documentation with new parameter and response format
- features/api-anchorage-skip-if-exists.md: Evolution documentation
This commit is contained in:
ncantu 2026-01-28 08:24:23 +01:00
parent 4833fdbb53
commit cb13ab6fbf
4 changed files with 236 additions and 4 deletions

View File

@ -122,22 +122,56 @@ Ancre un document sur Bitcoin Signet. La transaction est créée et envoyée au
```json ```json
{ {
"documentUid": "doc-123456", "documentUid": "doc-123456",
"hash": "a1b2c3d4e5f6..." "hash": "a1b2c3d4e5f6...",
"skipIfExists": false
} }
``` ```
**Paramètres** :
- `documentUid` : string (optionnel, identifiant du document)
- `hash` : string (requis, hash SHA256 du document en hex, 64 caractères)
- `skipIfExists` : boolean (optionnel, par défaut `false`, si `true`, ne réancrera pas un hash déjà ancré et retournera les informations existantes)
**Note** : La transaction est envoyée au mempool et retournée immédiatement. Aucun callback n'est supporté. **Note** : La transaction est envoyée au mempool et retournée immédiatement. Aucun callback n'est supporté.
**Réponse** : **Réponse** (nouvel ancrage) :
```json ```json
{ {
"ok": true,
"txid": "56504e002d95301ebcfb4b30eaedc5d3fd9a448e121ffdce4f356b8d34169e85", "txid": "56504e002d95301ebcfb4b30eaedc5d3fd9a448e121ffdce4f356b8d34169e85",
"status": "confirmed", "status": "confirmed",
"confirmations": 0, "confirmations": 0,
"block_height": 152321 "block_height": 152321,
"outputs": [...],
"fee": 0.00001,
"fee_sats": 1000,
"old": false
} }
``` ```
**Réponse** (hash déjà ancré avec `skipIfExists: true`) :
```json
{
"ok": true,
"txid": "56504e002d95301ebcfb4b30eaedc5d3fd9a448e121ffdce4f356b8d34169e85",
"status": "confirmed",
"confirmations": 5,
"block_height": 152316,
"old": true
}
```
**Champs de réponse** :
- `ok` : boolean, toujours `true` en cas de succès
- `txid` : string, ID de la transaction Bitcoin
- `status` : string, statut de la transaction (`"confirmed"` ou `"pending"`)
- `confirmations` : number, nombre de confirmations
- `block_height` : number|null, hauteur du bloc (null si pas encore dans un bloc)
- `outputs` : array, liste des outputs de la transaction (uniquement pour nouveaux ancrages)
- `fee` : number|null, frais de transaction en BTC (uniquement pour nouveaux ancrages)
- `fee_sats` : number|null, frais de transaction en sats (uniquement pour nouveaux ancrages)
- `old` : boolean, `true` si le hash était déjà ancré, `false` si c'est un nouvel ancrage
**Erreurs** : **Erreurs** :
- `400 Bad Request` : Hash invalide - `400 Bad Request` : Hash invalide
- `401 Unauthorized` : Clé API invalide ou manquante - `401 Unauthorized` : Clé API invalide ou manquante
@ -196,6 +230,16 @@ curl -X POST http://localhost:3010/api/anchor/document \
"hash": "a1b2c3d4e5f6789012345678901234567890123456789012345678901234567890" "hash": "a1b2c3d4e5f6789012345678901234567890123456789012345678901234567890"
}' }'
# Ancrer un document sans réancrer si déjà ancré
curl -X POST http://localhost:3010/api/anchor/document \
-H "Content-Type: application/json" \
-H "x-api-key: your-api-key-here" \
-d '{
"documentUid": "doc-123",
"hash": "a1b2c3d4e5f6789012345678901234567890123456789012345678901234567890",
"skipIfExists": true
}'
# Vérifier un ancrage # Vérifier un ancrage
curl -X POST http://localhost:3010/api/anchor/verify \ curl -X POST http://localhost:3010/api/anchor/verify \
-H "Content-Type: application/json" \ -H "Content-Type: application/json" \

View File

@ -741,6 +741,33 @@ class BitcoinRPC {
this.unlockUtxo(utxo.txid, utxo.vout); this.unlockUtxo(utxo.txid, utxo.vout);
} }
// Stocker l'ancre dans la base de données
try {
const dbForAnchor = getDatabase();
const date = new Date().toISOString();
dbForAnchor.prepare(`
INSERT OR REPLACE INTO anchors (hash, txid, block_height, confirmations, date, updated_at)
VALUES (?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
`).run(
hash,
txid,
txInfo.blockheight || null,
txInfo.confirmations || 0,
date
);
logger.debug('Anchor stored in database', {
hash: hash.substring(0, 16) + '...',
txid: txid.substring(0, 16) + '...',
});
} catch (error) {
logger.warn('Error storing anchor in database', {
error: error.message,
hash: hash.substring(0, 16) + '...',
txid: txid.substring(0, 16) + '...',
});
// Ne pas faire échouer la transaction si le stockage en base échoue
}
// Libérer le mutex // Libérer le mutex
releaseMutex(); releaseMutex();

View File

@ -50,10 +50,11 @@ anchorRouter.get('/locked-utxos', async (req, res) => {
* Body: * Body:
* - documentUid: string (identifiant du document) * - documentUid: string (identifiant du document)
* - hash: string (hash SHA256 du document en hex, 64 caractères) * - hash: string (hash SHA256 du document en hex, 64 caractères)
* - skipIfExists: boolean (optionnel, si true, ne réancrera pas un hash déjà ancré)
*/ */
anchorRouter.post('/document', async (req, res) => { anchorRouter.post('/document', async (req, res) => {
try { try {
const { documentUid, hash } = req.body; const { documentUid, hash, skipIfExists } = req.body;
// Validation // Validation
if (!hash) { if (!hash) {
@ -73,13 +74,43 @@ anchorRouter.post('/document', async (req, res) => {
logger.info('Anchor request received', { logger.info('Anchor request received', {
documentUid: documentUid || 'unknown', documentUid: documentUid || 'unknown',
hash: hash.substring(0, 16) + '...', hash: hash.substring(0, 16) + '...',
skipIfExists: skipIfExists || false,
}); });
// Si skipIfExists est activé, vérifier en base si le hash existe déjà
if (skipIfExists) {
const { getDatabase } = await import('../database.js');
const db = getDatabase();
const existingAnchor = db.prepare(`
SELECT txid, block_height, confirmations, date
FROM anchors
WHERE hash = ?
`).get(hash);
if (existingAnchor) {
logger.info('Hash already anchored, returning existing anchor', {
hash: hash.substring(0, 16) + '...',
txid: existingAnchor.txid.substring(0, 16) + '...',
});
// Retourner le résultat avec old: true
return res.status(200).json({
ok: true,
txid: existingAnchor.txid,
status: existingAnchor.confirmations > 0 ? 'confirmed' : 'pending',
confirmations: existingAnchor.confirmations || 0,
block_height: existingAnchor.block_height || null,
old: true,
});
}
}
// Créer la transaction d'ancrage et l'envoyer au mempool // Créer la transaction d'ancrage et l'envoyer au mempool
const result = await bitcoinRPC.createAnchorTransaction(hash); const result = await bitcoinRPC.createAnchorTransaction(hash);
// Retourner le résultat immédiatement (transaction dans le mempool) // Retourner le résultat immédiatement (transaction dans le mempool)
res.status(200).json({ res.status(200).json({
ok: true,
txid: result.txid, txid: result.txid,
status: result.status, status: result.status,
confirmations: result.confirmations, confirmations: result.confirmations,
@ -87,6 +118,7 @@ anchorRouter.post('/document', async (req, res) => {
outputs: result.outputs || [], outputs: result.outputs || [],
fee: result.fee || null, fee: result.fee || null,
fee_sats: result.fee_sats || null, fee_sats: result.fee_sats || null,
old: false,
}); });
} catch (error) { } catch (error) {
logger.error('Anchor error', { error: error.message, stack: error.stack }); logger.error('Anchor error', { error: error.message, stack: error.stack });

View File

@ -0,0 +1,129 @@
# API d'ancrage - Paramètre skipIfExists
**Date:** 2026-01-28
**Auteur:** Équipe 4NK
## Objectif
Ajouter un paramètre optionnel `skipIfExists` à l'endpoint `/api/anchor/document` pour éviter de réancrer des hash déjà ancrés. L'API doit renvoyer le résultat avec un tag `old: true/false` et les informations de la transaction depuis la base de données sans appel RPC supplémentaire.
## Motivations
- **Éviter les réancrages inutiles** : Si un hash est déjà ancré, ne pas créer une nouvelle transaction
- **Performance** : Utiliser la base de données comme source de vérité pour éviter les appels RPC coûteux
- **Cohérence** : Retourner toujours les mêmes informations (txid, confirmations, block_height) qu'un ancrage normal
## Impacts
### Fonctionnels
- L'endpoint `/api/anchor/document` accepte maintenant un paramètre optionnel `skipIfExists`
- Si `skipIfExists: true` et que le hash existe déjà en base, retourne immédiatement les informations sans créer de transaction
- Toutes les réponses incluent maintenant `ok: true` et `old: true/false`
- Les ancres sont automatiquement stockées dans la table `anchors` après création
### Techniques
- Enrichissement de la table `anchors` : Les ancres sont maintenant stockées automatiquement lors de la création
- Pas d'appel RPC supplémentaire : Les informations sont récupérées directement depuis la base de données
- Compatibilité : Le paramètre est optionnel, le comportement par défaut reste inchangé
## Modifications
### Fichiers modifiés
1. **`api-anchorage/src/routes/anchor.js`**
- Ajout du paramètre `skipIfExists` dans le body de la requête
- Vérification en base de données si le hash existe déjà
- Retour des informations existantes avec `old: true` si le hash est trouvé
- Ajout de `ok: true` et `old: false` dans la réponse pour les nouveaux ancrages
2. **`api-anchorage/src/bitcoin-rpc.js`**
- Stockage automatique de l'ancre dans la table `anchors` après création de la transaction
- Utilisation de `INSERT OR REPLACE` pour gérer les cas de mise à jour
3. **`api-anchorage/README.md`**
- Documentation du nouveau paramètre `skipIfExists`
- Documentation des nouveaux champs de réponse (`ok`, `old`)
- Exemples d'utilisation avec `skipIfExists: true`
### Structure de la réponse
**Nouvel ancrage** :
```json
{
"ok": true,
"txid": "...",
"status": "confirmed",
"confirmations": 0,
"block_height": 152321,
"outputs": [...],
"fee": 0.00001,
"fee_sats": 1000,
"old": false
}
```
**Hash déjà ancré (avec skipIfExists: true)** :
```json
{
"ok": true,
"txid": "...",
"status": "confirmed",
"confirmations": 5,
"block_height": 152316,
"old": true
}
```
## Modalités de déploiement
1. **Vérifier la base de données** : S'assurer que la table `anchors` existe (créée par `init-db.mjs`)
2. **Déployer le code** : Les modifications sont rétrocompatibles, aucun changement de schéma requis
3. **Tester** : Vérifier que les ancres existantes sont bien retournées avec `old: true`
4. **Monitoring** : Surveiller les logs pour vérifier que les ancres sont bien stockées en base
## Modalités d'analyse
### Vérification du fonctionnement
1. **Test avec hash nouveau** :
```bash
curl -X POST http://localhost:3010/api/anchor/document \
-H "Content-Type: application/json" \
-H "x-api-key: your-api-key" \
-d '{"hash": "nouveau_hash_64_caracteres_hex", "skipIfExists": true}'
```
- Doit créer une nouvelle transaction
- Doit retourner `old: false`
- Doit stocker l'ancre en base
2. **Test avec hash existant** :
```bash
curl -X POST http://localhost:3010/api/anchor/document \
-H "Content-Type: application/json" \
-H "x-api-key: your-api-key" \
-d '{"hash": "hash_deja_ancré_64_caracteres_hex", "skipIfExists": true}'
```
- Ne doit pas créer de nouvelle transaction
- Doit retourner `old: true`
- Doit retourner les informations depuis la base de données
3. **Vérification en base** :
```sql
SELECT * FROM anchors WHERE hash = 'hash_test';
```
- Doit contenir l'enregistrement avec txid, block_height, confirmations
### Logs à surveiller
- `Hash already anchored, returning existing anchor` : Hash trouvé en base avec skipIfExists
- `Anchor stored in database` : Ancre stockée après création
- `Error storing anchor in database` : Erreur lors du stockage (ne fait pas échouer la transaction)
## Notes
- Le paramètre `skipIfExists` est optionnel et par défaut à `false` pour maintenir la compatibilité
- Si `skipIfExists: false` ou non fourni, le comportement reste identique à avant (création systématique)
- Les ancres sont stockées avec `INSERT OR REPLACE` pour gérer les cas de mise à jour (confirmations, block_height)
- Le stockage en base ne fait pas échouer la transaction si une erreur survient (log warning uniquement)