//! # 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::*; /// 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 origin_chain_types::{AssetAttestation, AssetHash, ValidationAttestation}; /// 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>; // 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>; /// 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, }, } /// 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, } /// Dispatchables canonical (PROV-3 canonical: attest_asset). #[pallet::call] impl Pallet { /// PROV-3 canonical: `attest_asset` extrinsic canonical. /// /// Submits a `ValidationAttestation` (Ergon Slice 7.0r struct) para /// anchoring canonical on-chain. Flow canonical 6-step per DC PROV-3: /// /// 1. PROV-4 canonical: Ed25519 signature verification canonical /// 2. PROV-5 canonical: validate `validator_oit.level >= 3` institutional /// 3. PROV-6 canonical: check `is_anchorable()` (verdict + severity) /// 4. PROV-2 canonical: convert ValidationAttestation → AssetAttestation /// 5. PROV-2 canonical: insert StorageMap canonical /// 6. PROV-7 canonical: emit AssetAttested event canonical /// /// **SCAFFOLD v0.0.14 canonical**: real impl deferred post Ergon Cap 42+ /// sub-crate ship + Ed25519 verification wire-up. Current canonical solo /// validates basic state (origin signed + not already attested) + emite /// placeholder event canonical. #[pallet::call_index(0)] #[pallet::weight(Weight::from_parts(10_000, 0))] pub fn attest_asset( origin: OriginFor, validation_attestation: ValidationAttestation, ) -> DispatchResult { // Verify origin canonical signed. let _signer = ensure_signed(origin)?; let asset_hash = validation_attestation.asset_id; // PROV-2 idempotency check canonical. ensure!( !Attestations::::contains_key(asset_hash), Error::::AlreadyAttested ); // PROV-5 OIT level >= 3 enforcement canonical. ensure!( validation_attestation.validator_oit.is_institutional(), Error::::ValidatorTierInsufficient ); // PROV-6 is_anchorable check canonical. ensure!( validation_attestation.is_anchorable(), Error::::NotAnchorable ); // PROV-4 canonical: Real Ed25519 verification via ed25519-dalek (v0.0.16). // // Pattern canonical: // 1. Reconstruct VerifyingKey desde validator_oit.public_key (32 bytes) // 2. Reconstruct Signature desde attestation.signature (64 bytes) // 3. Compute signing_payload canonical (SCALE encoding interim Option B) // 4. verify_strict canonical (rejects malleable signatures + zero-keys) // // Cuando Ergon arkhe-noesis-attestation-types ship Cap 42+, swap canonical // a postcard encoding canonical sin breaking pallet logic — verify call site // remains identical canonical. let pubkey = ed25519_dalek::VerifyingKey::from_bytes( &validation_attestation.validator_oit.public_key, ) .map_err(|_| Error::::InvalidSignature)?; let signature = ed25519_dalek::Signature::from_bytes( &validation_attestation.signature, ); let payload = validation_attestation.signing_payload(); pubkey .verify_strict(&payload, &signature) .map_err(|_| Error::::InvalidSignature)?; // PROV-2 canonical: convert + insert canonical. // SCAFFOLD: minimal AssetAttestation construction (without creator/sig // mapping canonical — that's Ergon sub-crate concern post Cap 42+). let attestation = AssetAttestation { hash: asset_hash, creator: origin_chain_types::AccountId::new([0u8; 32]), // SCAFFOLD placeholder signature: origin_chain_types::Signature::new(validation_attestation.signature), anchored_at_block: 0, // SCAFFOLD — block_number wire post v0.0.16 agent_chain: origin_chain_types::AgentChain::default(), // BoundedVec empty }; Attestations::::insert(asset_hash, attestation); // PROV-7 emit event canonical. let block_number = >::block_number(); Self::deposit_event(Event::AssetAttested { asset_hash, block_number, }); Ok(()) } } } 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::*; use ed25519_dalek::{Signer, SigningKey}; use origin_chain_types::{OitIdentity, SeverityTier, ValidationAttestation, ValidationVerdict}; // Compile-time + Ed25519 verify standalone tests canonical. // Mock runtime integration tests canonical = Phase 2.1 v0.0.17 next slice. fn signed_attestation(verdict: ValidationVerdict, severity: SeverityTier) -> ValidationAttestation { let signing_key = SigningKey::from_bytes(&[7u8; 32]); let verifying_key = signing_key.verifying_key(); let mut att = ValidationAttestation { schema_version: *b"1.0.0 ", asset_id: origin_chain_types::AssetHash::new([1u8; 32]), validator_oit: OitIdentity { public_key: verifying_key.to_bytes(), level: 3, }, validated_at_us: 1234567890, 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 } #[test] fn ed25519_verify_real_valid_signature_canonical() { // PROV-4 canonical: signed attestation canonical verifies correctamente. let att = signed_attestation(ValidationVerdict::Passed, SeverityTier::Info); let pubkey = ed25519_dalek::VerifyingKey::from_bytes( &att.validator_oit.public_key, ) .expect("valid pubkey canonical"); let sig = ed25519_dalek::Signature::from_bytes(&att.signature); let payload = att.signing_payload(); assert!(pubkey.verify_strict(&payload, &sig).is_ok()); } #[test] fn ed25519_verify_tampered_signature_rejected_canonical() { // PROV-4 canonical: tampered signature canonical rejected. let mut att = signed_attestation(ValidationVerdict::Passed, SeverityTier::Info); // Flip a bit en signature canonical. att.signature[0] ^= 0xFF; let pubkey = ed25519_dalek::VerifyingKey::from_bytes( &att.validator_oit.public_key, ) .expect("valid pubkey canonical"); let sig = ed25519_dalek::Signature::from_bytes(&att.signature); let payload = att.signing_payload(); assert!(pubkey.verify_strict(&payload, &sig).is_err()); } #[test] fn ed25519_verify_tampered_payload_rejected_canonical() { // PROV-4 canonical: tampered payload canonical (signing_payload mismatch) rejected. let mut att = signed_attestation(ValidationVerdict::Passed, SeverityTier::Info); let original_sig = att.signature; // Flip a bit en asset_id (cambia signing_payload pero mantiene signature original). att.asset_id = origin_chain_types::AssetHash::new([99u8; 32]); let pubkey = ed25519_dalek::VerifyingKey::from_bytes( &att.validator_oit.public_key, ) .expect("valid pubkey canonical"); let sig = ed25519_dalek::Signature::from_bytes(&original_sig); let payload = att.signing_payload(); // payload canonical NEW (con asset_id tampered) assert!(pubkey.verify_strict(&payload, &sig).is_err()); } #[test] fn ed25519_zero_signature_fails_verify_canonical() { // PROV-4 canonical: all-zero signature canonical rejected por verify_strict. // (Note: ed25519-dalek `from_bytes` acepta all-zero pubkey/sig — son // valid Edwards points; el rejection canonical pasa en verify_strict.) let signing_key = SigningKey::from_bytes(&[7u8; 32]); let verifying_key = signing_key.verifying_key(); let payload = b"any payload canonical"; let zero_sig = ed25519_dalek::Signature::from_bytes(&[0u8; 64]); assert!(verifying_key.verify_strict(payload, &zero_sig).is_err()); } #[test] fn pallet_scaffold_compiles_canonical() { // Scaffold compiles — verified by cargo check. // Real mock runtime integration tests = Phase 2.1 v0.0.17. } #[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) ); } }