diff --git a/crates/origin-chain-agent-identity/src/lib.rs b/crates/origin-chain-agent-identity/src/lib.rs index 0e095c1..1219993 100644 --- a/crates/origin-chain-agent-identity/src/lib.rs +++ b/crates/origin-chain-agent-identity/src/lib.rs @@ -86,8 +86,13 @@ pub fn derive_did(public_key: &[u8; 32]) -> Did { // ⚠️ Cambia el DID para un mismo public_key vs el esquema previo — aceptable // pre-mainnet (greenfield): verificado que NINGÚN estado on-chain depende del // esquema viejo (sin GenesisConfig, sin DIDs en presets, StorageVersion 0). + // + // Condición #3 de Mathi (ratificada): `Did` es un NEWTYPE, no un alias de + // `AssetHash` — con el contexto compartido, un alias dejaría al compilador + // aceptar un hash de asset donde se espera una identidad. Ver `Did` en + // origin-chain-types::identity. let did_bytes = blake3::derive_key(ORIGIN_IDENTITY_KDF_CONTEXT, public_key); - origin_chain_types::AssetHash::new(did_bytes) + Did::new(did_bytes) } /// Contexto KDF **canónico de identidad de Origin** — DC ORIGIN IDENTITY @@ -514,7 +519,32 @@ mod tests { fn re_export_agent_identity_canonical() { // Verify re-exports canonical desde origin-chain-types let _kind: AgentKind = AgentKind::AGI; - let _did_alias: Did = origin_chain_types::AssetHash::new([0u8; 32]); + let _did: Did = Did::new([0u8; 32]); + } + + // Condición #3 de Mathi (vinculante, DC ratificado 2026-07-29). + // + // Este test reemplaza al que ANTES afirmaba lo contrario: + // let _did_alias: Did = AssetHash::new([0u8; 32]); // compilaba: era alias + // Ese assert pinneaba justo el defecto — que un hash de asset se aceptara como + // identidad. Con el contexto KDF compartido eso es peor: ambos son "32 bytes + // canónicos de Origin" y el compilador ya no distinguía dominios. + + #[test] + fn did_is_a_newtype_not_an_alias_of_asset_hash() { + // Si `Did` volviera a ser `pub type Did = AssetHash`, estas dos líneas + // compilarían y este test dejaría de proteger nada. El cruce de dominio + // ahora EXISTE pero es explícito y auditable en el código: + let did = derive_did(&[7u8; 32]); + let as_asset: origin_chain_types::AssetHash = did.into_asset_hash(); + let back: Did = Did::from_asset_hash(as_asset); + assert_eq!(did, back, "el round-trip explícito debe preservar los bytes"); + + // Y la garantía de wire: el newtype NO cambió el encoding SCALE (mismo + // layout que el alias anterior) ⇒ `DidToAccount` no requiere migración. + use parity_scale_codec::Encode; + assert_eq!(did.encode(), as_asset.encode()); + assert_eq!(core::mem::size_of::(), 32); } } diff --git a/crates/origin-chain-runtime/src/lib.rs b/crates/origin-chain-runtime/src/lib.rs index c43f490..f9d6bfd 100644 --- a/crates/origin-chain-runtime/src/lib.rs +++ b/crates/origin-chain-runtime/src/lib.rs @@ -275,7 +275,16 @@ pub const IMPL_NAME: &str = "origin-chain-charon"; /// `register_agent_did` (el DID calculado y almacenado) ⇒ spec bump obligatorio. /// Las FIRMAS de los extrinsics NO cambian ⇒ TRANSACTION_VERSION intacto. Sin /// migración: verificado que ningún estado on-chain dependía del esquema viejo. -pub const RUNTIME_VERSION: u32 = 10; +/// +/// **v11 (`Did` newtype)**: condición vinculante #3 de Mathi en la firma que +/// ratificó el DC — `Did` deja de ser alias de `AssetHash` y pasa a newtype +/// propio. ⚠️ A diferencia de v9/v10, esto **NO cambia la transición de estado**: +/// `derive_did` produce los MISMOS bytes y el encoding SCALE es byte-idéntico +/// (probado en `did_is_a_newtype_not_an_alias_of_asset_hash`), así que +/// `DidToAccount` no requiere migración. El bump es por **higiene de metadata**: +/// el `TypeInfo` de la clave de storage pasa a nombrarse `Did`, y los clientes +/// que cachean metadata deben re-leerla. TRANSACTION_VERSION intacto. +pub const RUNTIME_VERSION: u32 = 11; /// Canonical authoring version per Substrate semver canonical. pub const AUTHORING_VERSION: u32 = 1; @@ -934,14 +943,13 @@ mod tests { use super::*; #[test] - fn runtime_version_v10_unified_identity() { - // v10 = DC ORIGIN IDENTITY UNIFICATION V1: derive_did usa el contexto KDF - // COMPARTIDO `origin/identity/v1` → la misma pubkey Ed25519 produce el - // MISMO identificador de 32 bytes en chain y en OCF. Cambia la transición - // de estado de register_agent_did ⇒ spec bump. Las FIRMAS de los extrinsics - // NO cambian ⇒ TRANSACTION_VERSION intacto. - // (v9 fue la maquinaria de benchmarking: weights INTERIM DB-aware.) - assert_eq!(RUNTIME_VERSION, 10); + fn runtime_version_v11_did_newtype() { + // v11 = condición #3 de Mathi: `Did` pasa de alias de `AssetHash` a newtype + // propio (cierra la confusión cross-namespace que el contexto KDF compartido + // agravaba). NO cambia la transición de estado — mismos bytes, mismo encoding + // SCALE ⇒ sin migración; el bump es higiene de metadata (TypeInfo). + // (v10 = identidad unificada `origin/identity/v1`; v9 = weights DB-aware.) + assert_eq!(RUNTIME_VERSION, 11); assert_eq!(TRANSACTION_VERSION, 1); } diff --git a/crates/origin-chain-types/src/identity.rs b/crates/origin-chain-types/src/identity.rs index 2670480..578d4f4 100644 --- a/crates/origin-chain-types/src/identity.rs +++ b/crates/origin-chain-types/src/identity.rs @@ -41,12 +41,73 @@ use scale_info::TypeInfo; use crate::{AccountId, AgentChain, AssetHash}; -/// Agent DID canonical — BLAKE3 32-byte hash derived deterministic via public_key. +/// Agent DID canonical — identificador unificado de Origin, 32 bytes. /// -/// Per CHN-7: `did = BLAKE3(public_key || schema_version)`. Esto garantiza -/// determinismo cross-runtime canonical (Pi 5 + Desktop + M4 Ultra producen -/// el mismo DID para el mismo public_key, Inmutable #16 byte-determinism). -pub type Did = AssetHash; +/// Derivado determinísticamente de la pubkey Ed25519 con el contexto KDF +/// **compartido** `origin/identity/v1` (DC ORIGIN IDENTITY UNIFICATION V1, +/// ratificado 2026-07-29): los MISMOS 32 bytes identifican a una persona en la +/// chain, en el chat (OCF `OitFingerprint`) y en Palace. Determinismo +/// cross-runtime garantizado (Pi 5 · Desktop · M4 Ultra ⇒ mismo DID, Inmutable +/// #16 byte-determinism). +/// +/// # Por qué es un NEWTYPE y no un alias de [`AssetHash`] +/// +/// Condición vinculante **#3 de Mathi** en la firma que ratificó el DC: *"un +/// contexto compartido sin newtype reabre por la puerta de atrás la colisión +/// cross-namespace que V3-H2 cerró"*. +/// +/// El razonamiento es correcto y la unificación lo AGRAVA: mientras cada capa +/// derivaba con su propio contexto, un DID y un hash de asset eran valores de +/// dominios separados por construcción. Con un contexto compartido, ambos son +/// "32 bytes canónicos de Origin" y un alias deja que el compilador acepte +/// silenciosamente un `AssetHash` donde se espera un `Did` (y viceversa) — p.ej. +/// anclar una attestation bajo un DID, o resolver `DidToAccount` con el hash de +/// un asset. El newtype convierte esa clase entera de confusión en un error de +/// compilación. +/// +/// **Compatibilidad de wire preservada**: igual que [`AssetHash`], es un newtype +/// sobre `[u8; 32]` con los mismos derives ⇒ el encoding SCALE es byte-idéntico +/// al del alias anterior. `DidToAccount` no requiere migración de storage. +/// +/// La conversión sigue disponible pero es **explícita** ([`Did::from_asset_hash`] +/// / [`Did::into_asset_hash`]): cruzar el dominio es legítimo, pero debe leerse +/// en el código. +#[derive( + Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize, +)] +#[derive(Encode, Decode, DecodeWithMemTracking, MaxEncodedLen, TypeInfo)] +pub struct Did(pub [u8; 32]); + +impl Did { + /// DID cero — centinela; jamás una identidad válida. + pub const ZERO: Did = Did([0u8; 32]); + + /// Construye desde el digest crudo de 32 bytes. + #[inline] + pub const fn new(bytes: [u8; 32]) -> Self { + Did(bytes) + } + + /// Lee como slice crudo de bytes. + #[inline] + pub const fn as_bytes(&self) -> &[u8; 32] { + &self.0 + } + + /// Cruce EXPLÍCITO de dominio desde un [`AssetHash`] (p.ej. decodificar un + /// identificador que viajó como hash genérico). Deliberadamente verboso: si + /// aparece en el código, debe poder justificarse. + #[inline] + pub const fn from_asset_hash(hash: AssetHash) -> Self { + Did(*hash.as_bytes()) + } + + /// Cruce EXPLÍCITO de dominio hacia un [`AssetHash`]. + #[inline] + pub const fn into_asset_hash(self) -> AssetHash { + AssetHash::new(self.0) + } +} /// Tres clases canonical de agent en Origin Metaverse per Inmutable #41. #[derive(