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:
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);
|
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]
|
#[benchmark]
|
||||||
fn cast_vote() {
|
fn cast_vote() {
|
||||||
let caller: T::AccountId = whitelisted_caller();
|
let caller: T::AccountId = whitelisted_caller();
|
||||||
@@ -129,19 +149,38 @@ 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]);
|
||||||
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).
|
// 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(0) → avanza a 2.
|
// current_block debe ser > end_block → avanza más allá del periodo.
|
||||||
frame_system::Pallet::<T>::set_block_number(2u32.into());
|
frame_system::Pallet::<T>::set_block_number(
|
||||||
|
((MIN_VOTING_PERIOD_BLOCKS + 2) as u32).into(),
|
||||||
|
);
|
||||||
|
|
||||||
#[extrinsic_call]
|
#[extrinsic_call]
|
||||||
finalize_proposal(RawOrigin::Signed(caller), id);
|
finalize_proposal(RawOrigin::Signed(caller), id);
|
||||||
|
|
||||||
assert!(!matches!(
|
// Que de verdad midió la rama del quórum (la revisión de RV17 vio que con
|
||||||
Proposals::<T>::get(id).unwrap().status,
|
// `!Voting` el benchmark seguía verde sin ella).
|
||||||
ProposalStatus::Voting
|
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!(!ProposalBond::<T>::contains_key(id));
|
||||||
assert_eq!(T::Currency::balance(&proposer), MIN_PROPOSAL_STAKE);
|
assert_eq!(T::Currency::balance(&proposer), MIN_PROPOSAL_STAKE);
|
||||||
}
|
}
|
||||||
@@ -277,6 +316,9 @@ mod benchmarks {
|
|||||||
MIN_PROPOSAL_STAKE.saturating_mul(10),
|
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]
|
#[extrinsic_call]
|
||||||
treasury_disbursement(RawOrigin::Root, id, recipient, MIN_PROPOSAL_STAKE);
|
treasury_disbursement(RawOrigin::Root, id, recipient, MIN_PROPOSAL_STAKE);
|
||||||
|
|
||||||
@@ -364,7 +406,12 @@ mod benchmarks {
|
|||||||
VetoRejected::<T>::insert(id, ());
|
VetoRejected::<T>::insert(id, ());
|
||||||
OverrideDeadline::<T>::insert(id, 1u64);
|
OverrideDeadline::<T>::insert(id, 1u64);
|
||||||
OverrideQuorum::<T>::insert(id, T::MinOverrideQuorum::get());
|
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());
|
frame_system::Pallet::<T>::set_block_number(2u32.into());
|
||||||
|
|
||||||
#[extrinsic_call]
|
#[extrinsic_call]
|
||||||
@@ -375,6 +422,7 @@ mod benchmarks {
|
|||||||
ProposalStatus::Approved
|
ProposalStatus::Approved
|
||||||
));
|
));
|
||||||
assert!(!OverrideDeadline::<T>::contains_key(id));
|
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);
|
impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test);
|
||||||
|
|||||||
@@ -146,7 +146,8 @@ pub mod pallet {
|
|||||||
pub enum ProposalStatus {
|
pub enum ProposalStatus {
|
||||||
/// Voting period open canonical (block.number ∈ [start_block, end_block]).
|
/// Voting period open canonical (block.number ∈ [start_block, end_block]).
|
||||||
Voting,
|
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,
|
Approved,
|
||||||
/// Finalized canonical: nay_weight >= yes_weight (rejected canonical).
|
/// Finalized canonical: nay_weight >= yes_weight (rejected canonical).
|
||||||
Rejected,
|
Rejected,
|
||||||
@@ -328,9 +329,34 @@ pub mod pallet {
|
|||||||
/// participación del voto original (Aye + Nay): revertir al council exige al
|
/// 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,
|
/// 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.
|
/// 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>;
|
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
|
/// 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
|
/// determinística sin private key — nadie la controla por wallet, solo la
|
||||||
/// governance vía treasury_disbursement (Root). Honor Inmutable #16: la
|
/// governance vía treasury_disbursement (Root). Honor Inmutable #16: la
|
||||||
@@ -362,6 +388,14 @@ pub mod pallet {
|
|||||||
T::MinOverrideQuorum::get() > MIN_PROPOSAL_STAKE,
|
T::MinOverrideQuorum::get() > MIN_PROPOSAL_STAKE,
|
||||||
"MinOverrideQuorum debe superar MIN_PROPOSAL_STAKE (hallazgo C3)"
|
"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> =
|
pub type OverrideQuorum<T: Config> =
|
||||||
StorageMap<_, Blake2_128Concat, ProposalId, u128, OptionQuery>;
|
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.
|
/// CHN-4 canonical: `Proposals<StorageMap<ProposalId, Proposal>>` canonical.
|
||||||
#[pallet::storage]
|
#[pallet::storage]
|
||||||
#[pallet::getter(fn proposal_of)]
|
#[pallet::getter(fn proposal_of)]
|
||||||
@@ -743,6 +800,26 @@ pub mod pallet {
|
|||||||
/// Quórum que se exigía.
|
/// Quórum que se exigía.
|
||||||
quorum: u128,
|
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.
|
/// Pallet errors canonical.
|
||||||
@@ -827,6 +904,17 @@ pub mod pallet {
|
|||||||
OverrideNotPending,
|
OverrideNotPending,
|
||||||
/// RV16: un voto de la ventana de override tiene que pesar más de 0.
|
/// RV16: un voto de la ventana de override tiene que pesar más de 0.
|
||||||
ZeroWeightWindowVote,
|
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.
|
/// Minimum stake canonical proposal submission per CHN-4 5.3.
|
||||||
@@ -856,60 +944,38 @@ pub mod pallet {
|
|||||||
stake: u128,
|
stake: u128,
|
||||||
) -> DispatchResult {
|
) -> DispatchResult {
|
||||||
let who = ensure_signed(origin)?;
|
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!(
|
ensure!(
|
||||||
voting_period_blocks <= MAX_VOTING_PERIOD_BLOCKS,
|
amount >= T::Currency::minimum_balance().max(1),
|
||||||
Error::<T>::VotingPeriodTooLong
|
Error::<T>::TreasuryAmountBelowMinimum
|
||||||
);
|
);
|
||||||
ensure!(
|
ensure!(
|
||||||
voting_period_blocks >= MIN_VOTING_PERIOD_BLOCKS,
|
recipient != Self::treasury_account() && recipient != Self::proposal_bond_account(),
|
||||||
Error::<T>::VotingPeriodTooShort
|
Error::<T>::TreasuryRecipientInvalid
|
||||||
);
|
);
|
||||||
|
let id =
|
||||||
let proposer_acct = AccountId::new(account_bytes::<T>(&who));
|
Self::do_submit_proposal(who, title_hash, body_hash, voting_period_blocks, stake)?;
|
||||||
let id = crate::derive_proposal_id(&proposer_acct, &title_hash, &body_hash);
|
TreasuryActions::<T>::insert(id, (recipient.clone(), amount));
|
||||||
ensure!(!Proposals::<T>::contains_key(id), Error::<T>::ProposalIdCollision);
|
Self::deposit_event(Event::TreasuryProposalSubmitted { id, recipient, amount });
|
||||||
|
|
||||||
// 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,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1011,7 +1077,13 @@ pub mod pallet {
|
|||||||
Error::<T>::VotingPeriodOpen
|
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
|
ProposalStatus::Approved
|
||||||
} else {
|
} else {
|
||||||
ProposalStatus::Rejected
|
ProposalStatus::Rejected
|
||||||
@@ -1033,6 +1105,13 @@ pub mod pallet {
|
|||||||
yes_weight,
|
yes_weight,
|
||||||
nay_weight,
|
nay_weight,
|
||||||
});
|
});
|
||||||
|
if sin_quorum {
|
||||||
|
Self::deposit_event(Event::ProposalRejectedForQuorum {
|
||||||
|
id: proposal_id,
|
||||||
|
yes_weight,
|
||||||
|
quorum,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -1413,6 +1492,12 @@ pub mod pallet {
|
|||||||
!Disbursed::<T>::contains_key(proposal_id),
|
!Disbursed::<T>::contains_key(proposal_id),
|
||||||
Error::<T>::AlreadyDisbursed
|
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
|
// Marcar ANTES del transfer (defensa en profundidad; los pallets no
|
||||||
// reentran, pero el marker refleja la intención de gasto atómica).
|
// reentran, pero el marker refleja la intención de gasto atómica).
|
||||||
Disbursed::<T>::insert(proposal_id, ());
|
Disbursed::<T>::insert(proposal_id, ());
|
||||||
@@ -1540,10 +1625,15 @@ pub mod pallet {
|
|||||||
/// cualquiera.
|
/// cualquiera.
|
||||||
///
|
///
|
||||||
/// El veto se revierte (la propuesta pasa a `Approved`) solo si:
|
/// El veto se revierte (la propuesta pasa a `Approved`) solo si:
|
||||||
/// 1. la participación (revertir + sostener) alcanza el quórum, que es el
|
/// 1. revertir reúne al menos 4/5 del MAYOR entre el quórum del override (fijado
|
||||||
/// MAYOR entre `MinOverrideQuorum` y la participación del voto original
|
/// en el veto: `MinOverrideQuorum` o la participación del voto original, Aye +
|
||||||
/// (Aye + Nay + Abstain); y
|
/// Nay) y la participación de la ventana (revertir + sostener); y
|
||||||
/// 2. revertir reúne al menos 4/5 de esa participación.
|
/// 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.
|
/// En cualquier otro caso el veto se sostiene. Se decide una sola vez.
|
||||||
#[pallet::call_index(15)]
|
#[pallet::call_index(15)]
|
||||||
@@ -1572,10 +1662,12 @@ pub mod pallet {
|
|||||||
let participation = override_stake.saturating_add(uphold_stake);
|
let participation = override_stake.saturating_add(uphold_stake);
|
||||||
let quorum =
|
let quorum =
|
||||||
OverrideQuorum::<T>::get(proposal_id).ok_or(Error::<T>::OverrideNotPending)?;
|
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.
|
// Se decide una sola vez: sin ventana, no hay override pendiente.
|
||||||
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);
|
||||||
|
|
||||||
// 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
|
||||||
@@ -1585,7 +1677,9 @@ pub mod pallet {
|
|||||||
// de defender el veto. Con max(quorum, participación), cada OGC que sostiene
|
// de defender el veto. Con max(quorum, participación), cada OGC que sostiene
|
||||||
// solo puede subir el listón de revertir, nunca bajarlo.
|
// solo puede subir el listón de revertir, nunca bajarlo.
|
||||||
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 > proposal.nay_weight;
|
||||||
if overridden {
|
if overridden {
|
||||||
proposal.status = ProposalStatus::Approved;
|
proposal.status = ProposalStatus::Approved;
|
||||||
Proposals::<T>::insert(proposal_id, proposal);
|
Proposals::<T>::insert(proposal_id, proposal);
|
||||||
@@ -1771,6 +1865,75 @@ pub mod pallet {
|
|||||||
PROPOSAL_BOND_PALLET_ID.into_account_truncating()
|
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
|
/// Devuelve el bond retenido al proponente cuando la propuesta se decide
|
||||||
/// (finalize o veto). Idempotente: `take` consume la entrada, así que un
|
/// (finalize o veto). Idempotente: `take` consume la entrada, así que un
|
||||||
/// segundo intento es no-op (no doble-refund). Expendable: la cuenta-bond
|
/// 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.
|
/// FIX-6: PalletId de la cuenta-tesorería del mock.
|
||||||
pub const TestTreasuryPalletId: frame_support::PalletId =
|
pub const TestTreasuryPalletId: frame_support::PalletId =
|
||||||
frame_support::PalletId(*b"og/gvtry");
|
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 {
|
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).
|
// igual al bond, el proponente revertía solo con el bond devuelto (revisión de RV16).
|
||||||
type MinOverrideQuorum =
|
type MinOverrideQuorum =
|
||||||
frame_support::traits::ConstU128<{ 2 * crate::pallet::MIN_PROPOSAL_STAKE }>;
|
frame_support::traits::ConstU128<{ 2 * crate::pallet::MIN_PROPOSAL_STAKE }>;
|
||||||
|
type VoteQuorum = VoteQuorum;
|
||||||
type TreasuryPalletId = TestTreasuryPalletId;
|
type TreasuryPalletId = TestTreasuryPalletId;
|
||||||
type WeightInfo = ();
|
type WeightInfo = ();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,6 +49,32 @@ fn submit_default(origin: frame_system::pallet_prelude::OriginFor<Test>) -> Prop
|
|||||||
id
|
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] {
|
fn alice_account_bytes() -> [u8; 32] {
|
||||||
use parity_scale_codec::Encode;
|
use parity_scale_codec::Encode;
|
||||||
let encoded = alice_account().encode();
|
let encoded = alice_account().encode();
|
||||||
@@ -186,6 +212,10 @@ fn scenario_6_finalize_voting_open_rejected_canonical() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn scenario_7_finalize_approved_yes_majority_canonical() {
|
fn scenario_7_finalize_approved_yes_majority_canonical() {
|
||||||
new_test_ext().execute_with(|| {
|
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());
|
let id = submit_default(alice_origin());
|
||||||
|
|
||||||
// Vote Aye 1000 + Nay 500 → Aye majority
|
// Vote Aye 1000 + Nay 500 → Aye majority
|
||||||
@@ -892,11 +922,9 @@ fn treasury_scenario_3_insufficient_balance_rejected_canonical() {
|
|||||||
// Treasury 100 OGC only
|
// Treasury 100 OGC only
|
||||||
assert_ok!(Governance::treasury_deposit(alice_origin(),100));
|
assert_ok!(Governance::treasury_deposit(alice_origin(),100));
|
||||||
|
|
||||||
// Setup approved proposal
|
// RV17: propuesta de tesoro que fija 200 para Bob, aprobada.
|
||||||
let id = submit_default(alice_origin());
|
let id = submit_tesoro(bob_account(), 200);
|
||||||
assert_ok!(Governance::cast_vote(bob_origin(), id, VoteStance::Aye, 1000));
|
aprobar(id);
|
||||||
run_to_block(TRAS_EL_PERIODO);
|
|
||||||
assert_ok!(Governance::finalize_proposal(alice_origin(), id));
|
|
||||||
|
|
||||||
// Try disburse 200 (exceeds 100 balance)
|
// Try disburse 200 (exceeds 100 balance)
|
||||||
assert_noop!(
|
assert_noop!(
|
||||||
@@ -919,11 +947,9 @@ fn treasury_scenario_4_successful_disbursement_canonical() {
|
|||||||
// Treasury 1M OGC
|
// Treasury 1M OGC
|
||||||
assert_ok!(Governance::treasury_deposit(alice_origin(),1_000_000));
|
assert_ok!(Governance::treasury_deposit(alice_origin(),1_000_000));
|
||||||
|
|
||||||
// Setup approved proposal
|
// RV17: propuesta de tesoro que fija 300_000 para Bob, aprobada.
|
||||||
let id = submit_default(alice_origin());
|
let id = submit_tesoro(bob_account(), 300_000);
|
||||||
assert_ok!(Governance::cast_vote(bob_origin(), id, VoteStance::Aye, 1000));
|
aprobar(id);
|
||||||
run_to_block(TRAS_EL_PERIODO);
|
|
||||||
assert_ok!(Governance::finalize_proposal(alice_origin(), id));
|
|
||||||
|
|
||||||
// Disburse 300_000 canonical
|
// Disburse 300_000 canonical
|
||||||
assert_ok!(Governance::treasury_disbursement(
|
assert_ok!(Governance::treasury_disbursement(
|
||||||
@@ -989,10 +1015,8 @@ fn treasury_disbursement_es_single_use_anti_replay() {
|
|||||||
new_test_ext().execute_with(|| {
|
new_test_ext().execute_with(|| {
|
||||||
use frame_support::dispatch::RawOrigin;
|
use frame_support::dispatch::RawOrigin;
|
||||||
assert_ok!(Governance::treasury_deposit(alice_origin(), 1_000_000));
|
assert_ok!(Governance::treasury_deposit(alice_origin(), 1_000_000));
|
||||||
let id = submit_default(alice_origin());
|
let id = submit_tesoro(bob_account(), 300_000);
|
||||||
assert_ok!(Governance::cast_vote(bob_origin(), id, VoteStance::Aye, 1000));
|
aprobar(id);
|
||||||
run_to_block(TRAS_EL_PERIODO);
|
|
||||||
assert_ok!(Governance::finalize_proposal(alice_origin(), id));
|
|
||||||
|
|
||||||
// Primer desembolso: OK.
|
// Primer desembolso: OK.
|
||||||
assert_ok!(Governance::treasury_disbursement(
|
assert_ok!(Governance::treasury_disbursement(
|
||||||
@@ -1138,10 +1162,12 @@ fn council_scenario_7_veto_threshold_2_of_3_canonical() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn scenario_10_lifecycle_walkthrough_canonical() {
|
fn scenario_10_lifecycle_walkthrough_canonical() {
|
||||||
new_test_ext().execute_with(|| {
|
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
|
// Step 1: submit canonical
|
||||||
let id = submit_default(alice_origin());
|
let id = submit_default(alice_origin());
|
||||||
assert!(Proposals::<Test>::get(id).is_some());
|
assert!(Proposals::<Test>::get(id).is_some());
|
||||||
|
|
||||||
// Step 2: vote canonical (3 voters: bob Aye, charlie Aye, alice Nay = 2 Aye vs 1 Nay)
|
// 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(bob_origin(), id, VoteStance::Aye, 1500));
|
||||||
assert_ok!(Governance::cast_vote(charlie_origin(), id, VoteStance::Aye, 2000));
|
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
|
MIN_PROPOSAL_STAKE
|
||||||
));
|
));
|
||||||
// Simula un quórum guardado más bajo: si finalize recalculara, no lo vería.
|
// 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::OverrideQuorum::<Test>::insert(id, MIN_PROPOSAL_STAKE);
|
||||||
|
crate::VoteQuorumOf::<Test>::insert(id, MIN_PROPOSAL_STAKE);
|
||||||
run_to_block(cierre + 1);
|
run_to_block(cierre + 1);
|
||||||
assert_ok!(Governance::finalize_override(alice_origin(), id));
|
assert_ok!(Governance::finalize_override(alice_origin(), id));
|
||||||
assert!(matches!(estado(id), ProposalStatus::Approved));
|
assert!(matches!(estado(id), ProposalStatus::Approved));
|
||||||
assert!(crate::OverrideQuorum::<Test>::get(id).is_none());
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -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
|
// 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
|
// 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).
|
// (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.
|
//! Weights AUTOGENERADOS para `origin_chain_governance` — NO editar a mano.
|
||||||
//!
|
//!
|
||||||
@@ -102,6 +121,8 @@ pub trait WeightInfo {
|
|||||||
fn uphold_council_veto() -> Weight;
|
fn uphold_council_veto() -> Weight;
|
||||||
/// Peso de `finalize_override` — MEDIDO sobre el WASM, no estimado a mano.
|
/// Peso de `finalize_override` — MEDIDO sobre el WASM, no estimado a mano.
|
||||||
fn finalize_override() -> Weight;
|
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.
|
/// 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`)
|
/// Proof: `System::EventCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
||||||
/// Storage: `System::Events` (r:1 w:1)
|
/// Storage: `System::Events` (r:1 w:1)
|
||||||
/// Proof: `System::Events` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
|
/// 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)
|
/// Storage: `Governance::ProposalBond` (r:0 w:1)
|
||||||
/// Proof: `Governance::ProposalBond` (`max_values`: None, `max_size`: Some(96), added: 2571, mode: `MaxEncodedLen`)
|
/// Proof: `Governance::ProposalBond` (`max_values`: None, `max_size`: Some(96), added: 2571, mode: `MaxEncodedLen`)
|
||||||
fn submit_proposal() -> Weight {
|
fn submit_proposal() -> Weight {
|
||||||
// Proof size en bytes:
|
// Proof size en bytes:
|
||||||
// Medido: `108`
|
// Medido: `108`
|
||||||
// Estimado: `3722`
|
// Estimado: `3722`
|
||||||
// Tiempo minimo de ejecucion: 116_074_000 picosegundos.
|
// Tiempo minimo de ejecucion: 120_389_000 picosegundos.
|
||||||
Weight::from_parts(122_075_000, 0)
|
Weight::from_parts(124_315_000, 0)
|
||||||
.saturating_add(Weight::from_parts(0, 3722))
|
.saturating_add(Weight::from_parts(0, 3722))
|
||||||
.saturating_add(T::DbWeight::get().reads(7_u64))
|
.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)
|
/// Storage: `Governance::AdvisorFlags` (r:1 w:0)
|
||||||
/// Proof: `Governance::AdvisorFlags` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`)
|
/// 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`)
|
/// Proof: `Governance::Proposals` (`max_values`: None, `max_size`: Some(257), added: 2732, mode: `MaxEncodedLen`)
|
||||||
/// Storage: `System::Number` (r:1 w:0)
|
/// Storage: `System::Number` (r:1 w:0)
|
||||||
/// Proof: `System::Number` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
/// 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)
|
/// Storage: `Governance::ProposalBond` (r:1 w:1)
|
||||||
/// Proof: `Governance::ProposalBond` (`max_values`: None, `max_size`: Some(96), added: 2571, mode: `MaxEncodedLen`)
|
/// Proof: `Governance::ProposalBond` (`max_values`: None, `max_size`: Some(96), added: 2571, mode: `MaxEncodedLen`)
|
||||||
/// Storage: `Balances::TotalIssuance` (r:1 w:0)
|
/// 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`)
|
/// Proof: `System::Events` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
|
||||||
fn finalize_proposal() -> Weight {
|
fn finalize_proposal() -> Weight {
|
||||||
// Proof size en bytes:
|
// Proof size en bytes:
|
||||||
// Medido: `615`
|
// Medido: `701`
|
||||||
// Estimado: `6196`
|
// Estimado: `6196`
|
||||||
// Tiempo minimo de ejecucion: 132_890_000 picosegundos.
|
// Tiempo minimo de ejecucion: 143_834_000 picosegundos.
|
||||||
Weight::from_parts(146_594_000, 0)
|
Weight::from_parts(147_649_000, 0)
|
||||||
.saturating_add(Weight::from_parts(0, 6196))
|
.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(6_u64))
|
.saturating_add(T::DbWeight::get().writes(7_u64))
|
||||||
}
|
}
|
||||||
/// Storage: `Governance::CouncilSize` (r:1 w:1)
|
/// Storage: `Governance::CouncilSize` (r:1 w:1)
|
||||||
/// Proof: `Governance::CouncilSize` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
/// 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`)
|
/// Proof: `Governance::CouncilSize` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
|
||||||
/// Storage: `Governance::VetoVotes` (r:6 w:1)
|
/// Storage: `Governance::VetoVotes` (r:6 w:1)
|
||||||
/// Proof: `Governance::VetoVotes` (`max_values`: None, `max_size`: Some(96), added: 2571, mode: `MaxEncodedLen`)
|
/// 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)
|
/// Storage: `Balances::TotalIssuance` (r:1 w:0)
|
||||||
/// Proof: `Balances::TotalIssuance` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`)
|
/// 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)
|
/// Storage: `System::Account` (r:2 w:2)
|
||||||
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
|
/// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`)
|
||||||
/// Storage: `System::ExecutionPhase` (r:1 w:0)
|
/// Storage: `System::ExecutionPhase` (r:1 w:0)
|
||||||
@@ -267,8 +292,8 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
|
|||||||
// Proof size en bytes:
|
// Proof size en bytes:
|
||||||
// Medido: `1441`
|
// Medido: `1441`
|
||||||
// Estimado: `16416`
|
// Estimado: `16416`
|
||||||
// Tiempo minimo de ejecucion: 250_076_000 picosegundos.
|
// Tiempo minimo de ejecucion: 250_427_000 picosegundos.
|
||||||
Weight::from_parts(266_779_000, 0)
|
Weight::from_parts(279_612_000, 0)
|
||||||
.saturating_add(Weight::from_parts(0, 16416))
|
.saturating_add(Weight::from_parts(0, 16416))
|
||||||
.saturating_add(T::DbWeight::get().reads(21_u64))
|
.saturating_add(T::DbWeight::get().reads(21_u64))
|
||||||
.saturating_add(T::DbWeight::get().writes(11_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`)
|
/// Proof: `Governance::Proposals` (`max_values`: None, `max_size`: Some(257), added: 2732, mode: `MaxEncodedLen`)
|
||||||
/// Storage: `Governance::Disbursed` (r:1 w:1)
|
/// Storage: `Governance::Disbursed` (r:1 w:1)
|
||||||
/// Proof: `Governance::Disbursed` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`)
|
/// 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)
|
/// Storage: `Balances::TotalIssuance` (r:1 w:0)
|
||||||
/// Proof: `Balances::TotalIssuance` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`)
|
/// Proof: `Balances::TotalIssuance` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`)
|
||||||
/// Storage: `System::Account` (r:2 w:2)
|
/// 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`)
|
/// Proof: `System::Events` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
|
||||||
fn treasury_disbursement() -> Weight {
|
fn treasury_disbursement() -> Weight {
|
||||||
// Proof size en bytes:
|
// Proof size en bytes:
|
||||||
// Medido: `495`
|
// Medido: `615`
|
||||||
// Estimado: `6196`
|
// Estimado: `6196`
|
||||||
// Tiempo minimo de ejecucion: 126_964_000 picosegundos.
|
// Tiempo minimo de ejecucion: 133_815_000 picosegundos.
|
||||||
Weight::from_parts(138_038_000, 0)
|
Weight::from_parts(144_130_000, 0)
|
||||||
.saturating_add(Weight::from_parts(0, 6196))
|
.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))
|
.saturating_add(T::DbWeight::get().writes(5_u64))
|
||||||
}
|
}
|
||||||
/// Storage: `Balances::TotalIssuance` (r:1 w:0)
|
/// 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`)
|
/// Proof: `Governance::UpholdStakeTotal` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`)
|
||||||
/// Storage: `Governance::OverrideQuorum` (r:1 w:1)
|
/// Storage: `Governance::OverrideQuorum` (r:1 w:1)
|
||||||
/// Proof: `Governance::OverrideQuorum` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`)
|
/// 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)
|
/// Storage: `System::ExecutionPhase` (r:1 w:0)
|
||||||
/// Proof: `System::ExecutionPhase` (`max_values`: Some(1), `max_size`: Some(5), added: 500, mode: `MaxEncodedLen`)
|
/// Proof: `System::ExecutionPhase` (`max_values`: Some(1), `max_size`: Some(5), added: 500, mode: `MaxEncodedLen`)
|
||||||
/// Storage: `System::EventCount` (r:1 w:1)
|
/// 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`)
|
/// Proof: `System::Events` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
|
||||||
fn finalize_override() -> Weight {
|
fn finalize_override() -> Weight {
|
||||||
// Proof size en bytes:
|
// Proof size en bytes:
|
||||||
// Medido: `637`
|
// Medido: `723`
|
||||||
// Estimado: `3722`
|
// Estimado: `3722`
|
||||||
// Tiempo minimo de ejecucion: 73_297_000 picosegundos.
|
// Tiempo minimo de ejecucion: 83_908_000 picosegundos.
|
||||||
Weight::from_parts(78_037_000, 0)
|
Weight::from_parts(86_407_000, 0)
|
||||||
.saturating_add(Weight::from_parts(0, 3722))
|
.saturating_add(Weight::from_parts(0, 3722))
|
||||||
.saturating_add(T::DbWeight::get().reads(10_u64))
|
.saturating_add(T::DbWeight::get().reads(11_u64))
|
||||||
.saturating_add(T::DbWeight::get().writes(5_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 para mocks y tests — `RocksDbWeight` directo, sin necesitar `T`.
|
||||||
impl WeightInfo for () {
|
impl WeightInfo for () {
|
||||||
fn submit_proposal() -> Weight {
|
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(Weight::from_parts(0, 3722))
|
||||||
.saturating_add(RocksDbWeight::get().reads(7_u64))
|
.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 {
|
fn cast_vote() -> Weight {
|
||||||
Weight::from_parts(127_723_000, 0)
|
Weight::from_parts(127_723_000, 0)
|
||||||
@@ -566,10 +625,10 @@ impl WeightInfo for () {
|
|||||||
.saturating_add(RocksDbWeight::get().writes(6_u64))
|
.saturating_add(RocksDbWeight::get().writes(6_u64))
|
||||||
}
|
}
|
||||||
fn finalize_proposal() -> Weight {
|
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(Weight::from_parts(0, 6196))
|
||||||
.saturating_add(RocksDbWeight::get().reads(9_u64))
|
.saturating_add(RocksDbWeight::get().reads(10_u64))
|
||||||
.saturating_add(RocksDbWeight::get().writes(6_u64))
|
.saturating_add(RocksDbWeight::get().writes(7_u64))
|
||||||
}
|
}
|
||||||
fn add_council_member() -> Weight {
|
fn add_council_member() -> Weight {
|
||||||
Weight::from_parts(39_278_000, 0)
|
Weight::from_parts(39_278_000, 0)
|
||||||
@@ -584,7 +643,7 @@ impl WeightInfo for () {
|
|||||||
.saturating_add(RocksDbWeight::get().writes(4_u64))
|
.saturating_add(RocksDbWeight::get().writes(4_u64))
|
||||||
}
|
}
|
||||||
fn veto_proposal() -> Weight {
|
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(Weight::from_parts(0, 16416))
|
||||||
.saturating_add(RocksDbWeight::get().reads(21_u64))
|
.saturating_add(RocksDbWeight::get().reads(21_u64))
|
||||||
.saturating_add(RocksDbWeight::get().writes(11_u64))
|
.saturating_add(RocksDbWeight::get().writes(11_u64))
|
||||||
@@ -614,9 +673,9 @@ impl WeightInfo for () {
|
|||||||
.saturating_add(RocksDbWeight::get().writes(6_u64))
|
.saturating_add(RocksDbWeight::get().writes(6_u64))
|
||||||
}
|
}
|
||||||
fn treasury_disbursement() -> Weight {
|
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(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))
|
.saturating_add(RocksDbWeight::get().writes(5_u64))
|
||||||
}
|
}
|
||||||
fn treasury_deposit() -> Weight {
|
fn treasury_deposit() -> Weight {
|
||||||
@@ -644,10 +703,16 @@ impl WeightInfo for () {
|
|||||||
.saturating_add(RocksDbWeight::get().writes(6_u64))
|
.saturating_add(RocksDbWeight::get().writes(6_u64))
|
||||||
}
|
}
|
||||||
fn finalize_override() -> Weight {
|
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(Weight::from_parts(0, 3722))
|
||||||
.saturating_add(RocksDbWeight::get().reads(10_u64))
|
.saturating_add(RocksDbWeight::get().reads(11_u64))
|
||||||
.saturating_add(RocksDbWeight::get().writes(5_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()),
|
("unlock_vote_funds", <() as WeightInfo>::unlock_vote_funds()),
|
||||||
("uphold_council_veto", <() as WeightInfo>::uphold_council_veto()),
|
("uphold_council_veto", <() as WeightInfo>::uphold_council_veto()),
|
||||||
("finalize_override", <() as WeightInfo>::finalize_override()),
|
("finalize_override", <() as WeightInfo>::finalize_override()),
|
||||||
|
("submit_treasury_proposal", <() as WeightInfo>::submit_treasury_proposal()),
|
||||||
];
|
];
|
||||||
for (nombre, w) in casos {
|
for (nombre, w) in casos {
|
||||||
assert!(
|
assert!(
|
||||||
|
|||||||
@@ -54,7 +54,9 @@ mod service;
|
|||||||
// las Command Line Tools declara la arquitectura `arm64e.x1`, que ld no reconoce.
|
// las Command Line Tools declara la arquitectura `arm64e.x1`, que ld no reconoce.
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
use origin_chain_runtime::{genesis_config_presets, Block, IMPL_NAME, RUNTIME_VERSION, SPEC_NAME};
|
use origin_chain_runtime::{
|
||||||
|
genesis_config_presets, opaque::Block, IMPL_NAME, RUNTIME_VERSION, SPEC_NAME,
|
||||||
|
};
|
||||||
use sc_cli::SubstrateCli;
|
use sc_cli::SubstrateCli;
|
||||||
use sp_genesis_builder::PresetId;
|
use sp_genesis_builder::PresetId;
|
||||||
|
|
||||||
|
|||||||
@@ -12,16 +12,18 @@ use crate::service::FullClient;
|
|||||||
|
|
||||||
// El client es CONCRETO (`FullClient`), no un `C` genérico como en el template.
|
// El client es CONCRETO (`FullClient`), no un `C` genérico como en el template.
|
||||||
//
|
//
|
||||||
// El template puede ser genérico porque su node trabaja sobre `opaque::Block`, que no
|
// Hasta el 2026-10-28 el node trabajaba sobre el `Block` completo del runtime, y ese tipo
|
||||||
// menciona la cuenta. El nuestro trabaja sobre el `Block` completo del runtime (sobre
|
// lleva dentro `ChainAccountId`, que es una PROYECCIÓN:
|
||||||
// él están los `impl_runtime_apis!`), y ese tipo lleva dentro `ChainAccountId`, que es
|
// `<<MultiSignature as Verify>::Signer as IdentifyAccount>::AccountId`. Dentro de un
|
||||||
// una PROYECCIÓN: `<<MultiSignature as Verify>::Signer as IdentifyAccount>::AccountId`.
|
// where-clause genérico rustc no la normalizaba a `AccountId32`: el bound
|
||||||
// Dentro de un where-clause genérico rustc no la normaliza a `AccountId32`: el bound
|
// `C: ProvideRuntimeApi<Block>` quedaba escrito con una forma del tipo y `System` lo pedía
|
||||||
// `C: ProvideRuntimeApi<Block>` queda escrito con una forma del tipo y `System` lo
|
// con la otra, y el compilador informaba de que un bound declarado en la propia firma «no
|
||||||
// pide con la otra, y el compilador informa de que un bound declarado en la propia
|
// se satisface» (6 errores, medido el 2026-09-23). Con el tipo concreto los bounds se
|
||||||
// firma «no se satisface» (6 errores, medido el 2026-09-23). Con el tipo concreto no
|
// comprueban sobre el client real, que es además el único que este node construye.
|
||||||
// hay nada que normalizar en el param-env y los bounds se comprueban sobre el client
|
//
|
||||||
// real — que es además el único que este node construye.
|
// Desde el 2026-10-28 el node trabaja sobre `opaque::Block` (ver
|
||||||
|
// `origin_chain_runtime::opaque`: el node decodificaba las llamadas con tipos nativos y
|
||||||
|
// rechazaba las que añadía un `set_code`). El client sigue concreto.
|
||||||
|
|
||||||
/// Dependencias completas para construir los RPC del node.
|
/// Dependencias completas para construir los RPC del node.
|
||||||
pub struct FullDeps<P> {
|
pub struct FullDeps<P> {
|
||||||
|
|||||||
@@ -12,11 +12,12 @@ use sc_consensus_grandpa::SharedVoterState;
|
|||||||
use sc_service::{error::Error as ServiceError, Configuration, TaskManager, WarpSyncConfig};
|
use sc_service::{error::Error as ServiceError, Configuration, TaskManager, WarpSyncConfig};
|
||||||
use sc_telemetry::{Telemetry, TelemetryWorker};
|
use sc_telemetry::{Telemetry, TelemetryWorker};
|
||||||
use sc_transaction_pool_api::OffchainTransactionPoolFactory;
|
use sc_transaction_pool_api::OffchainTransactionPoolFactory;
|
||||||
// El runtime implementa `impl_runtime_apis!` sobre el `Block` root (con
|
// El node trabaja sobre `opaque::Block`: los extrinsics son bytes y sólo el WASM los
|
||||||
// UncheckedExtrinsic), no sobre un opaque block separado → el node usa ese mismo
|
// decodifica. Con el `Block` completo del runtime (hasta el 2026-10-28) el node decodificaba
|
||||||
// tipo para que `RuntimeApi` matchee. (El template solochain usa `opaque::Block`
|
// con sus tipos nativos y rechazaba toda llamada que un `set_code` hubiera añadido (ver
|
||||||
// porque su runtime expone las APIs así; el nuestro las expone sobre root Block.)
|
// `origin_chain_runtime::opaque`). Las runtime APIs que genera `impl_runtime_apis!` son
|
||||||
use origin_chain_runtime::{Block, RuntimeApi};
|
// genéricas en el bloque y cruzan la frontera en SCALE, así que valen igual sobre éste.
|
||||||
|
use origin_chain_runtime::{opaque::Block, RuntimeApi};
|
||||||
use sp_consensus_aura::sr25519::AuthorityPair as AuraPair;
|
use sp_consensus_aura::sr25519::AuthorityPair as AuraPair;
|
||||||
use std::{sync::Arc, time::Duration};
|
use std::{sync::Arc, time::Duration};
|
||||||
|
|
||||||
|
|||||||
@@ -13,10 +13,15 @@
|
|||||||
//! firmar <llamada> <nonce> <genesis_hex> [spec tx] extrinsic firmado por //Alice, en hex
|
//! firmar <llamada> <nonce> <genesis_hex> [spec tx] extrinsic firmado por //Alice, en hex
|
||||||
//! llamadas: remark · finalize_override (propuesta 0x09…09) · uphold0 (peso 0)
|
//! llamadas: remark · finalize_override (propuesta 0x09…09) · uphold0 (peso 0)
|
||||||
//! set_code:<ruta.wasm> (sudo de //Alice, ensayo de upgrade sin fork)
|
//! set_code:<ruta.wasm> (sudo de //Alice, ensayo de upgrade sin fork)
|
||||||
|
//! tesoro:<monto> (RV17: propuesta de tesoro de //Alice que paga <monto> plancks
|
||||||
|
//! a //Bob; 14 días y el bond mínimo) · pagar:<id_hex>:<monto> (sudo de
|
||||||
|
//! treasury_disbursement a //Bob) · votar:<id_hex>:<peso> (Aye de //Alice)
|
||||||
|
//! · depositar:<monto> (treasury_deposit de //Alice al vault)
|
||||||
|
//! · finalizar:<id_hex> (finalize_proposal)
|
||||||
//! spec y tx: los que da `state_getRuntimeVersion`. Por defecto, los de `VERSION`
|
//! spec y tx: los que da `state_getRuntimeVersion`. Por defecto, los de `VERSION`
|
||||||
//! compilado. Tras un `set_code` hay que pasarlos: el wallet firma con la versión
|
//! compilado. Tras un `set_code` hay que pasarlos: el wallet firma con la versión
|
||||||
//! nueva que anuncia el nodo, no con la que se compiló aquí.
|
//! nueva que anuncia el nodo, no con la que se compiló aquí.
|
||||||
//! eventos <hex de System::Events> decodifica los eventos de un bloque
|
//! eventos <hex de System::Events> decodifica los eventos de un bloque, con su fase
|
||||||
//! cuenta "<frase BIP39>" [//derivación] llave sr25519 y dirección SS58 (42) que verá un wallet
|
//! cuenta "<frase BIP39>" [//derivación] llave sr25519 y dirección SS58 (42) que verá un wallet
|
||||||
//! Substrate (substrate-bip39 → mini-secret → sr25519): vectores para la wallet Origin!
|
//! Substrate (substrate-bip39 → mini-secret → sr25519): vectores para la wallet Origin!
|
||||||
//! verificar <hex_extrinsic> <genesis_hex> pasa la firma por el check del runtime nativo
|
//! verificar <hex_extrinsic> <genesis_hex> pasa la firma por el check del runtime nativo
|
||||||
@@ -44,6 +49,10 @@ fn hex(arg: &str) -> Vec<u8> {
|
|||||||
sp_core::bytes::from_hex(arg).unwrap_or_else(|e| panic!("hex inválido {arg}: {e:?}"))
|
sp_core::bytes::from_hex(arg).unwrap_or_else(|e| panic!("hex inválido {arg}: {e:?}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn bob() -> sp_runtime::AccountId32 {
|
||||||
|
sr25519::Pair::from_string("//Bob", None).expect("//Bob").public().into()
|
||||||
|
}
|
||||||
|
|
||||||
fn llamada(nombre: &str) -> RuntimeCall {
|
fn llamada(nombre: &str) -> RuntimeCall {
|
||||||
let id = origin_chain_governance::ProposalId([9u8; 32]);
|
let id = origin_chain_governance::ProposalId([9u8; 32]);
|
||||||
match nombre {
|
match nombre {
|
||||||
@@ -59,6 +68,53 @@ fn llamada(nombre: &str) -> RuntimeCall {
|
|||||||
proposal_id: id,
|
proposal_id: id,
|
||||||
stake_weight: 0,
|
stake_weight: 0,
|
||||||
}),
|
}),
|
||||||
|
otra if otra.starts_with("tesoro:") => {
|
||||||
|
let amount: u128 = otra["tesoro:".len()..].parse().expect("tesoro:<monto en plancks>");
|
||||||
|
RuntimeCall::Governance(origin_chain_governance::Call::submit_treasury_proposal {
|
||||||
|
title_hash: sp_core::blake2_256(b"e2e-rv17/titulo"),
|
||||||
|
body_hash: sp_core::blake2_256(b"e2e-rv17/cuerpo"),
|
||||||
|
voting_period_blocks: origin_chain_governance::MIN_VOTING_PERIOD_BLOCKS,
|
||||||
|
stake: origin_chain_governance::MIN_PROPOSAL_STAKE,
|
||||||
|
recipient: bob(),
|
||||||
|
amount,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
otra if otra.starts_with("votar:") => {
|
||||||
|
let (id, peso) =
|
||||||
|
otra["votar:".len()..].split_once(':').expect("votar:<id_hex>:<peso>");
|
||||||
|
let id: [u8; 32] = hex(id).try_into().expect("el id son 32 bytes");
|
||||||
|
RuntimeCall::Governance(origin_chain_governance::Call::cast_vote {
|
||||||
|
proposal_id: origin_chain_governance::ProposalId(id),
|
||||||
|
stance: origin_chain_types::VoteStance::Aye,
|
||||||
|
stake_weight: peso.parse().expect("peso en plancks"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
otra if otra.starts_with("finalizar:") => {
|
||||||
|
let id: [u8; 32] =
|
||||||
|
hex(&otra["finalizar:".len()..]).try_into().expect("el id son 32 bytes");
|
||||||
|
RuntimeCall::Governance(origin_chain_governance::Call::finalize_proposal {
|
||||||
|
proposal_id: origin_chain_governance::ProposalId(id),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
otra if otra.starts_with("depositar:") => {
|
||||||
|
RuntimeCall::Governance(origin_chain_governance::Call::treasury_deposit {
|
||||||
|
amount: otra["depositar:".len()..].parse().expect("depositar:<monto en plancks>"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
otra if otra.starts_with("pagar:") => {
|
||||||
|
let (id, amount) =
|
||||||
|
otra["pagar:".len()..].split_once(':').expect("pagar:<id_hex>:<monto>");
|
||||||
|
let id: [u8; 32] = hex(id).try_into().expect("el id son 32 bytes");
|
||||||
|
RuntimeCall::Sudo(pallet_sudo::Call::sudo {
|
||||||
|
call: Box::new(RuntimeCall::Governance(
|
||||||
|
origin_chain_governance::Call::treasury_disbursement {
|
||||||
|
proposal_id: origin_chain_governance::ProposalId(id),
|
||||||
|
recipient: bob(),
|
||||||
|
amount: amount.parse().expect("monto en plancks"),
|
||||||
|
},
|
||||||
|
)),
|
||||||
|
})
|
||||||
|
}
|
||||||
otra => match otra.strip_prefix("set_code:") {
|
otra => match otra.strip_prefix("set_code:") {
|
||||||
Some(ruta) => {
|
Some(ruta) => {
|
||||||
let code = std::fs::read(ruta).unwrap_or_else(|e| panic!("{ruta}: {e}"));
|
let code = std::fs::read(ruta).unwrap_or_else(|e| panic!("{ruta}: {e}"));
|
||||||
@@ -67,7 +123,9 @@ fn llamada(nombre: &str) -> RuntimeCall {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
None => panic!(
|
None => panic!(
|
||||||
"llamada desconocida: {otra} (remark · finalize_override · uphold0 · set_code:<ruta>)"
|
"llamada desconocida: {otra} (remark · finalize_override · uphold0 · \
|
||||||
|
set_code:<ruta> · tesoro:<monto> · pagar:<id_hex>:<monto> · \
|
||||||
|
votar:<id_hex>:<peso> · depositar:<monto> · finalizar:<id_hex>)"
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -122,8 +180,9 @@ fn main() {
|
|||||||
let bytes = hex(&args[2]);
|
let bytes = hex(&args[2]);
|
||||||
let eventos = Vec::<EventRecord<RuntimeEvent, H256>>::decode(&mut &bytes[..])
|
let eventos = Vec::<EventRecord<RuntimeEvent, H256>>::decode(&mut &bytes[..])
|
||||||
.expect("System::Events");
|
.expect("System::Events");
|
||||||
|
// La fase va delante: dice a qué extrinsic pertenece cada evento.
|
||||||
for e in eventos {
|
for e in eventos {
|
||||||
println!("{:?}", e.event);
|
println!("{:?} · {:?}", e.phase, e.event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"cuenta" => {
|
"cuenta" => {
|
||||||
|
|||||||
@@ -547,6 +547,29 @@ parameter_types! {
|
|||||||
frame_support::PalletId(*b"og/gvtry");
|
frame_support::PalletId(*b"og/gvtry");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// RV17: el «10% quorum» de PILAR_4 §6.6, sobre la emisión VIGENTE, redondeado hacia arriba.
|
||||||
|
/// Nunca baja del bond de una propuesta: con menos, el proponente aprobaría solo su propuesta
|
||||||
|
/// (o revertiría solo un veto) con un voto del tamaño de su fianza.
|
||||||
|
///
|
||||||
|
/// Hasta RV17 eran 100M OGC fijos, el 10 % de 1B. Dos razones para medirlo sobre la emisión:
|
||||||
|
/// - las comisiones se queman al 100 % (`OU = ()`) y la emisión sólo transfiere desde el pool,
|
||||||
|
/// así que el suministro BAJA, y 100M acabaría siendo más que el 10 %;
|
||||||
|
/// - en una cadena con otro génesis el número fijo no significa nada. En la testnet de la casa
|
||||||
|
/// (génesis dev: 100M emitidos, todos de Alice) ninguna propuesta ordinaria podía llegar
|
||||||
|
/// nunca a 100M de Aye (revisión adversarial de RV17).
|
||||||
|
///
|
||||||
|
/// Mainnet (1B) da lo mismo que antes, 100M.
|
||||||
|
pub struct DiezPorCientoDeLaEmision;
|
||||||
|
impl frame_support::traits::Get<u128> for DiezPorCientoDeLaEmision {
|
||||||
|
fn get() -> u128 {
|
||||||
|
use frame_support::traits::fungible::Inspect;
|
||||||
|
let emision: u128 = <crate::Balances as Inspect<ChainAccountId>>::total_issuance();
|
||||||
|
emision
|
||||||
|
.div_ceil(10)
|
||||||
|
.max(origin_chain_governance::MIN_PROPOSAL_STAKE + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl origin_chain_governance::Config for Runtime {
|
impl origin_chain_governance::Config for Runtime {
|
||||||
type RuntimeEvent = RuntimeEvent;
|
type RuntimeEvent = RuntimeEvent;
|
||||||
type Currency = crate::Balances;
|
type Currency = crate::Balances;
|
||||||
@@ -555,11 +578,13 @@ impl origin_chain_governance::Config for Runtime {
|
|||||||
type RuntimeFreezeReason = RuntimeFreezeReason;
|
type RuntimeFreezeReason = RuntimeFreezeReason;
|
||||||
// RV15: votos VIVOS simultáneos por cuenta (los vencidos se descartan solos).
|
// RV15: votos VIVOS simultáneos por cuenta (los vencidos se descartan solos).
|
||||||
type MaxActiveVotes = frame_support::traits::ConstU32<32>;
|
type MaxActiveVotes = frame_support::traits::ConstU32<32>;
|
||||||
// RV16: participación mínima para revertir un veto del council: 100M OGC, el 10%
|
// RV16 y RV17: los dos quórums son el «10% quorum» de PILAR_4 §6.6, sobre la
|
||||||
// del suministro que ratifica PILAR_4 §6.6 como quórum («10% quorum»). Es un
|
// emisión VIGENTE (ver `DiezPorCientoDeLaEmision`). El pallet los lee en el veto y al
|
||||||
// parámetro de la constitución del DAO y lo fija David; mientras, este. Lo pinna
|
// crear la propuesta, y los guarda. Son parámetro de la constitución del DAO y los
|
||||||
// `quorum_de_override_del_runtime` (genesis_config_presets.rs).
|
// fija David; mientras, éstos. Los pinna `quorums_del_runtime`
|
||||||
type MinOverrideQuorum = frame_support::traits::ConstU128<{ 100_000_000 * OGC }>;
|
// (genesis_config_presets.rs).
|
||||||
|
type MinOverrideQuorum = DiezPorCientoDeLaEmision;
|
||||||
|
type VoteQuorum = DiezPorCientoDeLaEmision;
|
||||||
type TreasuryPalletId = GovernanceTreasuryPalletId;
|
type TreasuryPalletId = GovernanceTreasuryPalletId;
|
||||||
// Weights INTERIM hand-derived DB-aware (ver weights.rs). Regenerable en HW.
|
// Weights INTERIM hand-derived DB-aware (ver weights.rs). Regenerable en HW.
|
||||||
type WeightInfo = origin_chain_governance::weights::SubstrateWeight<Runtime>;
|
type WeightInfo = origin_chain_governance::weights::SubstrateWeight<Runtime>;
|
||||||
|
|||||||
@@ -677,21 +677,59 @@ mod tests {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// RV16: el quórum de override del runtime es el 10% del suministro que
|
/// RV16 y RV17: los dos quórums del runtime son el 10 % de la emisión VIGENTE, redondeado
|
||||||
/// ratifica PILAR_4 §6.6 (100M OGC). Sin este test, un typo como `{ 100_000_000 }`
|
/// hacia arriba y nunca por debajo del bond. Se miden sobre el estado que construye cada
|
||||||
/// sin `* OGC` (10^-10 OGC) o `ConstU128<0>` pasaba toda la suite (revisión de
|
/// preset, no sobre una constante. Sin este test pasaba toda la suite cualquiera de estos
|
||||||
/// RV16). `integrity_test` exige además que supere el bond de una propuesta.
|
/// errores: un typo como `{ 100_000_000 }` sin `* OGC`, `ConstU128<0>` (revisión de
|
||||||
|
/// RV16), dividir entre 100 u olvidar el piso.
|
||||||
#[test]
|
#[test]
|
||||||
fn quorum_de_override_del_runtime() {
|
fn quorums_del_runtime() {
|
||||||
use frame_support::traits::Get;
|
use frame_support::traits::{fungible::Inspect, Get};
|
||||||
let quorum: u128 =
|
type Gob = crate::Runtime;
|
||||||
<<crate::Runtime as origin_chain_governance::Config>::MinOverrideQuorum as Get<u128>>::get();
|
let voto = || <<Gob as origin_chain_governance::Config>::VoteQuorum as Get<u128>>::get();
|
||||||
assert_eq!(quorum, 100_000_000 * crate::configs::OGC);
|
let veto =
|
||||||
assert_eq!(
|
|| <<Gob as origin_chain_governance::Config>::MinOverrideQuorum as Get<u128>>::get();
|
||||||
quorum,
|
let ogc = crate::configs::OGC;
|
||||||
crate::configs::OGC * 1_000_000_000 / 10,
|
// pallet-revive acuña un depósito existencial en su cuenta al construir el génesis
|
||||||
"10% del suministro"
|
// (`mint_into(account_id, minimum_balance())`): la emisión es la suma de las
|
||||||
);
|
// asignaciones MÁS ese ED.
|
||||||
assert!(quorum > origin_chain_governance::MIN_PROPOSAL_STAKE);
|
let ed = crate::configs::EXISTENTIAL_DEPOSIT;
|
||||||
|
|
||||||
|
// Sin estado (así corre `integrity_test`): el piso, que supera el bond.
|
||||||
|
sp_io::TestExternalities::new_empty().execute_with(|| {
|
||||||
|
assert_eq!(voto(), origin_chain_governance::MIN_PROPOSAL_STAKE + 1);
|
||||||
|
assert_eq!(veto(), voto());
|
||||||
|
// «El 10 %» se redondea hacia ARRIBA: con 10·k + 1 de emisión, k no llega al
|
||||||
|
// 10 %. Las emisiones de los presets son múltiplos de 10 y no lo verían.
|
||||||
|
let k = 10_000_000 * ogc;
|
||||||
|
pallet_balances::TotalIssuance::<crate::Runtime>::put(10 * k + 1);
|
||||||
|
assert_eq!(voto(), k + 1);
|
||||||
|
assert_eq!(veto(), k + 1);
|
||||||
|
});
|
||||||
|
for (preset, emision, quorum) in [
|
||||||
|
(
|
||||||
|
PRESET_MAINNET_GENESIS,
|
||||||
|
Some(1_000_000_000 * ogc + ed),
|
||||||
|
Some(100_000_000 * ogc + ed / 10),
|
||||||
|
),
|
||||||
|
(PRESET_DEVELOPMENT, Some(100_000_000 * ogc + ed), Some(10_000_000 * ogc + ed / 10)),
|
||||||
|
(PRESET_LOCAL_TESTNET, None, None),
|
||||||
|
] {
|
||||||
|
let json = get_preset(&PresetId::from(preset)).expect("preset");
|
||||||
|
sp_io::TestExternalities::new_empty().execute_with(|| {
|
||||||
|
frame_support::genesis_builder_helper::build_state::<RuntimeGenesisConfig>(json)
|
||||||
|
.expect("el preset construye estado");
|
||||||
|
let total = <crate::Balances as Inspect<ChainAccountId>>::total_issuance();
|
||||||
|
if let Some(e) = emision {
|
||||||
|
assert_eq!(total, e, "{preset}: emisión");
|
||||||
|
}
|
||||||
|
assert_eq!(voto(), total.div_ceil(10), "{preset}: 10 % de la emisión");
|
||||||
|
if let Some(q) = quorum {
|
||||||
|
assert_eq!(voto(), q, "{preset}");
|
||||||
|
}
|
||||||
|
assert_eq!(veto(), voto(), "{preset}: los dos quórums son el mismo 10 %");
|
||||||
|
assert!(voto() > origin_chain_governance::MIN_PROPOSAL_STAKE);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -373,6 +373,19 @@ pub const IMPL_NAME: &str = borrowed_str(&VERSION.impl_name);
|
|||||||
/// de cada `weights.rs`). Sustituyen a los interinos medidos en el Mac. No cambia ninguna
|
/// de cada `weights.rs`). Sustituyen a los interinos medidos en el Mac. No cambia ninguna
|
||||||
/// 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`.
|
||||||
|
///
|
||||||
|
/// **v18 (2026-10-28), RV17:**
|
||||||
|
/// - el voto ordinario exige un quórum de Aye. Es el 10 % de PILAR_4 §6.6 sobre la emisión
|
||||||
|
/// vigente, se fija al crear la propuesta (`VoteQuorumOf`) y lo tiene que alcanzar también
|
||||||
|
/// un override del veto;
|
||||||
|
/// - el quórum mínimo del override pasa a ser el mismo 10 % (antes, 100M fijos);
|
||||||
|
/// - el tesoro sólo paga la acción que la propuesta fijó al crearse (`submit_treasury_proposal`,
|
||||||
|
/// storage `TreasuryActions`).
|
||||||
|
///
|
||||||
|
/// No cambia la codificación de ninguna llamada existente (transaction_version igual) ni migra
|
||||||
|
/// storage: lo nuevo es aditivo, y una propuesta anterior sin quórum guardado se decide como
|
||||||
|
/// se abrió. La llamada nueva exige un node con `opaque::Block` (ver `opaque.rs`): un node
|
||||||
|
/// que decodifica con tipos nativos la rechaza en el pool.
|
||||||
pub const RUNTIME_VERSION: u32 = VERSION.spec_version;
|
pub const RUNTIME_VERSION: u32 = VERSION.spec_version;
|
||||||
|
|
||||||
/// Canonical authoring version per Substrate semver canonical.
|
/// Canonical authoring version per Substrate semver canonical.
|
||||||
@@ -1025,7 +1038,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: 17,
|
spec_version: 18,
|
||||||
impl_version: 1,
|
impl_version: 1,
|
||||||
apis: RUNTIME_API_VERSIONS,
|
apis: RUNTIME_API_VERSIONS,
|
||||||
transaction_version: 1,
|
transaction_version: 1,
|
||||||
@@ -1186,7 +1199,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, 17);
|
assert_eq!(RUNTIME_VERSION, 18);
|
||||||
assert_eq!(TRANSACTION_VERSION, 1);
|
assert_eq!(TRANSACTION_VERSION, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,24 @@
|
|||||||
//! (puede correr cualquier runtime WASM compatible) + runtime sovereignty (puede
|
//! (puede correr cualquier runtime WASM compatible) + runtime sovereignty (puede
|
||||||
//! evolucionar pallets sin breakar node binaries).
|
//! evolucionar pallets sin breakar node binaries).
|
||||||
|
|
||||||
use sp_runtime::impl_opaque_keys;
|
use sp_runtime::{generic, impl_opaque_keys};
|
||||||
|
|
||||||
|
/// El extrinsic que ve el NODO: bytes sin interpretar. Sólo el WASM los decodifica.
|
||||||
|
///
|
||||||
|
/// Hasta el 2026-10-28 el node trabajaba sobre el `Block` completo del runtime y decodificaba
|
||||||
|
/// cada extrinsic con los tipos NATIVOS con que se compiló. Un `set_code` que añadía una
|
||||||
|
/// llamada no servía: el pool del node rechazaba la llamada nueva («Could not decode
|
||||||
|
/// `RuntimeCall::Governance.0`: variant doesn't exist») antes de que llegara al runtime.
|
||||||
|
/// Además, un node viejo no podía importar un bloque que la llevara. Se midió en el ensayo de
|
||||||
|
/// RV17, firmando `submit_treasury_proposal` contra un node de spec 16 subido a 18.
|
||||||
|
///
|
||||||
|
/// Los bytes son los mismos: el `UncheckedExtrinsic` ya va prefijado por su largo, que es lo
|
||||||
|
/// que lee `OpaqueExtrinsic`. Los bloques y la base de datos no cambian.
|
||||||
|
pub use sp_runtime::OpaqueExtrinsic as UncheckedExtrinsic;
|
||||||
|
/// La cabecera, idéntica a la del runtime: el hash de un bloque no depende de este cambio.
|
||||||
|
pub type Header = crate::Header;
|
||||||
|
/// El bloque que usa el node.
|
||||||
|
pub type Block = generic::Block<Header, UncheckedExtrinsic>;
|
||||||
|
|
||||||
// Slice 2e Layer 5 fix: import Vec for no_std impl_opaque_keys macro expansion.
|
// Slice 2e Layer 5 fix: import Vec for no_std impl_opaque_keys macro expansion.
|
||||||
extern crate alloc;
|
extern crate alloc;
|
||||||
@@ -36,3 +53,117 @@ impl_opaque_keys! {
|
|||||||
pub grandpa: crate::Grandpa,
|
pub grandpa: crate::Grandpa,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::{Runtime, RuntimeCall, TxExtension};
|
||||||
|
use parity_scale_codec::{Decode, Encode};
|
||||||
|
use sp_core::{sr25519, Pair, H256};
|
||||||
|
use sp_runtime::generic::{Era, SignedPayload};
|
||||||
|
use sp_runtime::traits::{Block as BlockT, Header as HeaderT};
|
||||||
|
use sp_runtime::{MultiAddress, MultiSignature};
|
||||||
|
|
||||||
|
/// Un extrinsic firmado por //Alice como lo firma un wallet, con una llamada de RV17.
|
||||||
|
fn firmado() -> (crate::UncheckedExtrinsic, Vec<u8>) {
|
||||||
|
let alice = sr25519::Pair::from_string("//Alice", None).expect("//Alice");
|
||||||
|
let tx_ext: TxExtension = (
|
||||||
|
frame_system::CheckNonZeroSender::<Runtime>::new(),
|
||||||
|
frame_system::CheckSpecVersion::<Runtime>::new(),
|
||||||
|
frame_system::CheckTxVersion::<Runtime>::new(),
|
||||||
|
frame_system::CheckGenesis::<Runtime>::new(),
|
||||||
|
frame_system::CheckEra::<Runtime>::from(Era::Immortal),
|
||||||
|
frame_system::CheckNonce::<Runtime>::from(0),
|
||||||
|
frame_system::CheckWeight::<Runtime>::new(),
|
||||||
|
pallet_transaction_payment::ChargeTransactionPayment::<Runtime>::from(0),
|
||||||
|
frame_metadata_hash_extension::CheckMetadataHash::<Runtime>::new(false),
|
||||||
|
frame_system::WeightReclaim::<Runtime>::new(),
|
||||||
|
);
|
||||||
|
let genesis = H256::repeat_byte(7);
|
||||||
|
let v = crate::VERSION;
|
||||||
|
let implicito = (
|
||||||
|
(),
|
||||||
|
v.spec_version,
|
||||||
|
v.transaction_version,
|
||||||
|
genesis,
|
||||||
|
genesis,
|
||||||
|
(),
|
||||||
|
(),
|
||||||
|
(),
|
||||||
|
None,
|
||||||
|
(),
|
||||||
|
);
|
||||||
|
let call =
|
||||||
|
RuntimeCall::Governance(origin_chain_governance::Call::submit_treasury_proposal {
|
||||||
|
title_hash: [1u8; 32],
|
||||||
|
body_hash: [2u8; 32],
|
||||||
|
voting_period_blocks: origin_chain_governance::MIN_VOTING_PERIOD_BLOCKS,
|
||||||
|
stake: origin_chain_governance::MIN_PROPOSAL_STAKE,
|
||||||
|
recipient: sp_runtime::AccountId32::new([3u8; 32]),
|
||||||
|
amount: 5,
|
||||||
|
});
|
||||||
|
let llamada = call.encode();
|
||||||
|
let payload = SignedPayload::<RuntimeCall, TxExtension>::from_raw(call, tx_ext, implicito);
|
||||||
|
let firma = payload.using_encoded(|p| alice.sign(p));
|
||||||
|
let (call, tx_ext, _) = payload.deconstruct();
|
||||||
|
let xt = crate::UncheckedExtrinsic::new_signed(
|
||||||
|
call,
|
||||||
|
MultiAddress::Id(alice.public().into()),
|
||||||
|
MultiSignature::Sr25519(firma),
|
||||||
|
tx_ext,
|
||||||
|
);
|
||||||
|
(xt, llamada)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// El node lee como `opaque::Block` lo que el runtime escribió como `crate::Block`, con los
|
||||||
|
/// mismos bytes y el mismo hash. Es lo que permite cambiar el binario de un validador
|
||||||
|
/// sobre su base de datos vieja (ensayado en la rp1 el 2026-10-28: 39 273 bloques
|
||||||
|
/// sincronizados desde cero y los mismos hashes en 8 alturas).
|
||||||
|
#[test]
|
||||||
|
fn el_bloque_opaco_es_el_mismo_bloque() {
|
||||||
|
let (xt, _) = firmado();
|
||||||
|
let header = crate::Header::new(
|
||||||
|
7,
|
||||||
|
Default::default(),
|
||||||
|
Default::default(),
|
||||||
|
H256::repeat_byte(9),
|
||||||
|
Default::default(),
|
||||||
|
);
|
||||||
|
let completo = crate::Block::new(header, alloc::vec![xt.clone()]);
|
||||||
|
let bytes = completo.encode();
|
||||||
|
let opaco = Block::decode(&mut &bytes[..]).expect("el node lo lee");
|
||||||
|
assert_eq!(opaco.encode(), bytes, "mismos bytes");
|
||||||
|
assert_eq!(
|
||||||
|
opaco.header().hash(),
|
||||||
|
completo.header().hash(),
|
||||||
|
"mismo hash"
|
||||||
|
);
|
||||||
|
assert_eq!(opaco.extrinsics()[0].encode(), xt.encode());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Y por qué hace falta: con una llamada que el runtime nativo no conoce (aquí,
|
||||||
|
/// Governance 99), los tipos nativos no decodifican el extrinsic y el opaco sí. El node
|
||||||
|
/// viejo rechazaba así, en su pool, la llamada 16 que añadía `set_code`.
|
||||||
|
#[test]
|
||||||
|
fn el_node_opaco_no_necesita_conocer_la_llamada() {
|
||||||
|
let (xt, llamada) = firmado();
|
||||||
|
let mut bytes = xt.encode();
|
||||||
|
let pos = bytes
|
||||||
|
.windows(llamada.len())
|
||||||
|
.position(|w| w == llamada)
|
||||||
|
.expect("la llamada");
|
||||||
|
assert_eq!(
|
||||||
|
&bytes[pos..pos + 2],
|
||||||
|
&[10, 16],
|
||||||
|
"Governance (10), llamada 16"
|
||||||
|
);
|
||||||
|
assert!(crate::UncheckedExtrinsic::decode(&mut &bytes[..]).is_ok());
|
||||||
|
bytes[pos + 1] = 99;
|
||||||
|
assert!(
|
||||||
|
crate::UncheckedExtrinsic::decode(&mut &bytes[..]).is_err(),
|
||||||
|
"los tipos nativos no conocen la llamada 99"
|
||||||
|
);
|
||||||
|
let opaco = UncheckedExtrinsic::decode(&mut &bytes[..]).expect("el opaco no la mira");
|
||||||
|
assert_eq!(opaco.encode(), bytes);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,258 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""E2E de RV17 contra un nodo vivo, firmando como un wallet: la acción del tesoro queda atada
|
||||||
|
a la propuesta y nada la cambia después.
|
||||||
|
|
||||||
|
ORIGIN_RPC=http://127.0.0.1:9955 python3 scripts/e2e/rv17_tesoro.py # pasos 0-5
|
||||||
|
ORIGIN_RPC=... python3 scripts/e2e/rv17_tesoro.py sembrar # además, siembra el pago
|
||||||
|
ORIGIN_RPC=... python3 scripts/e2e/rv17_tesoro.py cerrar <id_hex> # a los 14 días: aprueba y paga
|
||||||
|
|
||||||
|
Lo que NO prueba este guion: el camino feliz del pago. Exige 14 días de votación con el quórum
|
||||||
|
de la propuesta (el 10 % de la emisión: ~10M OGC en la testnet de la casa, que Alice alcanza),
|
||||||
|
y luego el sudo del pago. Ese camino lo cubren los tests del pallet, y en vivo se siembra con
|
||||||
|
una propuesta y se cierra a los 14 días. Aquí se mide lo que sólo se ve en la cadena: la
|
||||||
|
codificación de la llamada nueva, el storage por RPC, el peso que se cobra y los errores tal
|
||||||
|
como llegan.
|
||||||
|
|
||||||
|
Comprueba, en este orden (se detiene en el primer fallo, código de salida 1):
|
||||||
|
0. el node NO decodifica las llamadas: los bytes de una llamada que no existe (Governance 99)
|
||||||
|
los rechaza el runtime, no el cliente. Un node con el `Block` completo del runtime los
|
||||||
|
rechaza con 1040 «Client error: Could not decode», y con ese mismo error rechaza también
|
||||||
|
una llamada nueva que un `set_code` acaba de añadir (medido en la rp1 el 2026-10-28);
|
||||||
|
1. tesoro:<monto> entra y emite TreasuryProposalSubmitted. Lo cobrado es el peso que declara
|
||||||
|
weights.rs más el de las extensiones de la transacción (nonce, fee, CheckWeight…), que es
|
||||||
|
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;
|
||||||
|
2. Proposals(id) existe y TreasuryActions(id) = (Bob, monto), leído del storage por RPC;
|
||||||
|
3. tesoro:0 falla con TreasuryAmountBelowMinimum;
|
||||||
|
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
|
||||||
|
y no mueve el saldo de Bob.
|
||||||
|
"""
|
||||||
|
import hashlib, json, os, re, subprocess, sys, time
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
from upgrade_en_vivo import RAIZ, TOOL, ALICE, rpc, best, eventos, falla # noqa: E402
|
||||||
|
|
||||||
|
GOV = os.path.join(RAIZ, "crates/origin-chain-governance/src")
|
||||||
|
MONTO = 5 * 10**18 # 5 OGC
|
||||||
|
BOB = "5FHneW46xGXgs5mUiveU4sbTyGBzmstUspZC92UhjJM694ty"
|
||||||
|
BOB_HEX = "8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48"
|
||||||
|
|
||||||
|
|
||||||
|
def xxh64(data, seed):
|
||||||
|
P1, P2, P3, P4, P5 = (11400714785074694791, 14029467366897019727, 1609587929392839161,
|
||||||
|
9650029242287828579, 2870177450012600261)
|
||||||
|
M = (1 << 64) - 1
|
||||||
|
rotl = lambda x, r: ((x << r) | (x >> (64 - r))) & M # noqa: E731
|
||||||
|
|
||||||
|
def rnd(acc, x):
|
||||||
|
return (rotl((acc + x * P2) & M, 31) * P1) & M
|
||||||
|
|
||||||
|
n, i = len(data), 0
|
||||||
|
if n >= 32:
|
||||||
|
v = [(seed + P1 + P2) & M, (seed + P2) & M, seed, (seed - P1) & M]
|
||||||
|
while i + 32 <= n:
|
||||||
|
for k in range(4):
|
||||||
|
v[k] = rnd(v[k], int.from_bytes(data[i + 8 * k:i + 8 * k + 8], "little"))
|
||||||
|
i += 32
|
||||||
|
h = (rotl(v[0], 1) + rotl(v[1], 7) + rotl(v[2], 12) + rotl(v[3], 18)) & M
|
||||||
|
for k in range(4):
|
||||||
|
h = ((h ^ rnd(0, v[k])) * P1 + P4) & M
|
||||||
|
else:
|
||||||
|
h = (seed + P5) & M
|
||||||
|
h = (h + n) & M
|
||||||
|
while i + 8 <= n:
|
||||||
|
h = (rotl(h ^ rnd(0, int.from_bytes(data[i:i + 8], "little")), 27) * P1 + P4) & M
|
||||||
|
i += 8
|
||||||
|
if i + 4 <= n:
|
||||||
|
h = (rotl(h ^ ((int.from_bytes(data[i:i + 4], "little") * P1) & M), 23) * P2 + P3) & M
|
||||||
|
i += 4
|
||||||
|
while i < n:
|
||||||
|
h = (rotl(h ^ ((data[i] * P5) & M), 11) * P1) & M
|
||||||
|
i += 1
|
||||||
|
h ^= h >> 33
|
||||||
|
h = (h * P2) & M
|
||||||
|
h ^= h >> 29
|
||||||
|
h = (h * P3) & M
|
||||||
|
return h ^ (h >> 32)
|
||||||
|
|
||||||
|
|
||||||
|
def twox128(s):
|
||||||
|
b = s.encode()
|
||||||
|
return (xxh64(b, 0).to_bytes(8, "little") + xxh64(b, 1).to_bytes(8, "little")).hex()
|
||||||
|
|
||||||
|
|
||||||
|
def clave(item, id_hex):
|
||||||
|
"""StorageMap con Blake2_128Concat: twox128(pallet) ++ twox128(item) ++ blake2_128(k) ++ k."""
|
||||||
|
k = bytes.fromhex(id_hex)
|
||||||
|
return "0x" + twox128("Governance") + twox128(item) + hashlib.blake2b(k, digest_size=16).hexdigest() + k.hex()
|
||||||
|
|
||||||
|
|
||||||
|
def indice_de_error(nombre):
|
||||||
|
"""Índice SCALE de una variante de `Error<T>`: su posición en el enum del pallet."""
|
||||||
|
src = open(os.path.join(GOV, "lib.rs")).read()
|
||||||
|
cuerpo = re.search(r"pub enum Error<T> \{(.*?)\n \}", src, re.S).group(1)
|
||||||
|
sin_comentarios = re.sub(r"//[^\n]*", "", cuerpo)
|
||||||
|
variantes = re.findall(r"^\s*([A-Z]\w*)\s*,", sin_comentarios, re.M)
|
||||||
|
return variantes.index(nombre)
|
||||||
|
|
||||||
|
|
||||||
|
def peso_declarado(fn):
|
||||||
|
"""ref_time y proof_size que declara weights.rs, con RocksDbWeight (25 µs leer, 100 µs escribir)."""
|
||||||
|
src = open(os.path.join(GOV, "weights.rs")).read()
|
||||||
|
impl = src.split("for SubstrateWeight<T> {")[1].split("\nimpl WeightInfo for ()")[0]
|
||||||
|
b = re.search(r"fn " + fn + r"\(\) -> Weight \{(.*?)\n\t\}", impl, re.S).group(1)
|
||||||
|
n = lambda pat: int(re.search(pat, b).group(1).replace("_", "")) # noqa: E731
|
||||||
|
ref = n(r"from_parts\(([\d_]+), 0\)") + n(r"reads\((\d+)_u64") * 25_000_000 + n(r"writes\((\d+)_u64") * 100_000_000
|
||||||
|
return ref, n(r"from_parts\(0, ([\d_]+)\)")
|
||||||
|
|
||||||
|
|
||||||
|
def firmar(llamada, nonce, genesis, v):
|
||||||
|
return subprocess.run([TOOL, "firmar", llamada, str(nonce), genesis, str(v["specVersion"]),
|
||||||
|
str(v["transactionVersion"])], capture_output=True, text=True, check=True).stdout.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def enviar(llamada, genesis, v):
|
||||||
|
"""Firma con el nonce siguiente de Alice, envía y devuelve los eventos del bloque que la incluye."""
|
||||||
|
n0 = best()
|
||||||
|
nonce = rpc("system_accountNextIndex", [ALICE])["result"]
|
||||||
|
xt = firmar(llamada, nonce, genesis, v)
|
||||||
|
r = rpc("author_submitExtrinsic", [xt])
|
||||||
|
if "error" in r:
|
||||||
|
falla(f"{llamada}: rechazada en el pool: {r['error']}")
|
||||||
|
t0 = time.time()
|
||||||
|
while time.time() - t0 < 60:
|
||||||
|
for n in range(n0 + 1, best() + 1):
|
||||||
|
h = rpc("chain_getBlockHash", [n])["result"]
|
||||||
|
blk = rpc("chain_getBlock", [h])["result"]["block"]
|
||||||
|
# Un node con el Block concreto entrega cada extrinsic como lista de bytes; el
|
||||||
|
# opaco, como cadena hex (lo que espera polkadot.js). Se aceptan las dos.
|
||||||
|
hexes = [x if isinstance(x, str) else "0x" + bytes(x).hex() for x in blk["extrinsics"]]
|
||||||
|
if xt in hexes:
|
||||||
|
pos = hexes.index(xt)
|
||||||
|
evs = [l for l in eventos(n).splitlines() if f"ApplyExtrinsic({pos})" in l]
|
||||||
|
return n, evs
|
||||||
|
time.sleep(2)
|
||||||
|
falla(f"{llamada}: no se incluyó en 60 s")
|
||||||
|
|
||||||
|
|
||||||
|
def saldo_libre(cuenta_hex):
|
||||||
|
k = "0x" + twox128("System") + twox128("Account") + hashlib.blake2b(bytes.fromhex(cuenta_hex), digest_size=16).hexdigest() + cuenta_hex
|
||||||
|
v = rpc("state_getStorage", [k])["result"]
|
||||||
|
return int.from_bytes(bytes.fromhex(v[2:])[16:32], "little") if v else 0 # AccountInfo: nonce,consumers,providers,sufficients (4×u32) y luego free
|
||||||
|
|
||||||
|
|
||||||
|
def main(sembrar_pago=False):
|
||||||
|
genesis = rpc("chain_getBlockHash", [0])["result"]
|
||||||
|
v = rpc("state_getRuntimeVersion")["result"]
|
||||||
|
print(f"spec {v['specVersion']} · best #{best()}")
|
||||||
|
if v["specVersion"] < 18:
|
||||||
|
falla("RV17 llega con la spec 18")
|
||||||
|
|
||||||
|
nonce = rpc("system_accountNextIndex", [ALICE])["result"]
|
||||||
|
xt = firmar(f"tesoro:{MONTO}", nonce, genesis, v)
|
||||||
|
titulo = hashlib.blake2b(b"e2e-rv17/titulo", digest_size=32).hexdigest()
|
||||||
|
if xt.count("0a10" + titulo) != 1:
|
||||||
|
falla("no encuentro la llamada Governance 16 en el extrinsic")
|
||||||
|
r = rpc("author_submitExtrinsic", [xt.replace("0a10" + titulo, "0a63" + titulo)])
|
||||||
|
e = r.get("error", {})
|
||||||
|
if not e:
|
||||||
|
falla("una llamada que no existe ENTRÓ en el pool")
|
||||||
|
if e.get("code") == 1040 or "Client error: Could not decode" in e.get("message", ""):
|
||||||
|
falla(f"el node decodifica las llamadas con sus tipos nativos: {e}")
|
||||||
|
print(f"0 ✅ llamada inexistente → la rechaza el runtime, no el cliente: {e.get('code')} {e.get('message', '')[:70]!r}")
|
||||||
|
|
||||||
|
def cobrado(evs):
|
||||||
|
ok = [e for e in evs if "ExtrinsicSuccess" in e]
|
||||||
|
if not ok:
|
||||||
|
falla("no hay ExtrinsicSuccess\n" + "\n".join(evs))
|
||||||
|
return (int(re.search(r"ref_time: (\d+)", ok[0]).group(1)),
|
||||||
|
int(re.search(r"proof_size: (\d+)", ok[0]).group(1)))
|
||||||
|
|
||||||
|
def residuo(evs, fn):
|
||||||
|
c, d = cobrado(evs), peso_declarado(fn)
|
||||||
|
return (c[0] - d[0], c[1] - d[1])
|
||||||
|
|
||||||
|
_, evs = enviar(f"depositar:{2 * MONTO}", genesis, v)
|
||||||
|
extensiones = residuo(evs, "treasury_deposit")
|
||||||
|
n, evs = enviar(f"tesoro:{MONTO}", genesis, v)
|
||||||
|
sub = [e for e in evs if "TreasuryProposalSubmitted" in e]
|
||||||
|
if not sub:
|
||||||
|
falla("tesoro: no hay TreasuryProposalSubmitted\n" + "\n".join(evs))
|
||||||
|
id_hex = bytes(json.loads(re.search(r"ProposalId\((\[[\d, ]+\])\)", sub[0]).group(1))).hex()
|
||||||
|
if residuo(evs, "submit_treasury_proposal") != extensiones:
|
||||||
|
falla(f"cobrado {cobrado(evs)} ≠ declarado {peso_declarado('submit_treasury_proposal')} + extensiones {extensiones}")
|
||||||
|
print(f"1 ✅ #{n}: TreasuryProposalSubmitted id 0x{id_hex[:12]}… · cobrado = weights.rs "
|
||||||
|
f"{peso_declarado('submit_treasury_proposal')} + extensiones {extensiones} (las mismas que treasury_deposit)")
|
||||||
|
|
||||||
|
if not rpc("state_getStorage", [clave("Proposals", id_hex)])["result"]:
|
||||||
|
falla("Proposals(id) no existe en el storage")
|
||||||
|
acc = rpc("state_getStorage", [clave("TreasuryActions", id_hex)])["result"]
|
||||||
|
if not acc:
|
||||||
|
falla("TreasuryActions(id) no existe en el storage")
|
||||||
|
b = bytes.fromhex(acc[2:])
|
||||||
|
if b[:32].hex() != BOB_HEX or int.from_bytes(b[32:48], "little") != MONTO or len(b) != 48:
|
||||||
|
falla(f"TreasuryActions(id) = {acc}, no (Bob, {MONTO})")
|
||||||
|
print(f"2 ✅ storage por RPC: Proposals(id) existe · TreasuryActions(id) = (Bob, {MONTO})")
|
||||||
|
|
||||||
|
def falla_con(llamada, error, que):
|
||||||
|
n, evs = enviar(llamada, genesis, v)
|
||||||
|
idx = indice_de_error(error)
|
||||||
|
texto = "\n".join(evs)
|
||||||
|
patron = rf"index: 10, error: \[{idx}, 0, 0, 0\]"
|
||||||
|
if not re.search(patron, texto):
|
||||||
|
falla(f"{llamada}: se esperaba {error} (índice {idx})\n{texto}")
|
||||||
|
print(f"{que} ✅ #{n}: {llamada.split(':')[0]} → {error} (Module 10, error {idx})")
|
||||||
|
|
||||||
|
falla_con("tesoro:0", "TreasuryAmountBelowMinimum", "3")
|
||||||
|
falla_con(f"tesoro:{MONTO * 2}", "ProposalIdCollision", "4")
|
||||||
|
if rpc("state_getStorage", [clave("TreasuryActions", id_hex)])["result"] != acc:
|
||||||
|
falla("el reenvío cambió TreasuryActions(id)")
|
||||||
|
print(" ✅ TreasuryActions(id) sigue siendo (Bob, monto original)")
|
||||||
|
|
||||||
|
antes = saldo_libre(BOB_HEX)
|
||||||
|
falla_con(f"pagar:0x{id_hex}:{MONTO}", "ProposalNotApproved", "5")
|
||||||
|
if saldo_libre(BOB_HEX) != antes:
|
||||||
|
falla("el saldo de Bob cambió")
|
||||||
|
print(f" ✅ el saldo libre de Bob no se movió ({antes})")
|
||||||
|
if sembrar_pago:
|
||||||
|
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
|
||||||
|
con EXACTAMENTE el quórum guardado (la frontera `yes >= quórum`)."""
|
||||||
|
q = int.from_bytes(bytes.fromhex(rpc("state_getStorage", [clave("VoteQuorumOf", id_hex)])["result"][2:]), "little")
|
||||||
|
n, evs = enviar(f"depositar:{2 * MONTO}", genesis, v)
|
||||||
|
# (el paso 1 ya depositó otro tanto) treasury_deposit no emite evento propio: basta el Transfer de Balances y el éxito.
|
||||||
|
if not (any("Transfer" in e for e in evs) and any("ExtrinsicSuccess" in e for e in evs)):
|
||||||
|
falla("el depósito al vault no entró\n" + "\n".join(evs))
|
||||||
|
n, evs = enviar(f"votar:0x{id_hex}:{q}", genesis, v)
|
||||||
|
if not any("VoteCast" in e for e in evs):
|
||||||
|
falla("el voto Aye no entró\n" + "\n".join(evs))
|
||||||
|
p = rpc("state_getStorage", [clave("Proposals", id_hex)])["result"]
|
||||||
|
print(f"S ✅ #{n}: vault fondeado con {2 * MONTO} y Aye de Alice = quórum guardado ({q / 10**18:,.6f} OGC)")
|
||||||
|
print(f" cerrar después del fin del periodo: python3 scripts/e2e/rv17_tesoro.py cerrar {id_hex}")
|
||||||
|
|
||||||
|
|
||||||
|
def cerrar(id_hex):
|
||||||
|
"""A los 14 días: finaliza (debe quedar Approved), paga por sudo y comprueba el saldo de Bob."""
|
||||||
|
genesis = rpc("chain_getBlockHash", [0])["result"]
|
||||||
|
v = rpc("state_getRuntimeVersion")["result"]
|
||||||
|
n, evs = enviar(f"finalizar:0x{id_hex}", genesis, v)
|
||||||
|
if not any("ProposalFinalized" in e and "Approved" in e for e in evs):
|
||||||
|
falla("la propuesta no quedó Approved\n" + "\n".join(evs))
|
||||||
|
antes = saldo_libre(BOB_HEX)
|
||||||
|
n, evs = enviar(f"pagar:0x{id_hex}:{MONTO}", genesis, v)
|
||||||
|
if not any("Sudid { sudo_result: Ok" in e for e in evs):
|
||||||
|
falla("el pago por sudo falló\n" + "\n".join(evs))
|
||||||
|
if saldo_libre(BOB_HEX) - antes != MONTO:
|
||||||
|
falla(f"Bob recibió {saldo_libre(BOB_HEX) - antes}, no {MONTO}")
|
||||||
|
print(f"C ✅ #{n}: Approved y pagado: Bob recibió exactamente {MONTO} plancks")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
if len(sys.argv) == 3 and sys.argv[1] == "cerrar":
|
||||||
|
cerrar(sys.argv[2].removeprefix("0x"))
|
||||||
|
else:
|
||||||
|
main(sembrar_pago=len(sys.argv) == 2 and sys.argv[1] == "sembrar")
|
||||||
Reference in new issue
Block a user