diff --git a/Cargo.lock b/Cargo.lock index 4dd3ee8..5aa1bc1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3254,6 +3254,36 @@ dependencies = [ "sp-crypto-hashing", ] +[[package]] +name = "frame-election-provider-solution-type" +version = "16.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0b525f462fa8121c3d143ad0d876660584f160ad5baa68c57bfeeb293c6b8fb" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "frame-election-provider-support" +version = "43.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8ddd171c606313e738594a9dabe2d2a911239e6a78b5419faf2a2d6b106570c" +dependencies = [ + "frame-election-provider-solution-type", + "frame-support", + "frame-system", + "parity-scale-codec", + "scale-info", + "sp-arithmetic", + "sp-core", + "sp-npos-elections", + "sp-runtime 44.0.0", + "sp-std", +] + [[package]] name = "frame-executive" version = "43.0.1" @@ -6484,6 +6514,7 @@ dependencies = [ name = "origin-chain-runtime" version = "0.0.1" dependencies = [ + "frame-election-provider-support", "frame-executive", "frame-metadata-hash-extension", "frame-support", @@ -6503,6 +6534,7 @@ dependencies = [ "pallet-grandpa", "pallet-revive", "pallet-session", + "pallet-staking", "pallet-sudo", "pallet-timestamp", "pallet-transaction-payment", @@ -6520,6 +6552,7 @@ dependencies = [ "sp-offchain 39.0.0", "sp-runtime 44.0.0", "sp-session 41.0.0", + "sp-staking 41.0.0", "sp-std", "sp-transaction-pool 39.0.0", "sp-version 42.0.0", @@ -6742,6 +6775,28 @@ dependencies = [ "sp-trie", ] +[[package]] +name = "pallet-staking" +version = "43.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ce23099893b90f921c53ce92db08d3b9ada8ec532f3ea125e9b117236ee4808" +dependencies = [ + "frame-benchmarking", + "frame-election-provider-support", + "frame-support", + "frame-system", + "log", + "pallet-authorship", + "pallet-session", + "parity-scale-codec", + "scale-info", + "serde", + "sp-application-crypto 43.0.0", + "sp-io 43.0.0", + "sp-runtime 44.0.0", + "sp-staking 41.0.0", +] + [[package]] name = "pallet-sudo" version = "43.0.0" @@ -11179,6 +11234,20 @@ dependencies = [ "sp-application-crypto 42.0.0", ] +[[package]] +name = "sp-npos-elections" +version = "39.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb6b7ddeb995c6d57efbc66f42c70bd513f251025fbe9de8b5903a7f87a50de7" +dependencies = [ + "parity-scale-codec", + "scale-info", + "serde", + "sp-arithmetic", + "sp-core", + "sp-runtime 44.0.0", +] + [[package]] name = "sp-offchain" version = "38.0.0" diff --git a/crates/origin-chain-runtime/Cargo.toml b/crates/origin-chain-runtime/Cargo.toml index 108114d..c5552b9 100644 --- a/crates/origin-chain-runtime/Cargo.toml +++ b/crates/origin-chain-runtime/Cargo.toml @@ -44,9 +44,10 @@ pallet-session.workspace = true # (Root origin para dispatchables admin — sunset → governance real pre-mainnet) pallet-sudo.workspace = true -# FIX-6 Stage 2b NPoS (DC ORIGIN_CHAIN_NPOS_V1) — deps en workspace catalog -# (versiones verificadas contra stable2509-3). Se wirean aquí + std cuando se -# cablee el Config de pallet_staking (sesión dedicada, cambio de consenso). +# FIX-6 Stage 2b NPoS (DC ORIGIN_CHAIN_NPOS_V1) — infra dormida. +pallet-staking.workspace = true +frame-election-provider-support.workspace = true +sp-staking.workspace = true # Charon custom pallets Phase 2.x (per DCs) # Phase 2.1 v0.0.15: pallet-origin-provenance scaffold canonical wired @@ -135,6 +136,9 @@ std = [ "pallet-grandpa/std", "pallet-session/std", "pallet-sudo/std", + "pallet-staking/std", + "frame-election-provider-support/std", + "sp-staking/std", "origin-chain-provenance/std", "origin-chain-fees/std", "origin-chain-agent-identity/std", diff --git a/crates/origin-chain-runtime/src/genesis_config_presets.rs b/crates/origin-chain-runtime/src/genesis_config_presets.rs index 16e11de..70a8252 100644 --- a/crates/origin-chain-runtime/src/genesis_config_presets.rs +++ b/crates/origin-chain-runtime/src/genesis_config_presets.rs @@ -208,8 +208,9 @@ fn build_genesis( ) -> RuntimeGenesisConfig { use crate::{ AuraConfig, BalancesConfig, FeesConfig, GrandpaConfig, ReviveConfig, SessionConfig, - SudoConfig, SystemConfig, TransactionPaymentConfig, + StakingConfig, SudoConfig, SystemConfig, TransactionPaymentConfig, }; + use pallet_staking::Forcing; RuntimeGenesisConfig { system: SystemConfig::default(), balances: BalancesConfig { @@ -254,6 +255,19 @@ fn build_genesis( sudo: SudoConfig { key: Some(sudo_key), }, + // FIX-6 Stage 2b NPoS — INFRA DORMIDA (Paso A): staking presente pero el + // set sigue viniendo de session/genesis (PoA-equivalente). force_era = + // ForceNone congela cualquier elección; sin `stakers` no se disparan los + // asserts de genesis. Los validadores se siembran como stakers + // invulnerables + el SessionManager swap encienden NPoS en el siguiente + // paso (post-gates CHN-1 = governance flag ForceNew). + staking: StakingConfig { + force_era: Forcing::ForceNone, + validator_count: 0, + minimum_validator_count: 0, + invulnerables: vec![], + ..Default::default() + }, } } diff --git a/crates/origin-chain-runtime/src/lib.rs b/crates/origin-chain-runtime/src/lib.rs index 9677c3b..539d119 100644 --- a/crates/origin-chain-runtime/src/lib.rs +++ b/crates/origin-chain-runtime/src/lib.rs @@ -45,6 +45,8 @@ extern crate alloc; use alloc::vec::Vec; pub mod configs; +/// FIX-6 Stage 2b NPoS — pallet_staking config (infra dormida). +pub mod staking; #[cfg(feature = "std")] pub mod genesis_config_presets; pub mod opaque; @@ -225,7 +227,9 @@ pub const IMPL_NAME: &str = "origin-chain-charon"; /// v2: FIX-6 Stage 2a — fee unificado (OriginFeeAdapter reemplaza al /// FungibleAdapter: cambia el COMPORTAMIENTO del cobro de fees, no el formato /// de extrinsic — TRANSACTION_VERSION intacto). -pub const RUNTIME_VERSION: u32 = 2; +/// v3: FIX-6 Stage 2b NPoS Paso A — pallet_staking Index 14 cableado (infra +/// DORMIDA: presente pero inerte; nuevo pallet ⇒ cambia metadata del runtime). +pub const RUNTIME_VERSION: u32 = 3; /// Canonical authoring version per Substrate semver canonical. pub const AUTHORING_VERSION: u32 = 1; @@ -387,6 +391,15 @@ mod runtime { #[runtime::pallet_index(13)] pub type Emission = origin_chain_emission::Pallet; + /// Staking pallet (FIX-6 Stage 2b NPoS, DC ORIGIN_CHAIN_NPOS_V1). **Infra + /// DORMIDA**: la maquinaria NPoS está cableada pero el set de validadores + /// arranca fijo (invulnerables + ForceEra::ForceNone) — la cadena corre + /// PoA-equivalente hasta los 5 criterios de Phase 2 (CHN-1). La emisión + /// migra a EraPayout (preserva la tasa de Stage 1). Activación de la + /// elección-por-stake = governance flag (ForceNew) post-gates. + #[runtime::pallet_index(14)] + pub type Staking = pallet_staking::Pallet; + // Substrate pallet-identity DEFERRED canonical (decision empírica // 2026-05-21 documentada en configs.rs). pallet-identity 41.0.0 ReservableCurrency // compat work canonical hereda al custom `pallet_agent_identity` Phase 2.7 @@ -797,13 +810,22 @@ mod tests { use super::*; #[test] - fn runtime_version_v2_fee_unificado() { - // v2 = FIX-6 Stage 2a (fee unificado). El formato de extrinsic NO - // cambió: solo el associated type OnChargeTransaction. - assert_eq!(RUNTIME_VERSION, 2); + fn runtime_version_v3_npos_staking_dormant() { + // v3 = FIX-6 Stage 2b NPoS Paso A (pallet_staking Index 14, infra + // dormida). El formato de extrinsic NO cambió. + assert_eq!(RUNTIME_VERSION, 3); assert_eq!(TRANSACTION_VERSION, 1); } + #[test] + fn staking_pallet_index_14_npos_dormant() { + use frame_support::traits::PalletInfoAccess; + // FIX-6 Stage 2b: Staking=14 (post Emission=13). Infra NPoS dormida — + // presente pero el set sigue fijo (PoA-equivalente) hasta los gates CHN-1. + assert_eq!(::index(), 14); + assert_eq!(::name(), "Staking"); + } + #[test] fn canonical_spec_name_origin_chain() { assert_eq!(SPEC_NAME, "origin-chain"); diff --git a/crates/origin-chain-runtime/src/staking.rs b/crates/origin-chain-runtime/src/staking.rs new file mode 100644 index 0000000..52b9e09 --- /dev/null +++ b/crates/origin-chain-runtime/src/staking.rs @@ -0,0 +1,195 @@ +//! # NPoS — pallet_staking config (FIX-6 Stage 2b, DC ORIGIN_CHAIN_NPOS_V1) +//! +//! **Infra DORMIDA** (David ratificó NPoS-5 2026-06-12): toda la maquinaria de +//! staking cableada y lista, pero el set de validadores arranca FIJO +//! (`invulnerables` + `ForceEra::ForceNone`) — la cadena corre PoA-equivalente +//! hasta que se cumplan los 5 criterios empíricos de Phase 2 (CHN-1). La +//! activación de la elección-por-stake = un governance flag (`ForceNew`). +//! +//! La **emisión** migra a [`OriginEraPayout`] (por-era, preserva la tasa lineal +//! de FIX-6 Stage 1: `BlockReward` por bloque) con el `RewardRemainder` +//! ([`DealWithEraRemainder`]) ruteando treasury 20% + subsidy 10% — esto +//! mantiene el pool `og/subsy` alimentado (que el `OriginFeeAdapter` de Stage 2a +//! consume; sin él se rompería el subsidio anti-clasista #16). +//! +//! Versiones verificadas (stable2509-3): pallet-staking 43.0.0 + +//! frame-election-provider-support 43.0.0 + sp-staking 41.0.0. + +use frame_election_provider_support::onchain; +use frame_election_provider_support::{ + bounds::{ElectionBounds, ElectionBoundsBuilder}, + SequentialPhragmen, +}; +use frame_support::parameter_types; +use frame_support::traits::fungible::{Balanced, Credit}; +use frame_support::traits::tokens::imbalance::{Imbalance, OnUnbalanced}; +use frame_support::traits::{ConstBool, ConstU32, Nothing}; +use frame_system::EnsureRoot; +use sp_runtime::traits::AccountIdConversion; +use sp_runtime::Perbill; + +use crate::configs::{ + EmissionBlockReward, EmissionSubsidyPalletId, GovernanceTreasuryPalletId, MILLISECS_PER_BLOCK, +}; +use crate::{Balance, Balances, ChainAccountId, Runtime, RuntimeEvent, RuntimeHoldReason, Timestamp}; + +/// Crédito fungible de Balances (el tipo de imbalance que staking entrega a los +/// handlers `RewardRemainder`/`Slash` en stable2509 — API hold-based, NO el +/// `NegativeImbalance` clásico). +type CreditOf = Credit; + +// ─── EraPayout custom: emisión lineal (preserva FIX-6 Stage 1) ─────── + +/// Emisión por-era que preserva EXACTAMENTE la tasa de FIX-6 Stage 1: +/// `BlockReward` (10 OGC) por bloque, repartido 70% a los validadores +/// (`validator_payout`) y 30% al `RewardRemainder` (`rest` → treasury+subsidy). +/// Lineal-absoluta (fija por tiempo, no curva ideal-staking-rate de Polkadot): +/// `total_era = BlockReward × (era_duration_millis / MILLISECS_PER_BLOCK)`. +/// Ignora `total_staked`/`total_issuance` a propósito (la disinflación natural +/// viene de que el reward fijo pesa menos sobre un issuance creciente). +pub struct OriginEraPayout; + +impl pallet_staking::EraPayout for OriginEraPayout { + fn era_payout( + _total_staked: Balance, + _total_issuance: Balance, + era_duration_millis: u64, + ) -> (Balance, Balance) { + let blocks = (era_duration_millis / MILLISECS_PER_BLOCK) as u128; + let total = EmissionBlockReward::get().saturating_mul(blocks); + // 70% a validadores (AuthorShareBps = 7_000), 30% al remainder. + let validator_payout = total.saturating_mul(7_000) / 10_000; + let rest = total.saturating_sub(validator_payout); + (validator_payout, rest) + } +} + +// ─── RewardRemainder: split treasury (20%) + subsidy (10%) ─────────── + +parameter_types! { + /// Cuenta-tesorería (og/gvtry) — la MISMA de governance/emission. + pub TreasuryAccount: ChainAccountId = + GovernanceTreasuryPalletId::get().into_account_truncating(); + /// Cuenta del subsidy pool (og/subsy) — alimenta el subsidio #16 que el + /// OriginFeeAdapter de Stage 2a consume. + pub SubsidyAccount: ChainAccountId = + EmissionSubsidyPalletId::get().into_account_truncating(); +} + +/// Handler del `rest` de cada era (30% del total emitido): lo reparte 2/3 +/// treasury + 1/3 subsidy (= 20% + 10% del total de emisión, replicando el +/// split per-block de FIX-6 Stage 1). **Crítico #16**: mantiene `og/subsy` +/// alimentado bajo NPoS — sin esto el adapter de fees degradaría a +/// `pool_covered=0` y el usuario pagaría el 100%. +pub struct DealWithEraRemainder; + +impl OnUnbalanced for DealWithEraRemainder { + fn on_nonzero_unbalanced(amount: CreditOf) { + let total = amount.peek(); + // 2/3 del rest = 20% del total de emisión → treasury. + let treasury_part = total.saturating_mul(2) / 3; + let (to_treasury, to_subsidy) = amount.split(treasury_part); + // resolve deposita cada crédito en su cuenta vault; si falla, el crédito + // se descarta (drop = burn) — nunca se duplica ni se pierde a un tercero. + let _ = Balances::resolve(&TreasuryAccount::get(), to_treasury); + let _ = Balances::resolve(&SubsidyAccount::get(), to_subsidy); + } +} + +// ─── onchain ElectionProvider (dormido: ForceNone no dispara elección) ─ + +/// ZST de config para `OnChainExecution`. Con `ForceEra::ForceNone` la elección +/// casi nunca corre; el provider existe porque el Config lo exige. El genérico +/// de `OnChainExecution` es ESTE struct (NO el Runtime): `System = Runtime`, +/// `DataProvider = Staking`. +pub struct OnChainSeqPhragmen; + +parameter_types! { + /// Bounds de la elección on-chain. Con ForceNone es defensivo; acota voters/ + /// targets para que `elect` nunca panic-falle por tamaño. + pub OnChainElectionBounds: ElectionBounds = + ElectionBoundsBuilder::default().build(); +} + +impl onchain::Config for OnChainSeqPhragmen { + type Sort = ConstBool; + type System = Runtime; + type Solver = SequentialPhragmen; + type MaxBackersPerWinner = ConstU32<512>; + // Debe ser >= MaxValidatorSet (150) — reusa el mismo techo. + type MaxWinnersPerPage = MaxValidatorSet; + type DataProvider = crate::Staking; + type WeightInfo = (); + type Bounds = OnChainElectionBounds; +} + +// ─── Benchmarking config (obligatorio en no-std) ───────────────────── + +/// `BenchmarkingConfig` propio (el `TestBenchmarkingConfig` de pallet_staking +/// solo existe bajo `std`; un runtime wasm lo exige siempre). +pub struct OriginStakingBenchmarkingConfig; + +impl pallet_staking::BenchmarkingConfig for OriginStakingBenchmarkingConfig { + type MaxValidators = ConstU32<150>; + type MaxNominators = ConstU32<1_000>; +} + +// ─── pallet_staking::Config ────────────────────────────────────────── + +parameter_types! { + /// NPoS-6 (David ratificó): techo de 150 validadores activos (CHN-1). + pub const MaxValidatorSet: u32 = 150; + /// NPoS-8: 6 sessions × 4h = era de 24h. + pub const SessionsPerEra: u32 = 6; + /// NPoS-8: bonding de 7 eras = 7 días (PILAR_4 §6.1). + pub const BondingDuration: u32 = 7; + /// SlashDeferDuration < BondingDuration. 0 en dormido (sin slashes; los + /// invulnerables no se slashan). Se calibra al activar NPoS (NPoS-9). + pub const SlashDeferDuration: u32 = 0; +} + +impl pallet_staking::Config for Runtime { + // Balances: Currency es fungible hold-based; OldCurrency solo para migración. + type Currency = Balances; + type OldCurrency = Balances; + type RuntimeHoldReason = RuntimeHoldReason; + type CurrencyBalance = Balance; + type UnixTime = Timestamp; + type CurrencyToVote = sp_staking::currency_to_vote::U128CurrencyToVote; + + // Elección on-chain (dormida bajo ForceNone). DataProvider = la propia staking. + type ElectionProvider = onchain::OnChainExecution; + type GenesisElectionProvider = onchain::OnChainExecution; + type VoterList = pallet_staking::UseNominatorsAndValidatorsMap; + type TargetList = pallet_staking::UseValidatorsMap; + type NominationsQuota = pallet_staking::FixedNominationsQuota<16>; + + // Emisión: migra a EraPayout (preserva la tasa de FIX-6 Stage 1). + type EraPayout = OriginEraPayout; + type RewardRemainder = DealWithEraRemainder; + // Invulnerables nunca se slashan; rewards van por payout, no por estos hooks. + type Slash = (); + type Reward = (); + + type SessionsPerEra = SessionsPerEra; + type BondingDuration = BondingDuration; + type SlashDeferDuration = SlashDeferDuration; + type AdminOrigin = EnsureRoot; + + // DORMIDO (Paso A): sin session-swap → staking no avanza eras. El swap a + // `Self` + `Session` (que enciende EraPayout) es el siguiente paso. + type SessionInterface = (); + type NextNewSession = (); + + type MaxExposurePageSize = ConstU32<64>; + type MaxValidatorSet = MaxValidatorSet; + type MaxUnlockingChunks = ConstU32<32>; + type MaxControllersInDeprecationBatch = ConstU32<100>; + type HistoryDepth = ConstU32<84>; + + type EventListeners = (); + type Filter = Nothing; + type BenchmarkingConfig = OriginStakingBenchmarkingConfig; + type WeightInfo = pallet_staking::weights::SubstrateWeight; + type RuntimeEvent = RuntimeEvent; +}