feat(governance): RV15 — el peso de un voto sale del saldo, y la revision adversaria tumbo la primera version
Cierra L-10. cast_vote y override_council_veto sumaban el stake_weight que
DECLARABA el votante (hueco medido por Palace el 2026-09-06; test en rojo
publicado en d37a278). Ahora:
- El peso no puede superar el OGC LIBRE de la cuenta, y ese OGC queda
congelado (fungible freeze, FreezeReason::GovernanceVote) mientras el voto
pueda cambiar un resultado: hasta end_block en un voto normal y hasta el
cierre de la ventana en un override. El mismo OGC puede respaldar votos en
propuestas distintas (se solapan, como en Polkadot), pero no puede salir de
la cuenta: se cierra votar, transferir y votar otra vez desde otra cuenta.
- Un respaldo POR VOTO (VoteBacking: propuesta, tipo, importe, hasta), no uno
por cuenta. El freeze es siempre el maximo de los votos VIVOS; los vencidos
se descartan solos al votar o desbloquear. MaxActiveVotes = 32 en el runtime.
- unlock_vote_funds(who) lo puede llamar CUALQUIERA, como el unlock de
conviction-voting: quien vota con todo su saldo no puede pagar la comision de
su propio desbloqueo. Libera solo lo que ya no respalda un voto vivo.
- El override tiene ventana: 14 dias desde el veto que ALCANZA el umbral
(antes estaba abierto para siempre). Sin ventana registrada, falla cerrado.
- voting_period_blocks tiene tope: 28 dias (MAX_VOTING_PERIOD_BLOCKS).
- VoteFundsUnlocked va al FINAL del enum de eventos: no desplaza indices SCALE.
- spec_version 14 -> 15; transaction_version sigue en 1.
La revision adversaria (4 lentes independientes sobre la primera version)
encontro tres fallos serios, y los tres eran mios:
1. El lock unico por cuenta (max importe, max bloque) mas un periodo de voto
sin tope congelaba OGC PARA SIEMPRE: un voto de 1 unidad en una propuesta de
periodo u64::MAX arrastraba el voto mayor de la cuenta hasta ese bloque, y ni
el veto lo liberaba. Lo demostraron tres lentes por separado. Cura: respaldo
por voto + tope de periodo.
2. Un lock vencido y no desbloqueado revivia con el siguiente voto pequeno.
3. Votar con todo el saldo dejaba la cuenta sin poder pagar la comision del
unlock, y solo ella podia desbloquearse. Cura: unlock por terceros.
Y seis mutaciones sobrevivian a mi suite, entre ellas congelar el peso del
voto actual en vez del maximo (el propio ataque que el test decia cubrir).
Certificacion: 17 contrafactuales, TODOS en rojo con su pieza quitada
(freeze = peso actual, sin exigir saldo, reducible_balance, total_balance,
unlock en until, sin podar vencidos, ventana desde start_block, ventana 0,
peso 0 con thaw, evento released=0, unlock todo o nada, sin tope de periodo,
unlock solo propio, override hasta end_block, veto sin ventana, sin freeze, y
MaxFreezes=0 en el runtime). Cada test lleva control positivo. Test nuevo con
la configuracion REAL del runtime (preset local_testnet): el voto congela en
pallet-balances, lo congelado no sale, y un tercero lo libera al vencer.
Benchmarks en el PEOR caso real (la revision mostro que median la rama
barata): lista de respaldo casi llena, override que alcanza el umbral, veto de
un council de 7 que devuelve el bond, finalize que devuelve el bond, unlock
con la mitad vencida. Pesos regenerados con frame-omni-bencher (Mac; los
canonicos siguen pendientes del Pi 5, L-6). veto_proposal estaba
subdeclarado ~3x: 36M ps 10r/7w -> 112M 21r/10w, proof 6132 -> 16416.
regen-weights.sh: el blob de bench (runtime-benchmarks) se construye en
target-bench/ DENTRO del repo, nunca en target/release/wbuild, que es de donde
sale un set_code. Lo que debe coincidir con lo desplegado es el commit, no el
blob. Dentro del repo porque substrate-wasm-builder busca el Cargo.lock
subiendo desde el target: fuera no lo encuentra y resuelve dependencias
nuevas (medido).
Fuera de RV15, a RV16 (L-11): el override no es un 4/5 real (el bond que el
veto devuelve y el OGC Aye cuentan en el denominador y en el numerador), el
veto sigue siendo posible tras end_block, y un periodo de voto minimo.
Existian antes de RV15; los demostro la misma revision.
cargo test --workspace: 334 passed, 0 failed, 0 ignored.
cargo test -p origin-chain-governance --features runtime-benchmarks: 79 passed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
d37a278ba6
commit
a694cbd9f0
10 files changed
+1097
-106
No files matched your search
@@ -15,6 +15,7 @@
|
||||
use super::*;
|
||||
use frame_benchmarking::v2::*;
|
||||
use frame_support::traits::fungible::{Inspect, Mutate};
|
||||
use frame_support::traits::Get;
|
||||
use frame_system::RawOrigin;
|
||||
use origin_chain_types::AccountId;
|
||||
|
||||
@@ -52,6 +53,34 @@ fn seed_council<T: Config>(who: &T::AccountId) {
|
||||
CouncilSize::<T>::put(1u32);
|
||||
}
|
||||
|
||||
/// RV15: siembra `n` votos vivos respaldados por `who` (propuestas distintas del
|
||||
/// id de los benchmarks), con `until` dado. Es el peor caso de PoV: la lista de
|
||||
/// respaldo se lee y se reescribe entera.
|
||||
fn seed_backing<T: Config>(who: &T::AccountId, n: u32, until: u64) {
|
||||
for i in 0..n {
|
||||
Pallet::<T>::back_vote_with_funds(
|
||||
who,
|
||||
ProposalId([100u8.wrapping_add(i as u8); 32]),
|
||||
BackingKind::Vote,
|
||||
1_000u128.saturating_add(i as u128),
|
||||
until,
|
||||
)
|
||||
.expect("respaldo sembrado");
|
||||
}
|
||||
}
|
||||
|
||||
/// Bond real de la propuesta `id`, con fondos en la cuenta-bond: la rama cara
|
||||
/// del veto y del finalize es la que lo devuelve.
|
||||
fn seed_bond<T: Config>(id: ProposalId) -> T::AccountId {
|
||||
let proposer: T::AccountId = account("proposer", 0, 0);
|
||||
let _ = T::Currency::mint_into(
|
||||
&Pallet::<T>::proposal_bond_account(),
|
||||
MIN_PROPOSAL_STAKE.saturating_mul(2),
|
||||
);
|
||||
ProposalBond::<T>::insert(id, (proposer.clone(), MIN_PROPOSAL_STAKE));
|
||||
proposer
|
||||
}
|
||||
|
||||
const EM_TITLE: [u8; 32] = [3u8; 32];
|
||||
const EM_BODY: [u8; 32] = [4u8; 32];
|
||||
|
||||
@@ -73,6 +102,14 @@ mod benchmarks {
|
||||
#[benchmark]
|
||||
fn cast_vote() {
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
// RV15: el peso sale del saldo. Peor caso: la cuenta ya respalda
|
||||
// MaxActiveVotes - 1 votos vivos (lista y freeze que leer y reescribir).
|
||||
fund::<T>(&caller);
|
||||
seed_backing::<T>(
|
||||
&caller,
|
||||
T::MaxActiveVotes::get().saturating_sub(1),
|
||||
1_000_000u64,
|
||||
);
|
||||
let id = ProposalId([7u8; 32]);
|
||||
seed_proposal::<T>(id, ProposalStatus::Voting, 1_000_000u64);
|
||||
|
||||
@@ -87,6 +124,8 @@ mod benchmarks {
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
let id = ProposalId([7u8; 32]);
|
||||
seed_proposal::<T>(id, ProposalStatus::Voting, 0u64);
|
||||
// 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());
|
||||
|
||||
@@ -97,6 +136,8 @@ mod benchmarks {
|
||||
Proposals::<T>::get(id).unwrap().status,
|
||||
ProposalStatus::Voting
|
||||
));
|
||||
assert!(!ProposalBond::<T>::contains_key(id));
|
||||
assert_eq!(T::Currency::balance(&proposer), MIN_PROPOSAL_STAKE);
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
@@ -122,15 +163,43 @@ mod benchmarks {
|
||||
|
||||
#[benchmark]
|
||||
fn veto_proposal() {
|
||||
// Rama cara: council LLENO (7), 4 vetos vivos previos, y el del caller es el
|
||||
// 5.º que alcanza el umbral 2/3 → recorre todos los vetos, flipa a Rejected,
|
||||
// abre la ventana de override (RV15) y devuelve el bond.
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
seed_council::<T>(&caller);
|
||||
let id = ProposalId([7u8; 32]);
|
||||
seed_proposal::<T>(id, ProposalStatus::Voting, 1_000_000u64);
|
||||
let proposer = seed_bond::<T>(id);
|
||||
let mut miembros: alloc::vec::Vec<T::AccountId> = (0..MAX_COUNCIL_SIZE - 1)
|
||||
.map(|i| account("council", i, 0))
|
||||
.collect();
|
||||
miembros.push(caller.clone());
|
||||
for m in &miembros {
|
||||
Council::<T>::insert(
|
||||
m,
|
||||
CouncilMember {
|
||||
account: AccountId::new([0u8; 32]),
|
||||
joined_at_block: 0,
|
||||
role: CouncilRole::Member,
|
||||
},
|
||||
);
|
||||
}
|
||||
CouncilSize::<T>::put(MAX_COUNCIL_SIZE);
|
||||
let previos = council_veto_threshold(MAX_COUNCIL_SIZE) - 1;
|
||||
for m in miembros.iter().take(previos as usize) {
|
||||
VetoVotes::<T>::insert(id, m, ());
|
||||
}
|
||||
|
||||
#[extrinsic_call]
|
||||
veto_proposal(RawOrigin::Signed(caller.clone()), id);
|
||||
|
||||
assert!(VetoVotes::<T>::contains_key(id, &caller));
|
||||
assert!(matches!(
|
||||
Proposals::<T>::get(id).unwrap().status,
|
||||
ProposalStatus::Rejected
|
||||
));
|
||||
assert!(OverrideDeadline::<T>::contains_key(id));
|
||||
assert_eq!(T::Currency::balance(&proposer), MIN_PROPOSAL_STAKE);
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
@@ -160,11 +229,24 @@ mod benchmarks {
|
||||
let id = ProposalId([7u8; 32]);
|
||||
seed_proposal::<T>(id, ProposalStatus::Rejected, 0u64);
|
||||
VetoRejected::<T>::insert(id, ());
|
||||
// RV15: ventana abierta, peso respaldado por saldo y la lista de respaldo casi
|
||||
// llena (peor caso). Y rama cara: el override ALCANZA el umbral y aprueba.
|
||||
OverrideDeadline::<T>::insert(id, 1_000_000u64);
|
||||
fund::<T>(&caller);
|
||||
seed_backing::<T>(
|
||||
&caller,
|
||||
T::MaxActiveVotes::get().saturating_sub(1),
|
||||
1_000_000u64,
|
||||
);
|
||||
|
||||
#[extrinsic_call]
|
||||
override_council_veto(RawOrigin::Signed(caller.clone()), id, 100u128);
|
||||
override_council_veto(RawOrigin::Signed(caller.clone()), id, MIN_PROPOSAL_STAKE);
|
||||
|
||||
assert!(OverrideVotes::<T>::contains_key(id, &caller));
|
||||
assert!(matches!(
|
||||
Proposals::<T>::get(id).unwrap().status,
|
||||
ProposalStatus::Approved
|
||||
));
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
@@ -222,5 +304,33 @@ mod benchmarks {
|
||||
assert!(EmergencyEndorsements::<T>::contains_key(content_id, &caller));
|
||||
}
|
||||
|
||||
#[benchmark]
|
||||
fn unlock_vote_funds() {
|
||||
// Peor caso: lista de respaldo LLENA, la mitad vencida (until = 1) y la otra
|
||||
// mitad viva → se poda, se recalcula el máximo y se reescribe la lista.
|
||||
let caller: T::AccountId = whitelisted_caller();
|
||||
let votante: T::AccountId = account("votante", 0, 0);
|
||||
fund::<T>(&votante);
|
||||
let max = T::MaxActiveVotes::get();
|
||||
let vencidos = max / 2;
|
||||
seed_backing::<T>(&votante, vencidos, 1u64);
|
||||
for i in vencidos..max {
|
||||
Pallet::<T>::back_vote_with_funds(
|
||||
&votante,
|
||||
ProposalId([100u8.wrapping_add(i as u8); 32]),
|
||||
BackingKind::Vote,
|
||||
500u128,
|
||||
1_000_000u64,
|
||||
)
|
||||
.expect("respaldo vivo");
|
||||
}
|
||||
frame_system::Pallet::<T>::set_block_number(2u32.into());
|
||||
|
||||
#[extrinsic_call]
|
||||
unlock_vote_funds(RawOrigin::Signed(caller), votante.clone());
|
||||
|
||||
assert_eq!(VoteLocks::<T>::get(&votante).len() as u32, max - vencidos);
|
||||
}
|
||||
|
||||
impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test);
|
||||
}
|
||||
Reference in new issue
Block a user