From 13f8ac00a502ced9bea4eac9c49c0700a2440d5b Mon Sep 17 00:00:00 2001 From: DavidHerran Date: Tue, 28 Jul 2026 12:25:05 -0500 Subject: [PATCH] =?UTF-8?q?feat(identity):=20UNA=20identidad=20para=20todo?= =?UTF-8?q?=20Origin=20=E2=80=94=20contexto=20KDF=20compartido=20(RV=209?= =?UTF-8?q?=E2=86=9210)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DC ORIGIN IDENTITY UNIFICATION V1 (Larihana), Opción A + enmienda D-ID-7 mía. Ratificado por David 2026-07-28. Firmado approve (pendientes Larihana + Mathi). EL PROBLEMA (medido por Larihana, verificado por mí en ambos lados): chain: blake3::derive_key("origin-chain/agent-did/v1", pk) → Did OCF: blake3::hash(pk) → OitFingerprint La MISMA pubkey Ed25519 producía 32 bytes DISTINTOS en cada capa. No era un bug de criptografía: cada capa hizo domain separation correctamente por separado y nadie decidió la relación entre las dos. Consecuencia: viendo un fingerprint en un canal de chat NO se podía resolver el DID on-chain sin la pubkey completa — un usuario tenía una identidad en el chat y otra en la blockchain. Directiva de David: "que todo lo que creamos tenga una misma identidad para poder acceder". EL CAMBIO: - Nueva constante pública ORIGIN_IDENTITY_KDF_CONTEXT = "origin/identity/v1", el contexto CANÓNICO compartido por todas las capas de Origin. - derive_did pasa a derivar con ese contexto ⇒ el identificador de 32 bytes es ahora EL MISMO en chain, chat (OCF), assets y credenciales. - RUNTIME_VERSION 9→10: cambia la transición de estado de register_agent_did (el DID calculado y almacenado) ⇒ spec bump obligatorio. Las FIRMAS de los extrinsics NO cambian ⇒ TRANSACTION_VERSION intacto. POR QUÉ SIN MIGRACIÓN (la verificación que el DC me delegó, medida no asumida): - Sin GenesisConfig en el pallet → no se siembran identidades en génesis. - Sin DIDs en los 3 presets de génesis. - Sin golden vectors ni DID hex hardcodeado en el repo. - StorageVersion = 0, nunca migrado — greenfield puro. - derive_did = UNA función, UN call-site; los tests lo invocan (no pinnean bytes). ⇒ Ningún estado on-chain dependía del esquema viejo. La ventana pre-mainnet estaba abierta y el costo fue una constante de string. ENMIENDA D-ID-7 — golden vectors cross-repo (lo que aporté al DC): El DC arreglaba la INSTANCIA pero no la CLASE. Origin Chain es submódulo y NO puede depender de arkhe-fabric-core (dep-direction submódulo→monorepo = ILEGAL, la misma ley que impide el swap del attestation SSOT), así que las dos capas no pueden compartir la constante por Cargo — y sin ancla podían volver a divergir en silencio, que es justo el bug que cerramos. Añadidos 3 vectores pinneados a bytes (2 patrones + 1 pubkey Ed25519 real) para que fabric-core pinnee LOS MISMOS. Si cualquiera de los dos lados cambia el contexto o la función, su test falla. Mismo patrón que ya nos salvó con el attestation SSOT: paridad por matemática, no por disciplina humana. NO VACUO — verificado empíricamente: saboteé el contexto a "v2-DRIFT" y AMBOS tests fallaron con el diagnóstico correcto; restaurado, verdes de nuevo. (Un test que pasa no prueba nada hasta que se le ve fallar sin el fix.) TRADE-OFF asumido y dejado explícito en el acta del DC: un identificador único en chat + chain + assets + credenciales implica que ver el fingerprint de alguien en un canal permite resolver sus activos y transacciones on-chain — no hay frontera de seudonimato entre capas. Es INHERENTE a "una identidad para todo"; la alternativa (derivación por-contexto tipo HD) reintroduce los dos identificadores en dos pantallas que justamente eliminamos. Decisión consciente, no efecto secundario descubierto después. Verificado: workspace completo verde (0 fallos), 20/20 en agent-identity. Co-Authored-By: Claude Opus 4.8 --- crates/origin-chain-agent-identity/src/lib.rs | 115 +++++++++++++++++- crates/origin-chain-runtime/src/lib.rs | 26 ++-- 2 files changed, 130 insertions(+), 11 deletions(-) diff --git a/crates/origin-chain-agent-identity/src/lib.rs b/crates/origin-chain-agent-identity/src/lib.rs index 403e38a..0e095c1 100644 --- a/crates/origin-chain-agent-identity/src/lib.rs +++ b/crates/origin-chain-agent-identity/src/lib.rs @@ -75,12 +75,36 @@ pub fn derive_did(public_key: &[u8; 32]) -> Did { // FIX-3 (V3-H2): KDF domain-separado vía blake3::derive_key (context string // como dominio) en vez de un hash ad-hoc concat. Es el patrón KDF canonical de // BLAKE3 y previene colisión cross-uso del hash. Inmutable #16 preservado - // (determinista cross-runtime). ⚠️ Cambia el DID para un mismo public_key vs el - // esquema previo (BLAKE3(pk || version)) — aceptable pre-mainnet (greenfield). - let did_bytes = blake3::derive_key("origin-chain/agent-did/v1", public_key); + // (determinista cross-runtime). + // + // DC ORIGIN IDENTITY UNIFICATION V1 (2026-07-28, David): el contexto pasó de + // `"origin-chain/agent-did/v1"` (propio de la chain) al contexto COMPARTIDO + // `origin/identity/v1` — la MISMA pubkey Ed25519 ahora produce EL MISMO + // identificador de 32 bytes en chain, chat (OCF), assets y credenciales. + // Antes, chain derivaba con su contexto y OCF hacía `blake3::hash(pubkey)` + // plano: dos identificadores para el mismo usuario en dos pantallas. + // ⚠️ 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). + let did_bytes = blake3::derive_key(ORIGIN_IDENTITY_KDF_CONTEXT, public_key); origin_chain_types::AssetHash::new(did_bytes) } +/// Contexto KDF **canónico de identidad de Origin** — DC ORIGIN IDENTITY +/// UNIFICATION V1 (D-ID-3, Opción A). +/// +/// Una sola cuerda de dominio para TODAS las capas: `derive_key(este_contexto, +/// pubkey_ed25519)` es el identificador unificado de 32 bytes. Origin Chain lo usa +/// para el `Did`; `arkhe-fabric-core` lo usa para `OitFingerprint`. +/// +/// ⚠️ **NO puede compartirse como constante Rust entre los dos repos**: Origin Chain +/// es submódulo y depender de `arkhe-fabric-core` invertiría la dirección de +/// dependencias (submódulo→monorepo = ILEGAL; el chain dejaría de buildear +/// standalone). La paridad se preserva por el **golden vector** de D-ID-7 — +/// ver `tests::did_golden_vectors_cross_repo_parity`. Si cualquiera de las dos +/// capas cambia el contexto o la función, su test falla y la deriva se caza ahí. +pub const ORIGIN_IDENTITY_KDF_CONTEXT: &str = "origin/identity/v1"; + /// Domain tag canonical para la firma de binding del registro de DID (FIX-3 (b), /// V3-H1 anti-front-running). El registrante prueba control de `public_key` /// firmando este dominio + el AccountId del caller (`who`). @@ -402,6 +426,90 @@ mod tests { assert_ne!(did_a, did_b); } + // ═══════════════════════════════════════════════════════════════ + // D-ID-7 — GOLDEN VECTORS CROSS-REPO (DC ORIGIN IDENTITY UNIFICATION V1) + // ═══════════════════════════════════════════════════════════════ + // + // El DC unificó la derivación de identidad: la MISMA pubkey Ed25519 debe + // producir EL MISMO identificador de 32 bytes en Origin Chain (`Did`) y en + // OCF / arkhe-fabric-core (`OitFingerprint::from_ed25519_pubkey`). + // + // PROBLEMA QUE ESTE TEST RESUELVE: Origin Chain es submódulo y NO puede + // depender de `arkhe-fabric-core` (dep-direction submódulo→monorepo = ILEGAL), + // así que las dos capas NO pueden compartir la constante del contexto por + // Cargo. Sin un ancla, pueden volver a divergir en silencio — que es + // exactamente el bug que el DC cierra. Estos vectores son ese ancla: están + // pinneados a bytes en AMBOS repos. Si cualquiera de los dos lados cambia el + // contexto o la función de derivación, su copia de este test falla. + // + // MISMO PATRÓN que los golden vectors del attestation SSOT + // (`origin-chain-types::attestation_verify`): la paridad cross-repo se + // preserva por matemática pinneada, no por disciplina humana. + // + // Contraparte a pinnear en fabric-core: + // OitFingerprint::from_ed25519_pubkey(&pk).as_bytes() == + // + // Regenerar SOLO si el DC cambia el contexto canónico (requiere re-firma). + + /// Vectores canónicos: (pubkey, identificador unificado esperado). + /// Derivación: `blake3::derive_key("origin/identity/v1", pubkey)`. + const IDENTITY_GOLDEN_VECTORS: [([u8; 32], &str); 3] = [ + // Degenerado todo-ceros — trivialmente reproducible en cualquier lenguaje. + ( + [0x00u8; 32], + "d2d1e2fa7be9e8424f1528de50ec58c4e87c357c6b98204882272be56fb8f7db", + ), + // Patrón 0x11 — segundo control simple. + ( + [0x11u8; 32], + "668b46ad620dc9dd0b5f17abceb6357de9bfa5a6102a4174b03c8e13cdbac85b", + ), + // Pubkey Ed25519 REAL (par derivado de la seed [0x11; 32]) — prueba que + // funciona con un punto de curva legítimo, no solo con patrones. + ( + [ + 0xd0, 0x4a, 0xb2, 0x32, 0x74, 0x2b, 0xb4, 0xab, 0x3a, 0x13, 0x68, 0xbd, 0x46, 0x15, + 0xe4, 0xe6, 0xd0, 0x22, 0x4a, 0xb7, 0x1a, 0x01, 0x6b, 0xaf, 0x85, 0x20, 0xa3, 0x32, + 0xc9, 0x77, 0x87, 0x37, + ], + "ea484da28858c031dd846d75f443efa429c142466e68df478ec0761dcb1bc544", + ), + ]; + + fn hex32(bytes: &[u8; 32]) -> alloc::string::String { + use alloc::string::String; + let mut s = String::with_capacity(64); + for b in bytes { + s.push(core::char::from_digit((b >> 4) as u32, 16).unwrap()); + s.push(core::char::from_digit((b & 0x0f) as u32, 16).unwrap()); + } + s + } + + #[test] + fn did_golden_vectors_cross_repo_parity() { + for (pubkey, expected_hex) in IDENTITY_GOLDEN_VECTORS { + let did = derive_did(&pubkey); + assert_eq!( + hex32(did.as_bytes()), + expected_hex, + "DERIVA DE IDENTIDAD ROTA para pubkey {}: el DC ORIGIN IDENTITY \ + UNIFICATION V1 exige que este byte-a-byte coincida con \ + OitFingerprint::from_ed25519_pubkey en arkhe-fabric-core. \ + Si cambiaste el contexto KDF, el DC debe re-firmarse y AMBOS \ + repos regenerar sus vectores.", + hex32(&pubkey) + ); + } + } + + #[test] + fn identity_kdf_context_is_the_shared_canonical_one() { + // El contexto NO es libre: es el ratificado en el DC. Un cambio aquí + // rompe la unificación con OCF aunque los vectores se regeneren. + assert_eq!(ORIGIN_IDENTITY_KDF_CONTEXT, "origin/identity/v1"); + } + #[test] fn re_export_agent_identity_canonical() { // Verify re-exports canonical desde origin-chain-types @@ -409,3 +517,4 @@ mod tests { let _did_alias: Did = origin_chain_types::AssetHash::new([0u8; 32]); } } + diff --git a/crates/origin-chain-runtime/src/lib.rs b/crates/origin-chain-runtime/src/lib.rs index dec3e7c..c43f490 100644 --- a/crates/origin-chain-runtime/src/lib.rs +++ b/crates/origin-chain-runtime/src/lib.rs @@ -266,7 +266,16 @@ pub const IMPL_NAME: &str = "origin-chain-charon"; /// Las FIRMAS de los extrinsics NO cambian ⇒ TRANSACTION_VERSION intacto. La feature /// `runtime-benchmarks` gana `define_benchmarks!` + runtime API `Benchmark` (inerte /// sin la feature) + fix del bug `pallet-grandpa/try-runtime` en la feature. -pub const RUNTIME_VERSION: u32 = 9; +/// +/// **v10 (identidad unificada)**: DC ORIGIN IDENTITY UNIFICATION V1 — `derive_did` +/// pasa del contexto propio de la chain (`origin-chain/agent-did/v1`) al contexto +/// COMPARTIDO `origin/identity/v1`, de modo que la misma pubkey Ed25519 produce EL +/// MISMO identificador de 32 bytes en chain y en OCF (antes eran distintos: dos +/// identidades para el mismo usuario). Cambia la transición de estado de +/// `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; /// Canonical authoring version per Substrate semver canonical. pub const AUTHORING_VERSION: u32 = 1; @@ -925,13 +934,14 @@ mod tests { use super::*; #[test] - fn runtime_version_v9_benchmarking_weights() { - // v9 = benchmarking real machinery: los 19 extrinsics de los 5 pallets - // custom pasan de weights placeholder (proof_size=0, DoS) a weights INTERIM - // hand-derived DB-aware vía WeightInfo. Los weights son consenso-relevantes - // (fee + llenado de bloque) ⇒ spec bump. Las FIRMAS de los extrinsics NO - // cambian ⇒ TRANSACTION_VERSION intacto. - assert_eq!(RUNTIME_VERSION, 9); + 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); assert_eq!(TRANSACTION_VERSION, 1); }