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