From 266ee4084d5ceaa6936a96c09676c785b0c7ae08 Mon Sep 17 00:00:00 2001 From: DavidHerran Date: Tue, 7 Jul 2026 22:57:44 -0500 Subject: [PATCH] polish(gov+attestation): test de empate en finalize + ancla SSOT de attestation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Polish pass MEDIUM/LOW post-audit (2 items limpios + triage de los invasivos): governance — cobertura del desempate (consenso-relevante, sin cobertura previa): - scenario_8b_finalize_tie_rejected_canonical: yes==nay ⇒ `yes > nay` es false ⇒ Rejected. Fija que se exige MAYORÍA ESTRICTA (el empate favorece el status quo). Entre scenario_7 (yes-majority→Approved) y scenario_8 (nay-majority→Rejected) faltaba justo el borde del empate. origin-chain-types — SSOT de attestation ahora que el crate aterrizó: - `arkhe-noesis-attestation-types` ya existe (crate standalone no_std + feature scale en el monorepo). El vendor-copy SE QUEDA como type-mirror deliberado: el chain es submódulo y depender del crate del monorepo invierte la dep-direction (submódulo→monorepo = ILEGAL). La SSOT se preserva por los golden vectors del oráculo real (decode_golden_vector_1/3/4), no por Cargo. - attestation_verify: §SSOT parity anchor doc — documenta que esos vectores pinnean el decoder al field-order de `SigningPayload` y cómo regenerar ante deriva. - attestation.rs: comentario del mirror actualizado (el crate aterrizó; diferencia conocida schema_version [u8;8] vs String es irrelevante — el mirror no serializa el wire; el decoder secuencial sí es byte-fiel al postcard, probado por vectores). Triage documentado (items considerados y DIFERIDOS con rationale, no olvidados): - register_agent_did unbounded Vec: bound del arg a ConstU32<10> movería el rechazo a decode-time, volvería AgentChainOverflow inalcanzable + rompería el test de overflow + exigiría spec bump — invasivo para un DoS ya mitigado por length-fee + límite de bloque. Diferido. - SS58 prefix (42): NO es bug — placeholder testnet correcto, gated en un DC de ss58-registry pre-mainnet. Sin cambio. - no-author path (emission): requiere un FindAuthor mock que devuelva None (segundo runtime o toggle) — ripple > valor. Diferido. Verificado: origin-chain-types 77 tests verde (incl. golden parity) + tie test verde. Co-Authored-By: Claude Opus 4.8 --- .../src/tests_integration.rs | 31 +++++++++++++++++++ crates/origin-chain-types/src/attestation.rs | 13 ++++++-- .../src/attestation_verify.rs | 18 +++++++++++ 3 files changed, 60 insertions(+), 2 deletions(-) diff --git a/crates/origin-chain-governance/src/tests_integration.rs b/crates/origin-chain-governance/src/tests_integration.rs index 25fa1a4..6156140 100644 --- a/crates/origin-chain-governance/src/tests_integration.rs +++ b/crates/origin-chain-governance/src/tests_integration.rs @@ -222,6 +222,37 @@ fn scenario_8_finalize_rejected_nay_majority_canonical() { }); } +// ─── Scenario 8b: Finalize con EMPATE (yes == nay) → Rejected ──────── +// Polish audit: documenta la semántica del desempate. `finalize_proposal` usa +// `yes_weight > nay_weight` (mayoría ESTRICTA); un empate NO alcanza el umbral +// y la propuesta se RECHAZA. Esto es intencional (una propuesta necesita más +// síes que noes para pasar; el empate favorece el status quo). El gate estaba +// sin cobertura — este test lo fija como comportamiento consensuado. + +#[test] +fn scenario_8b_finalize_tie_rejected_canonical() { + new_test_ext().execute_with(|| { + let id = submit_default(alice_origin()); + + // EMPATE exacto: Aye 500 == Nay 500. + assert_ok!(Governance::cast_vote(bob_origin(), id, VoteStance::Aye, 500)); + assert_ok!(Governance::cast_vote(charlie_origin(), id, VoteStance::Nay, 500)); + + run_to_block(200); + + assert_ok!(Governance::finalize_proposal(alice_origin(), id)); + + let proposal = Proposals::::get(id).unwrap(); + assert_eq!(proposal.yes_weight, 500); + assert_eq!(proposal.nay_weight, 500); + // yes(500) > nay(500) es FALSE ⇒ Rejected (mayoría estricta requerida). + assert!( + matches!(proposal.status, ProposalStatus::Rejected), + "empate debe rechazar: se exige mayoría estricta" + ); + }); +} + // ─── Scenario 9: Vote closed period rejected canonical ────────────── #[test] diff --git a/crates/origin-chain-types/src/attestation.rs b/crates/origin-chain-types/src/attestation.rs index f37b112..69717b3 100644 --- a/crates/origin-chain-types/src/attestation.rs +++ b/crates/origin-chain-types/src/attestation.rs @@ -142,8 +142,17 @@ impl ValidationAttestation { // verificarían contra el verificador on-chain. El verificador real // (`crate::attestation_verify`) opera sobre los bytes postcard opacos del // oráculo de Ergon; este método muerto era un footgun (flagged en la - // verificación adversarial). El struct se conserva como type-mirror hasta - // que aterrice `arkhe-noesis-attestation-types`. + // verificación adversarial). + // + // SSOT (2026-07-07): `arkhe-noesis-attestation-types` YA aterrizó como crate + // standalone `no_std` (feature `scale` disponible) en el monorepo. Este struct + // se conserva como type-MIRROR deliberado — NO se puede reemplazar por un dep + // real porque el chain es submódulo (dep submódulo→monorepo = ILEGAL). La + // parity con la SSOT NO se garantiza por Cargo sino por los golden vectors del + // oráculo real en `crate::attestation_verify` (§SSOT parity anchor). Diferencia + // conocida del mirror: `schema_version: [u8;8]` aquí vs `String` en la SSOT — es + // irrelevante porque este struct NO serializa el wire (el decoder secuencial de + // `attestation_verify` sí es byte-fiel al postcard real, probado por los vectores). } #[cfg(test)] diff --git a/crates/origin-chain-types/src/attestation_verify.rs b/crates/origin-chain-types/src/attestation_verify.rs index 4d8aa3f..63f7c6f 100644 --- a/crates/origin-chain-types/src/attestation_verify.rs +++ b/crates/origin-chain-types/src/attestation_verify.rs @@ -252,6 +252,24 @@ pub fn decode_signing_payload(payload: &[u8]) -> Result