diff --git a/data/sync-utxos.log b/data/sync-utxos.log index f8a81cb..7341e27 100644 --- a/data/sync-utxos.log +++ b/data/sync-utxos.log @@ -35,3 +35,42 @@ - Non dépensés: 67955 ✅ Synchronisation terminée +🔍 Démarrage de la synchronisation des UTXOs dépensés... + +📊 UTXOs à vérifier: 66109 +📡 Récupération des UTXOs depuis Bitcoin... +📊 UTXOs disponibles dans Bitcoin: 189710 +💾 Création de la table temporaire... +💾 Insertion des UTXOs disponibles par batch... + ⏳ Traitement: 10000/189710 UTXOs insérés... + ⏳ Traitement: 20000/189710 UTXOs insérés... + ⏳ Traitement: 30000/189710 UTXOs insérés... + ⏳ Traitement: 40000/189710 UTXOs insérés... + ⏳ Traitement: 50000/189710 UTXOs insérés... + ⏳ Traitement: 60000/189710 UTXOs insérés... + ⏳ Traitement: 70000/189710 UTXOs insérés... + ⏳ Traitement: 80000/189710 UTXOs insérés... + ⏳ Traitement: 90000/189710 UTXOs insérés... + ⏳ Traitement: 100000/189710 UTXOs insérés... + ⏳ Traitement: 110000/189710 UTXOs insérés... + ⏳ Traitement: 120000/189710 UTXOs insérés... + ⏳ Traitement: 130000/189710 UTXOs insérés... + ⏳ Traitement: 140000/189710 UTXOs insérés... + ⏳ Traitement: 150000/189710 UTXOs insérés... + ⏳ Traitement: 160000/189710 UTXOs insérés... + ⏳ Traitement: 170000/189710 UTXOs insérés... + ⏳ Traitement: 180000/189710 UTXOs insérés... + ⏳ Traitement: 189710/189710 UTXOs insérés... +💾 Mise à jour des UTXOs dépensés... + +📊 Résumé: + - UTXOs vérifiés: 66109 + - UTXOs toujours disponibles: 66109 + - UTXOs dépensés détectés: 0 + +📈 Statistiques finales: + - Total UTXOs: 68398 + - Dépensés: 2294 + - Non dépensés: 66104 + +✅ Synchronisation terminée diff --git a/features/userwallet-contrat-login-reste-a-faire.md b/features/userwallet-contrat-login-reste-a-faire.md index a8367fc..0e2a82e 100644 --- a/features/userwallet-contrat-login-reste-a-faire.md +++ b/features/userwallet-contrat-login-reste-a-faire.md @@ -23,7 +23,7 @@ Référence : `userwallet/docs/specs.md` (modèle, objets, machine à états, ca - **Pairing** : 8 mots BIP32-style, WordInputGrid, confirmation croisée « membre finaliser », IndexedDB, statut « Connecté ». - **Relais** : config, health, GET/POST messages/signatures/keys, **GET /bloom** ; sync, HashCache (IndexedDB), dédup, fetch clés/signatures. - **Graphe** : GraphResolver, caches (services, contrats, champs, actions, membres, pairs), `resolveLoginPath`. -- **Login** : LoginBuilder (challenge, nonce, chiffrement « for all »), construction preuve, publication message → signatures → clés. **Anti-rejeu** : NonceStore (IndexedDB), vérification timestamp (fenêtre), `X_NONCE_REUSED` / `X_TIMESTAMP_OUT_OF_WINDOW` avant publish. +- **Login** : LoginBuilder (challenge, nonce, chiffrement « for all »), construction preuve, publication message → signatures → clés. **Notifications relais** : `runCollectLoop` `onProgress`, `collectProgress`, affichage « Signatures collectées : X / Y » dans LoginCollectShare. **Anti-rejeu** : NonceStore (IndexedDB), vérification timestamp (fenêtre), `X_NONCE_REUSED` / `X_TIMESTAMP_OUT_OF_WINDOW` avant publish. - **Iframe** : auth-request / auth-response / login-proof, useChannel, postMessage. - **Écrans** : Home, CreateIdentity, ImportIdentity, RelaySettings, Sync, Manage Pairs, Pairing (setup + display), Services, Login, Data Export/Import, Unlock. Navigation via GlobalActionBar. @@ -41,7 +41,7 @@ Référence : `userwallet/docs/specs.md` (modèle, objets, machine à états, ca **À valider avant implémentation.** Voir `features/userwallet-ecrans-login-a-valider.md`. - **Service iframe** : fourni par channel message (contrat + contrats fils). Si absent → contrat par défaut en dur dans le front jusqu’à réception. -- **Écrans login** : déjà en place. Reste à faire : **notifications** selon événements relais (collecte signatures, clés de déchiffrement → hash à fetch → signatures, contrats, membres, pairs, actions, champs sur le relai). +- **Écrans login** : déjà en place. **Notifications relais** : progression collecte signatures (X/Y) implémentée via `onProgress` dans `runCollectLoop` et affichage dans `LoginCollectShare` ; voir `features/userwallet-notifications-relais.md`. Reste à faire : réagir à d’autres événements relais si extension (ex. push) — hash à fetch pour signatures, contrats, membres, pairs, actions, champs. - Cette section **évoluera avec l'avancement des tests** une fois validée. - **Sélection service / sélection membre** : écrans dédiés « choisir le service cible du login » et « choisir le membre (quand plusieurs) » avec liste, statuts, validation des prérequis (action login, pairs). @@ -85,7 +85,7 @@ Référence : `userwallet/docs/specs.md` (modèle, objets, machine à états, ca | Domaine | Statut | Priorité | |--------|--------|----------| | Machine à états login | Fait (loginStateMachine, useLoginStateMachine, dispatch) | — | -| Écrans sélection service / membre, chemin login, collecte sig, publication, vérification | À valider avant implémentation (voir userwallet-ecrans-login-a-valider) ; notifications relais à implémenter | Haute | +| Écrans sélection service / membre, chemin login, collecte sig, publication, vérification | À valider avant implémentation (voir userwallet-ecrans-login-a-valider) ; notifications relais (progression X/Y) fait (userwallet-notifications-relais) | Haute | | États « indéchiffrable » / « signature manquante » / statut relais visibles | Fait (Sync) | — | | Anti-rejeu (nonce, fenêtre, cache) | Fait (NonceStore, verifyTimestamp, X_*) | — | | Validation stricte validateurs + clé autorisée | Fait (strict verify, version contrats) | — | diff --git a/mine.sh b/mine.sh index 9b3fb3c..93efcfb 100644 --- a/mine.sh +++ b/mine.sh @@ -1,8 +1,9 @@ #!/bin/bash NBITS=${NBITS:-"1e0377ae"} #minimum difficulty in signet +WALLET=${WALLET:-"custom_signet"} # Wallet name for descriptor wallet while true; do - ADDR=${MINETO:-$(bitcoin-cli getnewaddress)} + ADDR=${MINETO:-$(bitcoin-cli -rpcwallet=$WALLET getnewaddress)} if [[ -f "${BITCOIN_DIR}/BLOCKPRODUCTIONDELAY.txt" ]]; then BLOCKPRODUCTIONDELAY_OVERRIDE=$(cat ~/.bitcoin/BLOCKPRODUCTIONDELAY.txt) echo "Delay OVERRIDE before next block" $BLOCKPRODUCTIONDELAY_OVERRIDE "seconds." @@ -19,5 +20,8 @@ while true; do # The miner will automatically use PRIVKEY from environment for descriptorprocesspsbt # Export PRIVKEY to ensure it's available to the miner process export PRIVKEY=${PRIVKEY:-$(cat ~/.bitcoin/PRIVKEY.txt 2>/dev/null || echo "")} - miner --cli="bitcoin-cli" generate --grind-cmd="bitcoin-util grind" --address=$ADDR --nbits=$NBITS --set-block-time=$(date +%s) + # Get block template and pipe it to miner + # Use bitcoin-cli with -datadir but without -rpcwallet for miner (descriptorprocesspsbt is node RPC, not wallet RPC) + bitcoin-cli -rpcwallet=$WALLET getblocktemplate '{"rules": ["segwit", "signet"]}' | \ + miner --cli="bitcoin-cli -datadir=/root/.bitcoin" generate --grind-cmd="bitcoin-util grind" --address=$ADDR --nbits=$NBITS --set-block-time=$(date +%s) done \ No newline at end of file diff --git a/miner b/miner index bf4bcd9..d4bc797 100644 --- a/miner +++ b/miner @@ -504,17 +504,73 @@ def do_generate(args): if not privkey and hasattr(args, 'privkey') and args.privkey: privkey = args.privkey if privkey: + # Try descriptorprocesspsbt first using JSON-RPC HTTP to avoid "Argument list too long" + # descriptorprocesspsbt is needed for signet artificial transactions + cli_base = args.cli.split(" ") + cli_node = [c for c in cli_base if not c.startswith("-rpcwallet")] + if not cli_node: + cli_node = ["bitcoin-cli"] descriptor = "pk(%s)" % privkey - # Use descriptorprocesspsbt: PSBT, descriptors array (JSON), sighashtype, bip32derivs, finalize - # bitcoin_cli adds -signet automatically and converts args to strings descriptors_array = [descriptor] - psbt_signed = json.loads(args.bcli("descriptorprocesspsbt", psbt, json.dumps(descriptors_array), "ALL", "true", "true")) - logging.debug("Used descriptorprocesspsbt for signing") + try: + # Use JSON-RPC HTTP directly to avoid command line length limit + import urllib.request + import urllib.parse + # Extract RPC credentials from bitcoin-cli config or use defaults + rpc_url = "http://127.0.0.1:38332/" # Default signet RPC port + rpc_user = "bitcoin" + rpc_pass = "bitcoin" + # Try to get RPC credentials from environment or config + if "RPCUSER" in os.environ: + rpc_user = os.environ["RPCUSER"] + if "RPCPASSWORD" in os.environ: + rpc_pass = os.environ["RPCPASSWORD"] + # Create JSON-RPC request + rpc_request = { + "method": "descriptorprocesspsbt", + "params": [psbt, descriptors_array, "ALL", True, True], + "id": 1, + "jsonrpc": "2.0" + } + # Encode request + data = json.dumps(rpc_request).encode('utf-8') + # Create HTTP request with basic auth + req = urllib.request.Request(rpc_url, data=data, headers={"Content-Type": "application/json"}) + # Add basic auth + import base64 + credentials = base64.b64encode(("%s:%s" % (rpc_user, rpc_pass)).encode()).decode() + req.add_header("Authorization", "Basic %s" % credentials) + # Send request + with urllib.request.urlopen(req, timeout=30) as response: + rpc_response = json.loads(response.read().decode('utf-8')) + if "result" in rpc_response: + psbt_signed = rpc_response["result"] + elif "error" in rpc_response: + raise Exception("RPC error: %s" % rpc_response["error"]) + else: + psbt_signed = rpc_response + logging.debug("Used descriptorprocesspsbt via HTTP for signing with descriptor: %s" % descriptor) + except Exception as e: + # If descriptorprocesspsbt fails, try walletprocesspsbt as fallback + logging.debug("descriptorprocesspsbt via HTTP failed, trying walletprocesspsbt: %s" % str(e)) + input_stream = os.linesep.join([psbt, "true", "ALL"]).encode('utf8') + psbt_signed = json.loads(args.bcli("-stdin", "walletprocesspsbt", input=input_stream)) + logging.debug("walletprocesspsbt completed, complete=%s" % psbt_signed.get("complete", False)) else: # Fallback to walletprocesspsbt if no PRIVKEY available logging.debug("No PRIVKEY found, falling back to walletprocesspsbt") input_stream = os.linesep.join([psbt, "true", "ALL"]).encode('utf8') psbt_signed = json.loads(args.bcli("-stdin", "walletprocesspsbt", input=input_stream)) + except Exception as e: + # If descriptorprocesspsbt fails, try walletprocesspsbt as fallback + logging.debug("descriptorprocesspsbt failed, trying walletprocesspsbt: %s" % str(e)) + try: + input_stream = os.linesep.join([psbt, "true", "ALL"]).encode('utf8') + psbt_signed = json.loads(args.bcli("-stdin", "walletprocesspsbt", input=input_stream)) + logging.debug("walletprocesspsbt succeeded") + except Exception as e2: + logging.debug("walletprocesspsbt also failed: %s" % str(e2)) + raise e except Exception as e: # If descriptorprocesspsbt fails, try walletprocesspsbt as fallback logging.debug("descriptorprocesspsbt failed, trying walletprocesspsbt: %s" % str(e)) diff --git a/userwallet/features/userwallet-notifications-relais.md b/userwallet/features/userwallet-notifications-relais.md new file mode 100644 index 0000000..bb57419 --- /dev/null +++ b/userwallet/features/userwallet-notifications-relais.md @@ -0,0 +1,38 @@ +# UserWallet – Notifications relais (pull-based) + +**Author:** Équipe 4NK +**Date:** 2026-01-26 + +## Objectif + +Exposer les « notifications » liées aux événements relais (collecte signatures, clés) pour que l’UI sache **quel hash** aller chercher et afficher la **progression** (ex. X/Y signatures). Modèle **pull-only** : pas de push/WebSocket, uniquement GET sur les relais. + +## Implémenté + +### 1. Collecte signatures (login mFA) + +- **Hash connu** : le challenge login a un `hash` ; on fetch `GET /signatures/:hash` sur chaque relais. +- **Boucle de collecte** : `runCollectLoop` appelle `fetchSignaturesForHash` puis merge, jusqu’à `hasEnoughSignatures` ou timeout. +- **Progression** : `CollectLoopOpts.onProgress?: (merged) => void` appelé à chaque poll. L’UI peut afficher « Signatures collectées : X / Y » via `collectProgress(path, merged, pairToMembers)` → `{ satisfied, required }`. +- **LoginCollectShare** : reçoit `collectProgress` et affiche « Signatures collectées : X / Y » pendant la collecte. + +### 2. Clés et messages (sync) + +- **Scan keys** : `GET /keys?start=&end=` → liste de `MsgCle` avec `hash_message`. On en déduit les hashes à fetch. +- **Fetch par hash** : `GET /messages/:hash` pour chaque hash, puis ECDH decrypt. Pas de « notification » explicite : le sync fait scan → fetch → decrypt en une passe. Les stats (indéchiffrable, validé, non validé) sont remontées au `SyncScreen`. + +### 3. Pairing (membre finaliser) + +- Hash et fetch signatures par hash déjà utilisés dans `fetchSignaturesForHash` (pairingConfirm). Pas de progression affichée côté pairing. + +## À faire (évolutif) + +- **Événements relais** : si un jour le relais propose du push (ex. WebSocket), adapter l’UI pour réagir aux événements « nouveau message / nouvelles signatures / nouvelles clés » et déclencher fetch par hash en conséquence. +- **Optimisation** : utiliser éventuellement Bloom ou autre pour réduire les GET quand beaucoup de hashes. + +## Références + +- `features/userwallet-collecte-distante-2-devices.md` +- `features/userwallet-dh-systematique-scan-fetch.md` +- `userwallet/src/utils/collectSignatures.ts` (`runCollectLoop`, `CollectLoopOpts.onProgress`) +- `userwallet/src/utils/loginValidation.ts` (`collectProgress`) diff --git a/userwallet/src/components/LoginCollectShare.tsx b/userwallet/src/components/LoginCollectShare.tsx index 813aee0..3eda912 100644 --- a/userwallet/src/components/LoginCollectShare.tsx +++ b/userwallet/src/components/LoginCollectShare.tsx @@ -9,14 +9,38 @@ function buildLoginSignUrl(hash: string, nonce: string): string { return `${window.location.origin}/login-sign?${params.toString()}`; } +function ProgressLine(p: { satisfied: number; required: number }): JSX.Element { + return ( +

+ Signatures collectées : {p.satisfied} / {p.required} +

+ ); +} + +function MaybeProgress(p: { + collectProgress: { satisfied: number; required: number } | null | undefined; +}): JSX.Element | null { + const c = p.collectProgress; + if (c === undefined || c === null || c.required <= 0) { + return null; + } + return ; +} + interface LoginCollectShareProps { proof: LoginProof; + /** Progress from relay fetch (notifications). X/Y signatures. */ + collectProgress?: { satisfied: number; required: number } | null; } /** * Device 1: show link + QR for "Demander signature sur l'autre appareil" during collect. + * Displays progress (X/Y) when collectProgress provided. */ -export function LoginCollectShare({ proof }: LoginCollectShareProps): JSX.Element { +export function LoginCollectShare({ + proof, + collectProgress, +}: LoginCollectShareProps): JSX.Element { const [qrDataUrl, setQrDataUrl] = useState(null); const url = buildLoginSignUrl(proof.challenge.hash, proof.challenge.nonce); @@ -31,6 +55,7 @@ export function LoginCollectShare({ proof }: LoginCollectShareProps): JSX.Elemen return (

{`Demander signature sur l${"'"}autre appareil`}

+

Ouvrez ce lien ou scannez le QR sur le 2ᵉ appareil.

diff --git a/userwallet/src/components/LoginScreen.tsx b/userwallet/src/components/LoginScreen.tsx index 5fafc03..5286c97 100644 --- a/userwallet/src/components/LoginScreen.tsx +++ b/userwallet/src/components/LoginScreen.tsx @@ -13,6 +13,7 @@ import { isPairingSatisfied, getPairsForMember } from '../utils/pairing'; import { buildAllowedPubkeys, checkDependenciesSatisfied, + collectProgress, hasRemoteSignatures, } from '../utils/loginValidation'; import { publishMessageAndSigs } from '../utils/loginPublish'; @@ -54,6 +55,10 @@ export function LoginScreen(): JSX.Element { successCount: number; relaysCount: number; } | null>(null); + const [collectProgressState, setCollectProgressState] = useState<{ + satisfied: number; + required: number; + } | null>(null); const graphResolver = new GraphResolver(); @@ -221,6 +226,7 @@ export function LoginScreen(): JSX.Element { let merged = proof.signatures; if (loginPath !== null) { setIsCollecting(true); + setCollectProgressState(null); try { const pairToMembers = buildPairToMembers(loginPath.pairs_attendus); const pubkeyToPair = buildPubkeyToPair( @@ -235,10 +241,21 @@ export function LoginScreen(): JSX.Element { loginPath, pairToMembers, pubkeyToPair, - { pollMs: COLLECT_POLL_MS, timeoutMs: COLLECT_TIMEOUT_MS }, + { + pollMs: COLLECT_POLL_MS, + timeoutMs: COLLECT_TIMEOUT_MS, + onProgress: (m) => { + const p = collectProgress(loginPath, m, pairToMembers); + setCollectProgressState({ + satisfied: p.satisfied, + required: p.required, + }); + }, + }, ); } finally { setIsCollecting(false); + setCollectProgressState(null); } } @@ -577,7 +594,10 @@ export function LoginScreen(): JSX.Element { )} {isCollecting && proof !== null && ( - + )} {awaitingRemoteAccept && collectedMerged !== null && diff --git a/userwallet/src/utils/collectSignatures.ts b/userwallet/src/utils/collectSignatures.ts index a849b07..7fe95af 100644 --- a/userwallet/src/utils/collectSignatures.ts +++ b/userwallet/src/utils/collectSignatures.ts @@ -1,6 +1,6 @@ import { getSignatures } from './relay'; import { getStoredPairs } from './pairing'; -import { hasEnoughSignatures, collectProgress } from './loginValidation'; +import { hasEnoughSignatures } from './loginValidation'; import type { LoginPath } from '../types/identity'; import type { MsgSignature } from '../types/message';