From 11e532e251fb9faa6d861dda6811b7754fd00368 Mon Sep 17 00:00:00 2001 From: Charon Date: Wed, 7 Oct 2026 10:30:38 -0500 Subject: [PATCH] =?UTF-8?q?fix(runtime):=20frame=5Fsystem=20no=20sab=C3=AD?= =?UTF-8?q?a=20su=20propia=20versi=C3=B3n=20=E2=80=94=20ning=C3=BAn=20wall?= =?UTF-8?q?et=20pod=C3=ADa=20firmar?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Medido en el E2E de RV16 por el canal de un wallet: Alice firmando como polkadot.js (versión de state_getRuntimeVersion, hash del génesis, era inmortal) recibía del nodo vivo «Transaction has a bad signature». Causa: SolochainDefaultConfig trae type Version = (), es decir RuntimeVersion::default() (nombre vacío, spec 0, tx 0), y el impl de frame_system lo heredaba sin declararlo — el mismo agujero que DbWeight en RV14. El RPC anunciaba spec 16 / tx 1; CheckSpecVersion y CheckTxVersion reconstruían el payload con 0 / 0. Rompía tres cosas: 1. Ningún wallet podía firmar un extrinsic (BadProof). 2. set_code habría fallado con InvalidSpecName («origin-chain» contra «»): sin upgrades. 3. Las migraciones no corrían nunca: Executive::runtime_upgraded compara 0 contra 0. Nadie lo vio en 16 specs porque ningún test firmaba por el canal del consumidor: los del pallet llaman a las funciones directamente, y los E2E anteriores solo miraban bloques. Arreglo: type Version = Version, con Version = crate::VERSION. Tests (contrafactual: sin la línea, los dos reprueban en la ASERCIÓN — spec_name "" y BadProof): - frame_system_conoce_la_version_que_anuncia_el_rpc - una_firma_de_wallet_estandar_pasa_el_check_del_runtime: firma como un wallet sobre el estado del preset development y pasa por el check real de la firma. E2E después del arreglo, nodo --dev con WASM real: dos extrinsics firmados por //Alice entran por author_submitExtrinsic, se incluyen en el bloque #3 (finalizado) y fallan con los errores que tocaban: finalize_override → ProposalNotFound, uphold_council_veto de peso 0 → ZeroWeightWindowVote. La metadata viva trae las llamadas, el storage, MinOverrideQuorum = 1e26 (100M OGC) y los errores nuevos. Tests: runtime 46, workspace 350. Co-Authored-By: Claude Opus 5.5 --- crates/origin-chain-runtime/src/configs.rs | 19 +++++ crates/origin-chain-runtime/src/lib.rs | 93 ++++++++++++++++++++++ 2 files changed, 112 insertions(+) diff --git a/crates/origin-chain-runtime/src/configs.rs b/crates/origin-chain-runtime/src/configs.rs index 28e2e54..ed0287b 100644 --- a/crates/origin-chain-runtime/src/configs.rs +++ b/crates/origin-chain-runtime/src/configs.rs @@ -87,6 +87,8 @@ parameter_types! { pub const SS58Prefix: u16 = SS58_PREFIX; /// Block hash count canonical wrapped. pub const BlockHashCount: BlockNumber = BLOCK_HASH_COUNT; + /// La versión que el runtime cree tener. Ver `type Version` abajo. + pub const Version: sp_version::RuntimeVersion = crate::VERSION; } #[derive_impl(frame_system::config_preludes::SolochainDefaultConfig)] @@ -112,6 +114,23 @@ impl frame_system::Config for Runtime { /// ningún efecto en el runtime. Lo que RV13 sí arregló de verdad es el `proof_size` /// y el `ref_time` de ejecución, que no pasan por `DbWeight`. type DbWeight = frame_support::weights::constants::RocksDbWeight; + /// **La versión que el runtime cree tener.** Faltaba, y por eso era `spec 0`. + /// + /// Mismo agujero que `DbWeight`: `SolochainDefaultConfig` trae `type Version = ();`, + /// es decir `RuntimeVersion::default()` (nombre vacío, `spec 0`, `tx 0`), mientras el + /// RPC anunciaba `VERSION`. Rompía tres cosas, medidas el 2026-10-07 en el E2E de RV16: + /// + /// 1. **Ningún wallet podía firmar.** Un wallet firma con la versión que lee del RPC + /// y `CheckSpecVersion`/`CheckTxVersion` reconstruyen el payload con esta: `0 ≠ 16` + /// y el nodo respondía `bad signature` a Alice firmando como polkadot.js. + /// 2. **`set_code` fallaba:** `can_set_code` compara el nombre del WASM nuevo con + /// este (`"origin-chain"` contra `""`) → `InvalidSpecName`. + /// 3. **Las migraciones no corrían nunca:** `Executive::runtime_upgraded` compara + /// `LastRuntimeUpgrade` con esta versión, y `0` contra `0` nunca es una subida. + /// + /// Nadie lo vio en 16 specs porque ningún test firmaba un extrinsic por el canal de + /// un wallet: los del pallet llaman a las funciones directamente. + type Version = Version; } // ============================================================ diff --git a/crates/origin-chain-runtime/src/lib.rs b/crates/origin-chain-runtime/src/lib.rs index 8295b5b..dd86f18 100644 --- a/crates/origin-chain-runtime/src/lib.rs +++ b/crates/origin-chain-runtime/src/lib.rs @@ -1074,6 +1074,99 @@ mod tests { ); } + /// `frame_system` tiene que conocer la versión que el RPC anuncia. + /// + /// Mismo defecto que `DbWeight`: heredaba `Version = ()`, y con eso `spec 0` y nombre + /// vacío para `CheckSpecVersion`, `set_code` y la detección de migraciones (ver + /// `type Version` en `configs.rs`). Tipo anotado por la misma razón que arriba. + #[test] + fn frame_system_conoce_la_version_que_anuncia_el_rpc() { + use frame_support::traits::Get; + let v: sp_version::RuntimeVersion = ::Version::get(); + assert_eq!( + v.spec_name, VERSION.spec_name, + "set_code daría InvalidSpecName" + ); + assert_eq!( + v.spec_version, VERSION.spec_version, + "los wallets firmarían otra spec" + ); + assert_eq!(v.transaction_version, VERSION.transaction_version); + } + + /// El canal del consumidor: Alice firma como firma un wallet, y el runtime acepta. + /// + /// Un wallet (polkadot.js, Talisman, Ledger) lee `spec_version`/`transaction_version` + /// de `state_getRuntimeVersion` —que es `VERSION`—, toma el hash del génesis y firma + /// `(call, extensiones, implícito)`. Aquí se hace lo mismo sobre el estado del preset + /// `development` y se pasa por el `check` real de la firma. Antes del arreglo el nodo + /// vivo respondía `bad signature` a exactamente esto (E2E de RV16, 2026-10-07). + #[test] + fn una_firma_de_wallet_estandar_pasa_el_check_del_runtime() { + use parity_scale_codec::Encode; + use sp_core::{sr25519, Pair}; + use sp_runtime::generic::{Era, SignedPayload}; + use sp_runtime::traits::Checkable; + + let json = genesis_config_presets::get_preset(&sp_genesis_builder::PresetId::from( + sp_genesis_builder::DEV_RUNTIME_PRESET, + )) + .expect("preset development"); + let mut ext = sp_io::TestExternalities::new_empty(); + ext.execute_with(|| { + frame_support::genesis_builder_helper::build_state::(json) + .expect("estado del génesis"); + // El nodo guarda el hash real del génesis al iniciar el bloque 1. + let genesis = H256::repeat_byte(0x13); + frame_system::Pallet::::set_block_number(5); + frame_system::BlockHash::::insert(0, genesis); + + let alice = sr25519::Pair::from_string("//Alice", None).expect("//Alice"); + let call = RuntimeCall::System(frame_system::Call::remark { remark: vec![1, 6] }); + let tx_ext: TxExtension = ( + frame_system::CheckNonZeroSender::::new(), + frame_system::CheckSpecVersion::::new(), + frame_system::CheckTxVersion::::new(), + frame_system::CheckGenesis::::new(), + frame_system::CheckEra::::from(Era::Immortal), + frame_system::CheckNonce::::from(0), + frame_system::CheckWeight::::new(), + pallet_transaction_payment::ChargeTransactionPayment::::from(0), + frame_metadata_hash_extension::CheckMetadataHash::::new(false), + frame_system::WeightReclaim::::new(), + ); + // Lo que el wallet pone de su lado: la versión del RPC, no la del storage. + let implicito = ( + (), + VERSION.spec_version, + VERSION.transaction_version, + 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(); + let xt = UncheckedExtrinsic::new_signed( + call, + MultiAddress::Id(alice.public().into()), + sp_runtime::MultiSignature::Sr25519(firma), + tx_ext, + ); + let r = xt.check(&frame_system::ChainContext::::default()); + assert!( + r.is_ok(), + "el runtime rechaza la firma de un wallet: {:?}", + r.err() + ); + }); + } + /// Fija la version vigente y la invariante que la acompaña. /// /// El test NO cuenta la historia de cada version — eso vive en el doc comment