feat(identity): UNA identidad para todo Origin — contexto KDF compartido (RV 9→10)
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 <noreply@anthropic.com>
This commit is contained in:
1 parent
266ee4084d
commit
13f8ac00a5
2 files changed
+130
-11
No files matched your search
@@ -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
|
// 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
|
// 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
|
// 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
|
// (determinista cross-runtime).
|
||||||
// esquema previo (BLAKE3(pk || version)) — aceptable pre-mainnet (greenfield).
|
//
|
||||||
let did_bytes = blake3::derive_key("origin-chain/agent-did/v1", public_key);
|
// 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)
|
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),
|
/// 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`
|
/// V3-H1 anti-front-running). El registrante prueba control de `public_key`
|
||||||
/// firmando este dominio + el AccountId del caller (`who`).
|
/// firmando este dominio + el AccountId del caller (`who`).
|
||||||
@@ -402,6 +426,90 @@ mod tests {
|
|||||||
assert_ne!(did_a, did_b);
|
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() == <mismos 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]
|
#[test]
|
||||||
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
|
||||||
@@ -409,3 +517,4 @@ mod tests {
|
|||||||
let _did_alias: Did = origin_chain_types::AssetHash::new([0u8; 32]);
|
let _did_alias: Did = origin_chain_types::AssetHash::new([0u8; 32]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -266,7 +266,16 @@ pub const IMPL_NAME: &str = "origin-chain-charon";
|
|||||||
/// Las FIRMAS de los extrinsics NO cambian ⇒ TRANSACTION_VERSION intacto. La feature
|
/// Las FIRMAS de los extrinsics NO cambian ⇒ TRANSACTION_VERSION intacto. La feature
|
||||||
/// `runtime-benchmarks` gana `define_benchmarks!` + runtime API `Benchmark` (inerte
|
/// `runtime-benchmarks` gana `define_benchmarks!` + runtime API `Benchmark` (inerte
|
||||||
/// sin la feature) + fix del bug `pallet-grandpa/try-runtime` en la feature.
|
/// 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.
|
/// Canonical authoring version per Substrate semver canonical.
|
||||||
pub const AUTHORING_VERSION: u32 = 1;
|
pub const AUTHORING_VERSION: u32 = 1;
|
||||||
@@ -925,13 +934,14 @@ mod tests {
|
|||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn runtime_version_v9_benchmarking_weights() {
|
fn runtime_version_v10_unified_identity() {
|
||||||
// v9 = benchmarking real machinery: los 19 extrinsics de los 5 pallets
|
// v10 = DC ORIGIN IDENTITY UNIFICATION V1: derive_did usa el contexto KDF
|
||||||
// custom pasan de weights placeholder (proof_size=0, DoS) a weights INTERIM
|
// COMPARTIDO `origin/identity/v1` → la misma pubkey Ed25519 produce el
|
||||||
// hand-derived DB-aware vía WeightInfo. Los weights son consenso-relevantes
|
// MISMO identificador de 32 bytes en chain y en OCF. Cambia la transición
|
||||||
// (fee + llenado de bloque) ⇒ spec bump. Las FIRMAS de los extrinsics NO
|
// de estado de register_agent_did ⇒ spec bump. Las FIRMAS de los extrinsics
|
||||||
// cambian ⇒ TRANSACTION_VERSION intacto.
|
// NO cambian ⇒ TRANSACTION_VERSION intacto.
|
||||||
assert_eq!(RUNTIME_VERSION, 9);
|
// (v9 fue la maquinaria de benchmarking: weights INTERIM DB-aware.)
|
||||||
|
assert_eq!(RUNTIME_VERSION, 10);
|
||||||
assert_eq!(TRANSACTION_VERSION, 1);
|
assert_eq!(TRANSACTION_VERSION, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user