From 654cffdf31172a13802ff67b6a8d9e0f19780c6b Mon Sep 17 00:00:00 2001 From: DavidHerran Date: Sun, 24 May 2026 22:40:04 -0500 Subject: [PATCH] =?UTF-8?q?feat(provenance):=20v0.0.18=20mock=20runtime=20?= =?UTF-8?q?integration=20tests=20E2E=20=E2=80=94=209=20scenarios=20canonic?= =?UTF-8?q?al=20own=20fixtures?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 2.1 PROVENANCE pallet — 9 integration tests E2E via FRAME mock runtime composition (`construct_runtime!` macro + derive_impl + sp_io::TestExternalities). Cierra honest disclosure sesión 2 ("real chain runtime aún NO arranca"). Pallet end-to-end validado canonical sin esperar Slice 2e WASM resolution structural. ## 9 integration tests canonical PROV-3 flow 5 scenarios: - scenario_1_passed_info_anchored_canonical (PROV-2/3/7) ✅ - scenario_2_warning_anchored_canonical (PROV-2/3/6/7) ✅ - scenario_3_failed_critical_rejected_canonical (PROV-6) ❌ NotAnchorable - scenario_4_passed_block_security_rejected_canonical (PROV-6) ❌ NotAnchorable - scenario_5_anonymous_level_0_rejected_canonical (PROV-5) ❌ ValidatorTierInsufficient 4 additional: - additional_idempotency_already_attested_rejected_canonical (PROV-2) ❌ AlreadyAttested - additional_tampered_signature_rejected_canonical (PROV-4) ❌ InvalidSignature - additional_tampered_payload_rejected_canonical (PROV-4) ❌ InvalidSignature - additional_6_step_prov_3_walkthrough_canonical (PROV-3 full) ✅ Pre/post assertions ## Own fixtures canonical Per audit canonical sesión 3+ (docs/L7_roadmaps/AUDIT_CHARON_SESION_3_STATE_V1.md), Ergon E2E fixture diary claim aspirational sin shipped commit verified — filesystem + git history confirma no source file canonical. Charon construye own fixtures basados en vendor copy ValidationAttestation real existente. Pattern análogo Slice 3 mock-WASM + v0.0.16 Ed25519 interim encoding. ## Files canonical - NEW: crates/origin-chain-provenance/src/mock.rs (75 LOC mock runtime) - NEW: crates/origin-chain-provenance/src/tests_integration.rs (270 LOC, 9 tests) - EDIT: crates/origin-chain-provenance/Cargo.toml (sp-core+sp-io+sp-runtime dev-deps) - EDIT: crates/origin-chain-provenance/src/lib.rs (mod mock + mod tests_integration + docstring v0.0.18) - EDIT: CHANGELOG.md (v0.0.18 entry canonical pattern Phase 2.1) ## Verification - cargo test -p origin-chain-provenance — 17 tests verde, 0 failures (9 integration + 6 standalone Ed25519 + 2 auto-generated) - Workspace tests no regressions - Cero warnings críticos 🎉 Phase 2.1 PROVENANCE pallet validated E2E via mock runtime canonical. Co-Authored-By: Claude Opus 4.7 (1M context) --- CHANGELOG.md | 96 ++++++ Cargo.lock | 3 + crates/origin-chain-provenance/Cargo.toml | 8 + crates/origin-chain-provenance/src/lib.rs | 33 +- crates/origin-chain-provenance/src/mock.rs | 77 +++++ .../src/tests_integration.rs | 310 ++++++++++++++++++ 6 files changed, 523 insertions(+), 4 deletions(-) create mode 100644 crates/origin-chain-provenance/src/mock.rs create mode 100644 crates/origin-chain-provenance/src/tests_integration.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 560fd73..b9af44e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,102 @@ by [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Versions follow ## [Unreleased] +## [0.0.18] — 2026-05-24 — Phase 2.1 mock runtime integration tests E2E (own fixtures, sesión 3+) + +**Mock runtime canonical + 9 integration tests E2E PROV-3 flow** — Cierra honest disclosure +sesión 2 ("real chain runtime aún NO arranca por Slice 2e WASM blocker structural"). +Pallet end-to-end validado via FRAME mock runtime composition (`construct_runtime!` macro ++ derive_impl + `sp_io::TestExternalities`), demostrando dispatchable + storage + events +funcionan correctamente sin esperar Slice 2e WASM resolution. + +### Deliverables v0.0.18 + +**Mock runtime canonical (NEW file)**: +- `crates/origin-chain-provenance/src/mock.rs` — 75 LOC canonical +- `construct_runtime!` macro composition: `System` + `Provenance` (mínimo testing) +- `frame_system::Config` impl via `derive_impl(TestDefaultConfig)` + `AccountId32` canonical +- `new_test_ext()` helper canonical (`sp_io::TestExternalities` con `block_number=1` set) +- `alice_origin()` + `bob_origin()` helpers para testing dispatchables +- `TEST_SIGNING_SEED = [7u8; 32]` deterministic Ed25519 keypair canonical + +**Integration tests canonical E2E (NEW file)**: +- `crates/origin-chain-provenance/src/tests_integration.rs` — 270 LOC canonical, 9 tests verde + +**9 integration tests canonical PROV-3 flow** (5 scenarios + 4 additional): + +| # | Test canonical | PROV | Expected outcome | +|---|----------------|------|------------------| +| 1 | `scenario_1_passed_info_anchored_canonical` | PROV-2/3/7 | ✅ AssetAttested event + storage insert | +| 2 | `scenario_2_warning_anchored_canonical` | PROV-2/3/6/7 | ✅ Warning verdict canonical anchorable | +| 3 | `scenario_3_failed_critical_rejected_canonical` | PROV-6 | ❌ NotAnchorable error | +| 4 | `scenario_4_passed_block_security_rejected_canonical` | PROV-6 | ❌ NotAnchorable (Block severity override) | +| 5 | `scenario_5_anonymous_level_0_rejected_canonical` | PROV-5 | ❌ ValidatorTierInsufficient | +| 6 | `additional_idempotency_already_attested_rejected_canonical` | PROV-2 | ❌ AlreadyAttested (idempotency) | +| 7 | `additional_tampered_signature_rejected_canonical` | PROV-4 | ❌ InvalidSignature (bit flip) | +| 8 | `additional_tampered_payload_rejected_canonical` | PROV-4 | ❌ InvalidSignature (payload mismatch) | +| 9 | `additional_6_step_prov_3_walkthrough_canonical` | PROV-3 full | ✅ Pre/post flight assertions | + +**Cargo.toml dev-deps canonical (NEW)**: +- `sp-core` workspace default-features (AccountId32 + crypto canonical) +- `sp-io` workspace default-features (TestExternalities canonical) +- `sp-runtime` workspace default-features (BuildStorage + traits canonical) + +**lib.rs canonical updates**: +- Docstring header updated v0.0.14 → v0.0.18 (status + v0.0.18 section added) +- `#[cfg(test)] pub mod mock;` declaration canonical +- `#[cfg(test)] mod tests_integration;` declaration canonical + +### Own fixtures pattern canonical + +Per audit canonical sesión 3+ (`docs/L7_roadmaps/AUDIT_CHARON_SESION_3_STATE_V1.md`), +Ergon E2E fixture diary claim 2026-05-24T14:11Z (`slice_7_0r_attestation_e2e_fixture.rs` +~520 LOC + JSON 90 LOC) **aspirational sin shipped commit verified** — filesystem + +git history + cross-branch search confirma no source file canonical existe. + +Charon construye own fixtures canonical basados en vendor copy `ValidationAttestation` +real existente en `origin-chain-types/src/attestation.rs`. Builder canonical: + +```rust +fn build_signed_attestation(asset_seed, verdict, severity, oit_level) -> ValidationAttestation { + let signing_key = SigningKey::from_bytes(&TEST_SIGNING_SEED); + let mut att = ValidationAttestation { ... }; + let signature = signing_key.sign(&att.signing_payload()); + att.signature = signature.to_bytes(); + att +} +``` + +Pattern análogo Slice 3 mock-WASM canonical + v0.0.16 Ed25519 con interim encoding — +"avanza work conceptual mientras real (Ergon sub-crate Cap 42+) waits". + +### Verification end-to-end canonical + +- ✅ `cargo test -p origin-chain-provenance` — **17 tests verde, 0 failures**: + - 9 integration tests (`tests_integration` module) + - 6 standalone Ed25519 tests (`tests` module) + - 2 auto-generated (mock genesis + runtime integrity) +- ✅ Workspace tests no regressions +- ✅ Cero new warnings críticos (solo cosmetic missing_docs en construct_runtime macro-generated items) + +### Roadmap canonical post-v0.0.18 + +| Version | Trigger | +|---------|---------| +| v0.0.19 | Swap canonical SCALE → postcard encoding cuando Ergon arkhe-noesis-attestation-types Cap 42+ ship (2026-05-30 → 06-05) | +| **v0.0.20** | **Phase 2.4 pallet-origin-fees scaffold (sesión 3+ shipping ahora)** | +| v0.0.21 | Phase 2.4 fees pallet wired runtime composition Index 8 | +| v0.0.22 | Phase 2.4 fees pallet impl complete + mock runtime integration tests E2E | +| v0.0.23 | Phase 2.4 Runtime API FeesApi canonical → Phase 2.4 COMPLETE | +| v0.1.0 | Slice 2e WASM resolved + full testnet Q3 2026 launch | + +### Filosofía canonical sesión 3+ aplicada + +David canon ratificado 2026-05-14 + lessons sesión 2 internalized: pattern audit-first +comparative (verify Ergon fixture claim → aspirational confirmed → build own) + notify-and-continue +(diary Ergon notify sin requires_ack, sin esperar response) + completeness over speed +(9 scenarios canonical exhaustivos vs minimal demo). "El barquero estudia el río antes +de cruzar — y avanza paralelo cuando el cauce principal espera." + ## [0.0.17] — 2026-05-24 — Phase 2.1 COMPLETE — Runtime API verify_provenance (PROV-8 + 8/8 sub-decisions) **🎉 Phase 2.1 PROVENANCE pallet COMPLETE end-to-end** — 8/8 PROV sub-decisions diff --git a/Cargo.lock b/Cargo.lock index 7537036..8f19496 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2207,6 +2207,9 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-api", + "sp-core", + "sp-io", + "sp-runtime", ] [[package]] diff --git a/crates/origin-chain-provenance/Cargo.toml b/crates/origin-chain-provenance/Cargo.toml index d1afe6a..f6b79b1 100644 --- a/crates/origin-chain-provenance/Cargo.toml +++ b/crates/origin-chain-provenance/Cargo.toml @@ -28,6 +28,14 @@ ed25519-dalek = { workspace = true } parity-scale-codec = { workspace = true } scale-info = { workspace = true } +[dev-dependencies] +# Mock runtime canonical para integration tests v0.0.18 — PROV-3 E2E flow. +# Per pallet Substrate convention: sp-io TestExternalities + sp-runtime +# AccountId32 + frame_support construct_runtime macro. +sp-core = { workspace = true, default-features = true } +sp-io = { workspace = true, default-features = true } +sp-runtime = { workspace = true, default-features = true } + [features] default = ["std"] std = [ diff --git a/crates/origin-chain-provenance/src/lib.rs b/crates/origin-chain-provenance/src/lib.rs index c1cf647..054133a 100644 --- a/crates/origin-chain-provenance/src/lib.rs +++ b/crates/origin-chain-provenance/src/lib.rs @@ -1,19 +1,34 @@ -//! # pallet-origin-provenance — Phase 2.1 scaffold v0.0.14 +//! # pallet-origin-provenance — Phase 2.1 COMPLETE + v0.0.18 mock runtime integration tests //! //! Asset attestation pallet canonical per CHN-7 + DC PROVENANCE_PALLET_V1 -//! (5/10 firmas hoy 2026-05-24, deadline ratificación 2026-05-29). +//! (6/10 firmas sesión 3+ 2026-05-24, deadline ratificación 2026-05-29). //! -//! ## Status canonical: v0.0.16 — Real Ed25519 verification implementada +//! ## Status canonical: v0.0.18 — Mock runtime integration tests E2E own fixtures //! //! Este crate provee: //! - Storage layout canonical `StorageMap` (PROV-2) ✅ //! - Dispatchable canonical `attest_asset(origin, ValidationAttestation)` (PROV-3) ✅ -//! - **Real Ed25519 verify canonical via ed25519-dalek (PROV-4) ✅ v0.0.16** +//! - Real Ed25519 verify canonical via ed25519-dalek (PROV-4) ✅ v0.0.16 //! - OIT Level >= 3 enforcement canonical (PROV-5) ✅ //! - is_anchorable check canonical (PROV-6) ✅ //! - Events canonical `AssetAttested` + `AttestationRejected` (PROV-7) ✅ //! - Errors canonical (PROV-4/5/6) ✅ //! - Pallet Config trait canonical ✅ +//! - Runtime API canonical `verify_provenance` (PROV-8) ✅ v0.0.17 +//! - **Mock runtime + integration tests E2E own fixtures (v0.0.18 sesión 3+) ✅** +//! +//! ## v0.0.18 sesión 3+ mock runtime integration tests +//! +//! Per audit canonical sesión 3+ (`AUDIT_CHARON_SESION_3_STATE_V1.md`), +//! Ergon E2E fixture diary claim aspirational sin shipped commit verified. +//! Charon construye own fixtures basados en vendor copy ValidationAttestation +//! real existente. 9 integration tests canonical: +//! +//! - 5 scenarios PROV-3 flow: passed_info anchored + warning anchored + +//! failed_critical rejected + passed_block_security rejected + anonymous_level_0 rejected +//! - 4 additional: idempotency + tampered_signature + tampered_payload + 6-step walkthrough +//! +//! Mock runtime composition canonical via `construct_runtime!` macro (System + Provenance). //! //! ## Lo que NO funciona aún (post-Ergon sub-crate Cap 42+) //! @@ -287,6 +302,16 @@ pub mod pallet { extern crate alloc; +// v0.0.18 — Mock runtime canonical + integration tests E2E PROV-3 flow. +// Per audit canonical sesión 3+ (`AUDIT_CHARON_SESION_3_STATE_V1.md`), +// own fixtures basados en vendor copy ValidationAttestation real existente +// (no depender de Ergon E2E fixture diary claim aspirational). +#[cfg(test)] +pub mod mock; + +#[cfg(test)] +mod tests_integration; + #[cfg(test)] mod tests { use super::*; diff --git a/crates/origin-chain-provenance/src/mock.rs b/crates/origin-chain-provenance/src/mock.rs new file mode 100644 index 0000000..4a3d5d2 --- /dev/null +++ b/crates/origin-chain-provenance/src/mock.rs @@ -0,0 +1,77 @@ +//! # Mock runtime canonical para integration tests v0.0.18 +//! +//! Provee mock minimal `Test` runtime composition (`System` + `Provenance` solo) +//! para E2E tests dispatchable + storage + event assertions per PROV-3 flow +//! 6-step canonical: +//! +//! 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 + +#![cfg(test)] + +use crate as origin_chain_provenance; +use frame_support::derive_impl; +use sp_runtime::BuildStorage; + +/// Mock block type canonical para tests. +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime!( + pub enum Test { + System: frame_system, + Provenance: origin_chain_provenance, + } +); + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig)] +impl frame_system::Config for Test { + type Block = Block; + type AccountId = sp_runtime::AccountId32; + type Lookup = sp_runtime::traits::IdentityLookup; +} + +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 { + let mut ext: sp_io::TestExternalities = frame_system::GenesisConfig::::default() + .build_storage() + .expect("genesis canonical builds") + .into(); + ext.execute_with(|| { + System::set_block_number(1); + }); + 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 origin account canonical (32-byte zero pattern). +pub fn alice_origin() -> RuntimeOrigin { + RuntimeOrigin::signed(sp_runtime::AccountId32::new([1u8; 32])) +} + +/// Test origin account canonical (BOB pattern). +pub fn bob_origin() -> RuntimeOrigin { + RuntimeOrigin::signed(sp_runtime::AccountId32::new([2u8; 32])) +} diff --git a/crates/origin-chain-provenance/src/tests_integration.rs b/crates/origin-chain-provenance/src/tests_integration.rs new file mode 100644 index 0000000..90b2637 --- /dev/null +++ b/crates/origin-chain-provenance/src/tests_integration.rs @@ -0,0 +1,310 @@ +//! # Integration tests canonical v0.0.18 — Mock runtime E2E +//! +//! 5 scenarios canonical PROV-3 attest_asset flow end-to-end via mock runtime: +//! +//! | Scenario | Verdict | Severity | OIT Level | Expected outcome | +//! |----------|---------|----------|-----------|------------------| +//! | passed_info_anchored | Passed | Info | 3 | ✅ AssetAttested event + storage insert | +//! | warning_anchored | Warning | Warning | 3 | ✅ AssetAttested event + storage insert | +//! | failed_critical_rejected | Failed | Critical | 3 | ❌ NotAnchorable error | +//! | passed_block_security_rejected | Passed | Block | 3 | ❌ NotAnchorable error | +//! | anonymous_level_0_rejected | Passed | Info | 0 | ❌ ValidatorTierInsufficient error | +//! +//! Plus 4 additional canonical: +//! - idempotency: already attested asset rejected con `AlreadyAttested` error +//! - tampered_signature: Ed25519 verify fails con `InvalidSignature` error +//! - tampered_payload: signing_payload mismatch con `InvalidSignature` error +//! - 6_step_walkthrough: explicit PROV-3 flow assertions in sequence +//! +//! Total: 9 integration tests canonical. +//! +//! ## Own fixtures pattern +//! +//! Per audit canonical sesión 3+ (`AUDIT_CHARON_SESION_3_STATE_V1.md`), Ergon +//! E2E fixture diary claim aspirational sin shipped commit verified. +//! Charon construye own fixtures basados en vendor copy `ValidationAttestation` +//! real existente en `origin-chain-types/src/attestation.rs`. Cuando Ergon +//! ship `arkhe-noesis-attestation-types` sub-crate Cap 42+, fixtures can swap. + +#![cfg(test)] + +use crate::mock::{alice_origin, bob_origin, new_test_ext, Provenance, RuntimeEvent, System, Test, TEST_SIGNING_SEED}; +use crate::{Attestations, Error, Event}; +use ed25519_dalek::{Signer, SigningKey}; +use frame_support::{assert_noop, assert_ok}; +use origin_chain_types::{ + AssetHash, OitIdentity, SeverityTier, ValidationAttestation, ValidationVerdict, +}; + +// ─── Fixture builders canonical ────────────────────────────────────── + +/// Build canonical signed ValidationAttestation con (verdict, severity, oit_level) +/// parameters. Deterministic Ed25519 signature. +/// +/// asset_id es derivado de seed para uniqueness per scenario. +fn build_signed_attestation( + asset_seed: u8, + verdict: ValidationVerdict, + severity: SeverityTier, + oit_level: u8, +) -> ValidationAttestation { + let signing_key = SigningKey::from_bytes(&TEST_SIGNING_SEED); + let verifying_key = signing_key.verifying_key(); + + let mut att = ValidationAttestation { + schema_version: *b"1.0.0 ", + asset_id: AssetHash::new([asset_seed; 32]), + validator_oit: OitIdentity { + public_key: verifying_key.to_bytes(), + level: oit_level, + }, + validated_at_us: 1_234_567_890, + verdict, + severity, + constraints_checked: 42, + signature: [0u8; 64], // placeholder, replaced post sign + }; + + let payload = att.signing_payload(); + let signature = signing_key.sign(&payload); + att.signature = signature.to_bytes(); + att +} + +/// Build canonical fixture passed_info (most common anchorable scenario). +fn fixture_passed_info(asset_seed: u8) -> ValidationAttestation { + build_signed_attestation(asset_seed, ValidationVerdict::Passed, SeverityTier::Info, 3) +} + +// ─── Scenario 1: passed_info anchored canonical ────────────────────── + +#[test] +fn scenario_1_passed_info_anchored_canonical() { + new_test_ext().execute_with(|| { + let attestation = fixture_passed_info(1); + let asset_hash = attestation.asset_id; + + // PROV-3 attest_asset extrinsic canonical + assert_ok!(Provenance::attest_asset(alice_origin(), attestation.clone())); + + // PROV-2 storage insert verified + let stored = Attestations::::get(asset_hash); + assert!(stored.is_some(), "PROV-2 canonical: AssetAttestation inserted in StorageMap"); + let stored_unwrap = stored.unwrap(); + assert_eq!(stored_unwrap.hash, asset_hash); + assert_eq!(stored_unwrap.signature.as_bytes(), &attestation.signature); + + // PROV-7 event emitted canonical + System::assert_last_event(RuntimeEvent::Provenance(Event::AssetAttested { + asset_hash, + block_number: 1, + })); + }); +} + +// ─── Scenario 2: warning anchored canonical (non-blocking) ──────────── + +#[test] +fn scenario_2_warning_anchored_canonical() { + new_test_ext().execute_with(|| { + let attestation = build_signed_attestation( + 2, + ValidationVerdict::Warning, + SeverityTier::Warning, + 3, + ); + let asset_hash = attestation.asset_id; + + assert_ok!(Provenance::attest_asset(alice_origin(), attestation)); + + assert!(Attestations::::get(asset_hash).is_some()); + System::assert_last_event(RuntimeEvent::Provenance(Event::AssetAttested { + asset_hash, + block_number: 1, + })); + }); +} + +// ─── Scenario 3: failed_critical rejected canonical (PROV-6) ────────── + +#[test] +fn scenario_3_failed_critical_rejected_canonical() { + new_test_ext().execute_with(|| { + let attestation = build_signed_attestation( + 3, + ValidationVerdict::Failed, + SeverityTier::Critical, + 3, + ); + let asset_hash = attestation.asset_id; + + // PROV-6 is_anchorable canonical check fails (Failed verdict + Critical severity) + assert_noop!( + Provenance::attest_asset(alice_origin(), attestation), + Error::::NotAnchorable + ); + + // Storage NOT inserted + assert!(Attestations::::get(asset_hash).is_none()); + }); +} + +// ─── Scenario 4: passed_block security rejected canonical (PROV-6) ──── + +#[test] +fn scenario_4_passed_block_security_rejected_canonical() { + new_test_ext().execute_with(|| { + // Passed verdict pero Block severity (security violation override) + let attestation = build_signed_attestation( + 4, + ValidationVerdict::Passed, + SeverityTier::Block, + 3, + ); + let asset_hash = attestation.asset_id; + + // PROV-6 is_anchorable rejects Block severity even with Passed verdict + assert_noop!( + Provenance::attest_asset(alice_origin(), attestation), + Error::::NotAnchorable + ); + + assert!(Attestations::::get(asset_hash).is_none()); + }); +} + +// ─── Scenario 5: anonymous_level_0 rejected canonical (PROV-5) ──────── + +#[test] +fn scenario_5_anonymous_level_0_rejected_canonical() { + new_test_ext().execute_with(|| { + // Valid verdict + severity, pero OIT level 0 (anonymous) — PROV-5 reject + let attestation = build_signed_attestation( + 5, + ValidationVerdict::Passed, + SeverityTier::Info, + 0, // ANONYMOUS — institutional level >= 3 required + ); + let asset_hash = attestation.asset_id; + + // PROV-5 validator OIT tier check canonical fails + assert_noop!( + Provenance::attest_asset(alice_origin(), attestation), + Error::::ValidatorTierInsufficient + ); + + assert!(Attestations::::get(asset_hash).is_none()); + }); +} + +// ─── Additional canonical: idempotency ──────────────────────────────── + +#[test] +fn additional_idempotency_already_attested_rejected_canonical() { + new_test_ext().execute_with(|| { + let attestation = fixture_passed_info(6); + let asset_hash = attestation.asset_id; + + // First attest_asset succeeds canonical + assert_ok!(Provenance::attest_asset(alice_origin(), attestation.clone())); + assert!(Attestations::::get(asset_hash).is_some()); + + // Second attest_asset (same hash) — idempotency canonical rejects + assert_noop!( + Provenance::attest_asset(bob_origin(), attestation), + Error::::AlreadyAttested + ); + }); +} + +// ─── Additional canonical: tampered signature (PROV-4) ──────────────── + +#[test] +fn additional_tampered_signature_rejected_canonical() { + new_test_ext().execute_with(|| { + let mut attestation = fixture_passed_info(7); + let asset_hash = attestation.asset_id; + + // Flip bit en signature canonical post-sign + attestation.signature[0] ^= 0xFF; + + // PROV-4 Ed25519 verify canonical fails + assert_noop!( + Provenance::attest_asset(alice_origin(), attestation), + Error::::InvalidSignature + ); + + assert!(Attestations::::get(asset_hash).is_none()); + }); +} + +// ─── Additional canonical: tampered payload (PROV-4) ────────────────── + +#[test] +fn additional_tampered_payload_rejected_canonical() { + new_test_ext().execute_with(|| { + let mut attestation = fixture_passed_info(8); + let original_asset_hash = attestation.asset_id; + + // Tamper asset_id post-sign — cambia signing_payload pero mantiene signature original + attestation.asset_id = AssetHash::new([99u8; 32]); + let new_asset_hash = attestation.asset_id; + + // PROV-4 signing_payload mismatch canonical, Ed25519 verify fails + assert_noop!( + Provenance::attest_asset(alice_origin(), attestation), + Error::::InvalidSignature + ); + + // Ninguno de los dos hashes está en storage + assert!(Attestations::::get(original_asset_hash).is_none()); + assert!(Attestations::::get(new_asset_hash).is_none()); + }); +} + +// ─── Additional canonical: 6-step PROV-3 walkthrough explicit ───────── + +#[test] +fn additional_6_step_prov_3_walkthrough_canonical() { + new_test_ext().execute_with(|| { + let attestation = fixture_passed_info(9); + let asset_hash = attestation.asset_id; + + // Pre-flight assertions canonical + assert!( + attestation.is_anchorable(), + "PRE-flight PROV-6: fixture anchorable canonical" + ); + assert!( + attestation.validator_oit.is_institutional(), + "PRE-flight PROV-5: validator institutional canonical" + ); + assert!( + Attestations::::get(asset_hash).is_none(), + "PRE-flight PROV-2: storage empty para asset_hash canonical" + ); + + // Step 1: PROV-4 canonical Ed25519 signature verification (implicit via dispatchable) + // Step 2: PROV-5 canonical validate validator_oit.level >= 3 institutional + // Step 3: PROV-6 canonical check is_anchorable (verdict + severity) + // Step 4: PROV-2 canonical convert ValidationAttestation → AssetAttestation + // Step 5: PROV-2 canonical StorageMap insert + // Step 6: PROV-7 canonical emit AssetAttested event + assert_ok!(Provenance::attest_asset(alice_origin(), attestation.clone())); + + // Post-flight assertions canonical + let stored = Attestations::::get(asset_hash) + .expect("POST-flight PROV-2: AssetAttestation persisted canonical"); + assert_eq!(stored.hash, asset_hash, "POST-flight: hash preserved"); + assert_eq!( + stored.signature.as_bytes(), + &attestation.signature, + "POST-flight: signature preserved canonical" + ); + + System::assert_last_event(RuntimeEvent::Provenance(Event::AssetAttested { + asset_hash, + block_number: 1, + })); + }); +}