feat(identity): RV14 — una identidad sobrevive a perder su llave, y la revision encontro que el storage salia gratis

QUE NACE. rotate_agent_key (call_index 1 de agent-identity) y la runtime API
AgentKeyApi::current_key. Hasta hoy un DID quedaba atado de hecho a su llave de
nacimiento: no se re-derivaba, pero tampoco habia forma de pasar la autoridad a
otra llave. Perder o filtrar la llave obligaba a registrarse otra vez con OTRO
DID y perder credenciales, creditos e historial. Ahora el DID es de por vida y lo
que rota es que llave manda sobre el. Es la dependencia de dos hilos abiertos: el
DID del niño (Marihan) y los creditos de referido indexados por DID (Codex).

Diseño: CurrentKey[did] (ausente = manda la de nacimiento, que ya vivia en
creator; sin migracion), KeyToDid[llave] (una llave manda sobre un solo DID en
toda la vida de la cadena; las retiradas quedan quemadas) y RotationNonce[did].
Dos firmas sobre DOMAIN || genesis || did || vigente || nueva || nonce || who: la
vigente autoriza, la nueva prueba control.

LO QUE LA REVISION ADVERSARIAL ENCONTRO ANTES DE PUBLICAR. Cinco lentes, 18
hallazgos, 17 confirmados. Todos arreglados aqui y cada arreglo con su
contrafactual visto en ROJO:

1. DbWeight = () EN TODO EL RUNTIME. frame_system heredaba el valor por defecto
   de SolochainDefaultConfig sin declararlo. Cada reads(n)/writes(n) de TODOS los
   weights.rs valia cero: la E/S de storage no se cobraba en fees ni llenaba el
   bloque. Ahora RocksDbWeight, con un test que reprueba si vuelve a cero.
   ESTO CORRIGE LO QUE YO DIJE EN RV13: los reads/writes que medi alli no tenian
   efecto en el runtime. Lo que RV13 si arreglo es el proof_size y el ref_time de
   ejecucion, que no pasan por DbWeight.

2. LLAVES DE ORDEN PEQUEÑO. El verificador del host (ed25519-zebra, ZIP215) las
   acepta. Con el punto identidad como llave y la firma trivial R=identidad s=0,
   la «prueba de control» se superaba sin llave privada, en rotacion Y en
   registro: el DID quedaba bajo una llave con la que cualquiera firma en su
   nombre. Para un DID de un niño en Ethos eso es suplantacion. Ahora
   is_strong_ed25519_key descomprime el punto, exige codificacion canonica y
   rechaza orden pequeño (curve25519-dalek, que ya iba dentro del WASM).

3. EL REGISTRO ESCRIBIA KeyToDid PERO NO LO CONSULTABA. Una llave a la que otro
   DID ya habia rotado fundaba un DID nuevo y pisaba el indice. Hallado cinco
   veces por separado. Va detras de la colision de DID para no cambiar el error
   documentado de scenario_12.

4. REPLAY ENTRE CADENAS: el mensaje no llevaba genesis. Ahora si, mismo patron
   que provenance.

Y menores: el evento nuevo va al final del enum (insertarlo en medio corria el
indice SCALE de AgentUpdated); la insercion redundante de la llave vieja ya no
pisa entradas; la doc dice que perder la CUENTA tambien bloquea la rotacion, no
solo perder la llave; el comentario de creator decia «la cuenta que registro» y
el codigo guarda la llave de nacimiento.

DOS TESTS MIOS PASABAN POR LA RAZON EQUIVOCADA, y lo digo. El de «llave retirada
quemada» volvia a la de nacimiento, que atrapa otra clausula. El del ladron
firmaba con la llave vigente nueva, algo que un ladron real no tiene; con el
pallet roto seguia verde en su mitad de ataque. Reescritos, y ahora el rojo sale
de la mitad correcta (verificado por linea del panic).

LO QUE QUEDA ABIERTO Y NO ES DE ESTE COMMIT: tras rotar, derive(llave_vigente) ya
no es el DID. OCF y Palace hoy DERIVAN el identificador; para un usuario que
roto tienen que RESOLVERLO contra KeyToDid. Es una enmienda al DC de unificacion
con sus firmantes. La recuperacion por guardianes va aparte. Y KeyToDid crece
sin deposito; se reevalua ahora que las escrituras cuestan de verdad.

Medido sobre el WASM real (bencher 0.15.0, Mac, no canonico): rotate 97 us, 11
lecturas, 5 escrituras, proof 6100. 318 tests verde. RV13 -> RV14.
TRANSACTION_VERSION intacto: call nueva en indice nuevo.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
CharonandClaude Opus 5 committed 2026-09-16 15:45:26 -05:00
1 parent 7f68c5b656
commit defd3fa580
12 files changed
+1146 -20

No files matched your search

@@ -99,6 +99,19 @@ impl frame_system::Config for Runtime {
type AccountData = pallet_balances::AccountData<Balance>;
type SS58Prefix = SS58Prefix;
type MaxConsumers = ConstU32<16>;
/// **El coste de leer y escribir storage.** Faltaba, y por eso valía CERO.
///
/// `SolochainDefaultConfig` trae `type DbWeight = ();`, y este `impl` lo heredaba sin
/// declararlo: `RuntimeDbWeight { read: 0, write: 0 }`. Todo `.reads(n)` y
/// `.writes(n)` de los `weights.rs` de la cadena se multiplicaba por cero, así que la
/// E/S de storage no se cobraba en fees ni contaba para llenar el bloque. Lo midió la
/// revisión adversarial de RV14 con una sonda sobre el runtime real.
///
/// ⚠️ Esto corrige también lo que RV13 afirmaba haber arreglado. RV13 midió los 19
/// extrinsics y subió sus reads/writes, pero con `DbWeight = ()` esa parte no tuvo
/// ningún efecto en el runtime. Lo que RV13 sí arregló de verdad es el `proof_size`
/// y el `ref_time` de ejecución, que no pasan por `DbWeight`.
type DbWeight = frame_support::weights::constants::RocksDbWeight;
}
// ============================================================
+67 -2
View File
@@ -340,6 +340,33 @@ pub const IMPL_NAME: &str = borrowed_str(&VERSION.impl_name);
/// correr `scripts/bench/regen-weights.sh` en el Pi 5 con governor y aislamiento
/// declarados (peticion abierta a Laurha, `request:66f500d54e28a80c`).
/// TRANSACTION_VERSION intacto: ninguna firma de extrinsic cambia.
///
/// **v14 (una identidad sobrevive a perder su llave)**: nace `rotate_agent_key`
/// (call_index 1 de `origin-chain-agent-identity`) y la runtime API `AgentKeyApi`.
/// Hasta hoy el DID quedaba atado de hecho a su llave de nacimiento: no se
/// re-derivaba, pero tampoco había forma de pasar la autoridad a otra llave, así que
/// perder o filtrar la llave obligaba a registrarse otra vez con OTRO DID y perder
/// credenciales, créditos e historial. Ahora el DID es el mismo de por vida y lo que
/// rota es qué llave manda sobre él. Exige dos firmas —la vigente autoriza, la nueva
/// prueba control— y una llave retirada queda quemada para siempre. Es la
/// dependencia de dos hilos: el DID del niño (Marihan) y los créditos de referido
/// indexados por DID (Codex). Nuevo storage (`CurrentKey`, `KeyToDid`,
/// `RotationNonce`) + call aditiva en índice nuevo ⇒ spec bump; TRANSACTION_VERSION
/// intacto. Sin migración: una identidad sin entrada en `CurrentKey` resuelve a su
/// llave de nacimiento, que ya vivía en `creator`.
///
/// **Y lo que la revisión adversarial encontró antes de publicar** (5 lentes, 18
/// hallazgos, 17 confirmados, cada arreglo con su contrafactual visto en rojo):
/// - **`DbWeight = ()` en todo el runtime** — heredado sin declarar de
/// `SolochainDefaultConfig`. La E/S de storage valía CERO en fees y en llenado de
/// bloque para TODOS los extrinsics. Ahora `RocksDbWeight`. Esto invalida en parte lo
/// que dice la nota de v13: los reads/writes que midió no tenían efecto en el runtime;
/// el `proof_size` y el `ref_time` de ejecución sí.
/// - **Llaves de orden pequeño**: el verificador del host (ZIP215) las acepta, y con la
/// firma trivial `R = identidad, s = 0` la «prueba de control» se superaba sin llave
/// privada, en rotación Y en registro. Ahora `is_strong_ed25519_key` las rechaza.
/// - **El registro no consultaba `KeyToDid`**: una llave rotada fundaba otro DID.
/// - **Replay entre cadenas**: el mensaje de rotación no llevaba génesis. Ahora sí.
pub const RUNTIME_VERSION: u32 = VERSION.spec_version;
/// Canonical authoring version per Substrate semver canonical.
@@ -783,6 +810,12 @@ impl_runtime_apis! {
//
// Phase 2.7 COMPLETE canonical post-v0.0.27 🎉
impl origin_chain_agent_identity::AgentKeyApi<Block> for Runtime {
fn current_key(did: origin_chain_types::Did) -> Option<[u8; 32]> {
AgentIdentity::current_key_of(&did)
}
}
impl origin_chain_agent_identity::AgentIdentityApi<Block> for Runtime {
fn query_identity(
account: origin_chain_types::AccountId,
@@ -986,7 +1019,7 @@ pub const VERSION: sp_version::RuntimeVersion = sp_version::RuntimeVersion {
spec_name: alloc::borrow::Cow::Borrowed("origin-chain"),
impl_name: alloc::borrow::Cow::Borrowed("origin-chain-charon"),
authoring_version: 1,
spec_version: 13,
spec_version: 14,
impl_version: 1,
apis: RUNTIME_API_VERSIONS,
transaction_version: 1,
@@ -1009,6 +1042,38 @@ use sp_api::impl_runtime_apis;
mod tests {
use super::*;
/// El storage cuesta: leer y escribir no puede ser gratis en el runtime.
///
/// Nace de un defecto que sobrevivió a toda la vida de la cadena: `frame_system`
/// heredaba `DbWeight = ()` y cada `.reads(n)`/`.writes(n)` de los pesos valía cero.
/// Las 19 mediciones de RV13 subieron esos números sin cambiar nada en la práctica.
#[test]
fn leer_y_escribir_storage_no_es_gratis() {
use frame_support::traits::Get;
// Tipo anotado a propósito: con `DbWeight = ()` la llamada sin anotar ni compila
// (ambigüedad de `Get<T>`), y un test que solo reprueba por un error de inferencia
// protege por accidente. Anotado, compila en los dos casos y reprueba la ASERCIÓN.
let db: frame_support::weights::RuntimeDbWeight =
<Runtime as frame_system::Config>::DbWeight::get();
assert!(
db.read > 0,
"DbWeight.read = 0: las lecturas de storage salen gratis"
);
assert!(
db.write > 0,
"DbWeight.write = 0: las escrituras de storage salen gratis"
);
// Y llega de verdad a un peso real de la cadena.
let rot = <origin_chain_agent_identity::weights::SubstrateWeight<Runtime>
as origin_chain_agent_identity::weights::WeightInfo>::rotate_agent_key();
let base = <() as origin_chain_agent_identity::weights::WeightInfo>::rotate_agent_key();
assert_eq!(
rot.ref_time(),
base.ref_time(),
"el runtime debe usar RocksDbWeight, igual que el impl ()"
);
}
/// Fija la version vigente y la invariante que la acompaña.
///
/// El test NO cuenta la historia de cada version — eso vive en el doc comment
@@ -1022,7 +1087,7 @@ mod tests {
/// mismo commit en que la version subio a 12.
#[test]
fn spec_version_vigente_y_transaction_version_intacta() {
assert_eq!(RUNTIME_VERSION, 13);
assert_eq!(RUNTIME_VERSION, 14);
assert_eq!(TRANSACTION_VERSION, 1);
}