fix(runtime): frame_system no sabía su propia versión — ningún wallet podía firmar

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 <noreply@anthropic.com>
This commit is contained in:
CharonandClaude Opus 5.5 committed 2026-10-07 10:30:38 -05:00
1 parent 3771a15165
commit 11e532e251
2 files changed
+112

No files matched your search

@@ -87,6 +87,8 @@ parameter_types! {
pub const SS58Prefix: u16 = SS58_PREFIX; pub const SS58Prefix: u16 = SS58_PREFIX;
/// Block hash count canonical wrapped. /// Block hash count canonical wrapped.
pub const BlockHashCount: BlockNumber = BLOCK_HASH_COUNT; 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)] #[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` /// 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`. /// y el `ref_time` de ejecución, que no pasan por `DbWeight`.
type DbWeight = frame_support::weights::constants::RocksDbWeight; 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;
} }
// ============================================================ // ============================================================
+93
View File
@@ -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 = <Runtime as frame_system::Config>::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::<RuntimeGenesisConfig>(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::<Runtime>::set_block_number(5);
frame_system::BlockHash::<Runtime>::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::<Runtime>::new(),
frame_system::CheckSpecVersion::<Runtime>::new(),
frame_system::CheckTxVersion::<Runtime>::new(),
frame_system::CheckGenesis::<Runtime>::new(),
frame_system::CheckEra::<Runtime>::from(Era::Immortal),
frame_system::CheckNonce::<Runtime>::from(0),
frame_system::CheckWeight::<Runtime>::new(),
pallet_transaction_payment::ChargeTransactionPayment::<Runtime>::from(0),
frame_metadata_hash_extension::CheckMetadataHash::<Runtime>::new(false),
frame_system::WeightReclaim::<Runtime>::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::<RuntimeCall, TxExtension>::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::<Runtime>::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. /// 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 /// El test NO cuenta la historia de cada version — eso vive en el doc comment