Fix Bitcoin miner PSBT signing after bitcoind restart

**Motivations:**
- Miner stopped producing blocks after bitcoind and mempool restart
- PSBT signing failed with "PSBT signing failed" error
- descriptorprocesspsbt failed with "Argument list too long" error due to large PSBT size (1.5MB)

**Root causes:**
- descriptorprocesspsbt called via command line exceeded argument length limit when PSBT is large
- walletprocesspsbt succeeded but could not mark PSBT as complete because it cannot sign artificial signet transactions (to_spend/spend) which are not real UTXOs in wallet
- After restart, the miner needed descriptorprocesspsbt to sign artificial signet transactions, but command line approach failed

**Correctifs:**
- Modified miner to use JSON-RPC HTTP API directly for descriptorprocesspsbt instead of command line
- PSBT is now passed via HTTP request body, avoiding command line argument length limit
- Added fallback to walletprocesspsbt if HTTP RPC fails
- Updated mine.sh to use -rpcwallet and -datadir correctly for wallet operations

**Evolutions:**
- Miner now uses HTTP JSON-RPC for large PSBTs, making it more robust for signet mining with many transactions
- Improved error handling with fallback mechanisms

**Pages affectées:**
- miner: Modified PSBT signing logic to use HTTP JSON-RPC for descriptorprocesspsbt
- mine.sh: Updated to use -rpcwallet and -datadir correctly
This commit is contained in:
ncantu 2026-01-27 23:30:19 +01:00
parent 1d4b0d8f33
commit f7f9442156
8 changed files with 195 additions and 13 deletions

View File

@ -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

View File

@ -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 à dautres é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) | — |

View File

@ -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

64
miner
View File

@ -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))

View File

@ -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 lUI 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. LUI 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 lUI 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`)

View File

@ -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 (
<p role="status" aria-live="polite">
Signatures collectées : {p.satisfied} / {p.required}
</p>
);
}
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 <ProgressLine satisfied={c.satisfied} required={c.required} />;
}
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<string | null>(null);
const url = buildLoginSignUrl(proof.challenge.hash, proof.challenge.nonce);
@ -31,6 +55,7 @@ export function LoginCollectShare({ proof }: LoginCollectShareProps): JSX.Elemen
return (
<section aria-label={`Demander signature sur l${"'"}autre appareil`}>
<h3>{`Demander signature sur l${"'"}autre appareil`}</h3>
<MaybeProgress collectProgress={collectProgress} />
<p>Ouvrez ce lien ou scannez le QR sur le 2 appareil.</p>
<p>
<a href={url} target="_blank" rel="noopener noreferrer">

View File

@ -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 {
</button>
)}
{isCollecting && proof !== null && (
<LoginCollectShare proof={proof} />
<LoginCollectShare
proof={proof}
collectProgress={collectProgressState}
/>
)}
{awaitingRemoteAccept &&
collectedMerged !== null &&

View File

@ -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';