test(npos): tests de la aritmética constitucional 70/20/10 (meta-audit)
Meta-auditoría de mi propio trabajo (NPoS Paso A se shippeó en checkpoint sin verificación adversarial). Verificado contra el source de pallet-staking 43.0.0 que la infra es REALMENTE inerte (on_initialize=weight-only; on_finalize no-op con ActiveEra=None sin session) — cero regresión confirmada. Mejora aplicada: la aritmetica constitucional de OriginEraPayout + DealWithEraRemainder estaba escrita pero SIN tests (dormida, no se ejercita). Antes de encenderla en Paso C/D conviene probar el split: - era_remainder_split extraido a fn pura testeable (sin mock). - 5 tests: era_payout preserva tasa lineal + split 70/30 exacto + cero perdida; no overflow en era de 1 anio; remainder 20/10 del total; residual-al-subsidy (el dust del redondeo favorece el pool anti-clasista #16); invariante en montos reales. Sin bugs encontrados en Paso A. cargo +1.88. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
1f0122beda
commit
fcf768b046
1 file changed
+78
-3
@@ -83,11 +83,19 @@ parameter_types! {
|
|||||||
/// `pool_covered=0` y el usuario pagaría el 100%.
|
/// `pool_covered=0` y el usuario pagaría el 100%.
|
||||||
pub struct DealWithEraRemainder;
|
pub struct DealWithEraRemainder;
|
||||||
|
|
||||||
|
/// Reparte el `rest` de una era (30% de la emisión) en `(treasury, subsidy)` =
|
||||||
|
/// 2/3 + 1/3 = 20% + 10% del total. El residual (dust del redondeo entero) va al
|
||||||
|
/// subsidy — favorece el pool anti-clasista #16, cero pérdida. Función pura para
|
||||||
|
/// testear la invariante constitucional sin montar un mock runtime.
|
||||||
|
pub(crate) fn era_remainder_split(rest: u128) -> (u128, u128) {
|
||||||
|
let treasury = rest.saturating_mul(2) / 3;
|
||||||
|
let subsidy = rest.saturating_sub(treasury); // residual al subsidy
|
||||||
|
(treasury, subsidy)
|
||||||
|
}
|
||||||
|
|
||||||
impl OnUnbalanced<CreditOf> for DealWithEraRemainder {
|
impl OnUnbalanced<CreditOf> for DealWithEraRemainder {
|
||||||
fn on_nonzero_unbalanced(amount: CreditOf) {
|
fn on_nonzero_unbalanced(amount: CreditOf) {
|
||||||
let total = amount.peek();
|
let (treasury_part, _) = era_remainder_split(amount.peek());
|
||||||
// 2/3 del rest = 20% del total de emisión → treasury.
|
|
||||||
let treasury_part = total.saturating_mul(2) / 3;
|
|
||||||
let (to_treasury, to_subsidy) = amount.split(treasury_part);
|
let (to_treasury, to_subsidy) = amount.split(treasury_part);
|
||||||
// resolve deposita cada crédito en su cuenta vault; si falla, el crédito
|
// resolve deposita cada crédito en su cuenta vault; si falla, el crédito
|
||||||
// se descarta (drop = burn) — nunca se duplica ni se pierde a un tercero.
|
// se descarta (drop = burn) — nunca se duplica ni se pierde a un tercero.
|
||||||
@@ -193,3 +201,70 @@ impl pallet_staking::Config for Runtime {
|
|||||||
type WeightInfo = pallet_staking::weights::SubstrateWeight<Runtime>;
|
type WeightInfo = pallet_staking::weights::SubstrateWeight<Runtime>;
|
||||||
type RuntimeEvent = RuntimeEvent;
|
type RuntimeEvent = RuntimeEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Tests de la aritmética constitucional (verificar 70/20/10 ANTES de
|
||||||
|
// activar NPoS en Paso C/D — hoy la emisión está dormida) ──────────
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use pallet_staking::EraPayout as _;
|
||||||
|
|
||||||
|
const ERA_24H_MILLIS: u64 = 24 * 60 * 60 * 1_000; // 86_400_000
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn era_payout_preserva_tasa_lineal_y_split_70_30() {
|
||||||
|
// Era de 24h = 14_400 bloques de 6s.
|
||||||
|
let (validator_payout, rest) = OriginEraPayout::era_payout(0, 0, ERA_24H_MILLIS);
|
||||||
|
let blocks = (ERA_24H_MILLIS / MILLISECS_PER_BLOCK) as u128;
|
||||||
|
assert_eq!(blocks, 14_400, "14_400 bloques en 24h a 6s");
|
||||||
|
let total = EmissionBlockReward::get().saturating_mul(blocks);
|
||||||
|
// 70% validadores / 30% rest, y sin pérdida: validator + rest == total.
|
||||||
|
assert_eq!(validator_payout, total * 7_000 / 10_000, "70% a validadores");
|
||||||
|
assert_eq!(validator_payout + rest, total, "cero pérdida: validator + rest == total");
|
||||||
|
// El rest es exactamente el 30% (con el dust del redondeo capturado).
|
||||||
|
assert_eq!(rest, total - validator_payout);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn era_payout_no_overflow_en_era_larga() {
|
||||||
|
// Era de 1 año (peor caso plausible): no debe panic ni saturar mal.
|
||||||
|
let one_year_millis: u64 = 365 * ERA_24H_MILLIS;
|
||||||
|
let (validator_payout, rest) = OriginEraPayout::era_payout(0, 0, one_year_millis);
|
||||||
|
let blocks = (one_year_millis / MILLISECS_PER_BLOCK) as u128;
|
||||||
|
let total = EmissionBlockReward::get().saturating_mul(blocks);
|
||||||
|
assert_eq!(validator_payout + rest, total, "sin overflow: invariante intacta");
|
||||||
|
// ~5.256M bloques/año × 10 OGC = ~5.256e25, lejos del techo u128.
|
||||||
|
assert!(total < u128::MAX / 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn remainder_split_20_10_del_total() {
|
||||||
|
// El rest = 30% del total. Su split 2/3+1/3 = 20%+10% del total.
|
||||||
|
// Ejemplo redondo: total=100 → rest=30 → treasury=20, subsidy=10.
|
||||||
|
let (treasury, subsidy) = era_remainder_split(30);
|
||||||
|
assert_eq!(treasury, 20, "2/3 del rest = 20% del total");
|
||||||
|
assert_eq!(subsidy, 10, "1/3 del rest = 10% del total");
|
||||||
|
assert_eq!(treasury + subsidy, 30, "cero pérdida");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn remainder_split_residual_al_subsidy_anti_clasista() {
|
||||||
|
// Con rest no divisible por 3, el dust va al SUBSIDY (#16), no al treasury.
|
||||||
|
let (treasury, subsidy) = era_remainder_split(31);
|
||||||
|
assert_eq!(treasury, 20, "floor(31*2/3) = 20");
|
||||||
|
assert_eq!(subsidy, 11, "el residual (11) favorece el pool anti-clasista");
|
||||||
|
assert_eq!(treasury + subsidy, 31, "cero pérdida");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn remainder_split_invariante_para_montos_reales() {
|
||||||
|
// Para un rest real (30% de una era de 24h) la invariante se mantiene.
|
||||||
|
let (_, rest) = OriginEraPayout::era_payout(0, 0, ERA_24H_MILLIS);
|
||||||
|
let (treasury, subsidy) = era_remainder_split(rest);
|
||||||
|
assert_eq!(treasury + subsidy, rest, "cero pérdida en montos reales");
|
||||||
|
// treasury ≈ 2× subsidy (dust aparte).
|
||||||
|
assert!(treasury >= subsidy, "treasury (20%) >= subsidy (10%)");
|
||||||
|
assert!(treasury <= subsidy.saturating_mul(2).saturating_add(2), "ratio ~2:1");
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user