diff --git a/crates/origin-chain-node/src/main.rs b/crates/origin-chain-node/src/main.rs index 8bb62a0..eeb3312 100644 --- a/crates/origin-chain-node/src/main.rs +++ b/crates/origin-chain-node/src/main.rs @@ -243,6 +243,29 @@ fn print_preset(id: &str) { /// JSON canonical via [`chain_spec::serialize_chain_spec`]. Exit code canonical 1 /// si chain id canonical unknown o JSON serialization canonical fails. fn build_spec(chain: &str, raw: bool) { + // Con el WASM real, el chain spec que se emite es el MISMO con el que arranca `run` + // (`node_chain_spec`), y `--raw` lleva el génesis entero, `:code` incluido. Es lo que se + // reparte para que otro nodo se una a la red: el WASM no es reproducible entre máquinas + // (Mac y Linux dan génesis distintos con el mismo commit, medido el 2026-10-24), así que + // el génesis lo fija el chain spec, no quien compila. + if origin_chain_runtime::WASM_BINARY.is_some() + && !matches!(chain, "mainnet" | "mainnet_genesis") + { + match chain_spec::node_chain_spec(chain).and_then(|spec| spec.as_json(raw)) { + Ok(json) => println!("{json}"), + Err(e) => { + eprintln!("❌ Chain spec no resuelto: {e}"); + std::process::exit(1); + } + } + return; + } + if origin_chain_runtime::WASM_BINARY.is_none() { + eprintln!( + "⚠ binario sin el WASM del runtime: este chain spec lleva un WASM SIMULADO (\"0x\") \ + y no sirve para arrancar ni para unirse a una red (--features wasm-build)." + ); + } match chain_spec::load_spec(chain) { Ok(spec) => match chain_spec::serialize_chain_spec(&spec, raw) { Ok(json) => println!("{}", json), diff --git a/crates/origin-chain-runtime/examples/firmar_como_wallet.rs b/crates/origin-chain-runtime/examples/firmar_como_wallet.rs index 8a2884a..14ad01f 100644 --- a/crates/origin-chain-runtime/examples/firmar_como_wallet.rs +++ b/crates/origin-chain-runtime/examples/firmar_como_wallet.rs @@ -17,6 +17,8 @@ //! compilado. Tras un `set_code` hay que pasarlos: el wallet firma con la versión //! nueva que anuncia el nodo, no con la que se compiló aquí. //! eventos decodifica los eventos de un bloque +//! cuenta "" [//derivación] llave sr25519 y dirección SS58 (42) que verá un wallet +//! Substrate (substrate-bip39 → mini-secret → sr25519): vectores para la wallet Origin! //! verificar pasa la firma por el check del runtime nativo //! sobre el preset development (sin nodo) //! ``` @@ -124,6 +126,14 @@ fn main() { println!("{:?}", e.event); } } + "cuenta" => { + use sp_core::crypto::Ss58Codec; + let uri = format!("{}{}", args[2], args.get(3).map(String::as_str).unwrap_or("")); + let par = sr25519::Pair::from_string(&uri, None).expect("frase o derivación inválida"); + let publica = par.public(); + println!("sr25519 publica 0x{}", sp_core::hexdisplay::HexDisplay::from(&publica.0)); + println!("ss58 (42) {}", publica.to_ss58check()); + } "verificar" => { use sp_runtime::traits::Checkable; let bytes = hex(&args[2]); @@ -145,6 +155,6 @@ fn main() { println!("check del runtime: {:?}", r.map(|_| "firma aceptada")); }); } - _ => panic!("modo: firmar · eventos · verificar (ver la cabecera del fichero)"), + _ => panic!("modo: firmar · eventos · verificar · cuenta (ver la cabecera del fichero)"), } } diff --git a/scripts/bench/max-de-n.py b/scripts/bench/max-de-n.py new file mode 100755 index 0000000..6941fb2 --- /dev/null +++ b/scripts/bench/max-de-n.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +"""Funde N weights.rs del MISMO pallet en uno que cobra el MÁXIMO de los N procesos. + + python3 scripts/bench/max-de-n.py salida.rs proceso1.rs proceso2.rs ... (N >= 2) + +Por qué: Laurha midió el 2026-10-24 en la Pi 5, dentro del santuario (núcleo aislado, +governor performance, CI parada), que 8 procesos idénticos de frame-omni-bencher dan +base weights que se separan hasta un 66 % (revoke_validator: 18,8 a 31,3 µs), mientras el +mínimo apenas se mueve. Un weight cobra el caso malo, y el caso malo existe: se cobra el +máximo de N >= 5 procesos, no la media ni la mediana. + +Qué hace: +- Exige que los N ficheros tengan el MISMO esqueleto: todo el texto salvo los números del + cuerpo, desde `pub trait WeightInfo`. Si un fichero trae otra lista de extrinsics, otro + acceso a storage u otra plantilla, se niega. Mezclar pallets o plantillas no es un máximo. +- `Weight::from_parts(a, b)`: toma el máximo de cada coeficiente. +- `reads(n)` y `writes(n)`: deben ser IGUALES en los N (cuentan accesos, no tiempo). Si no lo + son, se niega. +- Los comentarios numéricos del cuerpo: «Medido» y «Estimado» toman el máximo; «Tiempo + mínimo» toma el mínimo. +- Cabecera: la del primer fichero, más una línea que declara N y el sha256 de cada entrada. +""" +import hashlib, re, sys + +NUM = re.compile(r"\d(?:[\d_]*\d)?") # «7_u64» → 7; nunca se come el guion bajo final +INICIO_CUERPO = "pub trait WeightInfo" + + +def partir(texto): + i = texto.index(INICIO_CUERPO) + return texto[:i], texto[i:] + + +def esqueleto(cuerpo): + return NUM.sub("#", cuerpo) + + +def numeros(cuerpo): + return [(m.start(), m.end(), int(m.group().replace("_", ""))) for m in NUM.finditer(cuerpo)] + + +def contexto(cuerpo, inicio): + """La línea donde cae un número, para decidir la regla.""" + a = cuerpo.rfind("\n", 0, inicio) + 1 + b = cuerpo.find("\n", inicio) + return cuerpo[a:b if b != -1 else len(cuerpo)] + + +def con_guiones(n): + s = str(n) + partes = [] + while s: + partes.insert(0, s[-3:]) + s = s[:-3] + return "_".join(partes) + + +def main(): + if len(sys.argv) < 4: + sys.exit(__doc__) + salida, entradas = sys.argv[1], sys.argv[2:] + if len(entradas) < 5: + print(f"AVISO: N = {len(entradas)} < 5. La regla de la casa (Laurha, 24-oct) cobra el máximo de N >= 5 procesos.", file=sys.stderr) + textos = [open(p).read() for p in entradas] + cabecera, cuerpo0 = partir(textos[0]) + cuerpos = [partir(t)[1] for t in textos] + sk = esqueleto(cuerpo0) + for p, c in zip(entradas, cuerpos): + if esqueleto(c) != sk: + sys.exit(f"RECHAZO: {p} no tiene el mismo esqueleto que {entradas[0]} (otra lista de extrinsics, storage o plantilla)") + listas = [numeros(c) for c in cuerpos] + out, cursor = [], 0 + for k, (ini, fin, _) in enumerate(listas[0]): + valores = [lst[k][2] for lst in listas] + linea = contexto(cuerpo0, ini) + if "reads(" in linea or "writes(" in linea: + if len(set(valores)) != 1: + sys.exit(f"RECHAZO: los conteos de storage difieren entre procesos en «{linea.strip()}»: {valores}") + v = valores[0] + elif "minimo" in linea or "mínimo" in linea: + v = min(valores) + else: + v = max(valores) + crudo = cuerpo0[ini:fin] + nuevo = con_guiones(v) if "_" in crudo else str(v) # conserva el formato del literal + out.append(cuerpo0[cursor:ini]) + out.append(nuevo) + cursor = fin + out.append(cuerpo0[cursor:]) + shas = " ".join(hashlib.sha256(t.encode()).hexdigest()[:12] for t in textos) + nota = (f"// MAXIMO DE N = {len(entradas)} PROCESOS (scripts/bench/max-de-n.py): cada coeficiente\n" + f"// de tiempo y de proof es el maximo de los N; reads/writes iguales en los N.\n" + f"// Entradas (sha256[:12]): {shas}\n\n") + with open(salida, "w") as f: + f.write(cabecera + nota + "".join(out)) + print(f"{salida}: máximo de {len(entradas)} procesos, {len(listas[0])} números revisados") + + +main() diff --git a/scripts/rp1/Dockerfile.aarch64 b/scripts/rp1/Dockerfile.aarch64 new file mode 100644 index 0000000..b89c309 --- /dev/null +++ b/scripts/rp1/Dockerfile.aarch64 @@ -0,0 +1,7 @@ +# Compila origin-chain-node para la Raspberry Pi 5 (linux/aarch64) en un Mac Apple Silicon. +# Bookworm (glibc 2.36): el binario corre en cualquier Debian con glibc >= 2.36 (la rp1 tiene 2.41). +FROM rust:1.88-bookworm +RUN apt-get update \ + && apt-get install -y --no-install-recommends clang libclang-dev llvm protobuf-compiler cmake \ + && rm -rf /var/lib/apt/lists/* +RUN rustup target add wasm32v1-none wasm32-unknown-unknown && rustup component add rust-src diff --git a/scripts/rp1/arrancar.sh b/scripts/rp1/arrancar.sh new file mode 100755 index 0000000..fccbb78 --- /dev/null +++ b/scripts/rp1/arrancar.sh @@ -0,0 +1,46 @@ +#!/usr/bin/env bash +# Arranca el nodo de Origin Chain en la rp1 si no está corriendo. Idempotente: lo llaman +# cron @reboot y el vigilante cada 5 min. Sin root. +# +# Convivencia con la casa (la rp1 es también el runner de CI y el banco de Laurha): +# - taskset -c 0-2: el nodo NUNCA toca el núcleo 3, que es el santuario de origin-banco. +# - nice 10 + ionice best-effort 7: la CI tiene prioridad sobre el nodo. +# - RPC sólo en 127.0.0.1: se entra por túnel SSH. P2P en 30333 para la LAN. +# +# Cadena: `dev` (Alice, llaves de desarrollo PÚBLICAS). Es la testnet de la casa, no la +# testnet pública: ningún valor real vive aquí. +set -euo pipefail +BASE="$HOME/origin-chain" +BIN="$BASE/bin/origin-chain-node" +PIDF="$BASE/nodo.pid" +mkdir -p "$BASE/data" "$BASE/logs" + +if [ -f "$PIDF" ] && kill -0 "$(cat "$PIDF")" 2>/dev/null; then + exit 0 +fi + +LOG="$BASE/logs/nodo-$(date +%Y%m%d-%H%M%S).log" +# Un validador fuera de --dev exige una llave de red estable, y este CLI no trae `key`. Se +# genera UNA vez (la vía 4 del propio nodo) y queda en el base-path; en los arranques +# siguientes la bandera no se pasa, así que el peer id del bootnode no cambia nunca. +LLAVE_RED="$BASE/data/chains/origin_chain_dev/network/secret_ed25519" +EXTRA=() +[ -f "$LLAVE_RED" ] || EXTRA=(--unsafe-force-node-key-generation) +# El génesis lo fija casa.json (chain spec raw con el :code dentro), no el binario: el WASM no +# es reproducible entre máquinas, y con `--chain dev` un binario nuevo crearía OTRO génesis +# sobre los mismos datos. Sin casa.json (el primer arranque) se usa el `dev` embebido; después +# se exporta con `bin/origin-chain-node build-spec --chain dev --raw > casa.json`. +CADENA=dev +[ -f "$BASE/casa.json" ] && CADENA="$BASE/casa.json" +nohup taskset -c 0-2 nice -n 10 ionice -c2 -n7 "$BIN" run \ + --chain "$CADENA" \ + --validator --alice \ + --name rp1-origin-casa \ + --base-path "$BASE/data" \ + --port 30333 \ + --rpc-port 9944 --rpc-methods safe \ + --no-telemetry --no-prometheus "${EXTRA[@]}" \ + >"$LOG" 2>&1 & +echo $! >"$PIDF" +ln -sfn "$LOG" "$BASE/logs/actual.log" +echo "$(date -Is) arrancado pid $(cat "$PIDF") log $LOG" >>"$BASE/logs/arranques.log" diff --git a/scripts/rp1/construir-aarch64.sh b/scripts/rp1/construir-aarch64.sh new file mode 100755 index 0000000..efe87ea --- /dev/null +++ b/scripts/rp1/construir-aarch64.sh @@ -0,0 +1,38 @@ +#!/usr/bin/env bash +# Compila origin-chain-node (release, con el WASM real) para la rp1, Raspberry Pi 5 linux/aarch64, +# en un Mac Apple Silicon con Docker (linux/arm64 corre nativo, sin emulación). +# +# scripts/rp1/construir-aarch64.sh +# +# Por qué así (medido el 2026-10-24): +# - La rp1 no compila Substrate: es el runner de CI de la casa y tiene 8 GB. +# - Imagen bookworm (glibc 2.36): el binario corre en la rp1 (Debian 13, glibc 2.41). +# - El target vive en un VOLUMEN de Docker y no en el disco externo de compilación: la VM de +# Docker Desktop no ve /Volumes/MacOSBukup si el disco se montó después de arrancar Docker +# («not a directory»), y reiniciar Docker tiraría los contenedores de otras hermanas. +# Al acabar se copia el binario fuera y se borra el volumen. +# - 4 jobs: la VM tiene 8 GB compartidos con las bases de datos de otras hermanas. +# - WASM_BUILD_WORKSPACE_HINT=/src: substrate-wasm-builder busca Cargo.lock subiendo desde el +# target; con el target fuera del workspace no lo encuentra, resuelve dependencias nuevas +# (que piden rustc 1.90) y el build del runtime falla. +set -euo pipefail +DESTINO="${1:?carpeta destino}" +RAIZ="$(cd "$(dirname "$0")/../.." && pwd)" +IMG=origin-chain-builder:1.88-bookworm +docker image inspect "$IMG" >/dev/null 2>&1 || docker build -q -t "$IMG" -f "$RAIZ/scripts/rp1/Dockerfile.aarch64" "$RAIZ/scripts/rp1" +docker volume create charon-arm64-target >/dev/null +docker volume create charon-arm64-registry >/dev/null +docker run --rm --name charon-aarch64 \ + -v "$RAIZ":/src:ro -v charon-arm64-target:/target -v charon-arm64-registry:/usr/local/cargo/registry \ + -e CARGO_TARGET_DIR=/target -e WASM_BUILD_WORKSPACE_HINT=/src -e WASM_BUILD_STD=0 -e CARGO_BUILD_JOBS=4 \ + -e CC_wasm32v1_none=clang -e AR_wasm32v1_none=llvm-ar \ + -e CC_wasm32_unknown_unknown=clang -e AR_wasm32_unknown_unknown=llvm-ar \ + -w /src "$IMG" cargo build --release -p origin-chain-node --features wasm-build +mkdir -p "$DESTINO" +# docker cp escribe desde el host: funciona aunque la VM no vea el destino (p. ej. /Volumes/…). +C=$(docker create -v charon-arm64-target:/target "$IMG" true) +docker cp "$C:/target/release/origin-chain-node" "$DESTINO/origin-chain-node" +docker rm "$C" >/dev/null +file "$DESTINO/origin-chain-node" +shasum -a 256 "$DESTINO/origin-chain-node" +echo "Para liberar el disco de Docker: docker volume rm charon-arm64-target charon-arm64-registry" diff --git a/scripts/rp1/desplegar.sh b/scripts/rp1/desplegar.sh new file mode 100755 index 0000000..725c7b2 --- /dev/null +++ b/scripts/rp1/desplegar.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# Despliega el nodo de Origin Chain en la rp1 desde el Mac (Charon, 2026-10-24). +# +# scripts/rp1/desplegar.sh +# +# - Copia el binario a ~/origin-chain/bin y deja el anterior como .anterior (volver atrás: +# mv .anterior encima y matar el pid; el vigilante lo rearranca). +# - Copia arrancar.sh y medir.sh, e instala en el crontab del usuario (sin root): +# @reboot y cada 5 min arrancar.sh (vigilante); cada 5 min medir.sh. +# - No toca nada fuera de ~/origin-chain ni el crontab de otras entradas. +set -euo pipefail +BIN="${1:?ruta al binario aarch64}" +HOST="${ORIGIN_RP1:-rp1}" +DIR="$(cd "$(dirname "$0")" && pwd)" +file "$BIN" | grep -q "ARM aarch64" || { echo "no es un binario aarch64: $BIN"; exit 1; } +SHA=$(shasum -a 256 "$BIN" | cut -d' ' -f1) + +ssh "$HOST" 'mkdir -p ~/origin-chain/bin ~/origin-chain/logs && { [ -f ~/origin-chain/bin/origin-chain-node ] && cp -p ~/origin-chain/bin/origin-chain-node ~/origin-chain/bin/origin-chain-node.anterior || true; }' +scp -q "$BIN" "$HOST:origin-chain/bin/origin-chain-node.nuevo" +scp -q "$DIR/arrancar.sh" "$DIR/medir.sh" "$HOST:origin-chain/" +ssh "$HOST" "set -e; cd ~/origin-chain + echo '$SHA bin/origin-chain-node.nuevo' | sha256sum -c --quiet + mv bin/origin-chain-node.nuevo bin/origin-chain-node; chmod +x bin/origin-chain-node arrancar.sh medir.sh + echo '$SHA' > bin/origin-chain-node.sha256 + # || true: con set -e, el grep -v sobre un crontab vacío sale 1 y abortaba la subshell + # ANTES de los echo, así que se instalaba un crontab VACÍO sin ningún error (medido el 24-oct). + ( { crontab -l 2>/dev/null | grep -v 'origin-chain/' || true; } ; \ + echo '@reboot \$HOME/origin-chain/arrancar.sh'; \ + echo '*/5 * * * * \$HOME/origin-chain/arrancar.sh'; \ + echo '*/5 * * * * \$HOME/origin-chain/medir.sh' ) | crontab - + bin/origin-chain-node version | head -3" +echo "desplegado en $HOST: sha256 $SHA" diff --git a/scripts/rp1/medir.sh b/scripts/rp1/medir.sh new file mode 100755 index 0000000..8615904 --- /dev/null +++ b/scripts/rp1/medir.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Una fila de medida del nodo de la rp1 cada vez que se llama (cron cada 5 min). +# CSV: fecha, best, finalizado, pares, rss_mb, cpu_pct, datos_mb, load1, temp_c, ci_activo +set -uo pipefail +BASE="$HOME/origin-chain" +CSV="$BASE/logs/medidas.csv" +[ -f "$CSV" ] || echo "fecha,best,finalizado,pares,rss_mb,cpu_pct,datos_mb,load1,temp_c,ci_activo" >"$CSV" + +rpc() { + curl -s -m 5 -H 'Content-Type: application/json' \ + -d "{\"id\":1,\"jsonrpc\":\"2.0\",\"method\":\"$1\",\"params\":[${2:-}]}" http://127.0.0.1:9944 +} +num() { python3 -c "import json,sys; r=json.load(sys.stdin)['result']; print(int(r['number'],16) if isinstance(r,dict) else r)" 2>/dev/null || echo NA; } + +best=$(rpc chain_getHeader | num) +fh=$(rpc chain_getFinalizedHead | python3 -c "import json,sys;print(json.load(sys.stdin)['result'])" 2>/dev/null || echo "") +fin=NA; [ -n "$fh" ] && fin=$(rpc chain_getHeader "\"$fh\"" | num) +pares=$(rpc system_health | python3 -c "import json,sys;print(json.load(sys.stdin)['result']['peers'])" 2>/dev/null || echo NA) +pid=$(cat "$BASE/nodo.pid" 2>/dev/null || echo 0) +rss=NA; cpu=NA +if kill -0 "$pid" 2>/dev/null; then + read -r rss cpu < <(ps -o rss=,pcpu= -p "$pid" | awk '{printf "%.0f %s\n", $1/1024, $2}') +fi +datos=$(du -sm "$BASE/data" 2>/dev/null | cut -f1) +load1=$(cut -d' ' -f1 /proc/loadavg) +temp=$(awk '{printf "%.1f", $1/1000}' /sys/class/thermal/thermal_zone0/temp) +ci=$(systemctl is-active gitea-runner 2>/dev/null || echo NA) +echo "$(date -Is),$best,$fin,$pares,$rss,$cpu,$datos,$load1,$temp,$ci" >>"$CSV"