feat(gobierno): spec 19, el quorum es el 10 % de lo que puede votar
Decision de David, 2026-10-29: «Si, del 10 % de lo que puede votar». Que puede votar: - Es la emision vigente menos el saldo que nadie puede votar ni recuperar: el pool de emision, el de subsidios y el vault de gobierno (cuentas_sin_llave en configs.rs). - Los bonds (og/props) y los depositos de pallet-revive NO se restan, porque vuelven enteros a una cuenta con llave. La revision adversaria de spec 19 lo demostro con sonda: restarlos dejaba bajar el quorum un 14 % en el genesis de mainnet, aparcando un bond en una propuesta de relleno y recuperandolo despues. Efecto: - En mainnet el quorum pasa de ~100M a ~35,1M OGC (10 % de 351M). - En la testnet de la casa apenas cambia. - Las propuestas abiertas conservan el quorum que guardaron al crearse. Pruebas: - quorums_del_runtime clasifica cada cuenta de pallet por su nombre. - aparcar_un_bond_no_baja_el_quorum reproduce la sonda con la configuracion real. - toda_cuenta_de_pallet_con_saldo_esta_clasificada vigila las cuentas modl del genesis. - 10 contrafactuales en rojo. - El E2E (paso 2b) descubre las cuentas de pallet recorriendo el storage, no copia la lista, y exige el quorum exacto. La comision se quema despues de despachar, y eso se mide en el BurnedDebt de la propia transaccion. Pesos: submit_proposal, submit_treasury_proposal y veto_proposal leen 3 cuentas mas. Medidos en la Pi: 5 procesos limpios, el nucleo calentado 3 s antes de cada uno, a 2,4 GHz. Operacion de la rp1, arrancar.sh: - en cada encendido desde el 26-oct el nodo moria al segundo porque la red no estaba; ahora espera a la ruta por defecto; - un cerrojo flock impide dos instancias; - el pid vivo se reconoce por su linea de comandos. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
b6d05eaf45
commit
158060bfe4
7 files changed
+355
-101
No files matched your search
@@ -22,24 +22,26 @@
|
||||
// Ed25519). Lo COBRADO sube menos, entre +4 % y +56 %, porque lecturas y escrituras de storage
|
||||
// (RocksDbWeight) no cambian y pesan más que la ejecución (revisión adversarial, 25-oct).
|
||||
//
|
||||
// SPEC 18 (RV17): SEIS de estos pesos NO son de Laurha. Los midió Charon en la misma Pi el
|
||||
// 2026-10-28, 21:49-21:52, porque su código cambió: `submit_proposal`, `submit_treasury_proposal`,
|
||||
// `finalize_proposal`, `finalize_override`, `veto_proposal` y `treasury_disbursement`.
|
||||
// Es un SMOKE DECLARADO, no un certificado:
|
||||
// governor ondemand (no performance) a 2,4 GHz, núcleo 3 por taskset y SIN aislar,
|
||||
// throttled 0x0, 48-64 °C.
|
||||
// 5 procesos LIMPIOS: ningún job de la CI vivo en ningún segundo de cada ventana (cualquier
|
||||
// proceso de gitea-runner salvo el daemon), y 30 s de calma antes de cada uno.
|
||||
// frame-omni-bencher 0.15.0 · wasm sha256 539d7bf4…9311eb9 (spec 18, RV17) · steps 50 ·
|
||||
// repeat 20 · máximo de los 5 (max-de-n.py).
|
||||
// Evidencia en la rp1: ~/charon/bench-spec18/v2 (limpios/, perfil-v3.csv, limpio.log,
|
||||
// SHA256SUMS-limpio).
|
||||
// Se descartó una medida anterior: su muestreador sólo contaba `rustc` y no veía los binarios de
|
||||
// test de la CI. Un proceso salió 10 veces más lento y el máximo lo habría cobrado.
|
||||
// Calibración: sobre los 11 que no cambian, este smoke da 0,75-1,25 veces lo certificado (el
|
||||
// ruido entre procesos que midió Laurha). Esos 11 se quedan con lo de Laurha. Las lecturas, las
|
||||
// escrituras y el proof de los 17 coinciden con el bench del Mac sobre el mismo wasm. Laurha
|
||||
// puede certificar los 6 en su banco y sustituirlos.
|
||||
// SPECS 18 Y 19 (RV17): SEIS de estos pesos NO son de Laurha. Son de Charon, medidos en la misma
|
||||
// Pi porque su código cambió. Es un SMOKE DECLARADO, no un certificado: governor ondemand, núcleo
|
||||
// 3 por taskset y SIN aislar, 5 procesos LIMPIOS (ningún proceso de gitea-runner salvo el daemon
|
||||
// en ningún segundo de cada ventana, y 30 s de calma antes), steps 50, repeat 20, y el máximo de
|
||||
// los 5 (max-de-n.py). frame-omni-bencher 0.15.0.
|
||||
// - `finalize_proposal`, `finalize_override` y `treasury_disbursement`: el 2026-10-28,
|
||||
// 21:49-21:52, wasm de spec 18 `539d7bf4…9311eb9`, 48-64 °C. Evidencia en
|
||||
// ~/charon/bench-spec18/v2 de la rp1.
|
||||
// - `submit_proposal`, `submit_treasury_proposal` y `veto_proposal`: el 2026-10-29,
|
||||
// 09:15-09:19, wasm de spec 19 `c817695a…f96b23a3`. Leen los pools y el vault para el quórum.
|
||||
// El núcleo se calentó 3 s antes de cada proceso: 2,4 GHz en todas las muestras, 59-64 °C.
|
||||
// Evidencia en ~/charon/bench-spec19/v2.
|
||||
// Sin calentar, ondemand arrancaba cada proceso a 1,5-2,1 GHz y el máximo salía hasta un 10 %
|
||||
// más alto (~/charon/bench-spec19/sin-calentar, sobre un wasm anterior).
|
||||
// Se descartó una medida del 28-oct: su muestreador sólo contaba `rustc` y no veía los tests de
|
||||
// la CI. Un proceso salió 10 veces más lento.
|
||||
// Calibración: sobre los que no cambian, estos smokes dan 0,75-1,24 veces lo certificado (el ruido
|
||||
// entre procesos que midió Laurha). Esos once se quedan con lo de Laurha. Las lecturas, las
|
||||
// escrituras y el proof de los 17 coinciden con el bench del Mac sobre el mismo wasm. Laurha puede
|
||||
// certificar los seis en su banco y sustituirlos.
|
||||
|
||||
//! Weights AUTOGENERADOS para `origin_chain_governance` — NO editar a mano.
|
||||
//!
|
||||
@@ -132,7 +134,7 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
|
||||
/// Proof: `Governance::Proposals` (`max_values`: None, `max_size`: Some(257), added: 2732, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Balances::TotalIssuance` (r:1 w:0)
|
||||
/// Proof: `Balances::TotalIssuance` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`)
|
||||
/// Storage: `System::Account` (r:1 w:1)
|
||||
/// Storage: `System::Account` (r:4 w:1)
|
||||
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
|
||||
/// Storage: `System::Number` (r:1 w:0)
|
||||
/// Proof: `System::Number` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
||||
@@ -149,11 +151,11 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
|
||||
fn submit_proposal() -> Weight {
|
||||
// Proof size en bytes:
|
||||
// Medido: `108`
|
||||
// Estimado: `3722`
|
||||
// Tiempo minimo de ejecucion: 120_389_000 picosegundos.
|
||||
Weight::from_parts(124_315_000, 0)
|
||||
.saturating_add(Weight::from_parts(0, 3722))
|
||||
.saturating_add(T::DbWeight::get().reads(7_u64))
|
||||
// Estimado: `11402`
|
||||
// Tiempo minimo de ejecucion: 133_445_000 picosegundos.
|
||||
Weight::from_parts(135_242_000, 0)
|
||||
.saturating_add(Weight::from_parts(0, 11402))
|
||||
.saturating_add(T::DbWeight::get().reads(10_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(6_u64))
|
||||
}
|
||||
/// Storage: `Governance::AdvisorFlags` (r:1 w:0)
|
||||
@@ -270,10 +272,10 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
|
||||
/// Proof: `Governance::VetoVotes` (`max_values`: None, `max_size`: Some(96), added: 2571, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Balances::TotalIssuance` (r:1 w:0)
|
||||
/// Proof: `Balances::TotalIssuance` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`)
|
||||
/// Storage: `System::Account` (r:5 w:2)
|
||||
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Governance::ProposalBond` (r:1 w:1)
|
||||
/// Proof: `Governance::ProposalBond` (`max_values`: None, `max_size`: Some(96), added: 2571, mode: `MaxEncodedLen`)
|
||||
/// Storage: `System::Account` (r:2 w:2)
|
||||
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
|
||||
/// Storage: `System::ExecutionPhase` (r:1 w:0)
|
||||
/// Proof: `System::ExecutionPhase` (`max_values`: Some(1), `max_size`: Some(5), added: 500, mode: `MaxEncodedLen`)
|
||||
/// Storage: `System::EventCount` (r:1 w:1)
|
||||
@@ -292,10 +294,10 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
|
||||
// Proof size en bytes:
|
||||
// Medido: `1441`
|
||||
// Estimado: `16416`
|
||||
// Tiempo minimo de ejecucion: 250_427_000 picosegundos.
|
||||
Weight::from_parts(279_612_000, 0)
|
||||
// Tiempo minimo de ejecucion: 263_724_000 picosegundos.
|
||||
Weight::from_parts(269_483_000, 0)
|
||||
.saturating_add(Weight::from_parts(0, 16416))
|
||||
.saturating_add(T::DbWeight::get().reads(21_u64))
|
||||
.saturating_add(T::DbWeight::get().reads(24_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(11_u64))
|
||||
}
|
||||
/// Storage: `Governance::AdvisorFlags` (r:1 w:1)
|
||||
@@ -582,7 +584,7 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
|
||||
/// Proof: `Governance::Proposals` (`max_values`: None, `max_size`: Some(257), added: 2732, mode: `MaxEncodedLen`)
|
||||
/// Storage: `Balances::TotalIssuance` (r:1 w:0)
|
||||
/// Proof: `Balances::TotalIssuance` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`)
|
||||
/// Storage: `System::Account` (r:1 w:1)
|
||||
/// Storage: `System::Account` (r:4 w:1)
|
||||
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
|
||||
/// Storage: `System::Number` (r:1 w:0)
|
||||
/// Proof: `System::Number` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
||||
@@ -601,11 +603,11 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
|
||||
fn submit_treasury_proposal() -> Weight {
|
||||
// Proof size en bytes:
|
||||
// Medido: `108`
|
||||
// Estimado: `3722`
|
||||
// Tiempo minimo de ejecucion: 128_000_000 picosegundos.
|
||||
Weight::from_parts(132_148_000, 0)
|
||||
.saturating_add(Weight::from_parts(0, 3722))
|
||||
.saturating_add(T::DbWeight::get().reads(7_u64))
|
||||
// Estimado: `11402`
|
||||
// Tiempo minimo de ejecucion: 140_537_000 picosegundos.
|
||||
Weight::from_parts(143_630_000, 0)
|
||||
.saturating_add(Weight::from_parts(0, 11402))
|
||||
.saturating_add(T::DbWeight::get().reads(10_u64))
|
||||
.saturating_add(T::DbWeight::get().writes(7_u64))
|
||||
}
|
||||
}
|
||||
@@ -613,9 +615,9 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
|
||||
/// Impl para mocks y tests — `RocksDbWeight` directo, sin necesitar `T`.
|
||||
impl WeightInfo for () {
|
||||
fn submit_proposal() -> Weight {
|
||||
Weight::from_parts(124_315_000, 0)
|
||||
.saturating_add(Weight::from_parts(0, 3722))
|
||||
.saturating_add(RocksDbWeight::get().reads(7_u64))
|
||||
Weight::from_parts(135_242_000, 0)
|
||||
.saturating_add(Weight::from_parts(0, 11402))
|
||||
.saturating_add(RocksDbWeight::get().reads(10_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(6_u64))
|
||||
}
|
||||
fn cast_vote() -> Weight {
|
||||
@@ -643,9 +645,9 @@ impl WeightInfo for () {
|
||||
.saturating_add(RocksDbWeight::get().writes(4_u64))
|
||||
}
|
||||
fn veto_proposal() -> Weight {
|
||||
Weight::from_parts(279_612_000, 0)
|
||||
Weight::from_parts(269_483_000, 0)
|
||||
.saturating_add(Weight::from_parts(0, 16416))
|
||||
.saturating_add(RocksDbWeight::get().reads(21_u64))
|
||||
.saturating_add(RocksDbWeight::get().reads(24_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(11_u64))
|
||||
}
|
||||
fn mark_as_advisor() -> Weight {
|
||||
@@ -709,9 +711,9 @@ impl WeightInfo for () {
|
||||
.saturating_add(RocksDbWeight::get().writes(6_u64))
|
||||
}
|
||||
fn submit_treasury_proposal() -> Weight {
|
||||
Weight::from_parts(132_148_000, 0)
|
||||
.saturating_add(Weight::from_parts(0, 3722))
|
||||
.saturating_add(RocksDbWeight::get().reads(7_u64))
|
||||
Weight::from_parts(143_630_000, 0)
|
||||
.saturating_add(Weight::from_parts(0, 11402))
|
||||
.saturating_add(RocksDbWeight::get().reads(10_u64))
|
||||
.saturating_add(RocksDbWeight::get().writes(7_u64))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,8 +13,9 @@
|
||||
//! firmar <llamada> <nonce> <genesis_hex> [spec tx] extrinsic firmado por //Alice, en hex
|
||||
//! llamadas: remark · finalize_override (propuesta 0x09…09) · uphold0 (peso 0)
|
||||
//! set_code:<ruta.wasm> (sudo de //Alice, ensayo de upgrade sin fork)
|
||||
//! tesoro:<monto> (RV17: propuesta de tesoro de //Alice que paga <monto> plancks
|
||||
//! a //Bob; 14 días y el bond mínimo) · pagar:<id_hex>:<monto> (sudo de
|
||||
//! tesoro:<monto>[:<etiqueta>] (RV17: propuesta de tesoro de //Alice que paga
|
||||
//! <monto> plancks a //Bob; 14 días y el bond mínimo; la etiqueta cambia el título
|
||||
//! y con él el id) · pagar:<id_hex>:<monto> (sudo de
|
||||
//! treasury_disbursement a //Bob) · votar:<id_hex>:<peso> (Aye de //Alice)
|
||||
//! · depositar:<monto> (treasury_deposit de //Alice al vault)
|
||||
//! · finalizar:<id_hex> (finalize_proposal)
|
||||
@@ -69,9 +70,14 @@ fn llamada(nombre: &str) -> RuntimeCall {
|
||||
stake_weight: 0,
|
||||
}),
|
||||
otra if otra.starts_with("tesoro:") => {
|
||||
let amount: u128 = otra["tesoro:".len()..].parse().expect("tesoro:<monto en plancks>");
|
||||
let (monto, etiqueta) = match otra["tesoro:".len()..].split_once(':') {
|
||||
Some((m, e)) => (m, format!("/{e}")),
|
||||
None => (&otra["tesoro:".len()..], String::new()),
|
||||
};
|
||||
let amount: u128 = monto.parse().expect("tesoro:<monto en plancks>[:<etiqueta>]");
|
||||
let titulo = [b"e2e-rv17/titulo".as_slice(), etiqueta.as_bytes()].concat();
|
||||
RuntimeCall::Governance(origin_chain_governance::Call::submit_treasury_proposal {
|
||||
title_hash: sp_core::blake2_256(b"e2e-rv17/titulo"),
|
||||
title_hash: sp_core::blake2_256(&titulo),
|
||||
body_hash: sp_core::blake2_256(b"e2e-rv17/cuerpo"),
|
||||
voting_period_blocks: origin_chain_governance::MIN_VOTING_PERIOD_BLOCKS,
|
||||
stake: origin_chain_governance::MIN_PROPOSAL_STAKE,
|
||||
|
||||
@@ -547,24 +547,72 @@ parameter_types! {
|
||||
frame_support::PalletId(*b"og/gvtry");
|
||||
}
|
||||
|
||||
/// RV17: el «10% quorum» de PILAR_4 §6.6, sobre la emisión VIGENTE, redondeado hacia arriba.
|
||||
/// Nunca baja del bond de una propuesta: con menos, el proponente aprobaría solo su propuesta
|
||||
/// (o revertiría solo un veto) con un voto del tamaño de su fianza.
|
||||
/// El OGC que NO puede votar: el de cuentas que se derivan de un `PalletId`, que nadie puede
|
||||
/// firmar, y del que nadie recupera nada por su cuenta. Son el pool de emisión, el de subsidios y
|
||||
/// el vault de gobierno. Lo que entra ahí no vuelve a quien lo puso: sale por la emisión o por una
|
||||
/// propuesta de tesoro aprobada, que a su vez tiene que pasar el quórum.
|
||||
///
|
||||
/// Hasta RV17 eran 100M OGC fijos, el 10 % de 1B. Dos razones para medirlo sobre la emisión:
|
||||
/// - las comisiones se queman al 100 % (`OU = ()`) y la emisión sólo transfiere desde el pool,
|
||||
/// así que el suministro BAJA, y 100M acabaría siendo más que el 10 %;
|
||||
/// - en una cadena con otro génesis el número fijo no significa nada. En la testnet de la casa
|
||||
/// (génesis dev: 100M emitidos, todos de Alice) ninguna propuesta ordinaria podía llegar
|
||||
/// nunca a 100M de Aye (revisión adversarial de RV17).
|
||||
/// NO entran dos cuentas de pallet que también tienen saldo:
|
||||
/// - `og/props`, el escrow de los bonds. Ese OGC es del proponente y le vuelve entero al decidirse
|
||||
/// la propuesta. Restarlo dejaba BAJAR el quórum aparcando un bond enorme en una propuesta de
|
||||
/// relleno: la revisión adversaria de spec 19 lo bajó un 14 % en el génesis de mainnet sin coste.
|
||||
/// - la cuenta de `pallet-revive`, que guarda los depósitos de subida de código. Se devuelven a
|
||||
/// quien subió el código, así que pasa lo mismo, aunque en pequeño. Su ED de génesis cuenta como
|
||||
/// votable: endurece el quórum en 0,0001 OGC.
|
||||
///
|
||||
/// Mainnet (1B) da lo mismo que antes, 100M.
|
||||
pub struct DiezPorCientoDeLaEmision;
|
||||
impl frame_support::traits::Get<u128> for DiezPorCientoDeLaEmision {
|
||||
/// Si una pallet nueva guarda OGC en una cuenta propia, se decide aquí, o en
|
||||
/// [`cuentas_de_pallet_que_si_cuentan`], y se dice por qué. Lo vigila
|
||||
/// `toda_cuenta_de_pallet_con_saldo_esta_clasificada` (genesis_config_presets.rs).
|
||||
pub fn cuentas_sin_llave() -> [ChainAccountId; 3] {
|
||||
use sp_runtime::traits::AccountIdConversion;
|
||||
[
|
||||
EmissionEcosystemPalletId::get().into_account_truncating(),
|
||||
EmissionSubsidyPalletId::get().into_account_truncating(),
|
||||
GovernanceTreasuryPalletId::get().into_account_truncating(),
|
||||
]
|
||||
}
|
||||
|
||||
/// Cuentas de pallet cuyo OGC SÍ cuenta como votable, porque vuelve a una cuenta con llave (ver
|
||||
/// [`cuentas_sin_llave`]).
|
||||
pub fn cuentas_de_pallet_que_si_cuentan() -> [ChainAccountId; 2] {
|
||||
use sp_runtime::traits::AccountIdConversion;
|
||||
[
|
||||
origin_chain_governance::PROPOSAL_BOND_PALLET_ID.into_account_truncating(),
|
||||
pallet_revive::Pallet::<Runtime>::account_id(),
|
||||
]
|
||||
}
|
||||
|
||||
/// RV17 (spec 19): el «10% quorum» de PILAR_4 §6.6 sobre lo que PUEDE VOTAR. Es la emisión vigente
|
||||
/// menos el saldo que nadie puede votar ni recuperar ([`cuentas_sin_llave`]), con el 10 %
|
||||
/// redondeado hacia arriba. Nunca baja del bond de una propuesta: con menos, el proponente
|
||||
/// aprobaría solo su propuesta (o revertiría solo un veto) con un voto del tamaño de su fianza.
|
||||
///
|
||||
/// David, 2026-10-29: «Sí, del 10 % de lo que puede votar». En el génesis de mainnet el pool de
|
||||
/// emisión (400M) y el vault (249M) no votan, así que el quórum es el 10 % de unos 351M, ~35,1M
|
||||
/// OGC. Medido sobre la emisión total habría sido 100M, ~28 % de lo votable.
|
||||
///
|
||||
/// Historia:
|
||||
/// - Hasta spec 18 eran 100M OGC fijos (RV16) y luego el 10 % de la emisión total (spec 18).
|
||||
/// - El número fijo no significaba nada en otro génesis: en la testnet de la casa (100M, todos de
|
||||
/// Alice) nada podía aprobarse.
|
||||
/// - Las comisiones se queman, así que la emisión baja.
|
||||
///
|
||||
/// Lo que NO resta: el OGC retenido (`hold`) en cuentas con llave, los bonds, los depósitos de
|
||||
/// revive y las cuentas de contratos. Una parte de ese OGC no puede votar en este momento, pero
|
||||
/// vuelve a quien sí puede: restarlo abriría una palanca reversible sobre el quórum. El quórum
|
||||
/// resulta algo más exigente que el 10 % exacto, nunca menos.
|
||||
pub struct DiezPorCientoDeLoQuePuedeVotar;
|
||||
impl frame_support::traits::Get<u128> for DiezPorCientoDeLoQuePuedeVotar {
|
||||
fn get() -> u128 {
|
||||
use frame_support::traits::fungible::Inspect;
|
||||
let emision: u128 = <crate::Balances as Inspect<ChainAccountId>>::total_issuance();
|
||||
type B = crate::Balances;
|
||||
let emision: u128 = <B as Inspect<ChainAccountId>>::total_issuance();
|
||||
let sin_llave: u128 = cuentas_sin_llave()
|
||||
.iter()
|
||||
.map(|cuenta| <B as Inspect<ChainAccountId>>::total_balance(cuenta))
|
||||
.fold(0, u128::saturating_add);
|
||||
emision
|
||||
.saturating_sub(sin_llave)
|
||||
.div_ceil(10)
|
||||
.max(origin_chain_governance::MIN_PROPOSAL_STAKE + 1)
|
||||
}
|
||||
@@ -578,13 +626,12 @@ impl origin_chain_governance::Config for Runtime {
|
||||
type RuntimeFreezeReason = RuntimeFreezeReason;
|
||||
// RV15: votos VIVOS simultáneos por cuenta (los vencidos se descartan solos).
|
||||
type MaxActiveVotes = frame_support::traits::ConstU32<32>;
|
||||
// RV16 y RV17: los dos quórums son el «10% quorum» de PILAR_4 §6.6, sobre la
|
||||
// emisión VIGENTE (ver `DiezPorCientoDeLaEmision`). El pallet los lee en el veto y al
|
||||
// crear la propuesta, y los guarda. Son parámetro de la constitución del DAO y los
|
||||
// fija David; mientras, éstos. Los pinna `quorums_del_runtime`
|
||||
// (genesis_config_presets.rs).
|
||||
type MinOverrideQuorum = DiezPorCientoDeLaEmision;
|
||||
type VoteQuorum = DiezPorCientoDeLaEmision;
|
||||
// RV16 y RV17: los dos quórums son el «10% quorum» de PILAR_4 §6.6 sobre lo que PUEDE
|
||||
// VOTAR (ver `DiezPorCientoDeLoQuePuedeVotar`; David, 29-oct). El pallet los lee en el veto
|
||||
// y al crear la propuesta, y los guarda. Son parámetro de la constitución del DAO y los fija
|
||||
// David. Los pinna `quorums_del_runtime` (genesis_config_presets.rs).
|
||||
type MinOverrideQuorum = DiezPorCientoDeLoQuePuedeVotar;
|
||||
type VoteQuorum = DiezPorCientoDeLoQuePuedeVotar;
|
||||
type TreasuryPalletId = GovernanceTreasuryPalletId;
|
||||
// Weights INTERIM hand-derived DB-aware (ver weights.rs). Regenerable en HW.
|
||||
type WeightInfo = origin_chain_governance::weights::SubstrateWeight<Runtime>;
|
||||
|
||||
@@ -677,14 +677,23 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
/// RV16 y RV17: los dos quórums del runtime son el 10 % de la emisión VIGENTE, redondeado
|
||||
/// hacia arriba y nunca por debajo del bond. Se miden sobre el estado que construye cada
|
||||
/// preset, no sobre una constante. Sin este test pasaba toda la suite cualquiera de estos
|
||||
/// errores: un typo como `{ 100_000_000 }` sin `* OGC`, `ConstU128<0>` (revisión de
|
||||
/// RV16), dividir entre 100 u olvidar el piso.
|
||||
/// Una cuenta de pallet, por su `PalletId`, NOMBRADA aquí y no sacada de las listas del
|
||||
/// runtime: así un error en las listas no se esconde detrás de sí mismo.
|
||||
fn de_pallet(id: &[u8; 8]) -> ChainAccountId {
|
||||
frame_support::PalletId(*id).into_account_truncating()
|
||||
}
|
||||
|
||||
/// RV16 y RV17: los dos quórums del runtime son el 10 % de lo que PUEDE VOTAR, redondeado
|
||||
/// hacia arriba y nunca por debajo del bond. Lo que puede votar es la emisión menos los pools y
|
||||
/// el vault: lo que nadie puede firmar ni recuperar. Se mide sobre el estado que construye cada
|
||||
/// preset, no sobre una constante. Sin este test pasaba la suite cualquiera de estos errores:
|
||||
/// - un typo como `{ 100_000_000 }` sin `* OGC`, o `ConstU128<0>` (revisión de RV16);
|
||||
/// - dividir entre 100, u olvidar el piso;
|
||||
/// - volver a medir sobre la emisión total;
|
||||
/// - restar los bonds o los depósitos de revive.
|
||||
#[test]
|
||||
fn quorums_del_runtime() {
|
||||
use frame_support::traits::{fungible::Inspect, Get};
|
||||
use frame_support::traits::{fungible::Inspect, fungible::Mutate, Get};
|
||||
type Gob = crate::Runtime;
|
||||
let voto = || <<Gob as origin_chain_governance::Config>::VoteQuorum as Get<u128>>::get();
|
||||
let veto =
|
||||
@@ -692,44 +701,157 @@ mod tests {
|
||||
let ogc = crate::configs::OGC;
|
||||
// pallet-revive acuña un depósito existencial en su cuenta al construir el génesis
|
||||
// (`mint_into(account_id, minimum_balance())`): la emisión es la suma de las
|
||||
// asignaciones MÁS ese ED.
|
||||
// asignaciones MÁS ese ED, y ese ED cuenta como votable (ver `cuentas_sin_llave`).
|
||||
let ed = crate::configs::EXISTENTIAL_DEPOSIT;
|
||||
|
||||
// Sin estado (así corre `integrity_test`): el piso, que supera el bond.
|
||||
sp_io::TestExternalities::new_empty().execute_with(|| {
|
||||
assert_eq!(voto(), origin_chain_governance::MIN_PROPOSAL_STAKE + 1);
|
||||
assert_eq!(veto(), voto());
|
||||
// «El 10 %» se redondea hacia ARRIBA: con 10·k + 1 de emisión, k no llega al
|
||||
// 10 %. Las emisiones de los presets son múltiplos de 10 y no lo verían.
|
||||
// «El 10 %» se redondea hacia ARRIBA: con 10·k + 1 de emisión, k no llega al 10 %.
|
||||
let k = 10_000_000 * ogc;
|
||||
pallet_balances::TotalIssuance::<crate::Runtime>::put(10 * k + 1);
|
||||
assert_eq!(voto(), k + 1);
|
||||
assert_eq!(veto(), k + 1);
|
||||
});
|
||||
for (preset, emision, quorum) in [
|
||||
(
|
||||
PRESET_MAINNET_GENESIS,
|
||||
Some(1_000_000_000 * ogc + ed),
|
||||
Some(100_000_000 * ogc + ed / 10),
|
||||
),
|
||||
(PRESET_DEVELOPMENT, Some(100_000_000 * ogc + ed), Some(10_000_000 * ogc + ed / 10)),
|
||||
(PRESET_LOCAL_TESTNET, None, None),
|
||||
// Cada cuenta de pallet, por su nombre: las que no vuelven a nadie se restan, y las que
|
||||
// vuelven a una cuenta con llave (bonds y depósitos de revive) cuentan como votables.
|
||||
sp_io::TestExternalities::new_empty().execute_with(|| {
|
||||
let k = 10_000_000 * ogc;
|
||||
let mint = |cuenta: &ChainAccountId| {
|
||||
<crate::Balances as Mutate<ChainAccountId>>::mint_into(cuenta, 7 * k).expect("mint")
|
||||
};
|
||||
<crate::Balances as Mutate<ChainAccountId>>::mint_into(
|
||||
&account_from_sr25519(ALICE_SR25519),
|
||||
10 * k,
|
||||
)
|
||||
.expect("mint");
|
||||
for no_vota in [b"og/ecosy", b"og/subsy", b"og/gvtry"] {
|
||||
mint(&de_pallet(no_vota));
|
||||
}
|
||||
for vuelve in [de_pallet(b"og/props"), de_pallet(b"py/reviv")] {
|
||||
mint(&vuelve);
|
||||
}
|
||||
assert_eq!(<crate::Balances as Inspect<ChainAccountId>>::total_issuance(), 45 * k);
|
||||
assert_eq!(voto(), 24 * k / 10, "10 + 7 + 7 votan; los 21 de pools y vault, no");
|
||||
assert_eq!(veto(), voto());
|
||||
});
|
||||
|
||||
for (preset, votable, quorum) in [
|
||||
// 1B + ED emitidos; no votan el pool (400M) ni el vault (249M).
|
||||
(PRESET_MAINNET_GENESIS, 351_000_000 * ogc + ed, 35_100_000 * ogc + ed / 10),
|
||||
// 100M de Alice + el ED de revive.
|
||||
(PRESET_DEVELOPMENT, 100_000_000 * ogc + ed, 10_000_000 * ogc + ed / 10),
|
||||
] {
|
||||
let json = get_preset(&PresetId::from(preset)).expect("preset");
|
||||
sp_io::TestExternalities::new_empty().execute_with(|| {
|
||||
frame_support::genesis_builder_helper::build_state::<RuntimeGenesisConfig>(json)
|
||||
.expect("el preset construye estado");
|
||||
let total = <crate::Balances as Inspect<ChainAccountId>>::total_issuance();
|
||||
if let Some(e) = emision {
|
||||
assert_eq!(total, e, "{preset}: emisión");
|
||||
}
|
||||
assert_eq!(voto(), total.div_ceil(10), "{preset}: 10 % de la emisión");
|
||||
if let Some(q) = quorum {
|
||||
assert_eq!(voto(), q, "{preset}");
|
||||
}
|
||||
let no_votan: u128 = [b"og/ecosy", b"og/subsy", b"og/gvtry"]
|
||||
.iter()
|
||||
.map(|id| {
|
||||
<crate::Balances as Inspect<ChainAccountId>>::total_balance(&de_pallet(id))
|
||||
})
|
||||
.sum();
|
||||
assert_eq!(total - no_votan, votable, "{preset}: lo que puede votar");
|
||||
assert_eq!(voto(), quorum, "{preset}");
|
||||
assert_eq!(veto(), voto(), "{preset}: los dos quórums son el mismo 10 %");
|
||||
assert!(voto() > origin_chain_governance::MIN_PROPOSAL_STAKE);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Revisión adversaria de spec 19: si los bonds se restaran, una cuenta grande aparcaba OGC en
|
||||
/// una propuesta de relleno y el quórum de las que se crearan mientras tanto BAJABA, y luego
|
||||
/// recuperaba su bond entero. Se mide con la configuración real sobre el génesis de mainnet.
|
||||
/// Aparcar un bond no mueve el quórum de nadie; lo que entra en el vault, que no vuelve, sí.
|
||||
#[test]
|
||||
fn aparcar_un_bond_no_baja_el_quorum() {
|
||||
use crate::{Governance, RuntimeOrigin, System};
|
||||
use frame_support::assert_ok;
|
||||
use origin_chain_governance::{derive_proposal_id, VoteQuorumOf, MIN_VOTING_PERIOD_BLOCKS};
|
||||
let ogc = crate::configs::OGC;
|
||||
let placeholder = |tag: &[u8]| {
|
||||
let mut b = [0u8; 32];
|
||||
b[..tag.len()].copy_from_slice(tag);
|
||||
b
|
||||
};
|
||||
let (liqd, sale) = (placeholder(b"LIQD"), placeholder(b"SALE"));
|
||||
// Crea, desde `quien`, una propuesta con `stake` de bond y devuelve su quórum guardado.
|
||||
let proponer = |quien: [u8; 32], titulo: u8, stake: u128| {
|
||||
assert_ok!(Governance::submit_proposal(
|
||||
RuntimeOrigin::signed(account_from_sr25519(quien)),
|
||||
[titulo; 32],
|
||||
[0u8; 32],
|
||||
MIN_VOTING_PERIOD_BLOCKS,
|
||||
stake,
|
||||
));
|
||||
let id = derive_proposal_id(
|
||||
&origin_chain_types::AccountId::new(quien),
|
||||
&[titulo; 32],
|
||||
&[0u8; 32],
|
||||
);
|
||||
VoteQuorumOf::<crate::Runtime>::get(id).expect("quórum guardado")
|
||||
};
|
||||
let medir = |antes: &dyn Fn()| {
|
||||
let json = get_preset(&PresetId::from(PRESET_MAINNET_GENESIS)).expect("preset");
|
||||
let mut ext = sp_io::TestExternalities::new_empty();
|
||||
ext.execute_with(|| {
|
||||
frame_support::genesis_builder_helper::build_state::<RuntimeGenesisConfig>(json)
|
||||
.expect("el preset construye estado");
|
||||
System::set_block_number(1);
|
||||
antes();
|
||||
proponer(sale, 2, origin_chain_governance::MIN_PROPOSAL_STAKE)
|
||||
})
|
||||
};
|
||||
let control = medir(&|| {});
|
||||
let con_bond_aparcado = medir(&|| {
|
||||
proponer(liqd, 1, 49_900_000 * ogc);
|
||||
});
|
||||
assert_eq!(con_bond_aparcado, control, "un bond aparcado bajó el quórum");
|
||||
// Control positivo: lo que entra en el vault no vuelve, y sí baja el quórum (X/10).
|
||||
let con_deposito = medir(&|| {
|
||||
assert_ok!(Governance::treasury_deposit(
|
||||
RuntimeOrigin::signed(account_from_sr25519(liqd)),
|
||||
10_000_000 * ogc,
|
||||
));
|
||||
});
|
||||
assert_eq!(control - con_deposito, 1_000_000 * ogc);
|
||||
}
|
||||
|
||||
/// Toda cuenta derivada de un `PalletId` («modl» + id) con saldo tiene que estar clasificada:
|
||||
/// o no vota (`cuentas_sin_llave`) o su OGC vuelve a una cuenta con llave
|
||||
/// (`cuentas_de_pallet_que_si_cuentan`). Mira el génesis de los tres presets. Una pallet que
|
||||
/// acumule OGC sólo en ejecución no la vería: para eso está la nota de `cuentas_sin_llave`.
|
||||
#[test]
|
||||
fn toda_cuenta_de_pallet_con_saldo_esta_clasificada() {
|
||||
use frame_support::traits::fungible::Inspect;
|
||||
let sin_llave = crate::configs::cuentas_sin_llave();
|
||||
let cuentan = crate::configs::cuentas_de_pallet_que_si_cuentan();
|
||||
let todas: alloc::collections::BTreeSet<_> =
|
||||
sin_llave.iter().chain(cuentan.iter()).collect();
|
||||
assert_eq!(todas.len(), sin_llave.len() + cuentan.len(), "una cuenta en dos listas");
|
||||
for preset in preset_names() {
|
||||
let json = get_preset(&preset).expect("preset");
|
||||
sp_io::TestExternalities::new_empty().execute_with(|| {
|
||||
frame_support::genesis_builder_helper::build_state::<RuntimeGenesisConfig>(json)
|
||||
.expect("el preset construye estado");
|
||||
let mut vistas = 0;
|
||||
for (cuenta, _) in frame_system::Account::<crate::Runtime>::iter() {
|
||||
let con_saldo =
|
||||
<crate::Balances as Inspect<ChainAccountId>>::total_balance(&cuenta) > 0;
|
||||
let es_de_pallet = AsRef::<[u8]>::as_ref(&cuenta).starts_with(b"modl");
|
||||
if con_saldo && es_de_pallet {
|
||||
vistas += 1;
|
||||
assert!(
|
||||
todas.contains(&cuenta),
|
||||
"{preset:?}: {cuenta} es de pallet y no está clasificada"
|
||||
);
|
||||
}
|
||||
}
|
||||
assert!(vistas >= 1, "{preset:?}: al menos la cuenta de revive tiene saldo");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -374,6 +374,11 @@ pub const IMPL_NAME: &str = borrowed_str(&VERSION.impl_name);
|
||||
/// lógica ni ningún storage, sólo lo que cuesta cada extrinsic. Es el primer upgrade que se
|
||||
/// aplica en vivo a la testnet de la casa (rp1), por `set_code`.
|
||||
///
|
||||
/// **v19 (2026-10-29), RV17 decidido por David:** el quórum es el 10 % de lo que PUEDE VOTAR (la
|
||||
/// emisión menos las cuentas sin llave: pools, vault, bonds y revive). «Sí, del 10 % de lo que
|
||||
/// puede votar» (David, 29-oct). En mainnet pasa de ~100M a ~35,1M OGC; en la testnet de la casa
|
||||
/// apenas cambia (todo es de Alice). Las propuestas abiertas conservan el quórum que guardaron.
|
||||
///
|
||||
/// **v18 (2026-10-28), RV17:**
|
||||
/// - el voto ordinario exige un quórum de Aye. Es el 10 % de PILAR_4 §6.6 sobre la emisión
|
||||
/// vigente, se fija al crear la propuesta (`VoteQuorumOf`) y lo tiene que alcanzar también
|
||||
@@ -1038,7 +1043,7 @@ pub const VERSION: sp_version::RuntimeVersion = sp_version::RuntimeVersion {
|
||||
spec_name: alloc::borrow::Cow::Borrowed("origin-chain"),
|
||||
impl_name: alloc::borrow::Cow::Borrowed("origin-chain-charon"),
|
||||
authoring_version: 1,
|
||||
spec_version: 18,
|
||||
spec_version: 19,
|
||||
impl_version: 1,
|
||||
apis: RUNTIME_API_VERSIONS,
|
||||
transaction_version: 1,
|
||||
@@ -1199,7 +1204,7 @@ mod tests {
|
||||
/// mismo commit en que la version subio a 12.
|
||||
#[test]
|
||||
fn spec_version_vigente_y_transaction_version_intacta() {
|
||||
assert_eq!(RUNTIME_VERSION, 18);
|
||||
assert_eq!(RUNTIME_VERSION, 19);
|
||||
assert_eq!(TRANSACTION_VERSION, 1);
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user