Compare commits
59
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
728ba655ee | ||
|
|
b57c44e5c0 | ||
|
|
6f750a1327 | ||
|
|
24ca225190 | ||
|
|
2b483e0df9 | ||
|
|
d0cd9ef587 | ||
|
|
7c12f63f7a | ||
|
|
1b6904d2e3 | ||
|
|
14a094f941 | ||
|
|
8c2725fb31 | ||
|
|
c2fb4f1388 | ||
|
|
4d242a6ac6 | ||
|
|
251981e383 | ||
|
|
2abcdee144 | ||
|
|
50c19ebc9f | ||
|
|
a5d58dadd4 | ||
|
|
4280274239 | ||
|
|
7f3e2b5abc | ||
|
|
b9477c6139 | ||
|
|
039c916f55 | ||
|
|
d8c1e845b3 | ||
|
|
fcc19c31e1 | ||
|
|
6f3ca4ed7a | ||
|
|
6fcffb2096 | ||
|
|
ee817a8c9b | ||
|
|
2e97a6b822 | ||
|
|
133e292aab | ||
|
|
e89fea499c | ||
|
|
a6fe438b06 | ||
|
|
f757a01b9b | ||
|
|
2966e79578 | ||
|
|
3816b0426e | ||
|
|
57291a381b | ||
|
|
0333f15be5 | ||
|
|
82b27977eb | ||
|
|
f690e1783b | ||
|
|
2874f7c933 | ||
|
|
46636bdba9 | ||
|
|
dca3758c68 | ||
|
|
d232f0bc0d | ||
|
|
8d0c2620d3 | ||
|
|
d8a35d94cf | ||
|
|
b77d21fd84 | ||
|
|
889ffeb8dd | ||
|
|
f9acd12548 | ||
|
|
b5f0c81b88 | ||
|
|
2950559203 | ||
|
|
d3971de3a8 | ||
|
|
a7c65ed7ec | ||
|
|
b101ca4210 | ||
|
|
aa5838c010 | ||
|
|
788ed3bada | ||
|
|
0110e84c49 | ||
|
|
b3dedc1865 | ||
|
|
43829fb570 | ||
|
|
89766e7fdc | ||
|
|
8f5bb0d2ba | ||
|
|
c2b7c37c1e | ||
|
|
9ee860a78a |
@@ -27,5 +27,3 @@ PRUEBA-PERITAJE.md
|
||||
HALLAZGOS-PERITAJE.md
|
||||
MEJORAS.md
|
||||
COHERENCIA.md
|
||||
|
||||
GIT.md
|
||||
|
||||
@@ -10,57 +10,6 @@ y el versionado sigue [Versionado Semántico](https://semver.org/lang/es/):
|
||||
- **MENOR** — características nuevas que no rompen lo anterior
|
||||
- **PARCHE** — arreglos de errores
|
||||
|
||||
---
|
||||
## [1.12.1] — 2026-07-27
|
||||
### Corregido
|
||||
- **El monitor mostraba una CPU por proceso que engañaba.** La columna `%CPU`
|
||||
de `ps aux` no es el consumo actual sino la **media desde que el proceso
|
||||
arrancó** (tiempo de CPU dividido por tiempo de vida). Un proceso que trabajó
|
||||
mucho hace tres días seguía apareciendo alto para siempre, y se leía como si
|
||||
estuviera saturando la máquina. Se detectó porque los porcentajes por proceso
|
||||
no cambiaban NUNCA entre lecturas mientras la CPU global sí variaba.
|
||||
- Ahora se mide de verdad: dos lecturas de `/proc/PID/stat` separadas 500 ms,
|
||||
el mismo método que ya usaba `getCpuUsage` para el total. Las dos esperas
|
||||
corren en paralelo dentro del mismo `Promise.all`, así que no cuesta tiempo.
|
||||
- La interfaz muestra el **porcentaje sobre el total de la máquina**, que es
|
||||
lo que suele querer saberse, con el porcentaje de un núcleo y la media de
|
||||
`ps` en el tooltip para poder comparar con `top`.
|
||||
- Comprobado con carga artificial: el proceso ocupado marcaba 100% de un
|
||||
núcleo (25% de un sistema de 4) mientras `ps` daba 152%, imposible para un
|
||||
proceso de un solo hilo.
|
||||
|
||||
### Cambiado
|
||||
- El UTXO Map pedía las transacciones de contexto con `fetchWithTimeout`
|
||||
directo, saltándose la caché añadida en la 1.11.0. Era el único punto que no
|
||||
la aprovechaba. Ahora pasa por `get()`, así que esas transacciones —ya
|
||||
confirmadas, e inmutables— se guardan toda la sesión.
|
||||
|
||||
### Documentación
|
||||
- **Reescrita la guía de instalación.** `SETUP.md` no era una guía: más de la
|
||||
mitad eran los pasos personales de git y Gitea del autor ("PASO 1 — Crear el
|
||||
repo en Gitea"), que se han movido a un documento privado. Ahora es una guía
|
||||
real, con una comprobación tras cada paso para saber si vas bien sin
|
||||
descubrirlo al final con una pantalla en blanco.
|
||||
- **Corregida la configuración de nginx documentada, que estaba rota.** El
|
||||
README indicaba `alias /var/www/txoko/dashboard.html` — un alias a un
|
||||
ARCHIVO. Desde la 1.11.0, con las librerías en `vendor/` y rutas relativas,
|
||||
eso hace que el navegador no las encuentre y la página quede en blanco sin
|
||||
ningún error visible. Debe apuntar al **directorio**. Es el fallo más común
|
||||
de esta instalación y ahora está avisado en tres sitios, con una comprobación
|
||||
concreta (`curl` mirando el `content-type`, no solo el código 200) para
|
||||
descartarlo.
|
||||
- Los pasos de instalación no mencionaban las librerías del frontend, sin las
|
||||
cuales la aplicación no arranca. Ya están integradas en el orden correcto.
|
||||
- Eliminados del repositorio los datos personales del autor: rutas de su
|
||||
máquina en la documentación y, sobre todo, su nombre de usuario del sistema
|
||||
y su ruta de instalación dentro de `txoko-metrics.service`, que llevaban ahí
|
||||
desde el primer commit. El servicio usa ahora marcadores que hay que ajustar
|
||||
antes de instalarlo.
|
||||
- README: estructura del repositorio actualizada (faltaban `tests/`,
|
||||
`instalar-fuentes.sh` y `FIXTURES.md`) y sección de instalación reducida a un
|
||||
resumen que remite a SETUP.md, para que haya una sola fuente de verdad.
|
||||
- SETUP.md incluye ahora tabla de problemas frecuentes y cómo actualizar.
|
||||
|
||||
---
|
||||
## [1.12.0] — 2026-07-27
|
||||
### Añadido
|
||||
@@ -91,30 +40,6 @@ y el versionado sigue [Versionado Semántico](https://semver.org/lang/es/):
|
||||
**Qué puedes hacer** —, que aquí cobra sentido literal: todavía estás a
|
||||
tiempo de cambiar la transacción.
|
||||
|
||||
### Corregido
|
||||
- **Revisión de la criptografía watch-only.** Se verificó la derivación completa
|
||||
contra los vectores oficiales de los estándares: RIPEMD-160 (los seis del
|
||||
estándar, incluido el de un millón de caracteres), secp256k1, BIP32 (vectores
|
||||
1 y 2) y bech32/BIP173. **La matemática es correcta y no se tocó.** Los fallos
|
||||
estaban en la validación de la entrada:
|
||||
- **No se comprobaba la suma de verificación del xpub.** Un carácter mal
|
||||
copiado se aceptaba sin protestar y generaba 200 direcciones ajenas: el
|
||||
usuario habría visto su cartera "sin actividad" y se habría quedado
|
||||
tranquilo. Falso negativo silencioso, el mismo patrón que el resto de fallos
|
||||
de esta jornada. Nuevo `decodeBase58Check`.
|
||||
- **No se validaba la longitud** del material decodificado (deben ser 78
|
||||
bytes) ni que la clave pública fuese comprimida (0x02/0x03).
|
||||
- **`tpub` se trataba como mainnet.** La red se detectaba mirando si el texto
|
||||
empezaba por "tb", "u" o "v" — y un `tpub`, el formato más común de testnet,
|
||||
empieza por "t" pero no por "tb". Generaba direcciones `bc1…` a partir de
|
||||
claves de testnet. Ahora se detecta por bytes de versión, que son
|
||||
inequívocos, con las diez variantes (x/y/z/Y/Z y t/u/v/U/V).
|
||||
- **`deriveChildPubkey` aceptaba índices endurecidos**, matemáticamente
|
||||
imposibles desde una clave pública. No era alcanzable desde la interfaz,
|
||||
pero una función criptográfica debe defenderse sola.
|
||||
- Añadida la carpeta `tests/` con las cuatro baterías y su documentación,
|
||||
incluido qué **no** cubren: no sustituyen una auditoría externa.
|
||||
|
||||
---
|
||||
## [1.11.0] — 2026-07-27
|
||||
### Añadido
|
||||
|
||||
@@ -96,17 +96,6 @@ Consecuencia práctica: **el dashboard funciona sin conexión a internet**. Solo
|
||||
- Bandas de privacidad con distinción explícita CERTEZA / PROBABLE / POSIBLE
|
||||
- Exportación del informe en JSON y Markdown
|
||||
|
||||
**Auditoría de PSBT — antes de firmar**
|
||||
|
||||
La única función de la app que llega a tiempo: el resto te cuenta lo que ya
|
||||
pasó, esta te avisa cuando todavía puedes cambiar la transacción.
|
||||
|
||||
- Parser completo de BIP174 escrito desde cero, validado contra los cinco vectores inválidos del propio estándar
|
||||
- **Todo offline**: una PSBT ya lleva dentro los importes y scripts de sus entradas, así que no se consulta el nodo. Funciona con el nodo sincronizando
|
||||
- Avisa de las **claves públicas maestras incrustadas**: las PSBT suelen llevarlas dentro y están hechas para compartirse — quien reciba el archivo puede ver todas tus direcciones, tu saldo y tu historial. No puede gastar, pero lo ve todo
|
||||
- Detecta cambio identificable por tipo de dirección, importes redondos que delatan cuál es el pago, envíos a tu propia cartera, carteras multifirma y PSBTs incompletas
|
||||
- Acepta base64, hexadecimal o el archivo `.psbt` que exporta Sparrow
|
||||
|
||||
**Peritaje forense**
|
||||
- Rastreo de fondos robados o perdidos hacia adelante, salto a salto, desde una transacción de origen hasta un punto de parada natural (custodio identificado, dilución, CoinJoin, o fondos aún sin gastar)
|
||||
- A demanda: tú decides cuándo se explora cada salto con el botón "Seguir el rastro" — nada corre sin que lo pidas, igual que el rastro de procedencia hacia atrás. Puedes generar el informe con lo explorado hasta ese momento, sin terminar el rastreo entero
|
||||
@@ -134,7 +123,7 @@ pasó, esta te avisa cuando todavía puedes cambiar la transacción.
|
||||
- Conversor sat/BTC/fiat
|
||||
- Validador de dirección
|
||||
- Detector OP_RETURN
|
||||
- Decodificador de transacción raw
|
||||
- Decodificador PSBT y transacción raw
|
||||
|
||||
---
|
||||
|
||||
@@ -162,29 +151,66 @@ Probado sobre Ubuntu Server 24.04 con HP EliteDesk (i5, 32GB RAM, 2TB NVMe).
|
||||
|
||||
## Instalación
|
||||
|
||||
**La guía completa está en [SETUP.md](SETUP.md)** — paso a paso, con una
|
||||
comprobación después de cada uno para que sepas si vas bien sin tener que
|
||||
descubrirlo al final.
|
||||
### 1. Clonar el repositorio
|
||||
|
||||
Resumen de lo que implica:
|
||||
```bash
|
||||
git clone https://git.bitcointxoko.org/pikaro/txoko-dashboard.git
|
||||
cd txoko-dashboard
|
||||
```
|
||||
|
||||
1. Clonar el repositorio.
|
||||
2. Copiar `dashboard.html` a un **directorio** propio servido por nginx.
|
||||
3. Descargar las librerías (React, Babel y las fuentes) a `vendor/`, junto al
|
||||
dashboard, y verificarlas por hash. No están en el repo: son de terceros y
|
||||
ocupan ~3 MB.
|
||||
4. Opcionalmente, instalar el monitor del sistema como servicio systemd.
|
||||
5. Configurar nginx: el dashboard, un proxy a la API de Mempool y otro al
|
||||
monitor.
|
||||
### 2. Copiar el dashboard
|
||||
|
||||
Un aviso que ahorra disgustos: en nginx, el `alias` del dashboard debe apuntar
|
||||
al **directorio** (con barra final), no al archivo `dashboard.html`. Si apunta
|
||||
al archivo, el navegador no encuentra `vendor/` y verás una página en blanco
|
||||
sin ningún error visible. Es el fallo más común, y en SETUP.md hay una
|
||||
comprobación concreta para descartarlo.
|
||||
```bash
|
||||
cp dashboard.html /var/www/txoko/dashboard.html
|
||||
# o donde lo sirvas con nginx
|
||||
```
|
||||
|
||||
Cuando termines, abre `http://TU-IP:4080/dashboard/` — **con la barra final** —,
|
||||
pulsa CONFIG e introduce la URL de tu Mempool.
|
||||
### 3. Copiar el backend de métricas
|
||||
|
||||
```bash
|
||||
cp system-metrics.js /home/armg/txoko/system-metrics.js
|
||||
```
|
||||
|
||||
Editar `system-metrics.js` y añadir tus credenciales RPC de Bitcoin Core
|
||||
(el archivo del repo usa placeholders).
|
||||
|
||||
### 4. Configurar el servicio systemd
|
||||
|
||||
```bash
|
||||
sudo cp txoko-metrics.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable txoko-metrics
|
||||
sudo systemctl start txoko-metrics
|
||||
```
|
||||
|
||||
### 5. Configurar nginx
|
||||
|
||||
Añadir a tu configuración de nginx:
|
||||
|
||||
```nginx
|
||||
# Dashboard
|
||||
location /dashboard {
|
||||
alias /var/www/txoko/dashboard.html;
|
||||
}
|
||||
|
||||
# API Mempool (ajusta el puerto según tu instalación)
|
||||
location /api/ {
|
||||
proxy_pass http://localhost:8999;
|
||||
}
|
||||
|
||||
# Métricas del sistema
|
||||
location /system/ {
|
||||
proxy_pass http://127.0.0.1:4082;
|
||||
}
|
||||
```
|
||||
|
||||
### 6. Abrir en el navegador
|
||||
|
||||
```
|
||||
http://TU-IP-TAILSCALE:4080/dashboard
|
||||
```
|
||||
|
||||
Introduce la URL de tu Mempool en el modal de configuración y empieza a analizar.
|
||||
|
||||
---
|
||||
|
||||
@@ -192,22 +218,15 @@ pulsa CONFIG e introduce la URL de tu Mempool.
|
||||
|
||||
```
|
||||
txoko-dashboard/
|
||||
├── dashboard.html # La aplicación entera (HTML + CSS + JS en un archivo)
|
||||
├── system-metrics.js # Monitor del nodo (Node.js) — opcional
|
||||
├── txoko-metrics.service # Servicio systemd para el monitor
|
||||
├── instalar-fuentes.sh # Descarga IBM Plex al nodo y genera su CSS
|
||||
├── tests/ # Pruebas de la criptografía contra vectores oficiales
|
||||
├── SETUP.md # Guía de instalación
|
||||
├── FIXTURES.md # Transacciones reales para probar las heurísticas
|
||||
├── CHANGELOG.md
|
||||
├── dashboard.html # Frontend completo (HTML + CSS + JS en un solo archivo)
|
||||
├── system-metrics.js # Backend de métricas del nodo (Node.js)
|
||||
├── txoko-metrics.service # Servicio systemd
|
||||
├── README.md
|
||||
├── SETUP.md
|
||||
├── CHANGELOG.md
|
||||
└── LICENSE
|
||||
```
|
||||
|
||||
Tras la instalación, junto al `dashboard.html` queda además un directorio
|
||||
`vendor/` con las librerías y las fuentes. No está en el repositorio: se
|
||||
descarga y se verifica durante la instalación (ver [SETUP.md](SETUP.md)).
|
||||
|
||||
El frontend es un único archivo HTML. Sin bundler, sin npm, sin proceso de build: Babel transpila el JSX en el navegador, así que lo que lees en el archivo es exactamente lo que se ejecuta — no hay una versión compilada que auditar por separado.
|
||||
|
||||
Las librerías (React, Babel, las fuentes) viven aparte, en `vendor/`, servidas desde tu propio nodo y verificadas por hash durante la instalación. Se dejan fuera del repo a propósito: son código de terceros, y mezclarlas con el tuyo haría más difícil auditar lo que de verdad importa.
|
||||
|
||||
@@ -1,93 +1,187 @@
|
||||
# Instalación de Txoko Node Dashboard
|
||||
# Cómo subir Txoko a Gitea — paso a paso
|
||||
|
||||
Guía completa, de principio a fin. Sigue los pasos en orden y comprueba cada
|
||||
uno antes de pasar al siguiente: cada comprobación te dice si vas bien, en vez
|
||||
de dejarte descubrirlo al final con una pantalla en blanco.
|
||||
|
||||
Todo se hace **en el nodo**, salvo donde se indique lo contrario.
|
||||
Instrucciones exactas. Copiar y pegar en la terminal del Mac.
|
||||
No hace falta entender git para seguir esto.
|
||||
|
||||
---
|
||||
|
||||
## Antes de empezar
|
||||
## PASO 1 — Crear el repo en Gitea (una sola vez)
|
||||
|
||||
Txoko no habla con la red Bitcoin directamente: se apoya en cosas que ya
|
||||
tienes montadas. Necesitas:
|
||||
1. Abre tu instancia de Gitea en el navegador
|
||||
2. Clic en el **+** (arriba a la derecha) → "New Repository"
|
||||
3. Rellena:
|
||||
- **Repository Name:** `txoko-dashboard`
|
||||
- **Description:** `Suite de auditoría de privacidad Bitcoin para nodos propios`
|
||||
- **Visibility:** Private (o Public si quieres compartirlo con la comunidad)
|
||||
- **Initialize repository:** NO marcar (ya traemos nuestros archivos)
|
||||
4. Clic en "Create Repository"
|
||||
5. Gitea te muestra una página con instrucciones — copia la URL del repo,
|
||||
será algo como: `https://gitea.tu-comunidad/tu-usuario/txoko-dashboard.git`
|
||||
|
||||
| Requisito | Para qué |
|
||||
|---|---|
|
||||
| **Bitcoin Core** con `txindex=1` | consultar cualquier transacción, no solo las tuyas |
|
||||
| **Mempool self-hosted** ([mempool/mempool](https://github.com/mempool/mempool)) | la API que Txoko consulta |
|
||||
| **Fulcrum** ([cculianu/Fulcrum](https://github.com/cculianu/Fulcrum)) | índice de direcciones; Mempool lo usa por debajo |
|
||||
| **nginx** | sirve el dashboard y hace de proxy hacia lo anterior |
|
||||
| **Node.js ≥ 18** | solo para el monitor del sistema (paso 4) |
|
||||
---
|
||||
|
||||
Si Mempool self-hosted ya te funciona en el navegador, tienes todo lo demás.
|
||||
|
||||
Comprueba que la API responde antes de seguir. Ajusta el puerto al tuyo:
|
||||
## PASO 2 — Configurar git en el Mac (una sola vez, si no lo tienes)
|
||||
|
||||
```bash
|
||||
curl -s http://127.0.0.1:8999/api/v1/fees/recommended
|
||||
git config --global user.name "tu nombre"
|
||||
git config --global user.email "tu@email.com"
|
||||
```
|
||||
|
||||
Debe devolver un JSON con comisiones. Si no responde, arregla eso primero:
|
||||
Txoko no puede funcionar sin ello.
|
||||
|
||||
> **Nunca expongas estos puertos a internet.** Txoko está pensado para
|
||||
> accederse por Tailscale, VPN o red local.
|
||||
Comprueba que git está instalado:
|
||||
```bash
|
||||
git --version
|
||||
```
|
||||
Si no está: `brew install git`
|
||||
|
||||
---
|
||||
|
||||
## Paso 1 — Descargar los archivos
|
||||
## PASO 3 — Crear el repo local y primer commit (una sola vez)
|
||||
|
||||
```bash
|
||||
git clone https://git.bitcointxoko.org/pikaro/txoko-dashboard.git
|
||||
cd txoko-dashboard
|
||||
# Crear carpeta del proyecto en el Mac
|
||||
mkdir ~/txoko-dashboard
|
||||
cd ~/txoko-dashboard
|
||||
|
||||
# Copiar los archivos del repo que te he preparado
|
||||
# (descarga los archivos de esta conversación y cópialos aquí)
|
||||
|
||||
# Inicializar git
|
||||
git init
|
||||
git branch -M main
|
||||
|
||||
# Añadir todos los archivos
|
||||
git add .
|
||||
|
||||
# Primer commit — el historial empieza aquí
|
||||
git commit -m "inicio: dashboard de privacidad Bitcoin con análisis on-chain"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Paso 2 — Elegir dónde vivirá el dashboard
|
||||
|
||||
Esta decisión condiciona el resto, así que conviene hacerla a conciencia.
|
||||
|
||||
Necesitas un **directorio** propio para Txoko. No vale colocar el
|
||||
`dashboard.html` suelto en cualquier sitio: la aplicación carga sus librerías
|
||||
desde un subdirectorio `vendor/` **junto al propio archivo**, así que ambos
|
||||
tienen que convivir.
|
||||
## PASO 4 — Conectar con Gitea y subir (una sola vez)
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /var/www/txoko
|
||||
sudo cp dashboard.html /var/www/txoko/
|
||||
# Sustituye la URL por la de tu repo de Gitea
|
||||
git remote add origin https://gitea.tu-comunidad/tu-usuario/txoko-dashboard.git
|
||||
|
||||
# Subir
|
||||
git push -u origin main
|
||||
```
|
||||
|
||||
Puedes usar otra ruta; solo recuerda cuál es, porque aparece en los pasos 3 y 5.
|
||||
Gitea te pedirá usuario y contraseña la primera vez.
|
||||
Si quieres evitar introducirlos cada vez, crea un token en
|
||||
Gitea → Settings → Applications → "Generate Token" y úsalo como contraseña.
|
||||
|
||||
---
|
||||
|
||||
## Paso 3 — Instalar las librerías del frontend
|
||||
|
||||
Txoko usa React y Babel, y la tipografía IBM Plex. **Se sirven desde tu propio
|
||||
nodo, nunca desde un CDN externo.**
|
||||
|
||||
El motivo es de privacidad, no de comodidad: un CDN no vería qué transacciones
|
||||
analizas, pero sí recibiría tu IP y la hora cada vez que abres el dashboard —
|
||||
sabría que usas Txoko, cuándo y desde dónde. Justo el metadato que esta
|
||||
herramienta enseña a proteger. Sirviéndolas en local, *"ninguna consulta sale
|
||||
de tu red"* es literal, y el dashboard funciona sin conexión a internet.
|
||||
|
||||
No están en el repositorio porque son código de terceros y ocupan unos 3 MB.
|
||||
|
||||
### React y Babel
|
||||
## PASO 5 — Flujo de trabajo normal (cada vez que yo te dé un archivo nuevo)
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /var/www/txoko/vendor
|
||||
cd /var/www/txoko/vendor
|
||||
cd ~/txoko-dashboard
|
||||
|
||||
# Copiar el archivo actualizado descargado de esta conversación
|
||||
cp ~/Downloads/txoko-dashboard.html ./dashboard.html
|
||||
|
||||
# Ver qué cambió (opcional pero útil)
|
||||
git diff dashboard.html
|
||||
|
||||
# Registrar el cambio con un mensaje descriptivo
|
||||
git add dashboard.html
|
||||
git commit -m "fix: descripción breve de lo que se arregló"
|
||||
|
||||
# Subir a Gitea
|
||||
git push
|
||||
|
||||
# Copiar al nodo (igual que antes)
|
||||
scp dashboard.html armg@100.116.19.86:/home/armg/txoko/dashboard.html
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Mensajes de commit — ejemplos
|
||||
|
||||
El mensaje va después de `-m` y describe QUÉ cambiaste.
|
||||
No tiene que ser perfecto, solo útil para ti en el futuro.
|
||||
|
||||
```
|
||||
"fix: timeout en consultas al nodo"
|
||||
"feat: score por bandas ALTA/MEDIA/BAJA"
|
||||
"fix: responsive pestaña Mempool en móvil"
|
||||
"fix: detección Whirlpool con tolerancia 2%"
|
||||
"feat: fingerprinting wallet completo"
|
||||
"fix: umbral dust por tipo de salida"
|
||||
"docs: actualizar README con nuevas funcionalidades"
|
||||
```
|
||||
|
||||
Convención (opcional pero ordenada):
|
||||
- `fix:` — corrige algo que no funcionaba bien
|
||||
- `feat:` — añade algo nuevo
|
||||
- `docs:` — solo documentación
|
||||
|
||||
---
|
||||
|
||||
## Si algo sale mal
|
||||
|
||||
**Subiste algo que no querías:**
|
||||
```bash
|
||||
git revert HEAD
|
||||
git push
|
||||
```
|
||||
|
||||
**Quieres volver a una versión anterior:**
|
||||
```bash
|
||||
git log --oneline # ver el historial
|
||||
git checkout HASH_DEL_COMMIT -- dashboard.html # recuperar ese archivo
|
||||
```
|
||||
|
||||
**Ver el historial:**
|
||||
```bash
|
||||
git log --oneline
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Resumen del flujo
|
||||
|
||||
```
|
||||
Claude te da dashboard.html
|
||||
↓
|
||||
cp ~/Downloads/dashboard.html ./dashboard.html
|
||||
↓
|
||||
git add . && git commit -m "descripción"
|
||||
↓
|
||||
git push
|
||||
↓
|
||||
scp dashboard.html armg@100.116.19.86:/home/armg/txoko/
|
||||
```
|
||||
|
||||
Cuatro comandos después de la descarga. Siempre los mismos.
|
||||
|
||||
---
|
||||
|
||||
## Librerías del frontend (obligatorio)
|
||||
|
||||
El dashboard usa React y Babel. **Se sirven desde tu propio nodo, no desde un
|
||||
CDN.** El motivo es de privacidad, no de comodidad: un CDN externo no ve qué
|
||||
transacciones analizas, pero sí ve que usas Txoko, cuándo y desde qué IP
|
||||
pública — exactamente la clase de metadato que esta herramienta enseña a
|
||||
proteger. Sirviéndolas en local, *"ninguna consulta sale de tu red"* pasa a ser
|
||||
literal, y el dashboard funciona sin conexión a internet.
|
||||
|
||||
Estas librerías **no están en el repo** (son de terceros y pesan ~3 MB). Se
|
||||
descargan una vez, en el nodo:
|
||||
|
||||
```bash
|
||||
sudo mkdir -p /usr/share/nginx/html/vendor
|
||||
cd /usr/share/nginx/html/vendor
|
||||
sudo curl -sSLO https://unpkg.com/react@18.3.1/umd/react.production.min.js
|
||||
sudo curl -sSLO https://unpkg.com/react-dom@18.3.1/umd/react-dom.production.min.js
|
||||
sudo curl -sSL -o babel.min.js https://unpkg.com/@babel/standalone@7.23.10/babel.min.js
|
||||
```
|
||||
|
||||
**Comprueba que has recibido lo que esperabas.** No te fíes: verifícalo.
|
||||
### Verifica lo que has descargado
|
||||
|
||||
No te fíes: comprueba que los archivos son los que deben ser.
|
||||
|
||||
```bash
|
||||
sha256sum *.js
|
||||
@@ -101,218 +195,118 @@ Debe dar exactamente esto:
|
||||
d949f1c3687aedadcedac85261865f29b17cd273997e7f6b2bfc53b2f9d4c4dd react.production.min.js
|
||||
```
|
||||
|
||||
Si alguno no coincide, **para aquí**: has recibido algo distinto de lo esperado.
|
||||
Si algún hash no coincide, **no uses esos archivos**: significa que has
|
||||
recibido algo distinto a lo esperado.
|
||||
|
||||
Las versiones están fijadas a propósito. Usar un rango como `react@18` dejaría
|
||||
que el servidor decidiera qué versión te entrega, y cambiaría con el tiempo sin
|
||||
que te enteres.
|
||||
Las versiones están fijadas a propósito (`18.3.1`, `7.23.10`). Usar un rango
|
||||
como `react@18` dejaría que el servidor decidiera qué versión te entrega, y
|
||||
cambiaría con el tiempo sin que te enteres.
|
||||
|
||||
### Las fuentes
|
||||
### Dónde deben quedar los archivos
|
||||
|
||||
```bash
|
||||
sudo /ruta/al/repo/instalar-fuentes.sh /var/www/txoko/vendor/fonts
|
||||
```
|
||||
El dashboard las busca en `vendor/` con **ruta relativa**, es decir, en un
|
||||
subdirectorio junto al propio `dashboard.html`. Así funciona tanto si sirves el
|
||||
dashboard en la raíz como bajo un prefijo, sin tocar nginx.
|
||||
|
||||
El script descarga IBM Plex (licencia libre OFL), comprueba cada archivo y
|
||||
aborta si algo falla. Son unos 350 KB.
|
||||
|
||||
---
|
||||
|
||||
## Paso 4 — El monitor del sistema (opcional)
|
||||
|
||||
Alimenta la pestaña NODO: CPU, RAM, disco, estado de Bitcoin Core y logs. Sin
|
||||
esto el resto de la aplicación funciona igual, solo que esa pestaña queda vacía.
|
||||
|
||||
```bash
|
||||
mkdir -p ~/txoko
|
||||
cp system-metrics.js ~/txoko/
|
||||
```
|
||||
|
||||
Edita `~/txoko/system-metrics.js` y sustituye los dos marcadores por tus
|
||||
credenciales RPC de Bitcoin Core (las de tu `bitcoin.conf`):
|
||||
|
||||
```js
|
||||
const RPC_USER = "TU_RPC_USER";
|
||||
const RPC_PASS = "TU_RPC_PASSWORD";
|
||||
```
|
||||
|
||||
Instala el servicio. Antes revisa el `.service`, porque trae rutas y usuario
|
||||
que probablemente tengas que ajustar a los tuyos:
|
||||
|
||||
```bash
|
||||
nano txoko-metrics.service
|
||||
sudo cp txoko-metrics.service /etc/systemd/system/
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable --now txoko-metrics
|
||||
```
|
||||
|
||||
**Comprueba que arrancó:**
|
||||
|
||||
```bash
|
||||
systemctl is-active txoko-metrics # debe decir: active
|
||||
curl -s http://127.0.0.1:4082/system/info | head -c 200
|
||||
```
|
||||
|
||||
> Si `systemctl status` muestra un punto que no es verde pero pone
|
||||
> `active (running)`, está bien. Lo que importa es el texto.
|
||||
|
||||
---
|
||||
|
||||
## Paso 5 — Configurar nginx
|
||||
|
||||
Aquí es donde más gente se atasca, así que léelo con calma.
|
||||
|
||||
Añade esto a tu configuración de nginx (si usas Mempool self-hosted, dentro del
|
||||
mismo bloque `server`):
|
||||
Con la configuración típica de Mempool self-hosted:
|
||||
|
||||
```nginx
|
||||
# Dashboard — OJO: alias a un DIRECTORIO, con barra final
|
||||
location /dashboard/ {
|
||||
alias /var/www/txoko/;
|
||||
index dashboard.html;
|
||||
try_files $uri $uri/ /dashboard/dashboard.html;
|
||||
}
|
||||
|
||||
# API de Mempool — ajusta el puerto al de tu instalación
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.0.1:8999;
|
||||
}
|
||||
|
||||
# Monitor del sistema (solo si hiciste el paso 4)
|
||||
location /system/ {
|
||||
proxy_pass http://127.0.0.1:4082;
|
||||
location /dashboard {
|
||||
alias /usr/share/nginx/html/;
|
||||
}
|
||||
```
|
||||
|
||||
**El detalle que rompe la instalación:** el `alias` tiene que apuntar al
|
||||
**directorio**, con barra final, no al archivo `dashboard.html`. Si apunta al
|
||||
archivo, el navegador no encontrará `vendor/` y verás una **página en blanco**
|
||||
sin ningún mensaje de error. Es el fallo más común de esta instalación.
|
||||
…el `dashboard.html` vive en `/usr/share/nginx/html/` y las librerías deben ir
|
||||
en `/usr/share/nginx/html/vendor/` — que es justo donde las deja el comando de
|
||||
arriba.
|
||||
|
||||
Recarga nginx:
|
||||
**Importante:** abre el dashboard **con la barra final** (`.../dashboard/`, no
|
||||
`.../dashboard`). Sin ella, el navegador resuelve las rutas relativas un nivel
|
||||
por encima y no encuentra las librerías.
|
||||
|
||||
Para comprobar que nginx las sirve bien, lo que importa no es solo el código
|
||||
200 sino el tipo de contenido:
|
||||
|
||||
```bash
|
||||
sudo nginx -t && sudo systemctl reload nginx
|
||||
curl -sk -o /dev/null -w '%{http_code} %{content_type}\n' \
|
||||
https://localhost:4081/dashboard/vendor/react.production.min.js
|
||||
```
|
||||
|
||||
Debe responder `200 application/javascript`. Si devuelve `200 text/html`,
|
||||
nginx está entregando otra cosa (por ejemplo el index de Mempool) y las rutas
|
||||
no son las correctas.
|
||||
|
||||
---
|
||||
|
||||
## Paso 6 — Comprobar que funciona
|
||||
|
||||
Antes de abrir el navegador, verifica desde el propio nodo. Ajusta el puerto:
|
||||
|
||||
```bash
|
||||
# El dashboard llega
|
||||
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:4080/dashboard/
|
||||
|
||||
# Y las librerías TAMBIÉN — esto es lo que suele fallar
|
||||
curl -s -o /dev/null -w '%{http_code} %{content_type}\n' \
|
||||
http://127.0.0.1:4080/dashboard/vendor/react.production.min.js
|
||||
```
|
||||
|
||||
La segunda debe responder **`200 application/javascript`**.
|
||||
|
||||
Si devuelve `200 text/html` la instalación **no** está bien: nginx te está
|
||||
sirviendo otra cosa (normalmente el index de Mempool) en lugar del archivo.
|
||||
Revisa el `alias` del paso 5. Fíjate en que aquí no basta con mirar el código
|
||||
200 — hay que mirar el tipo de contenido.
|
||||
|
||||
Ahora sí, abre en el navegador:
|
||||
|
||||
```
|
||||
http://TU-IP:4080/dashboard/
|
||||
```
|
||||
|
||||
**Con la barra final.** Sin ella, el navegador busca las librerías un nivel por
|
||||
encima y no las encuentra.
|
||||
|
||||
Pulsa **CONFIG** e introduce la URL de tu Mempool (por ejemplo
|
||||
`http://TU-IP:4080`). Dale a PROBAR: si dice "Conexión exitosa", ya está.
|
||||
|
||||
### Si ves una página en blanco
|
||||
|
||||
Abre la consola del navegador (F12). Si aparece `SyntaxError: Unexpected
|
||||
token '<'`, es exactamente el problema del `alias` del paso 5: nginx está
|
||||
devolviendo HTML donde debería devolver JavaScript.
|
||||
|
||||
---
|
||||
|
||||
## HTTPS — necesario para el watch-only (opcional)
|
||||
## HTTPS para watch-only (opcional)
|
||||
|
||||
La función watch-only (derivar tus direcciones desde el xpub) usa la Web Crypto
|
||||
API del navegador, que **solo funciona sobre HTTPS**. Por HTTP el resto de la
|
||||
aplicación funciona igual; solo esa función queda deshabilitada. Las etiquetas
|
||||
BIP-329 no necesitan HTTPS.
|
||||
API del navegador, que **solo funciona sobre HTTPS**. Si sirves el dashboard por
|
||||
HTTP, watch-only no estará disponible — el resto de la app funciona igual. Las
|
||||
etiquetas BIP-329 no necesitan HTTPS.
|
||||
|
||||
Si accedes por Tailscale o red local no tendrás un certificado válido, así que
|
||||
toca generar uno autofirmado.
|
||||
Si quieres usar watch-only y tu nodo va por HTTP (por ejemplo, acceso por
|
||||
Tailscale sin certificado), puedes generar un certificado autofirmado. Es lo que
|
||||
sigue. Todo se hace en el nodo.
|
||||
|
||||
### 1. Generar el certificado
|
||||
### 1. Generar el certificado autofirmado
|
||||
|
||||
Sustituye la IP por la de tu nodo:
|
||||
Sustituye la IP por la de tu nodo (Tailscale, local, etc.):
|
||||
|
||||
```bash
|
||||
sudo openssl req -x509 -nodes -days 3650 -newkey rsa:2048 \
|
||||
-keyout /etc/ssl/private/txoko.key \
|
||||
-out /etc/ssl/certs/txoko.crt \
|
||||
-subj "/CN=100.64.0.5" \
|
||||
-addext "subjectAltName=IP:100.64.0.5"
|
||||
-subj "/CN=100.116.19.86" \
|
||||
-addext "subjectAltName=IP:100.116.19.86"
|
||||
```
|
||||
|
||||
El `subjectAltName` no es opcional: sin él los navegadores modernos rechazan el
|
||||
certificado aunque el `CN` sea correcto.
|
||||
### 2. Configurar nginx para servir HTTPS
|
||||
|
||||
### 2. Servirlo en nginx
|
||||
|
||||
Duplica tu bloque `server` en otro puerto (4081 en este ejemplo) añadiendo:
|
||||
Añade un bloque `server` que escuche en un puerto con SSL (por ejemplo 4081),
|
||||
apuntando al certificado recién creado e incluyendo la misma configuración que
|
||||
tu servidor HTTP:
|
||||
|
||||
```nginx
|
||||
listen 4081 ssl;
|
||||
ssl_certificate /etc/ssl/certs/txoko.crt;
|
||||
ssl_certificate_key /etc/ssl/private/txoko.key;
|
||||
server {
|
||||
listen 4081 ssl;
|
||||
listen [::]:4081 ssl;
|
||||
server_name _;
|
||||
ssl_certificate /etc/ssl/certs/txoko.crt;
|
||||
ssl_certificate_key /etc/ssl/private/txoko.key;
|
||||
ssl_session_timeout 4h;
|
||||
ssl_protocols TLSv1.3;
|
||||
ssl_prefer_server_ciphers on;
|
||||
|
||||
# Incluye aquí tu misma config (location /dashboard, /api/, etc.)
|
||||
# Si ya tienes esos location en un snippet, basta con incluirlo:
|
||||
# include /etc/nginx/snippets/tu-config.conf;
|
||||
}
|
||||
```
|
||||
|
||||
Los `location` son los mismos del paso 5. Recarga con `sudo nginx -t &&
|
||||
sudo systemctl reload nginx`.
|
||||
Si el bloque `location /dashboard` ya viene de un snippet que incluyes, **no lo
|
||||
dupliques** dentro del server SSL: nginx dará error `duplicate location`.
|
||||
|
||||
### 3. Aceptar el certificado la primera vez
|
||||
|
||||
Al entrar en `https://TU-IP:4081/dashboard/` el navegador avisará de que el
|
||||
certificado no es de confianza. Es lo esperado: lo has firmado tú. Acepta la
|
||||
excepción una vez.
|
||||
|
||||
Que sea autofirmado no lo hace menos seguro **para este uso**: cifra igual, y
|
||||
como el certificado lo has generado tú en tu propia máquina, nadie externo
|
||||
puede suplantarlo. Lo que no tienes es el respaldo de una autoridad
|
||||
certificadora, que aquí no aporta nada porque el servidor y el cliente son
|
||||
tuyos.
|
||||
|
||||
---
|
||||
|
||||
## Actualizar a una versión nueva
|
||||
Comprueba y recarga:
|
||||
|
||||
```bash
|
||||
git pull
|
||||
sudo cp dashboard.html /var/www/txoko/
|
||||
sudo nginx -t && sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
Y recarga el navegador con **Ctrl+Shift+R** (o Cmd+Shift+R en Mac) para saltarte
|
||||
la caché.
|
||||
### 3. Confiar el certificado la primera vez
|
||||
|
||||
Las librerías de `vendor/` no hace falta volver a descargarlas salvo que el
|
||||
CHANGELOG diga lo contrario. Si además cambia `system-metrics.js`, cópialo de
|
||||
nuevo (conservando tus credenciales) y reinicia con
|
||||
`sudo systemctl restart txoko-metrics`.
|
||||
Abre `https://TU-IP:4081/dashboard/` en el navegador. Como el certificado es
|
||||
autofirmado, el navegador avisará de que la conexión no es privada. Es esperado
|
||||
—lo creaste tú— y es seguro en tu propia red:
|
||||
|
||||
---
|
||||
- **Safari:** clic en "visitar este sitio web" (abajo del aviso) y confirma
|
||||
- **Chrome/Brave:** "Configuración avanzada" → "Acceder a TU-IP (no seguro)"
|
||||
|
||||
## Problemas frecuentes
|
||||
A partir de ahí el navegador recuerda la excepción y la Web Crypto API queda
|
||||
disponible, así que watch-only funcionará.
|
||||
|
||||
> Nota: un certificado autofirmado es perfectamente válido para uso personal en
|
||||
> tu propia red. El aviso del navegador existe porque no hay una autoridad
|
||||
> certificadora de por medio, no porque la conexión sea insegura — el tráfico va
|
||||
> cifrado igual.
|
||||
|
||||
| Síntoma | Causa habitual |
|
||||
|---|---|
|
||||
| Página en blanco, consola con `Unexpected token '<'` | El `alias` de nginx apunta al archivo y no al directorio (paso 5) |
|
||||
| Página en blanco al entrar sin barra final | Entra en `/dashboard/`, con barra |
|
||||
| Aparece "DEMO · SIN NODO" | Falta configurar la URL del Mempool en CONFIG |
|
||||
| La pestaña NODO está vacía | El servicio `txoko-metrics` no corre, o falta el `location /system/` |
|
||||
| Watch-only no aparece | Estás entrando por HTTP; requiere HTTPS |
|
||||
| La app dice que el nodo no responde | Comprueba la API con el `curl` de "Antes de empezar" |
|
||||
|
||||
+11
-74
@@ -1055,27 +1055,8 @@
|
||||
for (const c of str) { if (c === "1") leadingZeros++; else break; }
|
||||
const full = new Uint8Array(leadingZeros + body.length);
|
||||
full.set(body, leadingZeros);
|
||||
return full; // completo: payload + 4 bytes de checksum
|
||||
},
|
||||
|
||||
// Decodifica y COMPRUEBA la checksum. Es async porque el hash lo hace
|
||||
// Web Crypto. Sin esta comprobación, un xpub con un solo carácter mal
|
||||
// copiado se acepta sin protestar y genera direcciones que no son las
|
||||
// del usuario — que entonces vería su cartera "sin actividad" y se
|
||||
// quedaría tranquilo. Un falso negativo silencioso, justo lo que este
|
||||
// proyecto evita en todo lo demás.
|
||||
decodeBase58Check: async (str) => {
|
||||
const full = B32.decodeBase58(str);
|
||||
if (full.length < 5) throw new Error("Cadena demasiado corta para ser una clave extendida válida.");
|
||||
const payload = full.slice(0, full.length - 4);
|
||||
const checksum = full.slice(full.length - 4);
|
||||
const h = await B32.sha256d(payload);
|
||||
for (let i = 0; i < 4; i++) {
|
||||
if (h[i] !== checksum[i]) {
|
||||
throw new Error("La clave no es válida: la suma de verificación no cuadra. Suele ser un carácter mal copiado — revísala y pégala entera.");
|
||||
}
|
||||
}
|
||||
return payload;
|
||||
// full = 82 bytes (78 payload + 4 checksum); devolvemos el payload
|
||||
return full.slice(0, full.length - 4);
|
||||
},
|
||||
|
||||
// SHA256 doble (para checksum base58)
|
||||
@@ -1172,13 +1153,7 @@
|
||||
|
||||
// Derivación BIP32 de clave pública hija (solo clave pública, sin hardened)
|
||||
const deriveChildPubkey = async (parentPub, parentChain, index) => {
|
||||
// Los índices endurecidos (≥ 2³¹) NO se pueden derivar desde una clave
|
||||
// pública: hace falta la privada. Hoy solo se llama con 0 y 1, así que
|
||||
// no es alcanzable, pero una función criptográfica debe defenderse sola.
|
||||
if (!Number.isInteger(index) || index < 0 || index >= 0x80000000) {
|
||||
throw new Error("Índice de derivación inválido: desde una clave pública no se pueden derivar índices endurecidos.");
|
||||
}
|
||||
const indexBytes = B32.u32be(index);
|
||||
const indexBytes = B32.u32be(index); // index < 0x80000000 (no hardened)
|
||||
const data = B32.concat(parentPub, indexBytes);
|
||||
const I = await B32.hmac512(parentChain, data);
|
||||
const IL = I.slice(0, 32);
|
||||
@@ -1297,42 +1272,17 @@
|
||||
};
|
||||
|
||||
// Derivar N direcciones desde xpub (rama 0=recepción, 1=cambio)
|
||||
// Redes por bytes de versión, no por el prefijo del texto. Mirar las
|
||||
// primeras letras parece equivalente y no lo es: un `tpub` —el formato más
|
||||
// habitual de testnet— empieza por "t" pero no por "tb", así que una
|
||||
// comprobación textual lo confunde con mainnet y genera direcciones bc1…
|
||||
// a partir de claves de testnet. Los bytes de versión son inequívocos.
|
||||
const XPUB_VERSIONS = {
|
||||
"0488b21e": { red:"bc", tipo:"xpub" }, "049d7cb2": { red:"bc", tipo:"ypub" },
|
||||
"04b24746": { red:"bc", tipo:"zpub" }, "0295b43f": { red:"bc", tipo:"Ypub" },
|
||||
"02aa7ed3": { red:"bc", tipo:"Zpub" },
|
||||
"043587cf": { red:"tb", tipo:"tpub" }, "044a5262": { red:"tb", tipo:"upub" },
|
||||
"045f1cf6": { red:"tb", tipo:"vpub" }, "024289ef": { red:"tb", tipo:"Upub" },
|
||||
"02575483": { red:"tb", tipo:"Vpub" },
|
||||
};
|
||||
|
||||
const deriveAddresses = async (xpubStr, count=20) => {
|
||||
// decodeBase58Check comprueba la suma de verificación: un carácter mal
|
||||
// copiado se detecta aquí y no acaba generando direcciones ajenas.
|
||||
const raw = await B32.decodeBase58Check(xpubStr.trim());
|
||||
// Decodificar xpub/zpub
|
||||
const raw = B32.decodeBase58(xpubStr.trim());
|
||||
// raw[0..3]=version, [4]=depth, [5..8]=fingerprint, [9..12]=childIndex
|
||||
// [13..44]=chainCode, [45..77]=pubKey
|
||||
if (raw.length !== 78) {
|
||||
throw new Error(`La clave extendida debería ocupar 78 bytes y ocupa ${raw.length}. Parece incompleta o de un formato que no reconozco.`);
|
||||
}
|
||||
const version = Array.from(raw.slice(0,4)).map(b=>b.toString(16).padStart(2,"0")).join("");
|
||||
const info = XPUB_VERSIONS[version];
|
||||
if (!info) throw new Error(`Formato de clave extendida no reconocido (versión ${version}).`);
|
||||
|
||||
const chainCode = raw.slice(13, 45);
|
||||
const pubKey = raw.slice(45, 78);
|
||||
if (pubKey[0] !== 0x02 && pubKey[0] !== 0x03) {
|
||||
throw new Error("La clave pública que contiene no tiene un formato comprimido válido.");
|
||||
}
|
||||
// Parent fingerprint (bytes 5-8): lo que Sparrow muestra junto al keystore
|
||||
const fingerprint = Array.from(raw.slice(5, 9)).map(b => b.toString(16).padStart(2,"0")).join("");
|
||||
|
||||
const hrp = info.red;
|
||||
const hrp = xpubStr.startsWith("tb") || xpubStr.startsWith("u") || xpubStr.startsWith("v") ? "tb" : "bc";
|
||||
|
||||
const result = { receive: [], change: [], fingerprint };
|
||||
for (const [branch, label] of [[0,"receive"],[1,"change"]]) {
|
||||
@@ -2837,22 +2787,11 @@
|
||||
<div key={i} style={{display:"flex",alignItems:"center",justifyContent:"space-between",padding:"5px 0",borderBottom:i<4?`1px solid ${C.border}`:"none"}}>
|
||||
<span style={{fontSize:"0.7rem",fontFamily:"monospace",color:C.blue,width:120,overflow:"hidden",textOverflow:"ellipsis",whiteSpace:"nowrap"}}>{p.command}</span>
|
||||
<div style={{display:"flex",gap:8}}>
|
||||
{/* Se muestra el % del sistema (lo que de verdad se
|
||||
quiere saber: cuánto de la máquina se lleva). El
|
||||
% de un núcleo va en el tooltip, para comparar con
|
||||
top si hace falta. */}
|
||||
<Badge color={p.cpuSistema>25?C.red:p.cpuSistema>10?C.amber:C.t2}>
|
||||
<span title={p.cpu!=null?`${p.cpu}% de un núcleo · media desde que arrancó según ps: ${p.cpuMedia}%`:""}>
|
||||
CPU {p.cpuSistema!=null?p.cpuSistema:p.cpu}%
|
||||
</span>
|
||||
</Badge>
|
||||
<Badge color={C.amber}>CPU {p.cpu}%</Badge>
|
||||
<Badge color={p.mem>30?C.red:p.mem>15?C.amber:C.t2}>RAM {p.mem}%</Badge>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div style={{fontSize:"0.55rem",color:C.t3,fontFamily:"monospace",marginTop:8,lineHeight:1.5}}>
|
||||
CPU sobre el total de la máquina ({sysData.cpu?.count||"?"} núcleos), medida en una ventana de 500 ms — no es la media desde el arranque que muestra <code>ps</code>, que engaña porque un proceso que trabajó mucho hace días sigue apareciendo alto.
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@@ -3582,15 +3521,13 @@
|
||||
if(sorted.length>MAX_UTXOS) setErr(`Mostrando ${MAX_UTXOS} de ${sorted.length} UTXOs — se muestran los de mayor valor.`);
|
||||
|
||||
// Cargar txs de origen para contexto (máx 10, las más grandes)
|
||||
// Vía get() y no fetch directo: así pasa por la caché compartida.
|
||||
// Las transacciones confirmadas son inmutables y se guardan toda la
|
||||
// sesión, de modo que volver a este UTXO Map (o analizar una de estas
|
||||
// txs en otra pestaña) no cuesta ni una petición más al nodo.
|
||||
const cache={};
|
||||
const toFetch=limited.slice(0,10);
|
||||
await Promise.all(toFetch.map(async u=>{
|
||||
const t = await get(`/api/tx/${u.txid}`, null);
|
||||
if (t) cache[u.txid] = t;
|
||||
try{
|
||||
const res=await fetchWithTimeout(`${base}/api/tx/${u.txid}`);
|
||||
if(res.ok) cache[u.txid]=await res.json();
|
||||
}catch{}
|
||||
}));
|
||||
|
||||
setTxCache(cache);
|
||||
|
||||
+2
-46
@@ -168,33 +168,10 @@ async function getDisk() {
|
||||
}
|
||||
|
||||
// ── Procesos destacados (nombre limpio del binario) ────────────────────────
|
||||
// Tiempo de CPU consumido por un proceso, en ticks, desde /proc/PID/stat.
|
||||
// Campos 14 (utime) y 15 (stime). El nombre del proceso va entre paréntesis y
|
||||
// puede contener espacios, así que se corta por el ÚLTIMO ')' antes de partir.
|
||||
function readProcCpuTicks(pid) {
|
||||
try {
|
||||
const stat = fs.readFileSync(`/proc/${pid}/stat`, "utf8");
|
||||
const resto = stat.slice(stat.lastIndexOf(")") + 2).split(" ");
|
||||
// resto[0] es el campo 3 (estado), así que utime=campo14 → resto[11]
|
||||
const utime = parseInt(resto[11], 10);
|
||||
const stime = parseInt(resto[12], 10);
|
||||
if (Number.isNaN(utime) || Number.isNaN(stime)) return null;
|
||||
return utime + stime;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
// OJO con la columna %CPU de `ps`: NO es el consumo actual, sino la media del
|
||||
// proceso desde que arrancó (tiempo de CPU / tiempo de vida). Un proceso que
|
||||
// trabajó mucho al principio y ahora está ocioso sigue mostrando un número
|
||||
// alto días después, y se lee como si estuviera saturando la máquina.
|
||||
// Aquí se mide de verdad: dos lecturas de /proc separadas 500 ms, el mismo
|
||||
// método que ya usa getCpuUsage para el total del sistema. Las dos esperas
|
||||
// corren en paralelo (van dentro del mismo Promise.all), así que no cuesta
|
||||
// tiempo extra.
|
||||
async function getProcesses() {
|
||||
const result = await sh("ps aux --no-headers --sort=-%mem | head -8");
|
||||
if (!result) return [];
|
||||
const filas = result.split("\n").map(line => {
|
||||
return result.split("\n").map(line => {
|
||||
const parts = line.trim().split(/\s+/);
|
||||
// Nombre limpio: basename del ejecutable; si es un intérprete (node,
|
||||
// python...), añade el basename del script que ejecuta.
|
||||
@@ -213,33 +190,12 @@ async function getProcesses() {
|
||||
}
|
||||
command = command.slice(0, 24);
|
||||
return {
|
||||
pid: parts[1],
|
||||
user: parts[0],
|
||||
cpuMedia: parseFloat(parts[2]), // media desde el arranque (lo que da ps)
|
||||
cpu: parseFloat(parts[2]),
|
||||
mem: parseFloat(parts[3]),
|
||||
command,
|
||||
};
|
||||
}).filter(p => p.mem > 0.5);
|
||||
|
||||
const ticks1 = filas.map(p => readProcCpuTicks(p.pid));
|
||||
await sleep(500);
|
||||
const ticks2 = filas.map(p => readProcCpuTicks(p.pid));
|
||||
|
||||
const USER_HZ = 100; // estándar en Linux
|
||||
const VENTANA_S = 0.5;
|
||||
const nucleos = os.cpus().length || 1;
|
||||
|
||||
return filas.map((p, i) => {
|
||||
let cpu = null, cpuSistema = null;
|
||||
if (ticks1[i] !== null && ticks2[i] !== null) {
|
||||
const seg = (ticks2[i] - ticks1[i]) / USER_HZ;
|
||||
// % de UN núcleo (criterio de top/ps: puede pasar de 100 si va en varios)
|
||||
cpu = Math.max(0, Math.round((seg / VENTANA_S) * 1000) / 10);
|
||||
// % del total de la máquina, que es lo que suele querer saberse
|
||||
cpuSistema = Math.round((cpu / nucleos) * 10) / 10;
|
||||
}
|
||||
return { user: p.user, command: p.command, mem: p.mem, cpu, cpuSistema, cpuMedia: p.cpuMedia };
|
||||
});
|
||||
}
|
||||
|
||||
// ── Load / Uptime ───────────────────────────────────────────────────────────
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
# Pruebas de la criptografía
|
||||
|
||||
Verifican la derivación watch-only (BIP32, secp256k1, RIPEMD-160, bech32)
|
||||
contra los **vectores oficiales de los estándares**, no contra resultados
|
||||
propios. Si un cambio rompe algo, estas pruebas lo dicen.
|
||||
|
||||
## Cómo ejecutarlas
|
||||
|
||||
Extraer el bloque criptográfico de `dashboard.html` a `crypto.js` (las líneas
|
||||
que van desde `const B32 = {` hasta el final de `deriveAddresses`), añadir al
|
||||
principio `const { webcrypto } = require("crypto"); const crypto = webcrypto;`
|
||||
y al final la exportación:
|
||||
|
||||
module.exports = { B32, SECP, deriveChildPubkey, ripemd160, hash160, toBech32, deriveAddresses };
|
||||
|
||||
Después:
|
||||
|
||||
node test1.js # RIPEMD-160 y aritmética de curva
|
||||
node test2.js # BIP32 y bech32 contra vectores oficiales
|
||||
node test3.js # búsqueda de casos borde (diagnóstico)
|
||||
node test4.js # regresión de los fallos ya corregidos
|
||||
|
||||
## Qué cubren
|
||||
|
||||
- **test1** — RIPEMD-160 con los seis vectores del estándar (incluido el de un
|
||||
millón de caracteres), generador de secp256k1, múltiplos conocidos, y que
|
||||
comprimir y descomprimir un punto sea reversible.
|
||||
- **test2** — BIP32: clave pública y chain code de la raíz, y derivación no
|
||||
endurecida `m/0`, contra los vectores 1 y 2 del propio BIP32. bech32: la
|
||||
dirección P2WPKH del generador, en mainnet y testnet (BIP173).
|
||||
- **test3** — sondeo de casos borde. Fue el que encontró los cuatro fallos de
|
||||
validación corregidos el 2026-07-27.
|
||||
- **test4** — comprueba que esos cuatro siguen cerrados: checksum rota, xpub
|
||||
truncado, índice endurecido, índice negativo. Y que un `tpub` genera
|
||||
direcciones de testnet, no de mainnet.
|
||||
|
||||
## Lo que estas pruebas NO cubren
|
||||
|
||||
La matemática es correcta, pero eso no es una auditoría. No cubren análisis
|
||||
formal ni una revisión independiente: quien las escribió conoce la
|
||||
implementación y comparte sus supuestos, que es justo el sesgo que rompe un
|
||||
revisor externo. Siguen haciendo falta ojos de fuera antes de difundir el
|
||||
proyecto ampliamente.
|
||||
|
||||
Tampoco aplican aquí los ataques de canal lateral: todo esto maneja **solo
|
||||
claves públicas**. No hay secreto que filtrar; lo único que importa es que el
|
||||
resultado sea correcto, y eso es lo que se comprueba.
|
||||
@@ -1,44 +0,0 @@
|
||||
// WebCrypto en Node
|
||||
const { webcrypto } = require('crypto');
|
||||
global.crypto = webcrypto;
|
||||
const fs=require("fs");
|
||||
const { B32, SECP, deriveChildPubkey, ripemd160, hash160, toBech32, deriveAddresses } = require('/tmp/cripto/crypto.js');
|
||||
|
||||
const hex = a => Array.from(a).map(b=>b.toString(16).padStart(2,'0')).join('');
|
||||
let pass=0, fail=0;
|
||||
const check=(nombre, got, want)=>{
|
||||
const ok = got===want;
|
||||
console.log(` ${ok?'✓':'✗'} ${nombre}`);
|
||||
if(!ok){ console.log(` obtenido: ${got}`); console.log(` esperado: ${want}`); fail++; } else pass++;
|
||||
};
|
||||
|
||||
(async () => {
|
||||
console.log("=== 1. RIPEMD-160 (vectores del estándar) ===");
|
||||
const enc = s => new TextEncoder().encode(s);
|
||||
check('""', hex(ripemd160(enc(""))), "9c1185a5c5e9fc54612808977ee8f548b2258d31");
|
||||
check('"a"', hex(ripemd160(enc("a"))), "0bdc9d2d256b3ee9daae347be6f4dc835a467ffe");
|
||||
check('"abc"', hex(ripemd160(enc("abc"))), "8eb208f7e05d987a9b044a8e98c6b087f15a0bfc");
|
||||
check('"message digest"', hex(ripemd160(enc("message digest"))), "5d0689ef49d2fae572b881b123a85ffa21595f36");
|
||||
check('abcdefghijklmnopqrstuvwxyz', hex(ripemd160(enc("abcdefghijklmnopqrstuvwxyz"))), "f71c27109c692c1b56bbdceb5b9d2865b3708dbc");
|
||||
check('1M x "a"', hex(ripemd160(enc("a".repeat(1000000)))), "52783243c1697bdbe16d37f97f68f08325dc1528");
|
||||
|
||||
console.log("\n=== 2. secp256k1: G y múltiplos conocidos ===");
|
||||
const G = SECP.G;
|
||||
check('G comprimido', hex(SECP.compress(G)), "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798");
|
||||
check('2G', hex(SECP.compress(SECP.mulPoint(2n, G))), "02c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5");
|
||||
check('3G', hex(SECP.compress(SECP.mulPoint(3n, G))), "02f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9");
|
||||
// n-1 * G = -G (mismo x, y opuesta)
|
||||
const nm1 = SECP.mulPoint(SECP.N - 1n, G);
|
||||
check('(n-1)G tiene la x de G', nm1[0].toString(16), G[0].toString(16));
|
||||
check('(n-1)G tiene y opuesta', ((nm1[1] + G[1]) % SECP.P).toString(), "0");
|
||||
|
||||
console.log("\n=== 3. compress → decompress (ida y vuelta) ===");
|
||||
for (const k of [1n, 2n, 7n, 12345n, 0xdeadbeefn]) {
|
||||
const pt = SECP.mulPoint(k, G);
|
||||
const c = SECP.compress(pt);
|
||||
const d = SECP.decompress(c);
|
||||
check(`k=${k}`, hex(SECP.compress(d)), hex(c));
|
||||
}
|
||||
|
||||
console.log(`\nRESULTADO: ${pass} correctas, ${fail} incorrectas`);
|
||||
})();
|
||||
@@ -1,40 +0,0 @@
|
||||
const { B32, SECP, deriveChildPubkey, ripemd160, hash160, toBech32, deriveAddresses } = require('/tmp/cripto/crypto.js');
|
||||
const hex = a => Array.from(a).map(b=>b.toString(16).padStart(2,'0')).join('');
|
||||
let pass=0, fail=0;
|
||||
const check=(n,g,w)=>{ const ok=g===w; console.log(` ${ok?'✓':'✗'} ${n}`); if(!ok){console.log(` obtenido: ${g}`);console.log(` esperado: ${w}`);fail++;}else pass++; };
|
||||
|
||||
(async () => {
|
||||
// ── BIP32, vectores oficiales del estándar ──
|
||||
// Vector 1: seed 000102...0e0f
|
||||
// m/0/1 derivado SOLO con clave pública (derivación no endurecida)
|
||||
console.log("=== 4. BIP32 — vector 1 oficial, derivación pública ===");
|
||||
// xpub de m (raíz) del vector 1
|
||||
const M = "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8";
|
||||
const raw = B32.decodeBase58(M);
|
||||
const chain = raw.slice(13,45), pub = raw.slice(45,78);
|
||||
check("clave pública de m", hex(pub), "0339a36013301597daef41fbe593a02cc513d0b55527ec2df1050e2e8ff49c85c2");
|
||||
check("chain code de m", hex(chain), "873dff81c02f525623fd1fe5167eac3a55a049de3d314bb42ee227ffed37d508");
|
||||
|
||||
// m/0 → xpub esperado del estándar
|
||||
const m0 = await deriveChildPubkey(pub, chain, 0);
|
||||
// del vector oficial: xpub de m/0'/1 ... usamos m/0 no endurecido del vector 2
|
||||
console.log("\n=== 5. BIP32 — vector 2 oficial (m/0, no endurecida) ===");
|
||||
const M2 = "xpub661MyMwAqRbcFW31YEwpkMuc5THy2PSt5bDMsktWQcFF8syAmRUapSCGu8ED9W6oDMSgv6Zz8idoc4a6mr8BDzTJY47LJhkJ8UB7WEGuduB";
|
||||
const r2 = B32.decodeBase58(M2);
|
||||
const c2 = r2.slice(13,45), p2 = r2.slice(45,78);
|
||||
const d2 = await deriveChildPubkey(p2, c2, 0);
|
||||
check("m/0 clave pública", hex(d2.pub), "02fc9e5af0ac8d9b3cecfe2a888e2117ba3d089d8585886c9c826b6b22a98d12ea");
|
||||
check("m/0 chain code", hex(d2.chain), "f0909affaa7ee7abe5dd4e100598d4dc53cd709d5a5c2cac40e7412f232f7c9c");
|
||||
|
||||
// m/0/2147483647 no se puede (endurecida). Probamos m/0/1 del vector 2:
|
||||
const d3 = await deriveChildPubkey(d2.pub, d2.chain, 1);
|
||||
console.log("\n=== 6. bech32 — vectores oficiales BIP173 ===");
|
||||
// P2WPKH conocido: pubkey 0279be66... → bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4
|
||||
const pk = Uint8Array.from(Buffer.from("0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798","hex"));
|
||||
const h = await hash160(pk);
|
||||
check("hash160 de G", hex(h), "751e76e8199196d454941c45d1b3a323f1433bd6");
|
||||
check("dirección P2WPKH", toBech32("bc", Array.from(h)), "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4");
|
||||
check("misma en testnet", toBech32("tb", Array.from(h)), "tb1qw508d6qejxtdg4y5r3zarvary0c5xw7kxpjzsx");
|
||||
|
||||
console.log(`\nRESULTADO: ${pass} correctas, ${fail} incorrectas`);
|
||||
})();
|
||||
@@ -1,63 +0,0 @@
|
||||
const { B32, SECP, deriveChildPubkey, ripemd160, hash160, toBech32, deriveAddresses } = require('/tmp/cripto/crypto.js');
|
||||
const hex = a => Array.from(a).map(b=>b.toString(16).padStart(2,'0')).join('');
|
||||
let hallazgos=[];
|
||||
(async () => {
|
||||
|
||||
console.log("=== 7. ¿Se verifica la checksum del xpub? ===");
|
||||
// xpub válido con UN carácter cambiado al final (checksum rota)
|
||||
const bueno = "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8";
|
||||
const malo = bueno.slice(0,-1) + (bueno.slice(-1)==="8" ? "9" : "8");
|
||||
try {
|
||||
const r = B32.decodeBase58(malo);
|
||||
console.log(" ✗ ACEPTA un xpub con checksum inválida — no la comprueba");
|
||||
hallazgos.push({sev:"medio", t:"No se verifica la checksum base58 del xpub", d:"decodeBase58 descarta los 4 bytes de checksum sin comprobarlos. Un xpub mal copiado (un carácter cambiado) se acepta y genera direcciones que no son las del usuario."});
|
||||
} catch(e) { console.log(" ✓ rechaza:", e.message); }
|
||||
|
||||
console.log("\n=== 8. ¿Se valida la longitud del xpub? ===");
|
||||
try {
|
||||
const r = B32.decodeBase58("xpub661MyMwAqRbcFtXgS5sYJ"); // truncado
|
||||
console.log(` ✗ ACEPTA un xpub truncado → ${r.length} bytes (deberían ser 78)`);
|
||||
hallazgos.push({sev:"medio", t:"No se valida la longitud del xpub decodificado", d:"Un xpub truncado produce un array corto; chainCode y pubKey salen vacíos o parciales y la derivación falla de forma confusa o produce basura."});
|
||||
} catch(e){ console.log(" ✓ rechaza:", e.message); }
|
||||
|
||||
console.log("\n=== 9. ¿Se valida que la clave pública esté en la curva? ===");
|
||||
// Punto que NO está en secp256k1: x=1 no tiene y entera para y²=x³+7 → 8 no es residuo
|
||||
try {
|
||||
const falso = Uint8Array.from([2, ...new Array(31).fill(0), 1]); // x=1
|
||||
const pt = SECP.decompress(falso);
|
||||
const enCurva = (pt[1]*pt[1] - (pt[0]**3n + 7n)) % SECP.P === 0n;
|
||||
if (!enCurva) {
|
||||
console.log(" ✗ decompress DEVUELVE un punto que no está en la curva (no valida)");
|
||||
hallazgos.push({sev:"medio", t:"decompress no comprueba que el punto esté en la curva", d:"Con una x que no corresponde a ningún punto de secp256k1, devuelve un par (x,y) inválido en vez de fallar. La derivación seguiría y produciría direcciones sin sentido. Solo alcanzable con un xpub manipulado."});
|
||||
} else console.log(" ✓ el punto resultante sí está en la curva");
|
||||
} catch(e){ console.log(" ✓ rechaza:", e.message); }
|
||||
|
||||
console.log("\n=== 10. Índices endurecidos (no derivables desde xpub) ===");
|
||||
const raw=B32.decodeBase58(bueno), ch=raw.slice(13,45), pb=raw.slice(45,78);
|
||||
try {
|
||||
await deriveChildPubkey(pb, ch, 0x80000000);
|
||||
console.log(" ✗ ACEPTA un índice endurecido — matemáticamente imposible desde una clave pública");
|
||||
hallazgos.push({sev:"bajo", t:"No se rechaza el índice endurecido en deriveChildPubkey", d:"Un índice ≥ 0x80000000 no se puede derivar desde una clave pública. Hoy no se llama nunca con esos valores (deriveAddresses usa 0 y 1), así que no es explotable, pero la función no se defiende sola."});
|
||||
} catch(e){ console.log(" ✓ rechaza:", e.message); }
|
||||
|
||||
console.log("\n=== 11. Consistencia: 100 direcciones seguidas ===");
|
||||
const a = await deriveAddresses(bueno, 100);
|
||||
const todas = [...a.receive, ...a.change];
|
||||
const unicas = new Set(todas);
|
||||
console.log(` direcciones generadas: ${todas.length}, únicas: ${unicas.size}`);
|
||||
console.log(` todas empiezan por bc1q: ${todas.every(x=>x.startsWith("bc1q"))}`);
|
||||
console.log(` longitud correcta (42): ${todas.every(x=>x.length===42)}`);
|
||||
if (unicas.size !== todas.length) hallazgos.push({sev:"alto", t:"Direcciones duplicadas en la derivación", d:"Dos índices distintos producen la misma dirección."});
|
||||
|
||||
console.log("\n=== 12. Detección de red (mainnet vs testnet) ===");
|
||||
for (const [p,esperado] of [["xpub","bc"],["zpub","bc"],["ypub","bc"],["tpub","?"],["vpub","tb"],["upub","tb"]]) {
|
||||
const hrp = p.startsWith("tb")||p.startsWith("u")||p.startsWith("v") ? "tb":"bc";
|
||||
const marca = (p==="tpub" && hrp==="bc") ? " ✗" : " ·";
|
||||
console.log(`${marca} ${p} → ${hrp}`);
|
||||
}
|
||||
hallazgos.push({sev:"medio", t:"tpub (testnet) se trata como mainnet", d:"La detección mira si empieza por 'tb', 'u' o 'v'. Un tpub —el formato más común de testnet— empieza por 't' y NO por 'tb', así que cae en la rama de mainnet y genera direcciones bc1... a partir de claves de testnet."});
|
||||
|
||||
console.log("\n\n════ HALLAZGOS ════");
|
||||
for (const h of hallazgos) console.log(`\n[${h.sev.toUpperCase()}] ${h.t}\n ${h.d}`);
|
||||
if (!hallazgos.length) console.log("ninguno");
|
||||
})();
|
||||
@@ -1,42 +0,0 @@
|
||||
const { B32, SECP, deriveChildPubkey, deriveAddresses } = require('/tmp/cripto/crypto.js');
|
||||
let ok=0, ko=0;
|
||||
const debeFallar = async (nombre, fn) => {
|
||||
try { await fn(); console.log(` ✗ ${nombre} — NO falló`); ko++; }
|
||||
catch(e){ console.log(` ✓ ${nombre}\n → "${e.message}"`); ok++; }
|
||||
};
|
||||
(async () => {
|
||||
const bueno = "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8";
|
||||
|
||||
console.log("=== los cuatro hallazgos, revisados ===\n");
|
||||
await debeFallar("checksum rota (un carácter cambiado)", () =>
|
||||
deriveAddresses(bueno.slice(0,-1) + (bueno.slice(-1)==="8"?"9":"8"), 2));
|
||||
await debeFallar("xpub truncado", () => deriveAddresses("xpub661MyMwAqRbcFtXgS5sYJ", 2));
|
||||
await debeFallar("índice endurecido", async () => {
|
||||
const raw = await B32.decodeBase58Check(bueno);
|
||||
return deriveChildPubkey(raw.slice(45,78), raw.slice(13,45), 0x80000000);
|
||||
});
|
||||
await debeFallar("índice negativo", async () => {
|
||||
const raw = await B32.decodeBase58Check(bueno);
|
||||
return deriveChildPubkey(raw.slice(45,78), raw.slice(13,45), -1);
|
||||
});
|
||||
|
||||
console.log("\n=== el xpub bueno sigue funcionando ===");
|
||||
const a = await deriveAddresses(bueno, 3);
|
||||
console.log(" recepción:", a.receive.join(", "));
|
||||
console.log(" huella:", a.fingerprint);
|
||||
const bien = a.receive.every(x=>x.startsWith("bc1q")&&x.length===42);
|
||||
console.log(` ${bien?'✓':'✗'} formato correcto`); bien?ok++:ko++;
|
||||
|
||||
console.log("\n=== testnet: tpub ahora se detecta bien ===");
|
||||
|
||||
// vector 1 del BIP32 con los bytes de versión de testnet — dato público, no de nadie
|
||||
const tp = "tpubD6NzVbkrYhZ4XgiXtGrdW5XDAPFCL9h7we1vwNCpn8tGbBcgfVYjXyhWo4E1xkh56hjod1RhGjxbaTLV3X4FyWuejifB9jusQ46QzG87VKp";
|
||||
try {
|
||||
const t = await deriveAddresses(tp, 2);
|
||||
const esTb = t.receive.every(x=>x.startsWith("tb1q"));
|
||||
console.log(` ${esTb?'✓':'✗'} genera direcciones de testnet: ${t.receive[0]}`);
|
||||
esTb?ok++:ko++;
|
||||
} catch(e){ console.log(" ✗ falló:", e.message); ko++; }
|
||||
|
||||
console.log(`\nRESULTADO: ${ok} correctas, ${ko} incorrectas`);
|
||||
})();
|
||||
@@ -4,15 +4,8 @@ After=network.target bitcoind.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
|
||||
# ── AJUSTA ESTAS DOS LÍNEAS ANTES DE INSTALARLO ──────────────────────────
|
||||
# User: el usuario con el que corre tu nodo (el mismo que ejecuta bitcoind
|
||||
# suele ser buena elección). No lo dejes en root.
|
||||
# ExecStart: la ruta donde hayas copiado system-metrics.js.
|
||||
User=TU_USUARIO
|
||||
ExecStart=/usr/bin/node /home/TU_USUARIO/txoko/system-metrics.js
|
||||
# ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
User=armg
|
||||
ExecStart=/usr/bin/node /home/armg/txoko/system-metrics.js
|
||||
Restart=on-failure
|
||||
RestartSec=5
|
||||
StandardOutput=journal
|
||||
|
||||
Reference in New Issue
Block a user