feat(identity): Did pasa a newtype propio — condición #3 de Mathi (RV 10→11)
Condición VINCULANTE #3 de la firma de Mathi que ratificó el DC ORIGIN IDENTITY UNIFICATION V1 (3/3, 2026-07-29): `Did` deja de ser `pub type Did = AssetHash` y pasa a ser un newtype propio en origin-chain-types::identity. POR QUÉ (su argumento, que resultó más fuerte de lo que parecía): Mientras cada capa derivaba con SU propio contexto KDF, un DID y un hash de asset eran valores de dominios separados por construcción. Con el contexto COMPARTIDO `origin/identity/v1` ambos pasaron a ser "32 bytes canónicos de Origin", y el alias dejaba que el compilador aceptara silenciosamente un `AssetHash` donde se espera un `Did` — anclar una attestation bajo un DID, o resolver `DidToAccount` con el hash de un asset. La unificación AGRAVÓ el riesgo que el alias escondía; el newtype convierte esa clase entera de confusión en error de compilación. GARANTÍAS PRESERVADAS (medidas, no asumidas): - Encoding SCALE byte-IDÉNTICO (newtype sobre [u8;32] con los mismos derives) ⇒ `DidToAccount` NO requiere migración. Probado: assert_eq!(did.encode(), as_asset.encode()) + size_of::<Did>() == 32. - `derive_did` produce los MISMOS bytes: los 3 golden vectors del DC (D-ID-7) siguen verdes ⇒ la paridad con fabric-core (9e51e58) y Palace (7a0b82a) intacta. - El cruce de dominio sigue disponible pero EXPLÍCITO: `Did::from_asset_hash` / `into_asset_hash`. Legítimo cuando hace falta, legible en el código. UN TEST MÍO AFIRMABA EL DEFECTO Y LO MATÉ: let _did_alias: Did = AssetHash::new([0u8; 32]); // compilaba: era alias Ese assert pinneaba exactamente lo que Mathi señaló. Reemplazado por `did_is_a_newtype_not_an_alias_of_asset_hash`, que pinnea la separación de dominios + la invariante de wire. RUNTIME_VERSION 10→11 — y con la severidad SIN inflar: esto NO es consenso-relevante (misma transición de estado, mismos bytes, mismo encoding). El bump es higiene de metadata: el `TypeInfo` de la clave de storage ahora se llama `Did` y los clientes que cachean metadata deben re-leerla. TRANSACTION_VERSION intacto. Blast radius real medido: 1 solo error de compilación en todo el workspace (el test obsoleto de arriba). Verificado: 288 tests verdes, 0 fallos. Queda abierta la condición #4 de Mathi (`kind` dentro del material hasheado): objetada por escrito con razones (rompe la unificación —misma pubkey daría N ids por kind—, vuelve el kind inmutable —Human→Hybrid rotaría la identidad entera— y rompe la paridad digest recién cerrada). Contrapropuesta: binding del kind vía atestación on-chain autenticada + firmas domain-tagged, y el prefijo `did:origin:<kind>:` tratado como etiqueta de display, nunca como autoridad. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
13f8ac00a5
commit
2d44042bd6
3 files changed
+115
-16
No files matched your search
@@ -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
|
// ⚠️ 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
|
// pre-mainnet (greenfield): verificado que NINGÚN estado on-chain depende del
|
||||||
// esquema viejo (sin GenesisConfig, sin DIDs en presets, StorageVersion 0).
|
// 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);
|
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
|
/// Contexto KDF **canónico de identidad de Origin** — DC ORIGIN IDENTITY
|
||||||
@@ -514,7 +519,32 @@ mod tests {
|
|||||||
fn re_export_agent_identity_canonical() {
|
fn re_export_agent_identity_canonical() {
|
||||||
// Verify re-exports canonical desde origin-chain-types
|
// Verify re-exports canonical desde origin-chain-types
|
||||||
let _kind: AgentKind = AgentKind::AGI;
|
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::<Did>(), 32);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,7 +275,16 @@ pub const IMPL_NAME: &str = "origin-chain-charon";
|
|||||||
/// `register_agent_did` (el DID calculado y almacenado) ⇒ spec bump obligatorio.
|
/// `register_agent_did` (el DID calculado y almacenado) ⇒ spec bump obligatorio.
|
||||||
/// Las FIRMAS de los extrinsics NO cambian ⇒ TRANSACTION_VERSION intacto. Sin
|
/// 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.
|
/// 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.
|
/// Canonical authoring version per Substrate semver canonical.
|
||||||
pub const AUTHORING_VERSION: u32 = 1;
|
pub const AUTHORING_VERSION: u32 = 1;
|
||||||
@@ -934,14 +943,13 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn runtime_version_v10_unified_identity() {
|
fn runtime_version_v11_did_newtype() {
|
||||||
// v10 = DC ORIGIN IDENTITY UNIFICATION V1: derive_did usa el contexto KDF
|
// v11 = condición #3 de Mathi: `Did` pasa de alias de `AssetHash` a newtype
|
||||||
// COMPARTIDO `origin/identity/v1` → la misma pubkey Ed25519 produce el
|
// propio (cierra la confusión cross-namespace que el contexto KDF compartido
|
||||||
// MISMO identificador de 32 bytes en chain y en OCF. Cambia la transición
|
// agravaba). NO cambia la transición de estado — mismos bytes, mismo encoding
|
||||||
// de estado de register_agent_did ⇒ spec bump. Las FIRMAS de los extrinsics
|
// SCALE ⇒ sin migración; el bump es higiene de metadata (TypeInfo).
|
||||||
// NO cambian ⇒ TRANSACTION_VERSION intacto.
|
// (v10 = identidad unificada `origin/identity/v1`; v9 = weights DB-aware.)
|
||||||
// (v9 fue la maquinaria de benchmarking: weights INTERIM DB-aware.)
|
assert_eq!(RUNTIME_VERSION, 11);
|
||||||
assert_eq!(RUNTIME_VERSION, 10);
|
|
||||||
assert_eq!(TRANSACTION_VERSION, 1);
|
assert_eq!(TRANSACTION_VERSION, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,12 +41,73 @@ use scale_info::TypeInfo;
|
|||||||
|
|
||||||
use crate::{AccountId, AgentChain, AssetHash};
|
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
|
/// Derivado determinísticamente de la pubkey Ed25519 con el contexto KDF
|
||||||
/// determinismo cross-runtime canonical (Pi 5 + Desktop + M4 Ultra producen
|
/// **compartido** `origin/identity/v1` (DC ORIGIN IDENTITY UNIFICATION V1,
|
||||||
/// el mismo DID para el mismo public_key, Inmutable #16 byte-determinism).
|
/// ratificado 2026-07-29): los MISMOS 32 bytes identifican a una persona en la
|
||||||
pub type Did = AssetHash;
|
/// 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.
|
/// Tres clases canonical de agent en Origin Metaverse per Inmutable #41.
|
||||||
#[derive(
|
#[derive(
|
||||||
|
|||||||
Reference in new issue
Block a user