feat(e2e): simulacro de set_code 16→17 — la cadena ya se actualiza sin fork, y sin 11e532e no podía

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:<ruta> (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 <noreply@anthropic.com>
This commit is contained in:
CharonandClaude Opus 5.5 committed 2026-10-12 19:15:49 -05:00
1 parent ce91aa4572
commit 3cd1068893
3 files changed
+194 -18

No files matched your search

+60
View File
@@ -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__))()
+105
View File
@@ -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]))