From 3cd10688937f1cdd1361be72e54805b7c5976b34 Mon Sep 17 00:00:00 2001 From: Charon Date: Mon, 12 Oct 2026 19:15:49 -0500 Subject: [PATCH] =?UTF-8?q?feat(e2e):=20simulacro=20de=20set=5Fcode=2016?= =?UTF-8?q?=E2=86=9217=20=E2=80=94=20la=20cadena=20ya=20se=20actualiza=20s?= =?UTF-8?q?in=20fork,=20y=20sin=2011e532e=20no=20pod=C3=ADa?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ensayo en vivo el 2026-10-12, nodo --dev con WASM real, todo firmado por RPC como un wallet: - A, control negativo: set_code con la MISMA spec 16 → Sudid Err(System, 1) = SpecVersionNeedsToIncrease. - B, el upgrade: sudo(set_code) con spec 17 → CodeUpdated + Sudid Ok. - El RPC anuncia 17 y LastRuntimeUpgrade pasa de {16,"origin-chain"} a {17,"origin-chain"}. - La migración del simulacro dejó su marca (0x11000000): on_runtime_upgrade corrió. - C, un wallet tras el upgrade: firmado con spec 16 → «bad signature»; con spec 17 → ExtrinsicSuccess. - La finalidad siguió: el bloque del upgrade (#5) quedó finalizado. Contrafactual, nodo sin type Version (el runtime de antes de 11e532e): - LastRuntimeUpgrade = {0, ""}. - Firmando como wallet → «bad signature». - Firmando con spec 0 para poder llegar → Sudid Err(System, 0) = InvalidSpecName. - Ningún upgrade y ninguna marca. O sea: hasta el 7-oct la cadena no podía cambiar su runtime sin un fork. Herramientas, para repetirlo antes de la testnet: - scripts/e2e/simulacro_set_code.py: los pasos A, B y C contra un nodo vivo. Lo corrí tal cual y reproduce lo de arriba. - scripts/e2e/parche_simulacro.py: aplica de forma temporal la spec N+1 con la migración marcada, y al restaurar comprueba por hash que lib.rs queda idéntico. - firmar_como_wallet: - modo set_code: (sudo de //Alice); - spec y tx opcionales: tras un upgrade, el wallet firma con la versión que anuncia el RPC; - hex completo: HexDisplay recortaba con «...» pasados 1024 bytes. Las builds son reproducibles: los dos WASM salieron con el mismo sha en dos builds separados (9b933ff1 spec 16, 51663085 spec 17). Co-Authored-By: Claude Opus 5.5 --- .../examples/firmar_como_wallet.rs | 47 +++++--- scripts/e2e/parche_simulacro.py | 60 ++++++++++ scripts/e2e/simulacro_set_code.py | 105 ++++++++++++++++++ 3 files changed, 194 insertions(+), 18 deletions(-) create mode 100644 scripts/e2e/parche_simulacro.py create mode 100644 scripts/e2e/simulacro_set_code.py diff --git a/crates/origin-chain-runtime/examples/firmar_como_wallet.rs b/crates/origin-chain-runtime/examples/firmar_como_wallet.rs index 97ef773..8a2884a 100644 --- a/crates/origin-chain-runtime/examples/firmar_como_wallet.rs +++ b/crates/origin-chain-runtime/examples/firmar_como_wallet.rs @@ -10,8 +10,12 @@ //! cargo build -p origin-chain-runtime --example firmar_como_wallet # con SKIP_WASM_BUILD=1 //! F=target/debug/examples/firmar_como_wallet //! -//! firmar extrinsic firmado por //Alice, en hex +//! firmar [spec tx] extrinsic firmado por //Alice, en hex //! llamadas: remark · finalize_override (propuesta 0x09…09) · uphold0 (peso 0) +//! set_code: (sudo de //Alice, ensayo de upgrade sin fork) +//! spec y tx: los que da `state_getRuntimeVersion`. Por defecto, los de `VERSION` +//! 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 //! verificar pasa la firma por el check del runtime nativo //! sobre el preset development (sin nodo) @@ -53,11 +57,21 @@ fn llamada(nombre: &str) -> RuntimeCall { proposal_id: id, stake_weight: 0, }), - otra => panic!("llamada desconocida: {otra} (remark · finalize_override · uphold0)"), + otra => match otra.strip_prefix("set_code:") { + Some(ruta) => { + let code = std::fs::read(ruta).unwrap_or_else(|e| panic!("{ruta}: {e}")); + RuntimeCall::Sudo(pallet_sudo::Call::sudo { + call: Box::new(RuntimeCall::System(frame_system::Call::set_code { code })), + }) + } + None => panic!( + "llamada desconocida: {otra} (remark · finalize_override · uphold0 · set_code:)" + ), + }, } } -fn firmar(call: RuntimeCall, nonce: u32, genesis: H256) -> UncheckedExtrinsic { +fn firmar(call: RuntimeCall, nonce: u32, genesis: H256, spec: u32, tx: u32) -> UncheckedExtrinsic { let alice = sr25519::Pair::from_string("//Alice", None).expect("//Alice"); let tx_ext: TxExtension = ( frame_system::CheckNonZeroSender::::new(), @@ -71,19 +85,8 @@ fn firmar(call: RuntimeCall, nonce: u32, genesis: H256) -> UncheckedExtrinsic { frame_metadata_hash_extension::CheckMetadataHash::::new(false), frame_system::WeightReclaim::::new(), ); - // Lo que pone el wallet: la versión del RPC (= VERSION), no la que crea el storage. - let implicito = ( - (), - VERSION.spec_version, - VERSION.transaction_version, - genesis, - genesis, - (), - (), - (), - None, - (), - ); + // Lo que pone el wallet: la versión que anuncia el RPC, no la que crea el storage. + let implicito = ((), spec, tx, genesis, genesis, (), (), (), None, ()); let payload = SignedPayload::::from_raw(call, tx_ext, implicito); let firma = payload.using_encoded(|p| alice.sign(p)); let (call, tx_ext, _) = payload.deconstruct(); @@ -102,8 +105,16 @@ fn main() { "firmar" => { let nonce: u32 = args[3].parse().expect("nonce"); let genesis = H256::from_slice(&hex(&args[4])); - let xt = firmar(llamada(&args[2]), nonce, genesis); - println!("0x{}", sp_core::hexdisplay::HexDisplay::from(&xt.encode())); + let spec = args + .get(5) + .map_or(VERSION.spec_version, |v| v.parse().expect("spec")); + let tx = args + .get(6) + .map_or(VERSION.transaction_version, |v| v.parse().expect("tx")); + let xt = firmar(llamada(&args[2]), nonce, genesis, spec, tx); + // Hex completo: `HexDisplay` recorta con «...» pasados 1024 bytes, y un + // `set_code` lleva el runtime entero (MB). + println!("{}", sp_core::bytes::to_hex(&xt.encode(), false)); } "eventos" => { let bytes = hex(&args[2]); diff --git a/scripts/e2e/parche_simulacro.py b/scripts/e2e/parche_simulacro.py new file mode 100644 index 0000000..0306d70 --- /dev/null +++ b/scripts/e2e/parche_simulacro.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +"""Parche TEMPORAL para el simulacro de set_code: el runtime siguiente (spec N+1) con una migración marcada. + + python3 scripts/e2e/parche_simulacro.py aplicar # spec N → N+1 + SimulacroSetCode + (construir el WASM: cargo build -p origin-chain-runtime --features wasm-build) + (copiar target/debug/wbuild/origin-chain-runtime/origin_chain_runtime.wasm fuera) + python3 scripts/e2e/parche_simulacro.py restaurar # deja lib.rs byte a byte como estaba + +La migración escribe `:origin:simulacro_set_code` = N+1 (u32 LE). Si esa marca aparece después +del upgrade, `on_runtime_upgrade` corrió. NUNCA se commitea: `restaurar` lo comprueba con +un hash. +""" +import hashlib, pathlib, re, shutil, sys + +LIB = pathlib.Path(__file__).resolve().parents[2] / "crates/origin-chain-runtime/src/lib.rs" +COPIA = LIB.with_suffix(".rs.simulacro-orig") + + +def sha(p): + return hashlib.sha256(p.read_bytes()).hexdigest() + + +def aplicar(): + if COPIA.exists(): + sys.exit(f"ya hay un parche aplicado ({COPIA}); restaura primero") + s = LIB.read_text() + m = re.search(r" spec_version: (\d+),", s) + n = int(m.group(1)) + 1 + exe = " AllPalletsWithSystem,\n (),\n>;" + if s.count(exe) != 1: + sys.exit("el Executive ya tiene migraciones: este parche no sabe combinarlas") + shutil.copy2(LIB, COPIA) + s = s.replace(m.group(0), f" spec_version: {n},", 1) + s = s.replace(exe, f""" AllPalletsWithSystem, + SimulacroSetCode, +>; + +/// SIMULACRO (no se commitea): deja una marca para probar que la migración corrió. +pub struct SimulacroSetCode; +impl frame_support::traits::OnRuntimeUpgrade for SimulacroSetCode {{ + fn on_runtime_upgrade() -> frame_support::weights::Weight {{ + frame_support::storage::unhashed::put(b":origin:simulacro_set_code", &{n}u32); + frame_support::weights::Weight::from_parts(1_000_000, 0) + }} +}}""", 1) + LIB.write_text(s) + print(f"aplicado: spec {n - 1} → {n} + SimulacroSetCode (original en {COPIA.name}, sha {sha(COPIA)[:12]})") + + +def restaurar(): + if not COPIA.exists(): + sys.exit("no hay parche que restaurar") + esperado = sha(COPIA) + shutil.copy2(COPIA, LIB) + assert sha(LIB) == esperado, "lib.rs no quedó idéntico al original" + COPIA.unlink() + print(f"restaurado: lib.rs idéntico al original (sha {esperado[:12]})") + + +{"aplicar": aplicar, "restaurar": restaurar}.get(sys.argv[1] if len(sys.argv) > 1 else "", lambda: sys.exit(__doc__))() diff --git a/scripts/e2e/simulacro_set_code.py b/scripts/e2e/simulacro_set_code.py new file mode 100644 index 0000000..d1c50ec --- /dev/null +++ b/scripts/e2e/simulacro_set_code.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +"""Simulacro de upgrade sin fork (set_code N → N+1) contra un nodo vivo, firmando como un wallet. + +Prueba lo que un upgrade real necesita, por el canal del consumidor (RPC): + A. control negativo: un `set_code` con la MISMA spec se rechaza (SpecVersionNeedsToIncrease). + Si sale InvalidSpecName, el runtime no sabe su propia versión (el defecto de 11e532e). + B. el upgrade: `sudo(set_code)` con la spec N+1 → CodeUpdated, el RPC anuncia N+1, + `LastRuntimeUpgrade` sube y la migración del simulacro deja su marca. + C. un wallet después del upgrade: firmado con la spec vieja da «bad signature»; con la nueva entra. + +Preparación (ver parche_simulacro.py): + 1. nodo con la spec N: cargo build -p origin-chain-node --features wasm-build + (con WASM_BUILD_STD=0 y CC/AR_wasm32v1_none = clang/llvm-ar de LLVM) + y copiar target/debug/wbuild/origin-chain-runtime/origin_chain_runtime.wasm → igual.wasm + 2. parche_simulacro.py aplicar · cargo build -p origin-chain-runtime --features wasm-build + · copiar el wasm → nuevo.wasm · parche_simulacro.py restaurar + 3. SKIP_WASM_BUILD=1 cargo build -p origin-chain-runtime --example firmar_como_wallet + 4. target/debug/origin-chain-node run --dev --rpc-port 9955 --rpc-max-request-size 64 + 5. python3 scripts/e2e/simulacro_set_code.py igual.wasm nuevo.wasm + +Medido la primera vez el 2026-10-12 (16 → 17): A, B y C en verde. El contrafactual, sin +`type Version`, da InvalidSpecName y ningún upgrade. +""" +import json, os, subprocess, sys, time, urllib.request + +RPC = os.environ.get("ORIGIN_RPC", "http://127.0.0.1:9955") +RAIZ = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +TOOL = os.path.join(RAIZ, "target/debug/examples/firmar_como_wallet") +ALICE = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" +EVENTS = "0x26aa394eea5630e07c48ae0c9558cef780d41e5e16056765bc8461851072c9d7" +LAST_UPGRADE = "0x26aa394eea5630e07c48ae0c9558cef7f9cce9c888469bb1a0dceaa129672ef8" +MARCA = "0x" + b":origin:simulacro_set_code".hex() + + +def rpc(method, params=None): + body = json.dumps({"id": 1, "jsonrpc": "2.0", "method": method, "params": params or []}).encode() + req = urllib.request.Request(RPC, body, {"Content-Type": "application/json"}) + with urllib.request.urlopen(req, timeout=120) as r: + return json.load(r) + + +def best(): + return int(rpc("chain_getHeader")["result"]["number"], 16) + + +def firmar(llamada, nonce, genesis, version): + args = [TOOL, "firmar", llamada, str(nonce), genesis, str(version["specVersion"]), str(version["transactionVersion"])] + return subprocess.run(args, capture_output=True, text=True, check=True).stdout.strip() + + +def eventos(n0, n1, filtro): + for n in range(n0, n1 + 1): + h = rpc("chain_getBlockHash", [n])["result"] + if len(rpc("chain_getBlock", [h])["result"]["block"]["extrinsics"]) > 1: + ev = rpc("state_getStorage", [EVENTS, h])["result"] + out = subprocess.run([TOOL, "eventos", ev], capture_output=True, text=True).stdout + for linea in out.splitlines(): + if any(f in linea for f in filtro): + print(f" #{n} {linea}") + + +def esperar(k): + b = best() + while best() < b + k: + time.sleep(1) + + +def enviar(xt): + r = rpc("author_submitExtrinsic", [xt]) + return r.get("result", r.get("error")) + + +def main(igual, nuevo): + genesis = rpc("chain_getBlockHash", [0])["result"] + v = rpc("state_getRuntimeVersion")["result"] + print(f"antes: {v['specName']} spec {v['specVersion']} tx {v['transactionVersion']} · LastRuntimeUpgrade {rpc('state_getStorage', [LAST_UPGRADE])['result']}") + + n0 = best() + nonce = rpc("system_accountNextIndex", [ALICE])["result"] + print("A) set_code con la misma spec →", enviar(firmar(f"set_code:{igual}", nonce, genesis, v))) + esperar(2) + eventos(n0 + 1, best(), ("Sudo", "Code")) + + n0 = best() + nonce = rpc("system_accountNextIndex", [ALICE])["result"] + print("B) set_code con la spec siguiente →", enviar(firmar(f"set_code:{nuevo}", nonce, genesis, v))) + esperar(3) + eventos(n0 + 1, best(), ("Sudo", "Code")) + v2 = rpc("state_getRuntimeVersion")["result"] + print(f"después: spec {v2['specVersion']} · LastRuntimeUpgrade {rpc('state_getStorage', [LAST_UPGRADE])['result']} · marca {rpc('state_getStorage', [MARCA])['result']}") + + nonce = rpc("system_accountNextIndex", [ALICE])["result"] + print("C1) remark firmado con la spec vieja →", enviar(firmar("remark", nonce, genesis, v))) + n0 = best() + print("C2) remark firmado con la spec nueva →", enviar(firmar("remark", nonce, genesis, v2))) + esperar(2) + eventos(n0 + 1, best(), ("ExtrinsicSuccess", "ExtrinsicFailed")) + fin = rpc("chain_getFinalizedHead")["result"] + print(f"finalizado #{int(rpc('chain_getHeader', [fin])['result']['number'], 16)} · best #{best()}") + + +if __name__ == "__main__": + if len(sys.argv) != 3: + sys.exit(__doc__) + main(os.path.abspath(sys.argv[1]), os.path.abspath(sys.argv[2]))