//! # 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 //! (6/10 firmas sesión 3+ 2026-05-24, deadline ratificación 2026-05-29). //! //! ## 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 //! - 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+) //! //! - Real `ValidationAttestation` consumption — usa vendor copy interim Option B //! (signing_payload usa SCALE encoding canonical; Ergon real sub-crate usará //! postcard encoding — swap canonical post Cap 42+ sin breaking pallet logic) //! - Runtime API canonical `verify_provenance` (PROV-8) — v0.0.17 next slice //! //! Per Ergon decision (diary `3f773a03` 2026-05-24): él ships //! `arkhe-noesis-attestation-types` standalone canonical Cap 42+ (target //! 2026-05-30 → 06-05). Mientras tanto, mi vendor copy en //! `origin-chain-types::attestation` permite avanzar canonical compile-only. //! //! ## Roadmap canonical post-ratificación DC PROVENANCE_PALLET_V1 //! //! 1. **v0.0.14 (ESTA)**: Scaffold skeleton canonical compile-only //! 2. **v0.0.15** (post 2026-05-29 DC ratify): Storage + events + errors wired //! 3. **v0.0.16** (post 2026-06-05 Ergon sub-crate): Real Ed25519 + OIT verify //! 4. **v0.0.17**: Runtime API canonical verify_provenance + wire en runtime //! 5. **v0.1.0** (post Q3 2026 testnet): full integration end-to-end //! //! ## Cross-pillar consumers canonical desbloqueados (post-impl) //! //! - Trinity (Phanes) asset mesh BLAKE3 → `attest_asset` canonical //! - Codex (.odoc) chain_attestation block populated by Vault tier //! - Ethos (Sussi) credential NFT mint + chain attestation //! - Atelier (Marihan) skill NFT mint via institutional wallet //! - Origindocs (David ratificado 2026-05-24): academic IP defensible #![cfg_attr(not(feature = "std"), no_std)] #![warn(missing_docs)] pub use pallet::*; /// Weights canonical (INTERIM hand-derived DB-aware — ver `weights.rs` §Estado). pub mod weights; pub use weights::WeightInfo; /// Re-export canonical types desde origin-chain-types para consumers. pub use origin_chain_types::{ AssetAttestation, AssetHash, OitIdentity, SeverityTier, ValidationAttestation, ValidationVerdict, }; /// PROV-8 canonical: Runtime API canonical para light clients + indexers + /// cross-pillar consumers. /// /// Permite query canonical de attestation status sin necesidad de full node /// access — light client envía hash → recibe `Option` con /// signature canonical + creator + block number canonical + agent chain. /// /// Consumers canonical: /// - Trinity (Phanes) — asset gallery render con attestation badge /// - Codex (.odoc) — Vault tier verify-before-render canonical /// - Codex origindocs — academic IP defensible verify /// - Ethos (Sussi) — credential verification canonical /// - Atelier (Marihan) — Skill Library Zona 6 verify-before-purchase /// /// Future Phase 2.3 expansion canonical: /// - `verify_by_creator(creator) -> Vec` (paginated) /// - `list_attestations_in_block(block_num) -> Vec` /// - `cross_runtime_proof(hash) -> CrossRuntimeProof` sp_api::decl_runtime_apis! { /// Provenance runtime API canonical canonical (PROV-8). pub trait ProvenanceApi { /// Verify provenance canonical de un asset por su hash canonical. /// /// Returns `Some(AssetAttestation)` si attested previamente on-chain, /// `None` si nunca attested canonical. Read-only query canonical sin /// transaction fee canonical (off-chain via RPC). fn verify_provenance(hash: AssetHash) -> Option; } } #[frame_support::pallet] pub mod pallet { use frame_support::pallet_prelude::*; use frame_system::pallet_prelude::*; use frame_support::sp_runtime::traits::{SaturatedConversion, Zero}; use origin_chain_types::{ attestation_signed_message, decode_signing_payload, AssetAttestation, AssetHash, }; // Trait de weights en scope para resolver `T::WeightInfo::()`. use crate::weights::WeightInfo; /// Cota DoS del `signing_payload` postcard recibido (el real ~115 bytes; /// 512 es holgado para schema/identifier futuros sin permitir flooding). pub type MaxPayloadLen = ConstU32<512>; /// Pallet configuration trait canonical. /// /// Per DC PROVENANCE_PALLET_V1 PROV-3 + ADR-0057 trait-seam discipline. #[pallet::config] pub trait Config: frame_system::Config { /// Runtime event type canonical wired into `RuntimeEvent` aggregate. type RuntimeEvent: From> + IsType<::RuntimeEvent>; /// Weight information canonical para los extrinsics de este pallet. /// El runtime lo cablea a `weights::SubstrateWeight`; los mocks /// usan `()`. Ver `weights.rs` + `benchmarking.rs`. type WeightInfo: crate::weights::WeightInfo; // Future canonical (post OCAFB ratify): // type OitFeeSchedule: arkhe_amis_server::OitFeeSchedule; } /// Pallet definition canonical. /// /// v0.0.15: `AssetAttestation.agent_chain` ahora es `BoundedVec>` canonical → MaxEncodedLen ahora satisfied canonical → /// `#[pallet::without_storage_info]` ya NO necesario canonical. /// StorageVersion en código (FIX-2 lifecycle, audit V1-H2/V2-CA2): arranca /// en 0 (genesis). Habilita migraciones futuras vía on_chain_storage_version(). const STORAGE_VERSION: StorageVersion = StorageVersion::new(0); #[pallet::pallet] #[pallet::storage_version(STORAGE_VERSION)] pub struct Pallet(_); /// PROV-2 canonical: `StorageMap` canonical. /// /// O(1) lookup canonical via Blake2_128Concat hasher (FRAME-native canonical). /// Memory canonical ~140 bytes per attestation canonical. #[pallet::storage] #[pallet::getter(fn attestation_of)] pub type Attestations = StorageMap<_, Blake2_128Concat, AssetHash, AssetAttestation, OptionQuery>; /// FIX-4 (cierra el HIGH "asset-id squat"): registro on-chain de validadores /// Noesis autorizados → nivel OIT asignado por governance. **La membresía es /// la VERDAD**, no el `level` que el firmante pone en su propio payload /// (que era self-attested → cualquiera con una clave Ed25519 podía anclar /// basura institucional bajo el hash público de un asset legítimo y /// envenenarlo de forma inmutable). Ahora solo una pubkey REGISTRADA puede /// anclar; un validador registrado que ancle basura es revocable y auditable /// (problema de governance, no ataque anónimo). Mismo patrón que /// `TierProfiles` de FIX-6 Stage 2a: legitimidad on-chain, no caller-supplied. /// /// **Root-only interim** (sudo Index 12 / governance). Sunset: cuando el /// registro OIT real de Mathi (PerceptionAdapter + DID binding) aterrice, /// migra a auto-registro attestation-driven. Cross-link futuro: AgentIdentity /// (Index 9). #[pallet::storage] #[pallet::getter(fn authorized_validator_level)] pub type AuthorizedValidators = StorageMap<_, Blake2_128Concat, [u8; 32], u8, OptionQuery>; /// PROV-7 canonical: Events emisión canonical. #[pallet::event] #[pallet::generate_deposit(pub(super) fn deposit_event)] pub enum Event { /// Asset attestation canonical anchored on-chain (PROV-7). AssetAttested { /// BLAKE3 content hash canonical del asset attested. asset_hash: AssetHash, /// Block number canonical at which attestation landed. block_number: BlockNumberFor, }, /// Attestation rejected canonical con reason canonical (PROV-7). AttestationRejected { /// BLAKE3 content hash canonical que falló validation. asset_hash: AssetHash, /// Reason canonical de rejection per RejectionReason enum. reason: RejectionReason, }, /// FIX-4: validador Noesis autorizado (Root) con su nivel OIT. Auditable /// on-chain: quién quedó habilitado para anclar y a qué nivel. ValidatorRegistered { /// Ed25519 public key del validador autorizado. public_key: [u8; 32], /// Nivel OIT asignado por governance. level: u8, }, /// FIX-4: validador revocado (Root) — ya no puede anclar. ValidatorRevoked { /// Ed25519 public key del validador revocado. public_key: [u8; 32], }, } /// Rejection reasons canonical PROV-7. /// /// **Marihan refinement Phase 2.2 noted** (DC PROVENANCE_PALLET_V1 signature /// `2e6b8469`): añadir variant `MalformedValidationAttestation` para casos /// donde `signing_payload()` falle parse pre-Ed25519-verify. Non-blocking /// Phase 2.1 — incluido aquí canonical anticipated. #[derive( Encode, Decode, DecodeWithMemTracking, MaxEncodedLen, TypeInfo, Clone, Copy, PartialEq, Eq, RuntimeDebug, )] pub enum RejectionReason { /// Ed25519 signature verification canonical failed (PROV-4). InvalidSignature, /// Validator OIT level canonical < 3 (institutional required PROV-5). ValidatorTierInsufficient, /// Attestation NOT anchorable canonical per is_anchorable check (PROV-6). NotAnchorable, /// Asset hash canonical already attested previamente (idempotency canonical). AlreadyAttested, /// Validation attestation canonical malformed (Marihan refinement Phase 2.2). MalformedValidationAttestation, } /// Pallet errors canonical (PROV-4/5/6). #[pallet::error] pub enum Error { /// PROV-4 canonical: Ed25519 signature verification canonical failed. InvalidSignature, /// PROV-5 canonical: validator OIT level canonical insufficient (< 3). ValidatorTierInsufficient, /// PROV-6 canonical: attestation NOT anchorable canonical (verdict/severity). NotAnchorable, /// PROV-2 canonical: asset hash canonical already attested previamente. AlreadyAttested, /// Marihan refinement Phase 2.2 noted canonical. MalformedValidationAttestation, /// FIX-4: la pubkey del firmante NO está en el registro on-chain de /// validadores autorizados (cierra el squat — solo validadores /// registrados por governance pueden anclar). ValidatorNotAuthorized, } /// Dispatchables (PROV-3: attest_asset). #[pallet::call] impl Pallet { /// PROV-3 + FIX-4: ancla una attestation Noesis verificada on-chain. /// /// El validador firma OFF-CHAIN (con el helper de Ergon /// `attestation_signed_message`) sobre `TAG ‖ genesis_hash ‖ payload`, /// donde `payload` es el `SigningPayload` postcard (Slice 7.0r). El /// extrinsic recibe esos **bytes postcard opacos** + la firma; el pallet /// re-deriva el mensaje y verifica — NUNCA re-serializa (contrato ASP-3 /// DC `076d15cc`). El `public_key` se DECODIFICA del payload (no es un /// parámetro aparte: la firma prueba que esa clave firmó esta attestation /// para esta cadena). /// /// Flow (gates baratos antes del crypto caro, patrón FIX-3): /// 1. `ensure_signed` — el submitter (relayer/pagador del fee). /// 2. Decode postcard → (asset_id, public_key, level, verdict, severity). /// 3. **Dedup-by-BLAKE3 con RECHAZO EXPLÍCITO**: la storage key ES el /// `asset_id` (= BLAKE3 content hash del asset); un replay del mismo /// attestation firmado reusa el mismo asset_id → `AlreadyAttested`. /// (Decisión Pilar 4 sesión 6+: replay = rechazo explícito, no dedup /// silencioso.) Va ANTES del verify para no gastar crypto en replays. /// 4. FIX-4: gate de legitimidad ON-CHAIN — la pubkey del firmante debe /// estar en `AuthorizedValidators` (registro governance) con nivel /// >= 3. NO se confía en el `level` self-attested del payload (cierra /// el squat + tier-forgery). /// 5. PROV-6: gate anchorable (verdict Passed|Warning, severity Info|Warning). /// 6. FIX-4/V3-C1: Ed25519 verify via `sp_io::crypto::ed25519_verify` /// (host function, no bloatea wasm) sobre el mensaje domain-separado /// con el `genesis_hash` de ESTA cadena (cierra replay cross-chain). /// 7. Insert `AssetAttestation` (creator = validador, block real) + evento. #[pallet::call_index(0)] #[pallet::weight(T::WeightInfo::attest_asset())] pub fn attest_asset( origin: OriginFor, signing_payload: BoundedVec, signature: [u8; 64], ) -> DispatchResult { // 1. Origin signed (submitter — paga el fee; el creator semántico es // el validador cuya clave está dentro del payload firmado). let _submitter = ensure_signed(origin)?; // 2. Decode postcard del SigningPayload real de Ergon (total, DoS-safe). let decoded = decode_signing_payload(&signing_payload) .map_err(|_| Error::::MalformedValidationAttestation)?; let asset_hash = AssetHash::new(decoded.asset_id); // 3. Dedup-by-BLAKE3 (asset_id) — rechazo explícito de replay, antes // del crypto (FIX-3 discipline). ensure!( !Attestations::::contains_key(asset_hash), Error::::AlreadyAttested ); // 4. FIX-4: legitimidad ON-CHAIN (cierra el squat + tier-forgery). // La pubkey del firmante DEBE estar en el registro de validadores // autorizados; el nivel es el del REGISTRO (governance), NO el // `level` self-attested del payload. Sin esto, cualquiera con una // clave Ed25519 anclaba basura institucional bajo el hash público // de un asset legítimo. let authorized_level = AuthorizedValidators::::get(decoded.public_key) .ok_or(Error::::ValidatorNotAuthorized)?; ensure!(authorized_level >= 3, Error::::ValidatorTierInsufficient); // 5. PROV-6: anchorable (Inmutable #26 — una vez anclado es inmutable). ensure!(decoded.is_anchorable(), Error::::NotAnchorable); // 6. FIX-4 (V3-C1): re-derivar el mensaje domain-separado con el // genesis_hash de ESTA cadena y verificar Ed25519 sobre los BYTES // recibidos (nunca re-serializar). Host function = no wasm bloat. let genesis_full = >::block_hash(BlockNumberFor::::zero()); let genesis_ref: &[u8] = genesis_full.as_ref(); let mut genesis_hash = [0u8; 32]; let n = genesis_ref.len().min(32); genesis_hash[..n].copy_from_slice(&genesis_ref[..n]); let message = attestation_signed_message(&signing_payload, &genesis_hash); let sig = sp_core::ed25519::Signature::from_raw(signature); let pubkey = sp_core::ed25519::Public::from_raw(decoded.public_key); ensure!( sp_io::crypto::ed25519_verify(&sig, &message, &pubkey), Error::::InvalidSignature ); // 7. Anclar: creator = el validador (su Ed25519 pubkey), block real. let block_number = >::block_number(); let attestation = AssetAttestation { hash: asset_hash, creator: origin_chain_types::AccountId::new(decoded.public_key), signature: origin_chain_types::Signature::new(signature), anchored_at_block: block_number.saturated_into::(), agent_chain: origin_chain_types::AgentChain::default(), }; Attestations::::insert(asset_hash, attestation); Self::deposit_event(Event::AssetAttested { asset_hash, block_number }); Ok(()) } /// FIX-4: registra (o re-nivela) un validador Noesis autorizado a anclar. /// **Root-only** (sudo Index 12 / governance) — cierra el squat: solo /// validadores que governance habilita pueden anclar. Sunset a /// attestation-driven cuando el registro OIT de Mathi aterrice. #[pallet::call_index(1)] #[pallet::weight(T::WeightInfo::register_validator())] pub fn register_validator( origin: OriginFor, public_key: [u8; 32], level: u8, ) -> DispatchResult { ensure_root(origin)?; AuthorizedValidators::::insert(public_key, level); Self::deposit_event(Event::ValidatorRegistered { public_key, level }); Ok(()) } /// FIX-4: revoca un validador — ya no puede anclar. **Root-only.** #[pallet::call_index(2)] #[pallet::weight(T::WeightInfo::revoke_validator())] pub fn revoke_validator( origin: OriginFor, public_key: [u8; 32], ) -> DispatchResult { ensure_root(origin)?; AuthorizedValidators::::remove(public_key); Self::deposit_event(Event::ValidatorRevoked { public_key }); Ok(()) } } } extern crate alloc; /// Benchmarks canonical (FRAME v2) — solo bajo la feature `runtime-benchmarks`. /// Verificados por `impl_benchmark_test_suite!` en el mock (ver `benchmarking.rs`). #[cfg(feature = "runtime-benchmarks")] mod benchmarking; // 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::*; // FIX-4: la verificación Ed25519 real (sp_io host function sobre el mensaje // domain-separado, con golden vectors del oráculo de Ergon) vive en // `tests_integration`. El decoder postcard + el mensaje firmado se prueban // en `origin-chain-types::attestation_verify`. Aquí quedan compile-checks. #[test] fn pallet_compiles_canonical() { // Verified by cargo check. E2E del fee-path verify = tests_integration. } #[test] fn rejection_reason_variants_canonical() { // Verify enum variants canonical exist + Marihan refinement included. let r1 = RejectionReason::InvalidSignature; let r2 = RejectionReason::ValidatorTierInsufficient; let _r3 = RejectionReason::NotAnchorable; let r4 = RejectionReason::AlreadyAttested; let r5 = RejectionReason::MalformedValidationAttestation; assert_ne!( core::mem::discriminant(&r1), core::mem::discriminant(&r2) ); assert_ne!( core::mem::discriminant(&r4), core::mem::discriminant(&r5) ); } }