feat(provenance): FIX-4 — verificador attestation on-chain (cierra V3-C1 + squat)

El verificador attestation queda VIVO: una firma producida por el código
REAL de Ergon (arkhe-noesis-core, postcard + mensaje domain-separado)
verifica byte-fiel en el pallet Substrate. Cierra el FIX-DEFERRED del
roadmap (helper de Ergon ya en árbol) + el replay cross-chain V3-C1.

origin-chain-types/attestation_verify.rs (nuevo):
- ATTESTATION_DOMAIN_TAG + attestation_signed_message: espejo BYTE-IDÉNTICO
  del oráculo (DC 076d15cc ASP-3). Mensaje = TAG ‖ genesis_hash(32) ‖ payload.
- decode_signing_payload: lector postcard SECUENCIAL total y estricto del
  SigningPayload de Ergon (sin dep nueva en el runtime wasm frágil). Lectores
  varint u64/u32 con los bounds exactos de postcard (cierra el LOW de fidelidad
  flagged: verdict/severity/constraints son u32, máx 5 bytes / 5º ≤ 15).
  Cotas DoS en strings. NUNCA re-serializa (contrato ASP-3).

pallet-origin-provenance:
- attest_asset(signing_payload: BoundedVec<u8,512>, signature: [u8;64]):
  recibe los bytes postcard OPACOS + la firma; decodifica, verifica Ed25519
  vía sp_io::crypto::ed25519_verify (host function, no bloatea wasm, precedente
  FIX-3) sobre el mensaje domain-separado con el genesis_hash de ESTA cadena.
  La pubkey se decodifica del payload (no param aparte). creator = el validador,
  anchored_at_block real (cerró los placeholders del scaffold).
- Dedup-by-BLAKE3 con rechazo EXPLÍCITO (asset_id = BLAKE3 content hash, dentro
  del payload firmado → replay del mismo blob reusa el asset_id → AlreadyAttested).

Hallazgos de la verificación adversarial (workflow ultracode, 3 lentes)
cerrados de RAÍZ:
- HIGH asset-id squat / provenance poisoning + MEDIUM tier-forgery: el gate
  era self-attested (level del payload) → cualquiera con una clave Ed25519
  anclaba basura institucional bajo el hash PÚBLICO de un asset legítimo y lo
  envenenaba de forma inmutable. FIX: registro on-chain AuthorizedValidators
  (pubkey → nivel OIT) gobernado por Root (register_validator/revoke_validator);
  la membresía es la VERDAD, no el payload. Mismo patrón que TierProfiles de
  FIX-6 Stage 2a (legitimidad on-chain, no caller-supplied). Sunset a
  attestation-driven cuando aterrice el registro OIT de Mathi.
- MEDIUM método muerto: removido el ValidationAttestation::signing_payload()
  SCALE divergente (producía bytes que jamás verificarían — footgun).

Decisiones (autoridad Pilar 4): sp_io ed25519_verify (consistente FIX-3,
estándar Substrate) · dedup-by-asset_id subsume replay exacto · golden vectors
generados desde el oráculo REAL de Ergon (seed [7u8;32], genesis 0xAB) =
prueba cross-runtime.

PENDIENTE (gap legítimo, no bug): OriginChainAttestationSink (impl del trait
ValidationAttestationSink de Ergon) es código cliente off-chain — vive en un
crate bridge del monorepo (subxt), NO en el chain repo (dependency direction
submódulo→monorepo prohibida). Se cablea con la integración Noesis→chain.

Tests: 19 provenance (golden vectors cross-runtime + squat cerrado + revoke +
anti-replay cross/intra-chain + tamper + malformed) + 72 types (decoder fuzz-
equivalente a postcard + fidelidad u32) + 21 suites workspace verde. cargo +1.88.

WASM: build VPS-gated (UnknownOpcode 252 ambiental, ver v0.0.58).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
davidherranandClaude Fable 5 committed 2026-06-11 19:52:57 -05:00
1 parent 3364994cce
commit ce18e191d0
7 files changed
+769 -475

No files matched your search

+40 -31
View File
@@ -1,29 +1,20 @@
//! # Mock runtime canonical para integration tests v0.0.18
//! # Mock runtime para integration tests
//!
//! Provee mock minimal `Test` runtime composition (`System` + `Provenance` solo)
//! para E2E tests dispatchable + storage + event assertions per PROV-3 flow
//! 6-step canonical:
//! Mock minimal (`System` + `Provenance`) para E2E del flow PROV-3 + FIX-4:
//! decode postcard → gates → Ed25519 verify domain-separado → anclar.
//!
//! 1. PROV-4 Ed25519 signature verification
//! 2. PROV-5 validator OIT level >= 3 institutional
//! 3. PROV-6 is_anchorable check (verdict + severity)
//! 4. PROV-2 ValidationAttestation → AssetAttestation conversion
//! 5. PROV-2 StorageMap insert
//! 6. PROV-7 AssetAttested event emission
//!
//! ## Cross-reference canonical
//!
//! - DC `ORIGIN_PROVENANCE_PALLET_V1` PROV-1..8 sub-decisions
//! - `tests_integration.rs` 5 scenarios canonical E2E
//! - `lib.rs::pallet::attest_asset` dispatchable impl real
//! FIX-4: `new_test_ext_with_genesis` siembra `block_hash(0)` = el genesis con
//! el que se firmaron los golden vectors (generados desde el oráculo REAL de
//! Ergon `arkhe-noesis-core`), para probar la verificación cross-runtime.
#![cfg(test)]
use crate as origin_chain_provenance;
use frame_support::derive_impl;
use sp_core::H256;
use sp_runtime::BuildStorage;
/// Mock block type canonical para tests.
/// Mock block type para tests.
type Block = frame_system::mocking::MockBlock<Test>;
frame_support::construct_runtime!(
@@ -44,34 +35,52 @@ impl origin_chain_provenance::Config for Test {
type RuntimeEvent = RuntimeEvent;
}
/// Test externalities builder canonical.
///
/// Returns new `sp_io::TestExternalities` con frame_system genesis configurado
/// y block_number set to 1 (PROV-7 events requieren block_number > 0 para
/// proper testing canonical).
pub fn new_test_ext() -> sp_io::TestExternalities {
/// Genesis hash de los golden vectors ASP-3 (`/tmp/golden-asp3`, GENESIS=0xAB×32).
/// El pallet lee `block_hash(0)`; el mock lo siembra con ESTE valor para que la
/// verificación Ed25519 coincida con las firmas del oráculo.
pub const GOLDEN_GENESIS: [u8; 32] = [0xABu8; 32];
/// Pubkey del validador golden (seed [7u8;32] → todos los vectores la usan).
pub const GOLDEN_VALIDATOR_PUBKEY: [u8; 32] = [
0xea, 0x4a, 0x6c, 0x63, 0xe2, 0x9c, 0x52, 0x0a, 0xbe, 0xf5, 0x50, 0x7b, 0x13, 0x2e, 0xc5, 0xf9,
0x95, 0x47, 0x76, 0xae, 0xbe, 0xbe, 0x7b, 0x92, 0x42, 0x1e, 0xea, 0x69, 0x14, 0x46, 0xd2, 0x2c,
];
/// Registra el validador golden con el nivel dado (vía Root) — helper de tests.
pub fn register_golden_validator(level: u8) {
crate::pallet::Pallet::<Test>::register_validator(
RuntimeOrigin::root(),
GOLDEN_VALIDATOR_PUBKEY,
level,
)
.expect("Root registra el validador");
}
/// Test externalities con `block_hash(0)` sembrado al genesis dado.
pub fn new_test_ext_with_genesis(genesis: [u8; 32]) -> sp_io::TestExternalities {
let mut ext: sp_io::TestExternalities = frame_system::GenesisConfig::<Test>::default()
.build_storage()
.expect("genesis canonical builds")
.expect("genesis builds")
.into();
ext.execute_with(|| {
System::set_block_number(1);
// Siembra el hash del bloque génesis (que el pallet lee para el dominio).
frame_system::BlockHash::<Test>::insert(0u64, H256(genesis));
});
ext
}
/// Test signing key seed canonical (deterministic Ed25519 keypair).
///
/// Per ed25519-dalek pattern: seed [7u8; 32] produces deterministic verifying_key.
/// Used across integration tests para reproducible signing.
pub const TEST_SIGNING_SEED: [u8; 32] = [7u8; 32];
/// Test externalities con el genesis de los golden vectors (caso por defecto).
pub fn new_test_ext() -> sp_io::TestExternalities {
new_test_ext_with_genesis(GOLDEN_GENESIS)
}
/// Test origin account canonical (32-byte zero pattern).
/// Submitter de prueba (un relayer cualquiera — paga el fee, no es el validador).
pub fn alice_origin() -> RuntimeOrigin {
RuntimeOrigin::signed(sp_runtime::AccountId32::new([1u8; 32]))
}
/// Test origin account canonical (BOB pattern).
/// Segundo submitter (BOB).
pub fn bob_origin() -> RuntimeOrigin {
RuntimeOrigin::signed(sp_runtime::AccountId32::new([2u8; 32]))
}