feat(npos): FIX-6 Stage 2b Paso A — pallet_staking Config cableado (infra dormida)
Materializa la infra NPoS DORMIDA del DC ORIGIN_CHAIN_NPOS_V1 (decisiones de fundador ratificadas). pallet_staking 43.0.0 wired como Index 14: los 31 associated types cableados contra el tren stable2509-3 (mapeo quirúrgico audit-before-claims; compiló en 2 iteraciones). Piezas (crates/origin-chain-runtime/src/staking.rs, nuevo): - OriginEraPayout: emisión por-era LINEAL que preserva EXACTAMENTE la tasa de FIX-6 Stage 1 (BlockReward 10 OGC/bloque, 70% validadores / 30% rest). NO la curva ideal-staking-rate de Polkadot (ignora total_staked/issuance a propósito). - DealWithEraRemainder (RewardRemainder): reparte el rest 2/3 treasury (og/gvtry) + 1/3 subsidy (og/subsy) = 20%/10% del total. CRÍTICO #16: mantiene og/subsy alimentado bajo NPoS (sin él, el OriginFeeAdapter de Stage 2a degradaría a pool_covered=0 y el usuario pagaría el 100%). - OnChainExecution<OnChainSeqPhragmen> como (Genesis)ElectionProvider (dormido: ForceNone no dispara elección). VoterList=UseNominatorsAndValidatorsMap + TargetList=UseValidatorsMap (Scores distintos, evitan bags-list + sus asserts). - Params ratificados: MaxValidatorSet=150 (NPoS-6) · SessionsPerEra=6 (era 24h) · BondingDuration=7 eras=7d (NPoS-8) · SlashDeferDuration=0 (sin slashes dormido). - fungible::hold (NPoS-10): Currency/OldCurrency=Balances; el RuntimeHoldReason agregado del macro absorbe HoldReason::Staking automáticamente. Estado: staking PRESENTE pero INERTE — SessionInterface/NextNewSession=() (no avanza eras), genesis force_era=ForceNone sin stakers (PoA-equivalente, cero regresión). El emission pallet Index 13 sigue siendo la fuente de emisión. NO hay doble-emisión (staking no emite mientras no avance eras). Pendiente (sesión fresca, encienden NPoS de verdad → verificación adversarial): Paso B genesis poblado (invulnerables+stakers, asserts-trampa) · Paso C SessionManager=Staking + historical (enciende EraPayout) · Paso D emission on_initialize off (kill-switch anti-doble-emisión) · Paso E tests E2E. spec_version 2→3 (nuevo pallet ⇒ metadata; TRANSACTION_VERSION=1 intacto). 21 suites workspace verde. cargo +1.88. WASM VPS-gated (UnknownOpcode 252). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
440135293e
commit
1f0122beda
5 files changed
+313
-9
No files matched your search
@@ -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<ChainAccountId, Balances>;
|
||||
|
||||
// ─── 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<Balance> 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<CreditOf> 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<T>` 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<true>;
|
||||
type System = Runtime;
|
||||
type Solver = SequentialPhragmen<ChainAccountId, Perbill>;
|
||||
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<OnChainSeqPhragmen>;
|
||||
type GenesisElectionProvider = onchain::OnChainExecution<OnChainSeqPhragmen>;
|
||||
type VoterList = pallet_staking::UseNominatorsAndValidatorsMap<Runtime>;
|
||||
type TargetList = pallet_staking::UseValidatorsMap<Runtime>;
|
||||
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<ChainAccountId>;
|
||||
|
||||
// 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<Runtime>;
|
||||
type RuntimeEvent = RuntimeEvent;
|
||||
}
|
||||
Reference in new issue
Block a user