feat(gobierno): GSUN-3, dos tercios para el tesoro (spec 21, pesos por certificar)
David, 2-nov-2026: «Sí a la súper-mayoría, dos tercios». - Una propuesta de tesoro se aprueba sólo si su Aye es al menos dos tercios del voto emitido (Aye + Nay; la abstención no cuenta), además del quórum y de la mayoría simple. - Se fija al crear la propuesta (storage TreasurySupermajorityOf) y se consume al decidirla. Las de tesoro abiertas antes de spec 21 conservan la mayoría simple con la que se abrieron. - Revertir el veto de una propuesta de tesoro exige los dos tercios frente a TODA la oposición: el Nay original más lo que sostuvo el veto en la ventana. Es el bloqueante de la revisión adversaria: con Nay 4Q y 2Q sosteniendo, 8Q pagaban cuando el voto ordinario pedía 12Q. - Evento nuevo ProposalRejectedForSupermajority, al final del enum. Sin migración y con transaction_version igual. - reaches_supermajority: exacta (aye por den >= num por emitido), monótona, sin Aye nunca aprueba y falla cerrada si desborda. Un const assert exige que la fracción sea mayor que 1/2. - Pruebas: 18 contrafactuales en rojo (17 en su aserción, 1 en compilación), 114 tests del pallet y 373 del workspace. - E2E: pasos 2c (la propuesta nueva guarda 2/3), 2d (la sembrada en spec 18 no) y 2e (la frontera del upgrade sobre todas las propuestas de tesoro). Falta antes de main: el certificado de Laurha para los tres pesos que cambian de esqueleto (submit_treasury_proposal, finalize_proposal, finalize_override), el E2E vivo y el upgrade. Sigue abierto, desde RV17: el override de una propuesta ORDINARIA no cuenta lo que sostiene. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
6910f52b1e
commit
4be7da78c3
5 files changed
+486
-19
No files matched your search
@@ -123,6 +123,7 @@ mod benchmarks {
|
|||||||
);
|
);
|
||||||
|
|
||||||
assert_eq!(TreasuryActions::<T>::iter().count(), 1);
|
assert_eq!(TreasuryActions::<T>::iter().count(), 1);
|
||||||
|
assert_eq!(TreasurySupermajorityOf::<T>::iter().count(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[benchmark]
|
#[benchmark]
|
||||||
@@ -149,16 +150,20 @@ mod benchmarks {
|
|||||||
fn finalize_proposal() {
|
fn finalize_proposal() {
|
||||||
let caller: T::AccountId = whitelisted_caller();
|
let caller: T::AccountId = whitelisted_caller();
|
||||||
let id = ProposalId([7u8; 32]);
|
let id = ProposalId([7u8; 32]);
|
||||||
// RV17: propuesta ORDINARIA (con su quórum guardado) con mayoría pero sin
|
// RV17 + GSUN-3: propuesta de TESORO con mayoría simple, pero sin quórum y
|
||||||
// quórum: es la rama más cara, porque consume el quórum, rechaza, devuelve el
|
// sin los dos tercios (3 Aye, 2 Nay, quórum 4). Es la rama más cara: consume el
|
||||||
// bond y emite DOS eventos (ProposalFinalized + ProposalRejectedForQuorum).
|
// quórum y la mayoría cualificada, rechaza, devuelve el bond y emite TRES
|
||||||
|
// eventos (ProposalFinalized, ProposalRejectedForQuorum y
|
||||||
|
// ProposalRejectedForSupermajority).
|
||||||
seed_proposal::<T>(id, ProposalStatus::Voting, MIN_VOTING_PERIOD_BLOCKS);
|
seed_proposal::<T>(id, ProposalStatus::Voting, MIN_VOTING_PERIOD_BLOCKS);
|
||||||
Proposals::<T>::mutate(id, |p| {
|
Proposals::<T>::mutate(id, |p| {
|
||||||
if let Some(p) = p {
|
if let Some(p) = p {
|
||||||
p.yes_weight = 1;
|
p.yes_weight = 3;
|
||||||
|
p.nay_weight = 2;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
VoteQuorumOf::<T>::insert(id, 2u128);
|
VoteQuorumOf::<T>::insert(id, 4u128);
|
||||||
|
TreasurySupermajorityOf::<T>::insert(id, TREASURY_SUPERMAJORITY);
|
||||||
// Rama cara: devuelve el bond (transfer real desde la cuenta-bond).
|
// Rama cara: devuelve el bond (transfer real desde la cuenta-bond).
|
||||||
let proposer = seed_bond::<T>(id);
|
let proposer = seed_bond::<T>(id);
|
||||||
// current_block debe ser > end_block → avanza más allá del periodo.
|
// current_block debe ser > end_block → avanza más allá del periodo.
|
||||||
@@ -172,15 +177,25 @@ mod benchmarks {
|
|||||||
// Que de verdad midió la rama del quórum (la revisión de RV17 vio que con
|
// Que de verdad midió la rama del quórum (la revisión de RV17 vio que con
|
||||||
// `!Voting` el benchmark seguía verde sin ella).
|
// `!Voting` el benchmark seguía verde sin ella).
|
||||||
assert!(matches!(Proposals::<T>::get(id).unwrap().status, ProposalStatus::Rejected));
|
assert!(matches!(Proposals::<T>::get(id).unwrap().status, ProposalStatus::Rejected));
|
||||||
frame_system::Pallet::<T>::assert_last_event(
|
frame_system::Pallet::<T>::assert_has_event(
|
||||||
<T as Config>::RuntimeEvent::from(Event::<T>::ProposalRejectedForQuorum {
|
<T as Config>::RuntimeEvent::from(Event::<T>::ProposalRejectedForQuorum {
|
||||||
id,
|
id,
|
||||||
yes_weight: 1,
|
yes_weight: 3,
|
||||||
quorum: 2,
|
quorum: 4,
|
||||||
|
})
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
frame_system::Pallet::<T>::assert_last_event(
|
||||||
|
<T as Config>::RuntimeEvent::from(Event::<T>::ProposalRejectedForSupermajority {
|
||||||
|
id,
|
||||||
|
yes_weight: 3,
|
||||||
|
nay_weight: 2,
|
||||||
|
required: TREASURY_SUPERMAJORITY,
|
||||||
})
|
})
|
||||||
.into(),
|
.into(),
|
||||||
);
|
);
|
||||||
assert!(!VoteQuorumOf::<T>::contains_key(id));
|
assert!(!VoteQuorumOf::<T>::contains_key(id));
|
||||||
|
assert!(!TreasurySupermajorityOf::<T>::contains_key(id));
|
||||||
assert!(!ProposalBond::<T>::contains_key(id));
|
assert!(!ProposalBond::<T>::contains_key(id));
|
||||||
assert_eq!(T::Currency::balance(&proposer), MIN_PROPOSAL_STAKE);
|
assert_eq!(T::Currency::balance(&proposer), MIN_PROPOSAL_STAKE);
|
||||||
}
|
}
|
||||||
@@ -408,6 +423,8 @@ mod benchmarks {
|
|||||||
OverrideQuorum::<T>::insert(id, T::MinOverrideQuorum::get());
|
OverrideQuorum::<T>::insert(id, T::MinOverrideQuorum::get());
|
||||||
// RV17: el override también lee (y consume) el quórum ordinario guardado.
|
// RV17: el override también lee (y consume) el quórum ordinario guardado.
|
||||||
VoteQuorumOf::<T>::insert(id, T::VoteQuorum::get());
|
VoteQuorumOf::<T>::insert(id, T::VoteQuorum::get());
|
||||||
|
// GSUN-3: y, si es de tesoro, su mayoría cualificada (sin Nay, se alcanza).
|
||||||
|
TreasurySupermajorityOf::<T>::insert(id, TREASURY_SUPERMAJORITY);
|
||||||
OverrideStakeTotal::<T>::insert(
|
OverrideStakeTotal::<T>::insert(
|
||||||
id,
|
id,
|
||||||
T::MinOverrideQuorum::get().max(T::VoteQuorum::get()).max(1),
|
T::MinOverrideQuorum::get().max(T::VoteQuorum::get()).max(1),
|
||||||
@@ -423,6 +440,7 @@ mod benchmarks {
|
|||||||
));
|
));
|
||||||
assert!(!OverrideDeadline::<T>::contains_key(id));
|
assert!(!OverrideDeadline::<T>::contains_key(id));
|
||||||
assert!(!VoteQuorumOf::<T>::contains_key(id));
|
assert!(!VoteQuorumOf::<T>::contains_key(id));
|
||||||
|
assert!(!TreasurySupermajorityOf::<T>::contains_key(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test);
|
impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test);
|
||||||
|
|||||||
@@ -147,7 +147,8 @@ pub mod pallet {
|
|||||||
/// Voting period open canonical (block.number ∈ [start_block, end_block]).
|
/// Voting period open canonical (block.number ∈ [start_block, end_block]).
|
||||||
Voting,
|
Voting,
|
||||||
/// Aprobada: `yes_weight > nay_weight` y, si es ordinaria, `yes_weight` alcanza su
|
/// Aprobada: `yes_weight > nay_weight` y, si es ordinaria, `yes_weight` alcanza su
|
||||||
/// quórum (RV17); o un override revirtió el veto del council (RV16).
|
/// quórum (RV17); si es de tesoro, además dos tercios del voto emitido (GSUN-3).
|
||||||
|
/// O un override revirtió el veto del council (RV16).
|
||||||
Approved,
|
Approved,
|
||||||
/// Finalized canonical: nay_weight >= yes_weight (rejected canonical).
|
/// Finalized canonical: nay_weight >= yes_weight (rejected canonical).
|
||||||
Rejected,
|
Rejected,
|
||||||
@@ -239,6 +240,36 @@ pub mod pallet {
|
|||||||
(total_stake.saturating_mul(4).saturating_add(4)) / 5
|
(total_stake.saturating_mul(4).saturating_add(4)) / 5
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// GSUN-3 (David, 2-nov-2026: «Sí a la súper-mayoría, dos tercios»): una propuesta
|
||||||
|
/// de TESORO se aprueba sólo si su Aye es al menos dos tercios del voto emitido
|
||||||
|
/// (Aye + Nay; la abstención no cuenta, como en el quórum). Se fija al crearla en
|
||||||
|
/// [`TreasurySupermajorityOf`], como `(num, den)`.
|
||||||
|
pub const TREASURY_SUPERMAJORITY: (u32, u32) = (2, 3);
|
||||||
|
|
||||||
|
// Una mayoría cualificada es una fracción de verdad y más exigente que la simple:
|
||||||
|
// 0 < num <= den y num/den > 1/2. Lo comprueba el compilador.
|
||||||
|
const _: () = assert!(
|
||||||
|
TREASURY_SUPERMAJORITY.0 > 0
|
||||||
|
&& TREASURY_SUPERMAJORITY.0 <= TREASURY_SUPERMAJORITY.1
|
||||||
|
&& 2 * TREASURY_SUPERMAJORITY.0 > TREASURY_SUPERMAJORITY.1
|
||||||
|
);
|
||||||
|
|
||||||
|
/// GSUN-3: `aye` alcanza la fracción `num/den` del voto emitido, sin redondeo:
|
||||||
|
/// `aye · den >= num · (aye + nay)`. Con dos tercios equivale a `aye >= 2 · nay`.
|
||||||
|
/// Sin ningún Aye no se alcanza nunca. Si una suma o un producto desborda, falla cerrado.
|
||||||
|
pub const fn reaches_supermajority(aye: u128, nay: u128, ratio: (u32, u32)) -> bool {
|
||||||
|
if aye == 0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let Some(emitido) = aye.checked_add(nay) else {
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
match (aye.checked_mul(ratio.1 as u128), emitido.checked_mul(ratio.0 as u128)) {
|
||||||
|
(Some(a), Some(b)) => a >= b,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// RV15: qué respalda un tramo de OGC congelado.
|
/// RV15: qué respalda un tramo de OGC congelado.
|
||||||
#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, Clone, Copy, PartialEq, Eq, RuntimeDebug)]
|
#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, Clone, Copy, PartialEq, Eq, RuntimeDebug)]
|
||||||
pub enum BackingKind {
|
pub enum BackingKind {
|
||||||
@@ -478,6 +509,18 @@ pub mod pallet {
|
|||||||
pub type TreasuryActions<T: Config> =
|
pub type TreasuryActions<T: Config> =
|
||||||
StorageMap<_, Blake2_128Concat, ProposalId, (T::AccountId, u128), OptionQuery>;
|
StorageMap<_, Blake2_128Concat, ProposalId, (T::AccountId, u128), OptionQuery>;
|
||||||
|
|
||||||
|
/// GSUN-3: la mayoría cualificada de cada propuesta de TESORO, fijada al crearla
|
||||||
|
/// (`submit_treasury_proposal`) como fracción `(num, den)` del voto emitido. Se
|
||||||
|
/// consume al decidirla, en `finalize_proposal` o en `finalize_override`.
|
||||||
|
///
|
||||||
|
/// Sin entrada basta la mayoría simple. Es lo correcto en los dos casos:
|
||||||
|
/// - la propuesta ordinaria, que no paga;
|
||||||
|
/// - la de tesoro creada antes de spec 21, que se abrió y se votó con mayoría simple:
|
||||||
|
/// exigirle dos tercios sería cambiarle la regla a mitad, como con `VoteQuorumOf`.
|
||||||
|
#[pallet::storage]
|
||||||
|
pub type TreasurySupermajorityOf<T: Config> =
|
||||||
|
StorageMap<_, Blake2_128Concat, ProposalId, (u32, u32), OptionQuery>;
|
||||||
|
|
||||||
/// CHN-4 canonical: `Proposals<StorageMap<ProposalId, Proposal>>` canonical.
|
/// CHN-4 canonical: `Proposals<StorageMap<ProposalId, Proposal>>` canonical.
|
||||||
#[pallet::storage]
|
#[pallet::storage]
|
||||||
#[pallet::getter(fn proposal_of)]
|
#[pallet::getter(fn proposal_of)]
|
||||||
@@ -820,6 +863,19 @@ pub mod pallet {
|
|||||||
/// Cuánto, en unidades base.
|
/// Cuánto, en unidades base.
|
||||||
amount: u128,
|
amount: u128,
|
||||||
},
|
},
|
||||||
|
/// GSUN-3: la propuesta de tesoro tuvo más Aye que Nay, pero no llegó a la
|
||||||
|
/// mayoría cualificada que se fijó al crearla. Va además del `ProposalFinalized`
|
||||||
|
/// con `Rejected`, para que se lea POR QUÉ.
|
||||||
|
ProposalRejectedForSupermajority {
|
||||||
|
/// Propuesta rechazada.
|
||||||
|
id: ProposalId,
|
||||||
|
/// OGC que votó Aye.
|
||||||
|
yes_weight: u128,
|
||||||
|
/// OGC que votó Nay.
|
||||||
|
nay_weight: u128,
|
||||||
|
/// La fracción del voto emitido (Aye + Nay) que se exigía, `(num, den)`.
|
||||||
|
required: (u32, u32),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Pallet errors canonical.
|
/// Pallet errors canonical.
|
||||||
@@ -952,6 +1008,9 @@ pub mod pallet {
|
|||||||
/// mismo periodo y mismo quórum) que además fija qué paga si se aprueba:
|
/// mismo periodo y mismo quórum) que además fija qué paga si se aprueba:
|
||||||
/// `recipient` y `amount`. Quedan en `TreasuryActions` y nadie los cambia
|
/// `recipient` y `amount`. Quedan en `TreasuryActions` y nadie los cambia
|
||||||
/// después. `treasury_disbursement` sólo puede pagar exactamente esto.
|
/// después. `treasury_disbursement` sólo puede pagar exactamente esto.
|
||||||
|
///
|
||||||
|
/// GSUN-3: para aprobarse exige además dos tercios del voto emitido, fijados
|
||||||
|
/// ahora en `TreasurySupermajorityOf`.
|
||||||
#[pallet::call_index(16)]
|
#[pallet::call_index(16)]
|
||||||
#[pallet::weight(T::WeightInfo::submit_treasury_proposal())]
|
#[pallet::weight(T::WeightInfo::submit_treasury_proposal())]
|
||||||
pub fn submit_treasury_proposal(
|
pub fn submit_treasury_proposal(
|
||||||
@@ -975,6 +1034,7 @@ pub mod pallet {
|
|||||||
let id =
|
let id =
|
||||||
Self::do_submit_proposal(who, title_hash, body_hash, voting_period_blocks, stake)?;
|
Self::do_submit_proposal(who, title_hash, body_hash, voting_period_blocks, stake)?;
|
||||||
TreasuryActions::<T>::insert(id, (recipient.clone(), amount));
|
TreasuryActions::<T>::insert(id, (recipient.clone(), amount));
|
||||||
|
TreasurySupermajorityOf::<T>::insert(id, TREASURY_SUPERMAJORITY);
|
||||||
Self::deposit_event(Event::TreasuryProposalSubmitted { id, recipient, amount });
|
Self::deposit_event(Event::TreasuryProposalSubmitted { id, recipient, amount });
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -1081,9 +1141,18 @@ pub mod pallet {
|
|||||||
// fijó al crearlo (ver `VoteQuorumOf`). Sin entrada (emergencia, o creada
|
// fijó al crearlo (ver `VoteQuorumOf`). Sin entrada (emergencia, o creada
|
||||||
// antes de RV17), no hay quórum.
|
// antes de RV17), no hay quórum.
|
||||||
let quorum = VoteQuorumOf::<T>::take(proposal_id).unwrap_or(0);
|
let quorum = VoteQuorumOf::<T>::take(proposal_id).unwrap_or(0);
|
||||||
|
// GSUN-3: la de tesoro exige además la mayoría cualificada que se fijó al
|
||||||
|
// crearla (ver `TreasurySupermajorityOf`). Sin entrada, basta la simple.
|
||||||
|
let cualificada = TreasurySupermajorityOf::<T>::take(proposal_id);
|
||||||
let mayoria = proposal.yes_weight > proposal.nay_weight;
|
let mayoria = proposal.yes_weight > proposal.nay_weight;
|
||||||
let sin_quorum = mayoria && proposal.yes_weight < quorum;
|
let sin_quorum = mayoria && proposal.yes_weight < quorum;
|
||||||
let final_status = if mayoria && !sin_quorum {
|
let sin_cualificada = match cualificada {
|
||||||
|
Some(r) => {
|
||||||
|
mayoria && !reaches_supermajority(proposal.yes_weight, proposal.nay_weight, r)
|
||||||
|
}
|
||||||
|
None => false,
|
||||||
|
};
|
||||||
|
let final_status = if mayoria && !sin_quorum && !sin_cualificada {
|
||||||
ProposalStatus::Approved
|
ProposalStatus::Approved
|
||||||
} else {
|
} else {
|
||||||
ProposalStatus::Rejected
|
ProposalStatus::Rejected
|
||||||
@@ -1112,6 +1181,14 @@ pub mod pallet {
|
|||||||
quorum,
|
quorum,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
if let (true, Some(required)) = (sin_cualificada, cualificada) {
|
||||||
|
Self::deposit_event(Event::ProposalRejectedForSupermajority {
|
||||||
|
id: proposal_id,
|
||||||
|
yes_weight,
|
||||||
|
nay_weight,
|
||||||
|
required,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -1634,6 +1711,13 @@ pub mod pallet {
|
|||||||
/// aprobación. Sin el Nay, con Aye Q y Nay 9Q, revertir con 8Q bastaba si la
|
/// aprobación. Sin el Nay, con Aye Q y Nay 9Q, revertir con 8Q bastaba si la
|
||||||
/// oposición no volvía a votar, cuando el voto ordinario pedía más de 9Q
|
/// oposición no volvía a votar, cuando el voto ordinario pedía más de 9Q
|
||||||
/// (segunda revisión adversarial de RV17).
|
/// (segunda revisión adversarial de RV17).
|
||||||
|
/// 3. GSUN-3: si es de tesoro, revertir alcanza además la mayoría cualificada que
|
||||||
|
/// se fijó al crearla (dos tercios) frente a TODA la oposición: el Nay del voto
|
||||||
|
/// original MÁS lo que sostuvo el veto en la ventana. Contra cada parte por
|
||||||
|
/// separado no basta: con Nay 4Q y 2Q sosteniendo, 8Q revertían (cumplían 2·Nay y
|
||||||
|
/// los 4/5 de la ventana) cuando el voto ordinario pedía 12Q (revisión adversarial
|
||||||
|
/// de GSUN-3). Quien votó Nay y además sostiene cuenta dos veces: sólo sube el
|
||||||
|
/// listón de revertir al council, que ya es difícil a propósito.
|
||||||
///
|
///
|
||||||
/// En cualquier otro caso el veto se sostiene. Se decide una sola vez.
|
/// En cualquier otro caso el veto se sostiene. Se decide una sola vez.
|
||||||
#[pallet::call_index(15)]
|
#[pallet::call_index(15)]
|
||||||
@@ -1668,6 +1752,7 @@ pub mod pallet {
|
|||||||
OverrideDeadline::<T>::remove(proposal_id);
|
OverrideDeadline::<T>::remove(proposal_id);
|
||||||
OverrideQuorum::<T>::remove(proposal_id);
|
OverrideQuorum::<T>::remove(proposal_id);
|
||||||
VoteQuorumOf::<T>::remove(proposal_id);
|
VoteQuorumOf::<T>::remove(proposal_id);
|
||||||
|
let cualificada = TreasurySupermajorityOf::<T>::take(proposal_id);
|
||||||
|
|
||||||
// Regla MONÓTONA: revertir necesita 4/5 del MAYOR entre el quórum y la
|
// Regla MONÓTONA: revertir necesita 4/5 del MAYOR entre el quórum y la
|
||||||
// participación. La primera versión exigía `participation >= quorum` y
|
// participación. La primera versión exigía `participation >= quorum` y
|
||||||
@@ -1679,7 +1764,15 @@ pub mod pallet {
|
|||||||
let overridden = override_stake > 0
|
let overridden = override_stake > 0
|
||||||
&& override_stake >= override_supermajority_threshold(quorum.max(participation))
|
&& override_stake >= override_supermajority_threshold(quorum.max(participation))
|
||||||
&& override_stake >= quorum_ordinario
|
&& override_stake >= quorum_ordinario
|
||||||
&& override_stake > proposal.nay_weight;
|
&& override_stake > proposal.nay_weight
|
||||||
|
&& match cualificada {
|
||||||
|
Some(r) => reaches_supermajority(
|
||||||
|
override_stake,
|
||||||
|
proposal.nay_weight.saturating_add(uphold_stake),
|
||||||
|
r,
|
||||||
|
),
|
||||||
|
None => true,
|
||||||
|
};
|
||||||
if overridden {
|
if overridden {
|
||||||
proposal.status = ProposalStatus::Approved;
|
proposal.status = ProposalStatus::Approved;
|
||||||
Proposals::<T>::insert(proposal_id, proposal);
|
Proposals::<T>::insert(proposal_id, proposal);
|
||||||
|
|||||||
@@ -2542,9 +2542,10 @@ fn vetar_nunca_abarata_la_aprobacion() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Propuesta de tesoro con oposición: Bob vota Aye `MIN` y Charlie Nay `4·MIN`. El council
|
/// Propuesta con oposición: Bob vota Aye `MIN` y Charlie Nay `4·MIN`. El council (Alice,
|
||||||
/// (Alice, Chair) la veta y Alice la revierte con `apoyo`.
|
/// Chair) la veta y Alice la revierte con `apoyo`. Si `tesoro`, es de tesoro (paga 300 000 a
|
||||||
fn vetar_con_oposicion(apoyo: u128) -> (ProposalStatus, bool) {
|
/// Bob); si no, ordinaria, y no puede pagar nada.
|
||||||
|
fn vetar_con_oposicion(tesoro: bool, apoyo: u128) -> (ProposalStatus, bool) {
|
||||||
use crate::pallet::CouncilRole;
|
use crate::pallet::CouncilRole;
|
||||||
use frame_support::dispatch::RawOrigin;
|
use frame_support::dispatch::RawOrigin;
|
||||||
crate::mock::VoteQuorum::set(2 * MIN_PROPOSAL_STAKE);
|
crate::mock::VoteQuorum::set(2 * MIN_PROPOSAL_STAKE);
|
||||||
@@ -2554,7 +2555,8 @@ fn vetar_con_oposicion(apoyo: u128) -> (ProposalStatus, bool) {
|
|||||||
alice_account(),
|
alice_account(),
|
||||||
CouncilRole::Chair
|
CouncilRole::Chair
|
||||||
));
|
));
|
||||||
let id = submit_tesoro(bob_account(), 300_000);
|
let id =
|
||||||
|
if tesoro { submit_tesoro(bob_account(), 300_000) } else { submit_default(alice_origin()) };
|
||||||
assert_ok!(Governance::cast_vote(bob_origin(), id, VoteStance::Aye, MIN_PROPOSAL_STAKE));
|
assert_ok!(Governance::cast_vote(bob_origin(), id, VoteStance::Aye, MIN_PROPOSAL_STAKE));
|
||||||
assert_ok!(Governance::cast_vote(
|
assert_ok!(Governance::cast_vote(
|
||||||
charlie_origin(),
|
charlie_origin(),
|
||||||
@@ -2581,15 +2583,287 @@ fn vetar_no_abarata_la_aprobacion_frente_a_la_oposicion() {
|
|||||||
// o sea 4·MIN justos, si Charlie no volvía a votar para sostener. Un veto convertía
|
// o sea 4·MIN justos, si Charlie no volvía a votar para sostener. Un veto convertía
|
||||||
// una propuesta perdida en un pago más barato que ganarla. Ahora el override tiene
|
// una propuesta perdida en un pago más barato que ganarla. Ahora el override tiene
|
||||||
// que SUPERAR el Nay original, como el voto ordinario.
|
// que SUPERAR el Nay original, como el voto ordinario.
|
||||||
|
//
|
||||||
|
// Se mide en una ordinaria: en una de tesoro manda ya la mayoría cualificada de GSUN-3,
|
||||||
|
// que pide el doble del Nay (lo mide el test siguiente).
|
||||||
new_test_ext().execute_with(|| {
|
new_test_ext().execute_with(|| {
|
||||||
let (estado, pagado) = vetar_con_oposicion(4 * MIN_PROPOSAL_STAKE);
|
let (estado, pagado) = vetar_con_oposicion(false, 4 * MIN_PROPOSAL_STAKE);
|
||||||
assert!(matches!(estado, ProposalStatus::Rejected), "igualar al Nay no basta");
|
assert!(matches!(estado, ProposalStatus::Rejected), "igualar al Nay no basta");
|
||||||
assert!(!pagado);
|
assert!(!pagado);
|
||||||
});
|
});
|
||||||
// Control positivo: superarlo por una unidad basta (y cumple los 4/5 y el quórum).
|
// Control positivo: superarlo por una unidad basta (y cumple los 4/5 y el quórum).
|
||||||
new_test_ext().execute_with(|| {
|
new_test_ext().execute_with(|| {
|
||||||
let (estado, pagado) = vetar_con_oposicion(4 * MIN_PROPOSAL_STAKE + 1);
|
let (estado, pagado) = vetar_con_oposicion(false, 4 * MIN_PROPOSAL_STAKE + 1);
|
||||||
|
assert!(matches!(estado, ProposalStatus::Approved));
|
||||||
|
assert!(!pagado, "una ordinaria no paga aunque se apruebe");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn revertir_el_veto_de_una_propuesta_de_tesoro_exige_dos_tercios_frente_al_nay() {
|
||||||
|
// GSUN-3. El voto ordinario de tesoro pide Aye >= 2·Nay. Si revertir el veto sólo
|
||||||
|
// pidiera superar el Nay, un veto abarataría el pago: con Nay 4·MIN, 4·MIN + 1 de
|
||||||
|
// apoyo bastaría donde el voto ordinario pide 8·MIN. Frontera exacta en 8·MIN.
|
||||||
|
for apoyo in [4 * MIN_PROPOSAL_STAKE + 1, 8 * MIN_PROPOSAL_STAKE - 1] {
|
||||||
|
new_test_ext().execute_with(|| {
|
||||||
|
let (estado, pagado) = vetar_con_oposicion(true, apoyo);
|
||||||
|
assert!(matches!(estado, ProposalStatus::Rejected), "apoyo {apoyo} < 2·Nay");
|
||||||
|
assert!(!pagado);
|
||||||
|
let id = derive_proposal_id(
|
||||||
|
&AccountId::new(alice_account_bytes()),
|
||||||
|
&TITLE_HASH,
|
||||||
|
&BODY_HASH,
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
crate::TreasurySupermajorityOf::<Test>::get(id).is_none(),
|
||||||
|
"se consume también cuando el veto se sostiene"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
new_test_ext().execute_with(|| {
|
||||||
|
let (estado, pagado) = vetar_con_oposicion(true, 8 * MIN_PROPOSAL_STAKE);
|
||||||
|
assert!(matches!(estado, ProposalStatus::Approved));
|
||||||
|
assert!(pagado);
|
||||||
|
let id =
|
||||||
|
derive_proposal_id(&AccountId::new(alice_account_bytes()), &TITLE_HASH, &BODY_HASH);
|
||||||
|
assert!(crate::TreasurySupermajorityOf::<Test>::get(id).is_none(), "se consume");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── GSUN-3: dos tercios para el tesoro ────────────────────────────────────────
|
||||||
|
|
||||||
|
/// Abre una propuesta de TESORO (paga 300 000 a Bob) con el quórum del mock por
|
||||||
|
/// defecto (MIN + 1), vota (Bob Aye, Charlie Nay, Alice Abstain), la finaliza e intenta
|
||||||
|
/// pagarla. Devuelve el estado y si pagó.
|
||||||
|
fn votar_tesoro(aye: u128, nay: u128, abstain: u128) -> (ProposalStatus, bool) {
|
||||||
|
use frame_support::dispatch::RawOrigin;
|
||||||
|
assert_ok!(Governance::treasury_deposit(alice_origin(), 1_000_000));
|
||||||
|
let id = submit_tesoro(bob_account(), 300_000);
|
||||||
|
if aye > 0 {
|
||||||
|
assert_ok!(Governance::cast_vote(bob_origin(), id, VoteStance::Aye, aye));
|
||||||
|
}
|
||||||
|
if nay > 0 {
|
||||||
|
assert_ok!(Governance::cast_vote(charlie_origin(), id, VoteStance::Nay, nay));
|
||||||
|
}
|
||||||
|
if abstain > 0 {
|
||||||
|
assert_ok!(Governance::cast_vote(alice_origin(), id, VoteStance::Abstain, abstain));
|
||||||
|
}
|
||||||
|
run_to_block(TRAS_EL_PERIODO);
|
||||||
|
assert_ok!(Governance::finalize_proposal(alice_origin(), id));
|
||||||
|
let estado = Proposals::<Test>::get(id).unwrap().status;
|
||||||
|
let pagado =
|
||||||
|
Governance::treasury_disbursement(RawOrigin::Root.into(), id, bob_account(), 300_000)
|
||||||
|
.is_ok();
|
||||||
|
(estado, pagado)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn una_propuesta_de_tesoro_exige_dos_tercios_y_dice_por_que() {
|
||||||
|
// Frontera exacta: con Nay N, 2N de Aye son justo dos tercios y se aprueba; 2N - 1
|
||||||
|
// tiene mayoría simple y quórum, pero no los dos tercios.
|
||||||
|
let n = MIN_PROPOSAL_STAKE;
|
||||||
|
new_test_ext().execute_with(|| {
|
||||||
|
let (estado, pagado) = votar_tesoro(2 * n - 1, n, 0);
|
||||||
|
assert!(matches!(estado, ProposalStatus::Rejected));
|
||||||
|
assert!(!pagado);
|
||||||
|
let id =
|
||||||
|
derive_proposal_id(&AccountId::new(alice_account_bytes()), &TITLE_HASH, &BODY_HASH);
|
||||||
|
System::assert_has_event(RuntimeEvent::Governance(
|
||||||
|
Event::ProposalRejectedForSupermajority {
|
||||||
|
id,
|
||||||
|
yes_weight: 2 * n - 1,
|
||||||
|
nay_weight: n,
|
||||||
|
required: (2, 3),
|
||||||
|
},
|
||||||
|
));
|
||||||
|
// Tenía quórum: el rechazo es sólo por los dos tercios.
|
||||||
|
assert!(!System::events().iter().any(|r| matches!(
|
||||||
|
r.event,
|
||||||
|
RuntimeEvent::Governance(Event::ProposalRejectedForQuorum { .. })
|
||||||
|
)));
|
||||||
|
});
|
||||||
|
new_test_ext().execute_with(|| {
|
||||||
|
let (estado, pagado) = votar_tesoro(2 * n, n, 0);
|
||||||
|
assert!(matches!(estado, ProposalStatus::Approved));
|
||||||
|
assert!(pagado);
|
||||||
|
assert!(!System::events().iter().any(|r| matches!(
|
||||||
|
r.event,
|
||||||
|
RuntimeEvent::Governance(Event::ProposalRejectedForSupermajority { .. })
|
||||||
|
)));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn la_abstencion_no_cuenta_para_los_dos_tercios() {
|
||||||
|
// Si la abstención contara en el voto emitido, abstenerse sería votar en contra
|
||||||
|
// (subiría el listón) o, contada como Aye, aprobaría sin tomar partido.
|
||||||
|
let n = MIN_PROPOSAL_STAKE;
|
||||||
|
new_test_ext().execute_with(|| {
|
||||||
|
let (estado, _) = votar_tesoro(2 * n, n, 5 * n);
|
||||||
|
assert!(matches!(estado, ProposalStatus::Approved), "abstenerse no sube el listón");
|
||||||
|
});
|
||||||
|
new_test_ext().execute_with(|| {
|
||||||
|
let (estado, _) = votar_tesoro(2 * n - 1, n, 5 * n);
|
||||||
|
assert!(matches!(estado, ProposalStatus::Rejected), "abstenerse no aprueba");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn una_propuesta_ordinaria_sigue_con_mayoria_simple() {
|
||||||
|
// Los dos tercios son sólo del tesoro: el mismo voto que rechaza un pago aprueba una
|
||||||
|
// ordinaria.
|
||||||
|
let n = MIN_PROPOSAL_STAKE;
|
||||||
|
new_test_ext().execute_with(|| {
|
||||||
|
let q = crate::mock::VoteQuorum::get();
|
||||||
|
assert!(matches!(votar_y_finalizar(q, 2 * n - 1, n, 0), ProposalStatus::Approved));
|
||||||
|
let id =
|
||||||
|
derive_proposal_id(&AccountId::new(alice_account_bytes()), &TITLE_HASH, &BODY_HASH);
|
||||||
|
assert!(crate::TreasurySupermajorityOf::<Test>::get(id).is_none());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn los_dos_tercios_se_fijan_al_crear_y_no_alcanzan_a_una_propuesta_anterior() {
|
||||||
|
let n = MIN_PROPOSAL_STAKE;
|
||||||
|
// Se fijan al crear y se consumen al decidir.
|
||||||
|
new_test_ext().execute_with(|| {
|
||||||
|
let id = submit_tesoro(bob_account(), 300_000);
|
||||||
|
assert_eq!(crate::TreasurySupermajorityOf::<Test>::get(id), Some((2, 3)));
|
||||||
|
run_to_block(TRAS_EL_PERIODO);
|
||||||
|
assert_ok!(Governance::finalize_proposal(alice_origin(), id));
|
||||||
|
assert!(crate::TreasurySupermajorityOf::<Test>::get(id).is_none(), "se consume");
|
||||||
|
});
|
||||||
|
// Una de tesoro creada antes de spec 21 se abrió y se votó con mayoría simple:
|
||||||
|
// decidirla con dos tercios sería cambiarle la regla a mitad. Es el pago sembrado
|
||||||
|
// en la testnet (fe92182e…, spec 18).
|
||||||
|
new_test_ext().execute_with(|| {
|
||||||
|
use frame_support::dispatch::RawOrigin;
|
||||||
|
assert_ok!(Governance::treasury_deposit(alice_origin(), 1_000_000));
|
||||||
|
let id = submit_tesoro(bob_account(), 300_000);
|
||||||
|
crate::TreasurySupermajorityOf::<Test>::remove(id); // como la dejó spec 20
|
||||||
|
assert_ok!(Governance::cast_vote(bob_origin(), id, VoteStance::Aye, 2 * n - 1));
|
||||||
|
assert_ok!(Governance::cast_vote(charlie_origin(), id, VoteStance::Nay, n));
|
||||||
|
run_to_block(TRAS_EL_PERIODO);
|
||||||
|
assert_ok!(Governance::finalize_proposal(alice_origin(), id));
|
||||||
|
assert!(matches!(Proposals::<Test>::get(id).unwrap().status, ProposalStatus::Approved));
|
||||||
|
assert_ok!(Governance::treasury_disbursement(
|
||||||
|
RawOrigin::Root.into(),
|
||||||
|
id,
|
||||||
|
bob_account(),
|
||||||
|
300_000
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reaches_supermajority_es_exacta_monotona_y_falla_cerrada() {
|
||||||
|
use crate::pallet::{reaches_supermajority, TREASURY_SUPERMAJORITY as R};
|
||||||
|
// Exacta: dos tercios es Aye >= 2·Nay, sin redondeo.
|
||||||
|
for nay in [1u128, 2, 3, 7, MIN_PROPOSAL_STAKE, 10u128.pow(27)] {
|
||||||
|
assert!(reaches_supermajority(2 * nay, nay, R), "2·{nay} frente a {nay}");
|
||||||
|
assert!(!reaches_supermajority(2 * nay - 1, nay, R), "2·{nay} - 1 frente a {nay}");
|
||||||
|
}
|
||||||
|
assert!(reaches_supermajority(1, 0, R));
|
||||||
|
assert!(!reaches_supermajority(0, 0, R), "sin ningún Aye no se alcanza");
|
||||||
|
// Monótona: más Aye nunca la pierde; más Nay nunca la gana.
|
||||||
|
for aye in 0u128..40 {
|
||||||
|
for nay in 0u128..40 {
|
||||||
|
let base = reaches_supermajority(aye, nay, R);
|
||||||
|
if base {
|
||||||
|
assert!(reaches_supermajority(aye + 1, nay, R), "Aye de más la perdió");
|
||||||
|
} else {
|
||||||
|
assert!(!reaches_supermajority(aye, nay + 1, R), "Nay de más la ganó");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Falla cerrada si desborda: nunca aprueba por saturación.
|
||||||
|
assert!(!reaches_supermajority(u128::MAX, 1, R));
|
||||||
|
assert!(!reaches_supermajority(u128::MAX / 2, 0, R));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// GSUN-3 · revisión adversarial. Propuesta de tesoro con Aye `MIN` (Bob) y Nay `4·MIN`
|
||||||
|
/// (Charlie). El council (Alice, Chair) la veta. En la ventana, Dave (que no había votado)
|
||||||
|
/// sostiene el veto con `sostener` y Eve lo revierte con `apoyo`. Si `sin_marca`, la
|
||||||
|
/// propuesta es como la dejó spec 20: sin `TreasurySupermajorityOf`.
|
||||||
|
fn vetar_con_oposicion_repartida(sostener: u128, apoyo: u128, sin_marca: bool) -> (ProposalStatus, bool) {
|
||||||
|
use crate::pallet::CouncilRole;
|
||||||
|
use frame_support::dispatch::RawOrigin;
|
||||||
|
use frame_support::traits::fungible::Mutate;
|
||||||
|
crate::mock::VoteQuorum::set(2 * MIN_PROPOSAL_STAKE);
|
||||||
|
assert_ok!(Governance::treasury_deposit(alice_origin(), 1_000_000));
|
||||||
|
assert_ok!(Governance::add_council_member(
|
||||||
|
RawOrigin::Root.into(),
|
||||||
|
alice_account(),
|
||||||
|
CouncilRole::Chair
|
||||||
|
));
|
||||||
|
let id = submit_tesoro(bob_account(), 300_000);
|
||||||
|
if sin_marca {
|
||||||
|
crate::TreasurySupermajorityOf::<Test>::remove(id);
|
||||||
|
}
|
||||||
|
assert_ok!(Governance::cast_vote(bob_origin(), id, VoteStance::Aye, MIN_PROPOSAL_STAKE));
|
||||||
|
assert_ok!(Governance::cast_vote(
|
||||||
|
charlie_origin(),
|
||||||
|
id,
|
||||||
|
VoteStance::Nay,
|
||||||
|
4 * MIN_PROPOSAL_STAKE
|
||||||
|
));
|
||||||
|
assert_ok!(Governance::veto_proposal(alice_origin(), id));
|
||||||
|
let cierre = crate::OverrideDeadline::<Test>::get(id).unwrap();
|
||||||
|
if sostener > 0 {
|
||||||
|
let dave = sp_runtime::AccountId32::new([71u8; 32]);
|
||||||
|
assert_ok!(Balances::mint_into(&dave, sostener + INITIAL_BALANCE));
|
||||||
|
assert_ok!(Governance::uphold_council_veto(RuntimeOrigin::signed(dave), id, sostener));
|
||||||
|
}
|
||||||
|
let eve = sp_runtime::AccountId32::new([72u8; 32]);
|
||||||
|
assert_ok!(Balances::mint_into(&eve, apoyo + INITIAL_BALANCE));
|
||||||
|
assert_ok!(Governance::override_council_veto(RuntimeOrigin::signed(eve), id, apoyo));
|
||||||
|
run_to_block(cierre + 1);
|
||||||
|
assert_ok!(Governance::finalize_override(bob_origin(), id));
|
||||||
|
let estado = Proposals::<Test>::get(id).unwrap().status;
|
||||||
|
let pagado =
|
||||||
|
Governance::treasury_disbursement(RawOrigin::Root.into(), id, bob_account(), 300_000)
|
||||||
|
.is_ok();
|
||||||
|
(estado, pagado)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn revertir_el_veto_de_tesoro_mide_los_dos_tercios_frente_a_toda_la_oposicion() {
|
||||||
|
// Revisión adversarial de GSUN-3 (bloqueante). Con Nay 4·MIN y 2·MIN sosteniendo en la
|
||||||
|
// ventana, 8·MIN cumplían el doble del Nay y los 4/5 de la ventana, y el tesoro PAGABA. Sin
|
||||||
|
// veto, esos mismos votantes (9·MIN de Aye frente a 6·MIN de Nay) se rechazan: hacen falta
|
||||||
|
// 12·MIN. Revertir mide los dos tercios contra el Nay original MÁS lo que sostiene.
|
||||||
|
let m = MIN_PROPOSAL_STAKE;
|
||||||
|
for apoyo in [8 * m, 12 * m - 1] {
|
||||||
|
new_test_ext().execute_with(|| {
|
||||||
|
let (estado, pagado) = vetar_con_oposicion_repartida(2 * m, apoyo, false);
|
||||||
|
assert!(matches!(estado, ProposalStatus::Rejected), "apoyo {apoyo} < 2·(4M + 2M)");
|
||||||
|
assert!(!pagado);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Control positivo: la frontera exacta, 2·(Nay + sostener).
|
||||||
|
new_test_ext().execute_with(|| {
|
||||||
|
let (estado, pagado) = vetar_con_oposicion_repartida(2 * m, 12 * m, false);
|
||||||
assert!(matches!(estado, ProposalStatus::Approved));
|
assert!(matches!(estado, ProposalStatus::Approved));
|
||||||
assert!(pagado);
|
assert!(pagado);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn revertir_el_veto_de_una_propuesta_de_tesoro_anterior_no_exige_dos_tercios() {
|
||||||
|
// Es el camino que tomaría el pago sembrado en spec 18 si lo vetaran después de spec 21:
|
||||||
|
// se abrió con mayoría simple, y revertir su veto sigue la regla de entonces (superar el
|
||||||
|
// Nay original, con los 4/5 y el quórum). Sin este test, exigirle dos tercios por el
|
||||||
|
// override pasaba en verde (revisión adversarial, mutación M5).
|
||||||
|
let m = MIN_PROPOSAL_STAKE;
|
||||||
|
new_test_ext().execute_with(|| {
|
||||||
|
let (estado, pagado) = vetar_con_oposicion_repartida(0, 4 * m + 1, true);
|
||||||
|
assert!(matches!(estado, ProposalStatus::Approved));
|
||||||
|
assert!(pagado);
|
||||||
|
});
|
||||||
|
// Y con la marca, el mismo apoyo no basta.
|
||||||
|
new_test_ext().execute_with(|| {
|
||||||
|
let (estado, pagado) = vetar_con_oposicion_repartida(0, 4 * m + 1, false);
|
||||||
|
assert!(matches!(estado, ProposalStatus::Rejected));
|
||||||
|
assert!(!pagado);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -374,6 +374,14 @@ 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
|
/// 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`.
|
/// aplica en vivo a la testnet de la casa (rp1), por `set_code`.
|
||||||
///
|
///
|
||||||
|
/// **v21 (2026-11-02), GSUN-3 decidido por David:** una propuesta de TESORO se aprueba sólo con
|
||||||
|
/// dos tercios del voto emitido (Aye + Nay; la abstención no cuenta), además del quórum y de la
|
||||||
|
/// mayoría simple. «Sí a la súper-mayoría, dos tercios» (David, 2-nov). Se fija al crear la
|
||||||
|
/// propuesta (storage `TreasurySupermajorityOf`) y lo tiene que alcanzar también un override del
|
||||||
|
/// veto frente al Nay original. Las propuestas de tesoro abiertas antes conservan la mayoría
|
||||||
|
/// simple con la que se abrieron. Aditivo: un storage y un evento nuevos, sin migración y sin
|
||||||
|
/// cambiar la codificación de ninguna llamada.
|
||||||
|
///
|
||||||
/// **v20 (2026-11-02), sólo pesos:** los 17 extrinsics del gobierno cobran lo que certificó Laurha
|
/// **v20 (2026-11-02), sólo pesos:** los 17 extrinsics del gobierno cobran lo que certificó Laurha
|
||||||
/// en el banco de referencia de la rp1 (ver la cabecera de `origin_chain_governance::weights`). Ya
|
/// en el banco de referencia de la rp1 (ver la cabecera de `origin_chain_governance::weights`). Ya
|
||||||
/// no queda ningún smoke. No cambia el código, ni el storage, ni la codificación.
|
/// no queda ningún smoke. No cambia el código, ni el storage, ni la codificación.
|
||||||
@@ -1047,7 +1055,7 @@ pub const VERSION: sp_version::RuntimeVersion = sp_version::RuntimeVersion {
|
|||||||
spec_name: alloc::borrow::Cow::Borrowed("origin-chain"),
|
spec_name: alloc::borrow::Cow::Borrowed("origin-chain"),
|
||||||
impl_name: alloc::borrow::Cow::Borrowed("origin-chain-charon"),
|
impl_name: alloc::borrow::Cow::Borrowed("origin-chain-charon"),
|
||||||
authoring_version: 1,
|
authoring_version: 1,
|
||||||
spec_version: 20,
|
spec_version: 21,
|
||||||
impl_version: 1,
|
impl_version: 1,
|
||||||
apis: RUNTIME_API_VERSIONS,
|
apis: RUNTIME_API_VERSIONS,
|
||||||
transaction_version: 1,
|
transaction_version: 1,
|
||||||
@@ -1208,7 +1216,7 @@ mod tests {
|
|||||||
/// mismo commit en que la version subio a 12.
|
/// mismo commit en que la version subio a 12.
|
||||||
#[test]
|
#[test]
|
||||||
fn spec_version_vigente_y_transaction_version_intacta() {
|
fn spec_version_vigente_y_transaction_version_intacta() {
|
||||||
assert_eq!(RUNTIME_VERSION, 20);
|
assert_eq!(RUNTIME_VERSION, 21);
|
||||||
assert_eq!(TRANSACTION_VERSION, 1);
|
assert_eq!(TRANSACTION_VERSION, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,6 +23,15 @@ Comprueba, en este orden (se detiene en el primer fallo, código de salida 1):
|
|||||||
el mismo para toda llamada: se mide con un treasury_deposit y el residuo tiene que
|
el mismo para toda llamada: se mide con un treasury_deposit y el residuo tiene que
|
||||||
coincidir. Prueba el cableado; que el número sea el coste real lo prueba el bench;
|
coincidir. Prueba el cableado; que el número sea el coste real lo prueba el bench;
|
||||||
2. Proposals(id) existe y TreasuryActions(id) = (Bob, monto), leído del storage por RPC;
|
2. Proposals(id) existe y TreasuryActions(id) = (Bob, monto), leído del storage por RPC;
|
||||||
|
2b. el quórum guardado es el 10 % de lo que puede votar (spec 19);
|
||||||
|
2c. desde spec 21 (GSUN-3), TreasurySupermajorityOf(id) = (2, 3): la propuesta de tesoro
|
||||||
|
nueva guarda sus dos tercios. Contra spec 20 este paso reprueba;
|
||||||
|
2d. la propuesta sembrada en spec 18 (SEMBRADA), si existe en esta cadena, NO tiene esa
|
||||||
|
entrada: se decide con la mayoría simple con la que se abrió;
|
||||||
|
2e. la frontera del upgrade: B es el primer bloque cuyo estado ya dice spec 21 (el del
|
||||||
|
set_code, que aún ejecutó el runtime anterior). Toda propuesta de tesoro con
|
||||||
|
start_block <= B no tiene marca, y toda la posterior la tiene. Se enumeran TODAS las
|
||||||
|
de TreasuryActions, no sólo la sembrada (revisión adversarial de GSUN-3);
|
||||||
3. tesoro:0 falla con TreasuryAmountBelowMinimum;
|
3. tesoro:0 falla con TreasuryAmountBelowMinimum;
|
||||||
4. reenviar la misma propuesta falla con ProposalIdCollision y TreasuryActions no cambia;
|
4. reenviar la misma propuesta falla con ProposalIdCollision y TreasuryActions no cambia;
|
||||||
5. sudo(treasury_disbursement) de esa propuesta, aún en votación, devuelve ProposalNotApproved
|
5. sudo(treasury_disbursement) de esa propuesta, aún en votación, devuelve ProposalNotApproved
|
||||||
@@ -45,6 +54,10 @@ TITULO = hashlib.blake2b(b"e2e-rv17/titulo" + (f"/{ETIQUETA}".encode() if ETIQUE
|
|||||||
# runtime: una omisión compartida pasaría en los dos.
|
# runtime: una omisión compartida pasaría en los dos.
|
||||||
VUELVEN = {(b"modl" + pid).ljust(32, b"\0").hex() for pid in (b"og/props", b"py/reviv")}
|
VUELVEN = {(b"modl" + pid).ljust(32, b"\0").hex() for pid in (b"og/props", b"py/reviv")}
|
||||||
MONTO = 5 * 10**18 # 5 OGC
|
MONTO = 5 * 10**18 # 5 OGC
|
||||||
|
# El pago sembrado en la testnet de la casa el 28-oct (spec 18), que se cierra hacia el 26-nov.
|
||||||
|
SEMBRADA = "fe92182ecff4df083b42da838a509c82aab7a4d0213a902e5a5a89729bd06ad0"
|
||||||
|
# GSUN-3: la mayoría cualificada del tesoro, (num, den) como dos u32 en little endian.
|
||||||
|
DOS_TERCIOS = "0x" + (2).to_bytes(4, "little").hex() + (3).to_bytes(4, "little").hex()
|
||||||
BOB = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty"
|
BOB = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty"
|
||||||
BOB_HEX = "8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48"
|
BOB_HEX = "8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48"
|
||||||
|
|
||||||
@@ -247,6 +260,24 @@ def main(sembrar_pago=False):
|
|||||||
print(f"2b ✅ quórum guardado {guardado / 10**18:,.6f} OGC = 10 % de lo que puede votar "
|
print(f"2b ✅ quórum guardado {guardado / 10**18:,.6f} OGC = 10 % de lo que puede votar "
|
||||||
f"({(emision - sin_llave) / 10**18:,.4f} OGC; las cuentas sin llave guardan {sin_llave / 10**18:,.4f})")
|
f"({(emision - sin_llave) / 10**18:,.4f} OGC; las cuentas sin llave guardan {sin_llave / 10**18:,.4f})")
|
||||||
|
|
||||||
|
# ORIGIN_E2E_EXIGE_SPEC=21 hace el paso aunque la cadena diga una spec anterior: es el
|
||||||
|
# contrafactual (contra spec 20 tiene que reprobar en el storage, no en la versión).
|
||||||
|
if v["specVersion"] >= 21 or os.environ.get("ORIGIN_E2E_EXIGE_SPEC", "") == "21":
|
||||||
|
cualificada = rpc("state_getStorage", [clave("TreasurySupermajorityOf", id_hex)])["result"]
|
||||||
|
if cualificada != DOS_TERCIOS:
|
||||||
|
falla(f"TreasurySupermajorityOf(id) = {cualificada}, no (2, 3)")
|
||||||
|
print("2c ✅ TreasurySupermajorityOf(id) = (2, 3): la propuesta de tesoro guarda sus dos tercios")
|
||||||
|
if rpc("state_getStorage", [clave("Proposals", SEMBRADA)])["result"]:
|
||||||
|
vieja = rpc("state_getStorage", [clave("TreasurySupermajorityOf", SEMBRADA)])["result"]
|
||||||
|
if vieja is not None:
|
||||||
|
falla(f"la propuesta sembrada en spec 18 tiene TreasurySupermajorityOf = {vieja}")
|
||||||
|
if not rpc("state_getStorage", [clave("TreasuryActions", SEMBRADA)])["result"]:
|
||||||
|
falla("la propuesta sembrada perdió su TreasuryActions")
|
||||||
|
print(f"2d ✅ la sembrada 0x{SEMBRADA[:12]}… (spec 18) no tiene dos tercios: se decide con mayoría simple")
|
||||||
|
else:
|
||||||
|
print("2d — esta cadena no tiene la propuesta sembrada; no aplica")
|
||||||
|
frontera_del_upgrade()
|
||||||
|
|
||||||
def falla_con(llamada, error, que):
|
def falla_con(llamada, error, que):
|
||||||
n, evs = enviar(llamada, genesis, v)
|
n, evs = enviar(llamada, genesis, v)
|
||||||
idx = indice_de_error(error)
|
idx = indice_de_error(error)
|
||||||
@@ -271,6 +302,49 @@ def main(sembrar_pago=False):
|
|||||||
sembrar(id_hex, genesis, v)
|
sembrar(id_hex, genesis, v)
|
||||||
|
|
||||||
|
|
||||||
|
def frontera_del_upgrade():
|
||||||
|
"""2e · B = primer bloque cuyo estado dice spec >= 21 (búsqueda binaria). Una propuesta de
|
||||||
|
tesoro con start_block <= B la creó el runtime anterior y no lleva marca; una posterior,
|
||||||
|
sí. Cualquier otra combinación reprueba."""
|
||||||
|
def spec(n):
|
||||||
|
return rpc("state_getRuntimeVersion", [rpc("chain_getBlockHash", [n])["result"]])["result"]["specVersion"]
|
||||||
|
lo, hi = 0, best()
|
||||||
|
if spec(hi) < 21:
|
||||||
|
falla("2e: la cadena no llegó a spec 21")
|
||||||
|
while lo < hi:
|
||||||
|
mid = (lo + hi) // 2
|
||||||
|
if spec(mid) >= 21:
|
||||||
|
hi = mid
|
||||||
|
else:
|
||||||
|
lo = mid + 1
|
||||||
|
b = lo
|
||||||
|
pref = "0x" + twox128("Governance") + twox128("TreasuryActions")
|
||||||
|
claves, desde = [], None
|
||||||
|
while True:
|
||||||
|
lote = rpc("state_getKeysPaged", [pref, 500, desde] if desde else [pref, 500])["result"]
|
||||||
|
claves += lote
|
||||||
|
if len(lote) < 500:
|
||||||
|
break
|
||||||
|
desde = lote[-1]
|
||||||
|
antes, despues = [], []
|
||||||
|
for k in claves:
|
||||||
|
pid = k[-64:]
|
||||||
|
p = rpc("state_getStorage", [clave("Proposals", pid)])["result"]
|
||||||
|
if not p:
|
||||||
|
falla(f"2e: TreasuryActions sin Proposals para 0x{pid[:12]}…")
|
||||||
|
inicio = int.from_bytes(bytes.fromhex(p[2:])[144:152], "little")
|
||||||
|
marca = rpc("state_getStorage", [clave("TreasurySupermajorityOf", pid)])["result"]
|
||||||
|
if inicio <= b and marca is not None:
|
||||||
|
falla(f"2e: 0x{pid[:12]}… es de antes del upgrade (#{inicio} <= #{b}) y tiene marca {marca}")
|
||||||
|
if inicio > b and marca != DOS_TERCIOS:
|
||||||
|
# Una ya decidida consumió su marca: sólo se exige en las que siguen en votación.
|
||||||
|
if bytes.fromhex(p[2:])[160] == 0:
|
||||||
|
falla(f"2e: 0x{pid[:12]}… es de después del upgrade (#{inicio} > #{b}), sigue en votación y no tiene (2, 3)")
|
||||||
|
(antes if inicio <= b else despues).append(f"0x{pid[:8]}…@#{inicio}")
|
||||||
|
print(f"2e ✅ frontera del upgrade en #{b}: {len(antes)} de antes sin marca ({', '.join(antes)}) · "
|
||||||
|
f"{len(despues)} de después con (2, 3)")
|
||||||
|
|
||||||
|
|
||||||
def sembrar(id_hex, genesis, v):
|
def sembrar(id_hex, genesis, v):
|
||||||
"""Deja la propuesta lista para cerrar el camino feliz en vivo: fondea el vault y vota Aye
|
"""Deja la propuesta lista para cerrar el camino feliz en vivo: fondea el vault y vota Aye
|
||||||
con EXACTAMENTE el quórum guardado (la frontera `yes >= quórum`)."""
|
con EXACTAMENTE el quórum guardado (la frontera `yes >= quórum`)."""
|
||||||
|
|||||||
Reference in new issue
Block a user