feat(gobierno): spec 18 (RV17), quorum del voto ordinario y pago del tesoro atado a la propuesta

Gobierno (origin-chain-governance):
- El voto ordinario exige un quorum de Aye, el 10 % de PILAR_4 §6.6. Se fija AL CREAR la
  propuesta (VoteQuorumOf) y se consume al decidirla: un set_code no mueve la meta de un
  voto abierto. Cuenta solo el Aye: con el Nay, un voto en contra podria aprobar; con la
  abstencion, el mismo OGC inflaria el quorum de muchas propuestas.
- Un override del veto tiene que alcanzar ese quorum y superar el Nay original, las dos
  condiciones del voto ordinario. Sin ellas, vetar abarataba una aprobacion (dos bloqueantes
  de las revisiones adversariales).
- submit_treasury_proposal (call 16) fija destinatario y monto (TreasuryActions), y
  treasury_disbursement solo paga exactamente eso, una vez. Rechaza montos bajo el deposito
  existencial y como destinatario la propia tesoreria o la cuenta-bond.
- Sin entrada en VoteQuorumOf no hay quorum: la emergencia (que nunca lleva accion de
  tesoro) y lo creado antes de RV17, que se abrio y voto sin quorum.

Runtime: los dos quorums son el 10 % de la emision VIGENTE, redondeado hacia arriba y nunca
por debajo del bond (DiezPorCientoDeLaEmision). Con 100M fijos, en la testnet de la casa
(100M emitidos, todos de Alice) nada se habria aprobado nunca; en mainnet sigue siendo ~100M.
Spec 18, transaction_version 1: todo lo nuevo es aditivo, sin migracion.

Node: pasa a opaque::Block. Decodificaba los extrinsics con tipos nativos y rechazaba en el
pool la llamada que anadia el set_code (1040, el mismo error que una llamada inexistente).
Los bytes y los hashes no cambian: el binario nuevo sincronizo desde cero los 39 273 bloques
de la testnet de la casa con los mismos hashes en 8 alturas. El RPC sirve los extrinsics en hex.

Pesos: seis extrinsics cambiaron de codigo y se midieron en la Pi 5 (smoke declarado: 5
procesos limpios, sin CI en ningun segundo, maximo de los 5). Los otros once siguen siendo los
certificados por Laurha. Se descarto una medida contaminada: su muestreador no veia los tests
de la CI.

Pruebas:
- 363 tests verdes en el workspace.
- 24 contrafactuales, los 24 en rojo.
- Ensayo en la rp1 sobre una cadena con el codigo vivo de spec 17: upgrade a 18 y E2E
  firmando como un wallet (scripts/e2e/rv17_tesoro.py).
- Ejemplo firmar_como_wallet con tesoro, pagar, votar, depositar y finalizar; los eventos
  llevan su fase.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
CharonandClaude Opus 5.5 committed 2026-10-28 21:55:24 -05:00
1 parent abe680e853
commit b6d05eaf45
14 files changed
+1368 -155

No files matched your search

@@ -105,6 +105,26 @@ mod benchmarks {
assert_eq!(Proposals::<T>::iter().count(), 1);
}
#[benchmark]
fn submit_treasury_proposal() {
let caller: T::AccountId = whitelisted_caller();
fund::<T>(&caller);
let recipient: T::AccountId = account("recipient", 0, 0);
#[extrinsic_call]
submit_treasury_proposal(
RawOrigin::Signed(caller),
[1u8; 32],
[2u8; 32],
MIN_VOTING_PERIOD_BLOCKS,
MIN_PROPOSAL_STAKE,
recipient,
MIN_PROPOSAL_STAKE,
);
assert_eq!(TreasuryActions::<T>::iter().count(), 1);
}
#[benchmark]
fn cast_vote() {
let caller: T::AccountId = whitelisted_caller();
@@ -129,19 +149,38 @@ mod benchmarks {
fn finalize_proposal() {
let caller: T::AccountId = whitelisted_caller();
let id = ProposalId([7u8; 32]);
seed_proposal::<T>(id, ProposalStatus::Voting, 0u64);
// RV17: propuesta ORDINARIA (con su quórum guardado) con mayoría pero sin
// quórum: es la rama más cara, porque consume el quórum, rechaza, devuelve el
// bond y emite DOS eventos (ProposalFinalized + ProposalRejectedForQuorum).
seed_proposal::<T>(id, ProposalStatus::Voting, MIN_VOTING_PERIOD_BLOCKS);
Proposals::<T>::mutate(id, |p| {
if let Some(p) = p {
p.yes_weight = 1;
}
});
VoteQuorumOf::<T>::insert(id, 2u128);
// Rama cara: devuelve el bond (transfer real desde la cuenta-bond).
let proposer = seed_bond::<T>(id);
// current_block debe ser > end_block(0) → avanza a 2.
frame_system::Pallet::<T>::set_block_number(2u32.into());
// current_block debe ser > end_block → avanza más allá del periodo.
frame_system::Pallet::<T>::set_block_number(
((MIN_VOTING_PERIOD_BLOCKS + 2) as u32).into(),
);
#[extrinsic_call]
finalize_proposal(RawOrigin::Signed(caller), id);
assert!(!matches!(
Proposals::<T>::get(id).unwrap().status,
ProposalStatus::Voting
));
// 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).
assert!(matches!(Proposals::<T>::get(id).unwrap().status, ProposalStatus::Rejected));
frame_system::Pallet::<T>::assert_last_event(
<T as Config>::RuntimeEvent::from(Event::<T>::ProposalRejectedForQuorum {
id,
yes_weight: 1,
quorum: 2,
})
.into(),
);
assert!(!VoteQuorumOf::<T>::contains_key(id));
assert!(!ProposalBond::<T>::contains_key(id));
assert_eq!(T::Currency::balance(&proposer), MIN_PROPOSAL_STAKE);
}
@@ -277,6 +316,9 @@ mod benchmarks {
MIN_PROPOSAL_STAKE.saturating_mul(10),
);
// RV17: la acción fijada al crear la propuesta, que el desembolso tiene que igualar.
TreasuryActions::<T>::insert(id, (recipient.clone(), MIN_PROPOSAL_STAKE));
#[extrinsic_call]
treasury_disbursement(RawOrigin::Root, id, recipient, MIN_PROPOSAL_STAKE);
@@ -364,7 +406,12 @@ mod benchmarks {
VetoRejected::<T>::insert(id, ());
OverrideDeadline::<T>::insert(id, 1u64);
OverrideQuorum::<T>::insert(id, T::MinOverrideQuorum::get());
OverrideStakeTotal::<T>::insert(id, T::MinOverrideQuorum::get().max(1));
// RV17: el override también lee (y consume) el quórum ordinario guardado.
VoteQuorumOf::<T>::insert(id, T::VoteQuorum::get());
OverrideStakeTotal::<T>::insert(
id,
T::MinOverrideQuorum::get().max(T::VoteQuorum::get()).max(1),
);
frame_system::Pallet::<T>::set_block_number(2u32.into());
#[extrinsic_call]
@@ -375,6 +422,7 @@ mod benchmarks {
ProposalStatus::Approved
));
assert!(!OverrideDeadline::<T>::contains_key(id));
assert!(!VoteQuorumOf::<T>::contains_key(id));
}
impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test);
+221 -58
View File
@@ -146,7 +146,8 @@ pub mod pallet {
pub enum ProposalStatus {
/// Voting period open canonical (block.number ∈ [start_block, end_block]).
Voting,
/// Finalized canonical: yes_weight > nay_weight (passed canonical CHN-4).
/// 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).
Approved,
/// Finalized canonical: nay_weight >= yes_weight (rejected canonical).
Rejected,
@@ -328,9 +329,34 @@ pub mod pallet {
/// participación del voto original (Aye + Nay): revertir al council exige al
/// menos tanta comunidad como la que votó. Es un parámetro de la constitución,
/// y `integrity_test` exige que supere el bond de una propuesta.
#[pallet::constant]
///
/// RV17: deja de ser `#[pallet::constant]`. En el runtime es el 10 % de la
/// emisión VIGENTE, que se lee en el momento del veto y se guarda.
type MinOverrideQuorum: Get<u128>;
/// RV17: quórum del voto ORDINARIO, en OGC (PILAR_4 §6.6: «14-day period, 10%
/// quorum»). Se lee AL CREAR la propuesta y se guarda en [`VoteQuorumOf`]: un
/// runtime upgrade que cambie el parámetro no mueve la meta de un voto abierto.
/// Es la misma regla que RV16 puso al override. En el runtime es el 10 % de la
/// emisión vigente, y por eso no es `#[pallet::constant]`.
///
/// Una propuesta ordinaria se aprueba sólo si `yes > nay` Y `yes >=` su quórum.
/// Un override del veto tiene que cumplir lo mismo con su propio apoyo (alcanzar
/// el quórum y superar el Nay original). Si no, vetar ABARATARÍA la aprobación:
/// 4/5 de un quórum de override igual al ordinario es menos que el ordinario, y
/// el Nay original no votaba en la ventana (revisiones adversariales de RV17).
///
/// Cuenta sólo el Aye:
/// - si contara el Nay, un voto en contra podría APROBAR (60 Aye no llegan al
/// quórum de 100; 40 Nay más lo cruzan con 60 > 40);
/// - si contara la abstención, el mismo OGC abstenido en varias propuestas a la
/// vez cruzaría el quórum de todas sin tomar partido (la razón de RV16 para
/// `OverrideQuorum`).
///
/// La emergencia (48 h, 5/7 del council) no lo exige: la doctrina lo fija para
/// el voto de 14 días. `integrity_test` exige que supere el bond de una propuesta.
type VoteQuorum: Get<u128>;
/// FIX-6: PalletId del que se deriva la cuenta-tesorería (vault). Cuenta
/// determinística sin private key — nadie la controla por wallet, solo la
/// governance vía treasury_disbursement (Root). Honor Inmutable #16: la
@@ -362,6 +388,14 @@ pub mod pallet {
T::MinOverrideQuorum::get() > MIN_PROPOSAL_STAKE,
"MinOverrideQuorum debe superar MIN_PROPOSAL_STAKE (hallazgo C3)"
);
// RV17: con un quórum menor que el bond, el proponente aprobaría solo su
// propuesta con un voto del tamaño de su fianza. Ojo: este test corre sin
// estado, así que con el quórum del runtime (10 % de la emisión) sólo ve el
// piso. El porcentaje lo pinna `quorums_del_runtime`.
assert!(
T::VoteQuorum::get() > MIN_PROPOSAL_STAKE,
"VoteQuorum debe superar MIN_PROPOSAL_STAKE (RV17)"
);
}
}
@@ -421,6 +455,29 @@ pub mod pallet {
pub type OverrideQuorum<T: Config> =
StorageMap<_, Blake2_128Concat, ProposalId, u128, OptionQuery>;
/// RV17: el quórum de Aye de cada propuesta ORDINARIA, fijado al crearla
/// (`Config::VoteQuorum` en ese momento). Se consume al decidirla.
///
/// Una propuesta sin entrada no exige quórum. Son dos casos, y en los dos es lo
/// correcto:
/// - la emergencia (48 h, 5/7 del council), que la doctrina exime y que nunca lleva
/// acción de tesoro;
/// - una propuesta creada antes de RV17, que se abrió y se votó sin quórum.
///
/// Es la marca explícita del tipo. Antes se deducía del periodo.
#[pallet::storage]
pub type VoteQuorumOf<T: Config> =
StorageMap<_, Blake2_128Concat, ProposalId, u128, OptionQuery>;
/// RV17: lo que paga una propuesta de tesoro, fijado AL CREARLA
/// (`submit_treasury_proposal`): destinatario y monto. `treasury_disbursement`
/// sólo paga si coinciden exactamente, y una propuesta sin acción no paga nada.
/// Antes el monto y el destinatario los ponía quien llamaba (GSUN-3, condición 5
/// de Palace). Nadie lo modifica después: quien vota sabe qué aprueba.
#[pallet::storage]
pub type TreasuryActions<T: Config> =
StorageMap<_, Blake2_128Concat, ProposalId, (T::AccountId, u128), OptionQuery>;
/// CHN-4 canonical: `Proposals<StorageMap<ProposalId, Proposal>>` canonical.
#[pallet::storage]
#[pallet::getter(fn proposal_of)]
@@ -743,6 +800,26 @@ pub mod pallet {
/// Quórum que se exigía.
quorum: u128,
},
/// RV17: la propuesta tuvo más Aye que Nay pero no llegó al quórum del voto
/// ordinario, y se rechaza. Va además del `ProposalFinalized` con `Rejected`,
/// para que se lea POR QUÉ.
ProposalRejectedForQuorum {
/// Propuesta rechazada.
id: ProposalId,
/// OGC que votó Aye (lo único que cuenta para el quórum).
yes_weight: u128,
/// Quórum que se exigía.
quorum: u128,
},
/// RV17: se creó una propuesta de tesoro con su acción fijada.
TreasuryProposalSubmitted {
/// Propuesta creada.
id: ProposalId,
/// Quien cobrará si se aprueba.
recipient: T::AccountId,
/// Cuánto, en unidades base.
amount: u128,
},
}
/// Pallet errors canonical.
@@ -827,6 +904,17 @@ pub mod pallet {
OverrideNotPending,
/// RV16: un voto de la ventana de override tiene que pesar más de 0.
ZeroWeightWindowVote,
/// RV17: el desembolso no coincide con la acción fijada al crear la propuesta,
/// o la propuesta no tiene acción de tesoro.
TreasuryActionMismatch,
/// RV17: una propuesta de tesoro tiene que pagar al menos el depósito
/// existencial. Con menos, el pago a una cuenta que no existe fallaría siempre y
/// la propuesta quedaría aprobada e impagable.
TreasuryAmountBelowMinimum,
/// RV17: el destinatario no puede ser la propia tesorería ni la cuenta-bond. Una
/// transferencia a sí misma devuelve Ok sin mover nada (se marcaría pagada sin
/// pago), y lo que llega a la cuenta-bond queda atrapado.
TreasuryRecipientInvalid,
}
/// Minimum stake canonical proposal submission per CHN-4 5.3.
@@ -856,60 +944,38 @@ pub mod pallet {
stake: u128,
) -> DispatchResult {
let who = ensure_signed(origin)?;
ensure!(stake >= MIN_PROPOSAL_STAKE, Error::<T>::StakeInsufficient);
Self::do_submit_proposal(who, title_hash, body_hash, voting_period_blocks, stake)?;
Ok(())
}
/// RV17: crea una propuesta de TESORO. Es una propuesta ordinaria (mismo bond,
/// 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
/// después. `treasury_disbursement` sólo puede pagar exactamente esto.
#[pallet::call_index(16)]
#[pallet::weight(T::WeightInfo::submit_treasury_proposal())]
pub fn submit_treasury_proposal(
origin: OriginFor<T>,
title_hash: [u8; 32],
body_hash: [u8; 32],
voting_period_blocks: u64,
stake: u128,
recipient: T::AccountId,
amount: u128,
) -> DispatchResult {
let who = ensure_signed(origin)?;
ensure!(
voting_period_blocks <= MAX_VOTING_PERIOD_BLOCKS,
Error::<T>::VotingPeriodTooLong
amount >= T::Currency::minimum_balance().max(1),
Error::<T>::TreasuryAmountBelowMinimum
);
ensure!(
voting_period_blocks >= MIN_VOTING_PERIOD_BLOCKS,
Error::<T>::VotingPeriodTooShort
recipient != Self::treasury_account() && recipient != Self::proposal_bond_account(),
Error::<T>::TreasuryRecipientInvalid
);
let proposer_acct = AccountId::new(account_bytes::<T>(&who));
let id = crate::derive_proposal_id(&proposer_acct, &title_hash, &body_hash);
ensure!(!Proposals::<T>::contains_key(id), Error::<T>::ProposalIdCollision);
// Audit 2026-07-04: RESERVA REAL del stake (piel en el juego anti-spam).
// Antes el `stake` era caller-supplied SIN respaldo — una cuenta vacía
// spameaba propuestas gratis y el `baseline` del override era
// manipulable. Ahora se retiene de verdad en la cuenta-bond og/props
// (falla si el proponente no tiene los fondos) y se devuelve al decidir.
T::Currency::transfer(
&who,
&Self::proposal_bond_account(),
stake,
Preservation::Preserve,
)
.map_err(|_| Error::<T>::StakeInsufficientBalance)?;
ProposalBond::<T>::insert(id, (who.clone(), stake));
let start_block: u64 = <frame_system::Pallet<T>>::block_number()
.saturated_into::<u64>();
let end_block = start_block.saturating_add(voting_period_blocks);
let proposal = Proposal {
id,
proposer: proposer_acct,
title_hash,
body_hash,
stake,
start_block,
end_block,
status: ProposalStatus::Voting,
yes_weight: 0,
nay_weight: 0,
abstain_weight: 0,
};
Proposals::<T>::insert(id, proposal);
Self::deposit_event(Event::ProposalSubmitted {
id,
proposer: who,
voting_period: voting_period_blocks,
});
let id =
Self::do_submit_proposal(who, title_hash, body_hash, voting_period_blocks, stake)?;
TreasuryActions::<T>::insert(id, (recipient.clone(), amount));
Self::deposit_event(Event::TreasuryProposalSubmitted { id, recipient, amount });
Ok(())
}
@@ -1011,7 +1077,13 @@ pub mod pallet {
Error::<T>::VotingPeriodOpen
);
let final_status = if proposal.yes_weight > proposal.nay_weight {
// RV17: además de la mayoría, el voto ordinario exige el quórum de Aye que se
// fijó al crearlo (ver `VoteQuorumOf`). Sin entrada (emergencia, o creada
// antes de RV17), no hay quórum.
let quorum = VoteQuorumOf::<T>::take(proposal_id).unwrap_or(0);
let mayoria = proposal.yes_weight > proposal.nay_weight;
let sin_quorum = mayoria && proposal.yes_weight < quorum;
let final_status = if mayoria && !sin_quorum {
ProposalStatus::Approved
} else {
ProposalStatus::Rejected
@@ -1033,6 +1105,13 @@ pub mod pallet {
yes_weight,
nay_weight,
});
if sin_quorum {
Self::deposit_event(Event::ProposalRejectedForQuorum {
id: proposal_id,
yes_weight,
quorum,
});
}
Ok(())
}
@@ -1413,6 +1492,12 @@ pub mod pallet {
!Disbursed::<T>::contains_key(proposal_id),
Error::<T>::AlreadyDisbursed
);
// RV17: sólo se paga lo que la propuesta fijó al crearse, a quien fijó.
// Una propuesta sin acción de tesoro no paga nada.
ensure!(
TreasuryActions::<T>::get(proposal_id) == Some((recipient.clone(), amount)),
Error::<T>::TreasuryActionMismatch
);
// Marcar ANTES del transfer (defensa en profundidad; los pallets no
// reentran, pero el marker refleja la intención de gasto atómica).
Disbursed::<T>::insert(proposal_id, ());
@@ -1540,10 +1625,15 @@ pub mod pallet {
/// cualquiera.
///
/// El veto se revierte (la propuesta pasa a `Approved`) solo si:
/// 1. la participación (revertir + sostener) alcanza el quórum, que es el
/// MAYOR entre `MinOverrideQuorum` y la participación del voto original
/// (Aye + Nay + Abstain); y
/// 2. revertir reúne al menos 4/5 de esa participación.
/// 1. revertir reúne al menos 4/5 del MAYOR entre el quórum del override (fijado
/// en el veto: `MinOverrideQuorum` o la participación del voto original, Aye +
/// Nay) y la participación de la ventana (revertir + sostener); y
/// 2. RV17: revertir alcanza además el quórum ordinario de la propuesta
/// (`VoteQuorumOf`) y SUPERA el Nay del voto original. Son las dos condiciones
/// del voto ordinario (`yes >= quórum` y `yes > nay`): vetar nunca abarata una
/// 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
/// (segunda revisión adversarial de RV17).
///
/// En cualquier otro caso el veto se sostiene. Se decide una sola vez.
#[pallet::call_index(15)]
@@ -1572,10 +1662,12 @@ pub mod pallet {
let participation = override_stake.saturating_add(uphold_stake);
let quorum =
OverrideQuorum::<T>::get(proposal_id).ok_or(Error::<T>::OverrideNotPending)?;
let quorum_ordinario = VoteQuorumOf::<T>::get(proposal_id).unwrap_or(0);
// Se decide una sola vez: sin ventana, no hay override pendiente.
OverrideDeadline::<T>::remove(proposal_id);
OverrideQuorum::<T>::remove(proposal_id);
VoteQuorumOf::<T>::remove(proposal_id);
// 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
@@ -1585,7 +1677,9 @@ pub mod pallet {
// de defender el veto. Con max(quorum, participación), cada OGC que sostiene
// solo puede subir el listón de revertir, nunca bajarlo.
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 > proposal.nay_weight;
if overridden {
proposal.status = ProposalStatus::Approved;
Proposals::<T>::insert(proposal_id, proposal);
@@ -1771,6 +1865,75 @@ pub mod pallet {
PROPOSAL_BOND_PALLET_ID.into_account_truncating()
}
/// Crea una propuesta ordinaria (bond retenido, periodo dentro de
/// [MIN, MAX]) y devuelve su id. La comparten `submit_proposal` y, desde RV17,
/// `submit_treasury_proposal`.
fn do_submit_proposal(
who: T::AccountId,
title_hash: [u8; 32],
body_hash: [u8; 32],
voting_period_blocks: u64,
stake: u128,
) -> Result<ProposalId, DispatchError> {
ensure!(stake >= MIN_PROPOSAL_STAKE, Error::<T>::StakeInsufficient);
ensure!(
voting_period_blocks <= MAX_VOTING_PERIOD_BLOCKS,
Error::<T>::VotingPeriodTooLong
);
ensure!(
voting_period_blocks >= MIN_VOTING_PERIOD_BLOCKS,
Error::<T>::VotingPeriodTooShort
);
let proposer_acct = AccountId::new(account_bytes::<T>(&who));
let id = crate::derive_proposal_id(&proposer_acct, &title_hash, &body_hash);
ensure!(!Proposals::<T>::contains_key(id), Error::<T>::ProposalIdCollision);
// Audit 2026-07-04: RESERVA REAL del stake (piel en el juego anti-spam).
// Antes el `stake` era caller-supplied SIN respaldo — una cuenta vacía
// spameaba propuestas gratis y el `baseline` del override era
// manipulable. Ahora se retiene de verdad en la cuenta-bond og/props
// (falla si el proponente no tiene los fondos) y se devuelve al decidir.
T::Currency::transfer(
&who,
&Self::proposal_bond_account(),
stake,
Preservation::Preserve,
)
.map_err(|_| Error::<T>::StakeInsufficientBalance)?;
ProposalBond::<T>::insert(id, (who.clone(), stake));
let start_block: u64 = <frame_system::Pallet<T>>::block_number()
.saturated_into::<u64>();
let end_block = start_block.saturating_add(voting_period_blocks);
let proposal = Proposal {
id,
proposer: proposer_acct,
title_hash,
body_hash,
stake,
start_block,
end_block,
status: ProposalStatus::Voting,
yes_weight: 0,
nay_weight: 0,
abstain_weight: 0,
};
Proposals::<T>::insert(id, proposal);
// RV17: el quórum se fija ahora, no al decidir (ver `VoteQuorumOf`).
VoteQuorumOf::<T>::insert(id, T::VoteQuorum::get());
Self::deposit_event(Event::ProposalSubmitted {
id,
proposer: who,
voting_period: voting_period_blocks,
});
Ok(id)
}
/// Devuelve el bond retenido al proponente cuando la propuesta se decide
/// (finalize o veto). Idempotente: `take` consume la entrada, así que un
/// segundo intento es no-op (no doble-refund). Expendable: la cuenta-bond
@@ -40,6 +40,11 @@ frame_support::parameter_types! {
/// FIX-6: PalletId de la cuenta-tesorería del mock.
pub const TestTreasuryPalletId: frame_support::PalletId =
frame_support::PalletId(*b"og/gvtry");
/// RV17: quórum del voto ordinario del mock. Por defecto, un bond más una unidad:
/// el mínimo que acepta `integrity_test`, para que las pruebas que no van de
/// quórum aprueben con un voto pequeño. Las que sí van de quórum lo suben con
/// `VoteQuorum::set`.
pub static VoteQuorum: u128 = crate::pallet::MIN_PROPOSAL_STAKE + 1;
}
impl origin_chain_governance::Config for Test {
@@ -53,6 +58,7 @@ impl origin_chain_governance::Config for Test {
// igual al bond, el proponente revertía solo con el bond devuelto (revisión de RV16).
type MinOverrideQuorum =
frame_support::traits::ConstU128<{ 2 * crate::pallet::MIN_PROPOSAL_STAKE }>;
type VoteQuorum = VoteQuorum;
type TreasuryPalletId = TestTreasuryPalletId;
type WeightInfo = ();
}
@@ -49,6 +49,32 @@ fn submit_default(origin: frame_system::pallet_prelude::OriginFor<Test>) -> Prop
id
}
/// RV17: propuesta de TESORO de Alice, con su acción fijada (quién cobra y cuánto).
fn submit_tesoro(recipient: <Test as frame_system::Config>::AccountId, amount: u128) -> ProposalId {
let proposer = AccountId::new(alice_account_bytes());
let id = derive_proposal_id(&proposer, &TITLE_HASH, &BODY_HASH);
assert_ok!(Governance::submit_treasury_proposal(
alice_origin(),
TITLE_HASH,
BODY_HASH,
VOTING_PERIOD,
MIN_PROPOSAL_STAKE,
recipient,
amount,
));
id
}
/// RV17: Bob vota Aye con el peso EXACTO del quórum, se cierra el periodo y se
/// finaliza. Deja la propuesta Approved con lo mínimo que la regla admite.
fn aprobar(id: ProposalId) {
let quorum = crate::mock::VoteQuorum::get();
assert_ok!(Governance::cast_vote(bob_origin(), id, VoteStance::Aye, quorum));
run_to_block(TRAS_EL_PERIODO);
assert_ok!(Governance::finalize_proposal(alice_origin(), id));
assert!(matches!(Proposals::<Test>::get(id).unwrap().status, ProposalStatus::Approved));
}
fn alice_account_bytes() -> [u8; 32] {
use parity_scale_codec::Encode;
let encoded = alice_account().encode();
@@ -186,6 +212,10 @@ fn scenario_6_finalize_voting_open_rejected_canonical() {
#[test]
fn scenario_7_finalize_approved_yes_majority_canonical() {
new_test_ext().execute_with(|| {
// RV17: prueba de MAYORÍA. El quórum se fija igual al Aye, la frontera exacta
// (`yes >= quórum`), para que lo que se mire aquí sea yes > nay. Va antes de
// crear la propuesta: el quórum se guarda al crearla.
crate::mock::VoteQuorum::set(1000);
let id = submit_default(alice_origin());
// Vote Aye 1000 + Nay 500 → Aye majority
@@ -892,11 +922,9 @@ fn treasury_scenario_3_insufficient_balance_rejected_canonical() {
// Treasury 100 OGC only
assert_ok!(Governance::treasury_deposit(alice_origin(),100));
// Setup approved proposal
let id = submit_default(alice_origin());
assert_ok!(Governance::cast_vote(bob_origin(), id, VoteStance::Aye, 1000));
run_to_block(TRAS_EL_PERIODO);
assert_ok!(Governance::finalize_proposal(alice_origin(), id));
// RV17: propuesta de tesoro que fija 200 para Bob, aprobada.
let id = submit_tesoro(bob_account(), 200);
aprobar(id);
// Try disburse 200 (exceeds 100 balance)
assert_noop!(
@@ -919,11 +947,9 @@ fn treasury_scenario_4_successful_disbursement_canonical() {
// Treasury 1M OGC
assert_ok!(Governance::treasury_deposit(alice_origin(),1_000_000));
// Setup approved proposal
let id = submit_default(alice_origin());
assert_ok!(Governance::cast_vote(bob_origin(), id, VoteStance::Aye, 1000));
run_to_block(TRAS_EL_PERIODO);
assert_ok!(Governance::finalize_proposal(alice_origin(), id));
// RV17: propuesta de tesoro que fija 300_000 para Bob, aprobada.
let id = submit_tesoro(bob_account(), 300_000);
aprobar(id);
// Disburse 300_000 canonical
assert_ok!(Governance::treasury_disbursement(
@@ -989,10 +1015,8 @@ fn treasury_disbursement_es_single_use_anti_replay() {
new_test_ext().execute_with(|| {
use frame_support::dispatch::RawOrigin;
assert_ok!(Governance::treasury_deposit(alice_origin(), 1_000_000));
let id = submit_default(alice_origin());
assert_ok!(Governance::cast_vote(bob_origin(), id, VoteStance::Aye, 1000));
run_to_block(TRAS_EL_PERIODO);
assert_ok!(Governance::finalize_proposal(alice_origin(), id));
let id = submit_tesoro(bob_account(), 300_000);
aprobar(id);
// Primer desembolso: OK.
assert_ok!(Governance::treasury_disbursement(
@@ -1138,10 +1162,12 @@ fn council_scenario_7_veto_threshold_2_of_3_canonical() {
#[test]
fn scenario_10_lifecycle_walkthrough_canonical() {
new_test_ext().execute_with(|| {
// RV17: el quórum se fija igual al Aye total (1500 + 2000), la frontera exacta,
// antes de crear la propuesta (se guarda al crearla).
crate::mock::VoteQuorum::set(3500);
// Step 1: submit canonical
let id = submit_default(alice_origin());
assert!(Proposals::<Test>::get(id).is_some());
// Step 2: vote canonical (3 voters: bob Aye, charlie Aye, alice Nay = 2 Aye vs 1 Nay)
assert_ok!(Governance::cast_vote(bob_origin(), id, VoteStance::Aye, 1500));
assert_ok!(Governance::cast_vote(charlie_origin(), id, VoteStance::Aye, 2000));
@@ -1915,11 +1941,15 @@ fn finalize_override_usa_el_quorum_fijado_en_el_veto() {
MIN_PROPOSAL_STAKE
));
// Simula un quórum guardado más bajo: si finalize recalculara, no lo vería.
// RV17: el quórum ordinario de la propuesta también está guardado, y el override
// tiene que alcanzarlo; se baja igual.
crate::OverrideQuorum::<Test>::insert(id, MIN_PROPOSAL_STAKE);
crate::VoteQuorumOf::<Test>::insert(id, MIN_PROPOSAL_STAKE);
run_to_block(cierre + 1);
assert_ok!(Governance::finalize_override(alice_origin(), id));
assert!(matches!(estado(id), ProposalStatus::Approved));
assert!(crate::OverrideQuorum::<Test>::get(id).is_none());
assert!(crate::VoteQuorumOf::<Test>::get(id).is_none());
});
}
@@ -2192,3 +2222,374 @@ fn una_propuesta_ordinaria_dura_al_menos_14_dias() {
));
});
}
// ─── RV17: quórum del voto ordinario y tesoro atado a la propuesta ─────────────
/// Abre una propuesta ordinaria con el quórum del mock fijado a `quorum`, vota
/// (Bob Aye, Charlie Nay, Alice Abstain) y la finaliza. Devuelve su estado.
fn votar_y_finalizar(quorum: u128, aye: u128, nay: u128, abstain: u128) -> ProposalStatus {
crate::mock::VoteQuorum::set(quorum);
let id = submit_default(alice_origin());
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));
Proposals::<Test>::get(id).unwrap().status
}
#[test]
fn una_mayoria_sin_quorum_se_rechaza_y_dice_por_que() {
// Frontera exacta: con quórum Q, Q-1 de Aye no basta y Q sí.
let q = 10_000u128;
new_test_ext().execute_with(|| {
assert!(matches!(votar_y_finalizar(q, q - 1, 0, 0), ProposalStatus::Rejected));
let id =
derive_proposal_id(&AccountId::new(alice_account_bytes()), &TITLE_HASH, &BODY_HASH);
System::assert_has_event(RuntimeEvent::Governance(Event::ProposalRejectedForQuorum {
id,
yes_weight: q - 1,
quorum: q,
}));
});
new_test_ext().execute_with(|| {
assert!(matches!(votar_y_finalizar(q, q, 0, 0), ProposalStatus::Approved));
assert!(!System::events().iter().any(|r| matches!(
r.event,
RuntimeEvent::Governance(Event::ProposalRejectedForQuorum { .. })
)));
});
}
#[test]
fn un_nay_nunca_ayuda_a_aprobar_y_la_abstencion_no_cuenta() {
// La paradoja del quórum de participación: 60 Aye no llegan a 100; si el Nay
// contara, 40 Nay más cruzarían el quórum con 60 > 40 y el voto EN CONTRA
// aprobaría. Con quórum de Aye, ni el Nay ni la abstención mueven el quórum.
let q = 100_000u128;
for (aye, nay, abstain) in
[(60_000, 0, 0), (60_000, 40_000, 0), (60_000, 0, 40_000), (60_000, 39_999, 40_001)]
{
new_test_ext().execute_with(|| {
assert!(
matches!(votar_y_finalizar(q, aye, nay, abstain), ProposalStatus::Rejected),
"Aye {aye} < quórum {q}: ni Nay {nay} ni Abstain {abstain} pueden aprobarla"
);
});
}
// Monotonía en una rejilla: añadir Nay nunca convierte Rejected en Approved, y
// añadir Aye nunca convierte Approved en Rejected.
let pasos = [0u128, 50_000, 100_000, 150_000];
for &aye in &pasos {
for &nay in &pasos {
let base = new_test_ext().execute_with(|| votar_y_finalizar(q, aye, nay, 0));
let mas_nay =
new_test_ext().execute_with(|| votar_y_finalizar(q, aye, nay + 50_000, 0));
let mas_aye =
new_test_ext().execute_with(|| votar_y_finalizar(q, aye + 50_000, nay, 0));
if matches!(base, ProposalStatus::Rejected) {
assert!(
matches!(mas_nay, ProposalStatus::Rejected),
"Nay de más aprobó (aye {aye}, nay {nay})"
);
}
if matches!(base, ProposalStatus::Approved) {
assert!(
matches!(mas_aye, ProposalStatus::Approved),
"Aye de más rechazó (aye {aye}, nay {nay})"
);
}
}
}
}
#[test]
fn la_emergencia_no_exige_quorum_y_nunca_paga() {
// La doctrina fija el quórum para el voto de 14 días; la emergencia (48 h) va
// por el 5/7 del council. Con un Aye minúsculo y el quórum altísimo, se aprueba.
// Y no puede pagar: no lleva acción de tesoro. Con un council de un solo miembro,
// el 5/7 es una persona; esta invariante es la que hace inocua la emergencia.
new_test_ext().execute_with(|| {
use crate::pallet::{CouncilRole, EMERGENCY_VOTING_PERIOD_BLOCKS};
use frame_support::dispatch::RawOrigin;
crate::mock::VoteQuorum::set(MIN_PROPOSAL_STAKE * 9);
assert_ok!(Governance::add_council_member(
RawOrigin::Root.into(),
alice_account(),
CouncilRole::Member
));
for i in 1..7u8 {
assert_ok!(Governance::add_council_member(
RawOrigin::Root.into(),
sp_runtime::AccountId32::new([i + 50; 32]),
CouncilRole::Member,
));
}
let miembro = |i: u8| RawOrigin::Signed(sp_runtime::AccountId32::new([i + 50; 32])).into();
assert_ok!(Governance::endorse_emergency_proposal(alice_origin(), [10u8; 32], [20u8; 32]));
for i in 1..5u8 {
assert_ok!(Governance::endorse_emergency_proposal(miembro(i), [10u8; 32], [20u8; 32]));
}
assert_ok!(Governance::submit_emergency_proposal(
alice_origin(),
[10u8; 32],
[20u8; 32],
MIN_PROPOSAL_STAKE
));
let id =
derive_proposal_id(&AccountId::new(alice_account_bytes()), &[10u8; 32], &[20u8; 32]);
assert!(crate::VoteQuorumOf::<Test>::get(id).is_none(), "la emergencia no guarda quórum");
assert_ok!(Governance::cast_vote(bob_origin(), id, VoteStance::Aye, 10));
run_to_block(1 + EMERGENCY_VOTING_PERIOD_BLOCKS + 10);
assert_ok!(Governance::finalize_proposal(alice_origin(), id));
assert!(matches!(Proposals::<Test>::get(id).unwrap().status, ProposalStatus::Approved));
assert!(crate::TreasuryActions::<Test>::get(id).is_none());
assert_ok!(Governance::treasury_deposit(alice_origin(), 1_000_000));
assert_noop!(
Governance::treasury_disbursement(RawOrigin::Root.into(), id, bob_account(), 1),
Error::<Test>::TreasuryActionMismatch
);
});
}
#[test]
#[should_panic(expected = "VoteQuorum debe superar MIN_PROPOSAL_STAKE")]
fn un_quorum_del_tamano_del_bond_no_pasa_la_integridad() {
use frame_support::traits::Hooks;
crate::mock::VoteQuorum::set(MIN_PROPOSAL_STAKE);
<Governance as Hooks<u64>>::integrity_test();
}
#[test]
fn el_tesoro_solo_paga_lo_que_fijo_la_propuesta() {
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);
System::assert_has_event(RuntimeEvent::Governance(Event::TreasuryProposalSubmitted {
id,
recipient: bob_account(),
amount: 300_000,
}));
assert_eq!(crate::TreasuryActions::<Test>::get(id), Some((bob_account(), 300_000)));
aprobar(id);
for (quien, cuanto) in
[(charlie_account(), 300_000), (bob_account(), 300_001), (bob_account(), 299_999)]
{
assert_noop!(
Governance::treasury_disbursement(RawOrigin::Root.into(), id, quien, cuanto),
Error::<Test>::TreasuryActionMismatch
);
}
assert_ok!(Governance::treasury_disbursement(
RawOrigin::Root.into(),
id,
bob_account(),
300_000
));
assert_eq!(Governance::treasury_balance(), 700_000);
});
}
#[test]
fn una_propuesta_sin_accion_de_tesoro_no_paga_nada() {
// Antes de RV17, cualquier propuesta Approved se podía desembolsar con el monto y
// el destinatario que pusiera quien llamaba.
new_test_ext().execute_with(|| {
use frame_support::dispatch::RawOrigin;
assert_ok!(Governance::treasury_deposit(alice_origin(), 1_000_000));
let id = submit_default(alice_origin());
aprobar(id);
assert_noop!(
Governance::treasury_disbursement(RawOrigin::Root.into(), id, bob_account(), 1),
Error::<Test>::TreasuryActionMismatch
);
});
}
#[test]
fn una_propuesta_de_tesoro_no_paga_menos_del_minimo_ni_a_si_misma_y_exige_quorum() {
new_test_ext().execute_with(|| {
assert_noop!(
Governance::submit_treasury_proposal(
alice_origin(),
TITLE_HASH,
BODY_HASH,
VOTING_PERIOD,
MIN_PROPOSAL_STAKE,
bob_account(),
0,
),
Error::<Test>::TreasuryAmountBelowMinimum
);
// Ni la propia tesorería ni la cuenta-bond pueden ser el destinatario.
for destino in [Governance::treasury_account(), Governance::proposal_bond_account()] {
assert_noop!(
Governance::submit_treasury_proposal(
alice_origin(),
TITLE_HASH,
BODY_HASH,
VOTING_PERIOD,
MIN_PROPOSAL_STAKE,
destino,
500,
),
Error::<Test>::TreasuryRecipientInvalid
);
}
// Una propuesta de tesoro es una ordinaria: sin quórum se rechaza y no paga.
crate::mock::VoteQuorum::set(MIN_PROPOSAL_STAKE * 2);
let id = submit_tesoro(bob_account(), 500);
assert_ok!(Governance::cast_vote(bob_origin(), id, VoteStance::Aye, MIN_PROPOSAL_STAKE));
run_to_block(TRAS_EL_PERIODO);
assert_ok!(Governance::finalize_proposal(alice_origin(), id));
assert!(matches!(Proposals::<Test>::get(id).unwrap().status, ProposalStatus::Rejected));
});
}
#[test]
fn el_quorum_se_fija_al_crear_la_propuesta() {
// Revisión adversarial de RV17: leído al decidir, un `set_code` que cambiara el
// parámetro movía la meta de los votos ya abiertos. Ahora se guarda al crear, en
// las dos direcciones.
new_test_ext().execute_with(|| {
let q = 2 * MIN_PROPOSAL_STAKE;
crate::mock::VoteQuorum::set(q);
let id = submit_default(alice_origin());
assert_eq!(crate::VoteQuorumOf::<Test>::get(id), Some(q));
assert_ok!(Governance::cast_vote(bob_origin(), id, VoteStance::Aye, q));
crate::mock::VoteQuorum::set(10 * q); // «el upgrade lo sube»
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!(crate::VoteQuorumOf::<Test>::get(id).is_none(), "se consume al decidir");
});
new_test_ext().execute_with(|| {
let q = 2 * MIN_PROPOSAL_STAKE;
crate::mock::VoteQuorum::set(q);
let id = submit_default(alice_origin());
assert_ok!(Governance::cast_vote(bob_origin(), id, VoteStance::Aye, q - 1));
crate::mock::VoteQuorum::set(1); // «el upgrade lo baja»
run_to_block(TRAS_EL_PERIODO);
assert_ok!(Governance::finalize_proposal(alice_origin(), id));
assert!(matches!(Proposals::<Test>::get(id).unwrap().status, ProposalStatus::Rejected));
});
}
#[test]
fn una_propuesta_anterior_a_rv17_no_exige_quorum() {
// Se abrió y se votó sin quórum: decidirla con uno sería cambiarle la regla a mitad.
new_test_ext().execute_with(|| {
crate::mock::VoteQuorum::set(5 * MIN_PROPOSAL_STAKE);
let id = submit_default(alice_origin());
crate::VoteQuorumOf::<Test>::remove(id); // como la dejó spec 17
assert_ok!(Governance::cast_vote(bob_origin(), id, VoteStance::Aye, 10));
run_to_block(TRAS_EL_PERIODO);
assert_ok!(Governance::finalize_proposal(alice_origin(), id));
assert!(matches!(Proposals::<Test>::get(id).unwrap().status, ProposalStatus::Approved));
});
}
/// Abre una propuesta de tesoro con el quórum ordinario `q`, la veta el council
/// (Alice, Chair) y Charlie la revierte con `apoyo`. Devuelve el estado final y si se
/// pudo pagar.
fn vetar_y_revertir(q: u128, apoyo: u128) -> (ProposalStatus, bool) {
use crate::pallet::CouncilRole;
use frame_support::dispatch::RawOrigin;
crate::mock::VoteQuorum::set(q);
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);
assert_ok!(Governance::veto_proposal(alice_origin(), id));
let cierre = crate::OverrideDeadline::<Test>::get(id).unwrap();
assert_ok!(Governance::override_council_veto(charlie_origin(), 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 vetar_nunca_abarata_la_aprobacion() {
// Revisión adversarial de RV17 (bloqueante 1): con el quórum de override igual al
// ordinario, como en el runtime, revertir pedía 4/5 de él. Una propuesta que con
// ese apoyo se rechazaba sin quórum quedaba APROBADA si el council la vetaba, y el
// tesoro pagaba. El override tiene que alcanzar también el quórum ordinario.
let q = 2 * MIN_PROPOSAL_STAKE; // = MinOverrideQuorum del mock
new_test_ext().execute_with(|| {
let (estado, pagado) = vetar_y_revertir(q, q / 5 * 4);
assert!(matches!(estado, ProposalStatus::Rejected), "4/5 del quórum no basta");
assert!(!pagado);
});
// Control positivo: con el quórum ordinario completo, el override revierte y paga.
new_test_ext().execute_with(|| {
let (estado, pagado) = vetar_y_revertir(q, q);
assert!(matches!(estado, ProposalStatus::Approved));
assert!(pagado);
});
}
/// Propuesta de tesoro con oposición: Bob vota Aye `MIN` y Charlie Nay `4·MIN`. El council
/// (Alice, Chair) la veta y Alice la revierte con `apoyo`.
fn vetar_con_oposicion(apoyo: u128) -> (ProposalStatus, bool) {
use crate::pallet::CouncilRole;
use frame_support::dispatch::RawOrigin;
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);
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();
assert_ok!(Governance::override_council_veto(alice_origin(), 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 vetar_no_abarata_la_aprobacion_frente_a_la_oposicion() {
// Segunda revisión adversarial de RV17. Con Aye MIN y Nay 4·MIN, el voto ordinario
// pide más de 4·MIN de Aye. Revertir pedía 4/5 de la participación original (5·MIN),
// 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
// que SUPERAR el Nay original, como el voto ordinario.
new_test_ext().execute_with(|| {
let (estado, pagado) = vetar_con_oposicion(4 * MIN_PROPOSAL_STAKE);
assert!(matches!(estado, ProposalStatus::Rejected), "igualar al Nay no basta");
assert!(!pagado);
});
// Control positivo: superarlo por una unidad basta (y cumple los 4/5 y el quórum).
new_test_ext().execute_with(|| {
let (estado, pagado) = vetar_con_oposicion(4 * MIN_PROPOSAL_STAKE + 1);
assert!(matches!(estado, ProposalStatus::Approved));
assert!(pagado);
});
}
+98 -32
View File
@@ -21,6 +21,25 @@
// TIEMPO DE EJECUCIÓN sale de 2,5 a 3,9 veces el del Mac M3 Max (~6 veces en los que verifican
// 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.
//! Weights AUTOGENERADOS para `origin_chain_governance` — NO editar a mano.
//!
@@ -102,6 +121,8 @@ pub trait WeightInfo {
fn uphold_council_veto() -> Weight;
/// Peso de `finalize_override` — MEDIDO sobre el WASM, no estimado a mano.
fn finalize_override() -> Weight;
/// Peso de `submit_treasury_proposal` — MEDIDO sobre el WASM, no estimado a mano.
fn submit_treasury_proposal() -> Weight;
}
/// Weights parametrizados por el `DbWeight` real del runtime.
@@ -121,17 +142,19 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
/// Proof: `System::EventCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
/// Storage: `System::Events` (r:1 w:1)
/// Proof: `System::Events` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
/// Storage: `Governance::VoteQuorumOf` (r:0 w:1)
/// Proof: `Governance::VoteQuorumOf` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`)
/// Storage: `Governance::ProposalBond` (r:0 w:1)
/// Proof: `Governance::ProposalBond` (`max_values`: None, `max_size`: Some(96), added: 2571, mode: `MaxEncodedLen`)
fn submit_proposal() -> Weight {
// Proof size en bytes:
// Medido: `108`
// Estimado: `3722`
// Tiempo minimo de ejecucion: 116_074_000 picosegundos.
Weight::from_parts(122_075_000, 0)
// 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))
.saturating_add(T::DbWeight::get().writes(5_u64))
.saturating_add(T::DbWeight::get().writes(6_u64))
}
/// Storage: `Governance::AdvisorFlags` (r:1 w:0)
/// Proof: `Governance::AdvisorFlags` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`)
@@ -167,6 +190,8 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
/// Proof: `Governance::Proposals` (`max_values`: None, `max_size`: Some(257), added: 2732, mode: `MaxEncodedLen`)
/// Storage: `System::Number` (r:1 w:0)
/// Proof: `System::Number` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
/// Storage: `Governance::VoteQuorumOf` (r:1 w:1)
/// Proof: `Governance::VoteQuorumOf` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`)
/// Storage: `Governance::ProposalBond` (r:1 w:1)
/// Proof: `Governance::ProposalBond` (`max_values`: None, `max_size`: Some(96), added: 2571, mode: `MaxEncodedLen`)
/// Storage: `Balances::TotalIssuance` (r:1 w:0)
@@ -181,13 +206,13 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
/// Proof: `System::Events` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
fn finalize_proposal() -> Weight {
// Proof size en bytes:
// Medido: `615`
// Medido: `701`
// Estimado: `6196`
// Tiempo minimo de ejecucion: 132_890_000 picosegundos.
Weight::from_parts(146_594_000, 0)
// Tiempo minimo de ejecucion: 143_834_000 picosegundos.
Weight::from_parts(147_649_000, 0)
.saturating_add(Weight::from_parts(0, 6196))
.saturating_add(T::DbWeight::get().reads(9_u64))
.saturating_add(T::DbWeight::get().writes(6_u64))
.saturating_add(T::DbWeight::get().reads(10_u64))
.saturating_add(T::DbWeight::get().writes(7_u64))
}
/// Storage: `Governance::CouncilSize` (r:1 w:1)
/// Proof: `Governance::CouncilSize` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
@@ -243,10 +268,10 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
/// Proof: `Governance::CouncilSize` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
/// Storage: `Governance::VetoVotes` (r:6 w:1)
/// Proof: `Governance::VetoVotes` (`max_values`: None, `max_size`: Some(96), added: 2571, mode: `MaxEncodedLen`)
/// Storage: `Governance::ProposalBond` (r:1 w:1)
/// Proof: `Governance::ProposalBond` (`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: `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)
@@ -267,8 +292,8 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
// Proof size en bytes:
// Medido: `1441`
// Estimado: `16416`
// Tiempo minimo de ejecucion: 250_076_000 picosegundos.
Weight::from_parts(266_779_000, 0)
// Tiempo minimo de ejecucion: 250_427_000 picosegundos.
Weight::from_parts(279_612_000, 0)
.saturating_add(Weight::from_parts(0, 16416))
.saturating_add(T::DbWeight::get().reads(21_u64))
.saturating_add(T::DbWeight::get().writes(11_u64))
@@ -387,6 +412,8 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
/// Proof: `Governance::Proposals` (`max_values`: None, `max_size`: Some(257), added: 2732, mode: `MaxEncodedLen`)
/// Storage: `Governance::Disbursed` (r:1 w:1)
/// Proof: `Governance::Disbursed` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`)
/// Storage: `Governance::TreasuryActions` (r:1 w:0)
/// Proof: `Governance::TreasuryActions` (`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:2 w:2)
@@ -401,12 +428,12 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
/// Proof: `System::Events` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
fn treasury_disbursement() -> Weight {
// Proof size en bytes:
// Medido: `495`
// Medido: `615`
// Estimado: `6196`
// Tiempo minimo de ejecucion: 126_964_000 picosegundos.
Weight::from_parts(138_038_000, 0)
// Tiempo minimo de ejecucion: 133_815_000 picosegundos.
Weight::from_parts(144_130_000, 0)
.saturating_add(Weight::from_parts(0, 6196))
.saturating_add(T::DbWeight::get().reads(9_u64))
.saturating_add(T::DbWeight::get().reads(10_u64))
.saturating_add(T::DbWeight::get().writes(5_u64))
}
/// Storage: `Balances::TotalIssuance` (r:1 w:0)
@@ -533,6 +560,8 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
/// Proof: `Governance::UpholdStakeTotal` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`)
/// Storage: `Governance::OverrideQuorum` (r:1 w:1)
/// Proof: `Governance::OverrideQuorum` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`)
/// Storage: `Governance::VoteQuorumOf` (r:1 w:1)
/// Proof: `Governance::VoteQuorumOf` (`max_values`: None, `max_size`: Some(64), added: 2539, 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)
@@ -541,23 +570,53 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
/// Proof: `System::Events` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
fn finalize_override() -> Weight {
// Proof size en bytes:
// Medido: `637`
// Medido: `723`
// Estimado: `3722`
// Tiempo minimo de ejecucion: 73_297_000 picosegundos.
Weight::from_parts(78_037_000, 0)
// Tiempo minimo de ejecucion: 83_908_000 picosegundos.
Weight::from_parts(86_407_000, 0)
.saturating_add(Weight::from_parts(0, 3722))
.saturating_add(T::DbWeight::get().reads(10_u64))
.saturating_add(T::DbWeight::get().writes(5_u64))
.saturating_add(T::DbWeight::get().reads(11_u64))
.saturating_add(T::DbWeight::get().writes(6_u64))
}
/// Storage: `Governance::Proposals` (r:1 w:1)
/// 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)
/// 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`)
/// 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)
/// Proof: `System::EventCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
/// Storage: `System::Events` (r:1 w:1)
/// Proof: `System::Events` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
/// Storage: `Governance::VoteQuorumOf` (r:0 w:1)
/// Proof: `Governance::VoteQuorumOf` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`)
/// Storage: `Governance::TreasuryActions` (r:0 w:1)
/// Proof: `Governance::TreasuryActions` (`max_values`: None, `max_size`: Some(96), added: 2571, mode: `MaxEncodedLen`)
/// Storage: `Governance::ProposalBond` (r:0 w:1)
/// Proof: `Governance::ProposalBond` (`max_values`: None, `max_size`: Some(96), added: 2571, mode: `MaxEncodedLen`)
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))
.saturating_add(T::DbWeight::get().writes(7_u64))
}
}
/// Impl para mocks y tests — `RocksDbWeight` directo, sin necesitar `T`.
impl WeightInfo for () {
fn submit_proposal() -> Weight {
Weight::from_parts(122_075_000, 0)
Weight::from_parts(124_315_000, 0)
.saturating_add(Weight::from_parts(0, 3722))
.saturating_add(RocksDbWeight::get().reads(7_u64))
.saturating_add(RocksDbWeight::get().writes(5_u64))
.saturating_add(RocksDbWeight::get().writes(6_u64))
}
fn cast_vote() -> Weight {
Weight::from_parts(127_723_000, 0)
@@ -566,10 +625,10 @@ impl WeightInfo for () {
.saturating_add(RocksDbWeight::get().writes(6_u64))
}
fn finalize_proposal() -> Weight {
Weight::from_parts(146_594_000, 0)
Weight::from_parts(147_649_000, 0)
.saturating_add(Weight::from_parts(0, 6196))
.saturating_add(RocksDbWeight::get().reads(9_u64))
.saturating_add(RocksDbWeight::get().writes(6_u64))
.saturating_add(RocksDbWeight::get().reads(10_u64))
.saturating_add(RocksDbWeight::get().writes(7_u64))
}
fn add_council_member() -> Weight {
Weight::from_parts(39_278_000, 0)
@@ -584,7 +643,7 @@ impl WeightInfo for () {
.saturating_add(RocksDbWeight::get().writes(4_u64))
}
fn veto_proposal() -> Weight {
Weight::from_parts(266_779_000, 0)
Weight::from_parts(279_612_000, 0)
.saturating_add(Weight::from_parts(0, 16416))
.saturating_add(RocksDbWeight::get().reads(21_u64))
.saturating_add(RocksDbWeight::get().writes(11_u64))
@@ -614,9 +673,9 @@ impl WeightInfo for () {
.saturating_add(RocksDbWeight::get().writes(6_u64))
}
fn treasury_disbursement() -> Weight {
Weight::from_parts(138_038_000, 0)
Weight::from_parts(144_130_000, 0)
.saturating_add(Weight::from_parts(0, 6196))
.saturating_add(RocksDbWeight::get().reads(9_u64))
.saturating_add(RocksDbWeight::get().reads(10_u64))
.saturating_add(RocksDbWeight::get().writes(5_u64))
}
fn treasury_deposit() -> Weight {
@@ -644,10 +703,16 @@ impl WeightInfo for () {
.saturating_add(RocksDbWeight::get().writes(6_u64))
}
fn finalize_override() -> Weight {
Weight::from_parts(78_037_000, 0)
Weight::from_parts(86_407_000, 0)
.saturating_add(Weight::from_parts(0, 3722))
.saturating_add(RocksDbWeight::get().reads(10_u64))
.saturating_add(RocksDbWeight::get().writes(5_u64))
.saturating_add(RocksDbWeight::get().reads(11_u64))
.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))
.saturating_add(RocksDbWeight::get().writes(7_u64))
}
}
@@ -692,6 +757,7 @@ mod guard {
("unlock_vote_funds", <() as WeightInfo>::unlock_vote_funds()),
("uphold_council_veto", <() as WeightInfo>::uphold_council_veto()),
("finalize_override", <() as WeightInfo>::finalize_override()),
("submit_treasury_proposal", <() as WeightInfo>::submit_treasury_proposal()),
];
for (nombre, w) in casos {
assert!(