ncantu cad73cb265 UTXO-list: dates/blockTime historiques, récupération frais depuis ancrages, diagnostic Bloc Rewards
**Motivations:**
- Ajouter dates manquantes dans hash_list.txt et compléter historique
- Compléter blockTime manquants dans utxo_list.txt et compléter historique
- Récupérer frais depuis transactions d'ancrage (OP_RETURN) et les stocker
- Bouton UI pour déclencher récupération frais
- Diagnostic Bloc Rewards (pourquoi ~4700 BTC au lieu de 50 BTC)

**Root causes:**
- hash_list.txt sans date (format ancien)
- utxo_list.txt blockTime souvent vide
- Frais absents du fichier (métadonnées OP_RETURN non stockées)
- Pas de moyen de récupérer/compléter frais depuis UI

**Correctifs:**
- hash_list.txt : format étendu avec date (rétrocompatible)
- utxo_list.txt : blockTime complété automatiquement lors écritures
- fees_list.txt : nouveau fichier pour stocker frais
- updateFeesFromAnchors() : récupère frais depuis OP_RETURN ancrages
- Endpoint /api/utxo/fees/update pour déclencher récupération
- Bouton "Récupérer les frais depuis les ancrages" dans section Frais (spinner)
- Scripts batch : complete-hash-list-dates.js, complete-utxo-list-blocktime.js
- Script diagnostic : diagnose-bloc-rewards.js (subsidy, coinbase, listunspent)

**Evolutions:**
- Frais chargés depuis fees_list.txt dans getUtxoList
- Complétion automatique dates/blockTime lors écritures futures

**Pages affectées:**
- signet-dashboard/src/bitcoin-rpc.js
- signet-dashboard/src/server.js
- signet-dashboard/public/utxo-list.html
- scripts/complete-hash-list-dates.js
- scripts/complete-utxo-list-blocktime.js
- scripts/diagnose-bloc-rewards.js
- features/utxo-list-fees-update-and-historical-completion.md
2026-01-26 01:59:46 +01:00

180 lines
4.0 KiB
TypeScript

import { getPublicKey, getSharedSecret } from '@noble/secp256k1';
import { bytesToHex, hexToBytes } from '@noble/hashes/utils';
/**
* Encrypt data with AES-GCM using a shared secret derived from ECDH.
*/
export async function encryptWithECDH(
data: string,
recipientPublicKey: string,
senderPrivateKey: string,
): Promise<{
encrypted: string;
iv: string;
publicKey: string;
}> {
const recipientPubKey = hexToBytes(recipientPublicKey);
const senderPrivKey = hexToBytes(senderPrivateKey);
const sharedSecret = getSharedSecret(senderPrivKey, recipientPubKey, true);
const keyMaterial = await crypto.subtle.importKey(
'raw',
sharedSecret.slice(0, 32) as BufferSource,
{ name: 'HKDF' },
false,
['deriveBits', 'deriveKey'],
);
const derivedKey = await crypto.subtle.deriveKey(
{
name: 'HKDF',
hash: 'SHA-256',
salt: new Uint8Array(32),
info: new Uint8Array(0),
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['encrypt'],
);
const iv = crypto.getRandomValues(new Uint8Array(12));
const encoder = new TextEncoder();
const dataBytes = encoder.encode(data);
const encrypted = await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv,
},
derivedKey,
dataBytes,
);
return {
encrypted: bytesToHex(new Uint8Array(encrypted)),
iv: bytesToHex(iv),
publicKey: bytesToHex(getPublicKey(senderPrivKey, true)),
};
}
/**
* Decrypt data with AES-GCM using a shared secret derived from ECDH.
*/
export async function decryptWithECDH(
encrypted: string,
iv: string,
senderPublicKey: string,
recipientPrivateKey: string,
): Promise<string> {
const senderPubKey = hexToBytes(senderPublicKey);
const recipientPrivKey = hexToBytes(recipientPrivateKey);
const sharedSecret = getSharedSecret(recipientPrivKey, senderPubKey, true);
const keyMaterial = await crypto.subtle.importKey(
'raw',
sharedSecret.slice(0, 32) as BufferSource,
{ name: 'HKDF' },
false,
['deriveBits', 'deriveKey'],
);
const derivedKey = await crypto.subtle.deriveKey(
{
name: 'HKDF',
hash: 'SHA-256',
salt: new Uint8Array(32),
info: new Uint8Array(0),
},
keyMaterial,
{ name: 'AES-GCM', length: 256 },
false,
['decrypt'],
);
const encryptedBytes = hexToBytes(encrypted);
const ivBytes = hexToBytes(iv);
const decrypted = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: ivBytes as BufferSource,
},
derivedKey,
encryptedBytes as BufferSource,
);
const decoder = new TextDecoder();
return decoder.decode(decrypted);
}
/**
* Encrypt data for "publish to all" scenario.
* Uses a symmetric key that can be shared via ECDH with multiple recipients.
*/
export async function encryptForAll(
data: string,
encryptionKey: Uint8Array,
): Promise<{
encrypted: string;
iv: string;
}> {
const key = await crypto.subtle.importKey(
'raw',
encryptionKey as BufferSource,
{ name: 'AES-GCM' },
false,
['encrypt'],
);
const iv = crypto.getRandomValues(new Uint8Array(12));
const encoder = new TextEncoder();
const dataBytes = encoder.encode(data);
const encrypted = await crypto.subtle.encrypt(
{
name: 'AES-GCM',
iv: iv as BufferSource,
},
key,
dataBytes,
);
return {
encrypted: bytesToHex(new Uint8Array(encrypted)),
iv: bytesToHex(iv),
};
}
/**
* Decrypt data encrypted with encryptForAll.
*/
export async function decryptForAll(
encrypted: string,
iv: string,
encryptionKey: Uint8Array,
): Promise<string> {
const key = await crypto.subtle.importKey(
'raw',
encryptionKey as BufferSource,
{ name: 'AES-GCM' },
false,
['decrypt'],
);
const encryptedBytes = hexToBytes(encrypted);
const ivBytes = hexToBytes(iv);
const decrypted = await crypto.subtle.decrypt(
{
name: 'AES-GCM',
iv: ivBytes as BufferSource,
},
key,
encryptedBytes as BufferSource,
);
const decoder = new TextDecoder();
return decoder.decode(decrypted);
}