polish(gov+attestation): test de empate en finalize + ancla SSOT de attestation

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 <noreply@anthropic.com>
This commit is contained in:
davidherranandClaude Opus 4.8 committed 2026-07-07 22:57:44 -05:00
1 parent c7fc6a88fc
commit 266ee4084d
3 files changed
+60 -2

No files matched your search

@@ -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::<Test>::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]
+11 -2
View File
@@ -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)]
@@ -252,6 +252,24 @@ pub fn decode_signing_payload(payload: &[u8]) -> Result<DecodedSigningPayload, S
#[cfg(test)]
mod tests {
//! ## SSOT parity anchor — `arkhe-noesis-attestation-types`
//!
//! Estos golden vectors SON el "golden parity test" de la SSOT: fueron generados
//! por el oráculo REAL (`arkhe-noesis-core`, que serializa los tipos canónicos de
//! **`arkhe-noesis-attestation-types`** — ahora un crate standalone `no_std` en el
//! monorepo, feature `scale` disponible). El decoder [`decode_signing_payload`]
//! DEBE parsear el wire postcard en el MISMO orden de campos que
//! `arkhe-noesis-attestation-types::SigningPayload` (§SSOT): `schema_version` →
//! `asset_id` → `validator_oit{identifier, level, public_key}` → `validated_at_us`
//! → `verdict` → `severity` → `constraints_checked`.
//!
//! **Por qué VENDOR-COPY y no dep directo**: Origin Chain es un submódulo; depender
//! del crate del monorepo invertiría la dirección de dependencias
//! (submódulo→monorepo = ILEGAL, el chain dejaría de buildear standalone). La SSOT
//! se preserva por estos vectores byte-fieles, NO por un `Cargo.toml` compartido.
//! Si Ergon cambia el orden/tipo de un campo del `SigningPayload`, estos tests
//! fallan al decodificar → la deriva se caza aquí. Regenerar: re-emitir los
//! vectores desde `arkhe-noesis-core` con la nueva SSOT y pegar el hex.
use super::*;
/// Golden vector 1 generado desde el oráculo REAL de Ergon