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:
davidherranandClaude Opus 4.8 committed 2026-08-01 17:27:38 -05:00
1 parent 13f8ac00a5
commit 2d44042bd6
3 files changed
+115 -16

No files matched your search

+32 -2
View File
@@ -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::<Did>(), 32);
}
}