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

@@ -19,7 +19,9 @@
use super::*;
use frame_benchmarking::v2::*;
use frame_support::pallet_prelude::Decode;
use frame_system::pallet_prelude::BlockNumberFor;
use frame_system::RawOrigin;
use sp_runtime::traits::Zero;
// `AgentKind` llega vía `super::*` (re-exportado en el crate root).
use alloc::vec::Vec;
@@ -92,5 +94,53 @@ mod benchmarks {
assert!(Identities::<T>::contains_key(&who));
}
/// Rotación ALICE→DAVE con las dos firmas dorados (`crate::benchmark_golden`).
///
/// Mide el camino FELIZ completo: dos verificaciones Ed25519 y las cuatro
/// escrituras. Si las firmas quedaran viejas, la primera verificación fallaría y
/// el benchmark mediría el error temprano — por eso el assert del final, y por
/// eso un test fija los vectores contra el builder del mensaje.
#[benchmark]
fn rotate_agent_key() {
let who: T::AccountId =
Decode::decode(&mut &CALLER[..]).expect("AccountId32 decodes from 32 bytes");
// El mensaje de rotación lleva el génesis (anti-replay entre cadenas). Las firmas
// doradas se calcularon con `0xAB×32`, el mismo valor que siembra el mock; en el
// runtime real `block_hash(0)` es otro, así que hay que sembrarlo aquí.
//
// Sin esta línea el benchmark pasaba en el mock y FALLABA en el WASM real con
// `InvalidCurrentKeySignature`: lo cazó el bencher en el primer intento de medir
// RV14. Si no fallara ruidoso, habría medido el camino de error y producido un
// peso falso — por eso el assert del final existe.
let genesis: T::Hash =
Decode::decode(&mut &[0xABu8; 32][..]).expect("H256 decodes from 32 bytes");
frame_system::BlockHash::<T>::insert(BlockNumberFor::<T>::zero(), genesis);
// Precondición: ALICE registrada con su llave de nacimiento.
Pallet::<T>::register_agent_did(
RawOrigin::Signed(who.clone()).into(),
to_32(BENCH_PUBKEY),
AgentKind::Human,
Vec::new(),
to_64(BENCH_SIG),
)
.expect("registro dorado válido");
let new_key = to_32(crate::benchmark_golden::ROT_DAVE_PUBKEY);
let sig_current = to_64(crate::benchmark_golden::ROT_SIG_CURRENT);
let sig_new = to_64(crate::benchmark_golden::ROT_SIG_NEW);
#[extrinsic_call]
rotate_agent_key(
RawOrigin::Signed(who.clone()),
new_key,
sig_current,
sig_new,
);
// Si esto falla, se midió el camino de error y el peso es falso.
let did = crate::derive_did(&to_32(BENCH_PUBKEY));
assert_eq!(CurrentKey::<T>::get(did), Some(new_key));
}
impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test);
}
+335 -2
View File
@@ -127,6 +127,83 @@ pub fn agent_did_register_message(who_encoded: &[u8]) -> alloc::vec::Vec<u8> {
msg
}
/// Dominio de la firma de ROTACIÓN de llave. Distinto del de registro a propósito:
/// una firma de registro nunca puede reutilizarse como autorización de rotación.
pub const AGENT_KEY_ROTATE_DOMAIN: &[u8] = b"origin-chain/agent-key-rotate/v1";
/// Mensaje que firman AMBAS llaves —la vigente y la nueva— para rotar.
///
/// `AGENT_KEY_ROTATE_DOMAIN || genesis || did || llave_vigente || llave_nueva || nonce_le || who`
///
/// Cada campo cierra un ataque distinto, y por eso están todos:
/// - `genesis`: la firma solo vale en ESTA cadena. Sin él, una rotación firmada en
/// testnet se podía reenviar en mainnet si alguien reusaba cuenta y llaves, y quien
/// robara la cuenta en mainnet podía aplicar una autorización vieja de la llave sin
/// tener la llave. Encontrado por la revisión adversarial de RV14, medido con dos
/// entornos de prueba independientes. Mismo patrón que `origin-chain-provenance`.
/// - `did`: la firma solo vale para ESTA identidad.
/// - `llave_vigente`: ata la autorización a la llave que manda HOY; tras rotar, la
/// misma firma ya no describe el estado y no puede reutilizarse.
/// - `llave_nueva`: la nueva llave prueba que su dueño consiente ser la autoridad.
/// - `nonce`: anti-replay explícito, independiente de lo anterior.
/// - `who`: la firma queda atada a la cuenta que la envía; nadie puede tomar una
/// rotación firmada del mempool y enviarla desde otra cuenta.
///
/// Una sola fuente de verdad para el verificador on-chain y para el firmante.
pub fn agent_key_rotate_message(
genesis_hash: &[u8; 32],
did: &[u8; 32],
current_key: &[u8; 32],
new_key: &[u8; 32],
nonce: u32,
who_encoded: &[u8],
) -> alloc::vec::Vec<u8> {
let mut msg = alloc::vec::Vec::with_capacity(
AGENT_KEY_ROTATE_DOMAIN.len() + 32 * 4 + 4 + who_encoded.len(),
);
msg.extend_from_slice(AGENT_KEY_ROTATE_DOMAIN);
msg.extend_from_slice(genesis_hash);
msg.extend_from_slice(did);
msg.extend_from_slice(current_key);
msg.extend_from_slice(new_key);
msg.extend_from_slice(&nonce.to_le_bytes());
msg.extend_from_slice(who_encoded);
msg
}
/// ¿Es esta llave Ed25519 una llave con la que solo su dueño puede firmar?
///
/// Exige tres cosas: que el punto esté en la curva, que su codificación sea la
/// CANÓNICA (volver a comprimirlo da los mismos 32 bytes) y que **no sea de orden
/// pequeño**.
///
/// **Por qué hace falta, y es grave:** el verificador del host
/// (`sp_io::crypto::ed25519_verify`, ed25519-zebra con reglas ZIP215) ACEPTA llaves de
/// orden pequeño por compatibilidad de consenso. Con el punto identidad como llave
/// (`01 00..00`) y la firma trivial `R = identidad, s = 0`, la verificación da `true`
/// **para cualquier mensaje**. Sin este chequeo, la «prueba de control de la llave
/// nueva» de una rotación —y la firma de registro— se superaban sin llave privada, y
/// el DID quedaba bajo una llave con la que CUALQUIERA firma en su nombre. Para
/// Ethos, donde un DID puede ser el de un niño, eso es suplantación.
///
/// Encontrado por la revisión adversarial de RV14 y medido con sonda: la rotación y el
/// registro devolvían `Ok` con `01 00..00`, con el punto de orden 2 (`ec ff..ff 7f`) y
/// con una codificación NO canónica de la identidad (`ee ff..ff 7f`). Tener que
/// cubrir codificaciones no canónicas es la razón para comprimir de vuelta en vez de
/// mantener una lista: una lista escrita a mano se deja fuera alguna.
///
/// No se exige `is_torsion_free`: una llave de orden mixto no se puede falsificar (hace
/// falta el logaritmo discreto de su componente de orden primo) y ese chequeo cuesta
/// una multiplicación escalar completa por extrinsic.
pub fn is_strong_ed25519_key(key: &[u8; 32]) -> bool {
use curve25519_dalek::edwards::CompressedEdwardsY;
let compressed = CompressedEdwardsY(*key);
let Some(point) = compressed.decompress() else {
return false;
};
point.compress().to_bytes() == *key && !point.is_small_order()
}
/// Runtime API canonical para cross-pillar consumers + light clients.
///
/// Permite query canonical de agent identity canonical sin necesidad de full
@@ -149,6 +226,10 @@ sp_api::decl_runtime_apis! {
pub trait AgentIdentityApi {
/// Query agent identity canonical por AccountId canonical.
///
/// ⚠️ El campo `creator` que devuelve es la llave de NACIMIENTO y no cambia al
/// rotar. **No lo uses para verificar firmas del titular**: usa
/// `AgentKeyApi::current_key`.
///
/// Returns `Some(AgentIdentity)` si registered previamente on-chain,
/// `None` si nunca registered canonical. Read-only canonical sin
/// transaction fee canonical (off-chain via RPC).
@@ -156,6 +237,36 @@ sp_api::decl_runtime_apis! {
}
}
sp_api::decl_runtime_apis! {
/// Resolución DID → llave vigente.
///
/// Es la pregunta que hace cualquiera que verifique una firma del titular de un
/// DID (Ethos para `subject_pubkey`, Codex para créditos, un light client): no
/// «¿qué llave generó este DID?» sino «¿qué llave manda HOY sobre él?». Tras una
/// rotación son distintas, y un consumidor que cachee la llave de nacimiento
/// dejaría de validar al titular legítimo.
///
/// Trait aparte de `AgentIdentityApi` a propósito: añadir un método a una API ya
/// publicada obliga a versionarla, y una API nueva no toca a sus consumidores.
///
/// **Tres reglas para quien consuma esta API**, y las tres salieron de la revisión
/// adversarial de RV14:
/// 1. **Responde «ahora».** Para validar una firma hecha en el bloque N, llama a esta
/// API EN el hash del bloque N (toda runtime API de Substrate lo admite), o
/// reconstruye la historia con los eventos `KeyRotated`. Verificar una firma
/// antigua contra la llave de hoy la rechaza aunque fuera legítima.
/// 2. **Verifica con reglas estrictas** (`verify_strict` en dalek o equivalente). La
/// cadena ya rechaza llaves de orden pequeño, pero un verificador permisivo fuera
/// de cadena sigue aceptando firmas maleables.
/// 3. **Separa dominios.** Esta misma llave firma cosas fuera de cadena. Si algún
/// protocolo le hace firmar bytes elegidos por otro sin prefijo propio, quien tenga
/// solo la cuenta podría conseguir una firma de rotación.
pub trait AgentKeyApi {
/// Llave Ed25519 vigente de `did`, o `None` si el DID no está registrado.
fn current_key(did: origin_chain_types::Did) -> Option<[u8; 32]>;
}
}
#[frame_support::pallet]
pub mod pallet {
use alloc::vec::Vec;
@@ -163,7 +274,7 @@ pub mod pallet {
use frame_system::pallet_prelude::*;
use origin_chain_types::{AccountId, AgentChain, AgentIdentity, AgentKind, Did};
use sp_core::ed25519;
use sp_runtime::traits::SaturatedConversion;
use sp_runtime::traits::{SaturatedConversion, Zero};
// Trait de weights en scope para resolver `T::WeightInfo::<method>()`.
use crate::weights::WeightInfo;
@@ -216,6 +327,32 @@ pub mod pallet {
pub type DidToAccount<T: Config> =
StorageMap<_, Blake2_128Concat, Did, T::AccountId, OptionQuery>;
/// Llave Ed25519 VIGENTE de cada DID.
///
/// **Ausente ⇒ manda la llave de nacimiento**, que ya vive en
/// `Identities[cuenta].creator`. Así las identidades registradas antes de que
/// existiera la rotación no necesitan migración: para ellas la vigente y la de
/// nacimiento son la misma, y el resolutor lo sabe.
///
/// El DID NO cambia al rotar. Nunca se re-deriva: se calculó una vez en el
/// registro y es el identificador de por vida. Lo que cambia es qué llave tiene
/// autoridad sobre él.
#[pallet::storage]
pub type CurrentKey<T: Config> = StorageMap<_, Blake2_128Concat, Did, [u8; 32], OptionQuery>;
/// Registro de TODA llave que alguna vez tuvo autoridad sobre un DID.
///
/// Una llave manda sobre un solo DID en toda la vida de la cadena. Una llave
/// retirada queda **quemada**: no puede volver ni al mismo DID ni a otro. La
/// razón es la más común para rotar —que la llave vieja se haya filtrado— y
/// dejar reutilizarla sería devolverle la autoridad a quien la robó.
#[pallet::storage]
pub type KeyToDid<T: Config> = StorageMap<_, Blake2_128Concat, [u8; 32], Did, OptionQuery>;
/// Contador de rotaciones por DID. Entra en el mensaje firmado (anti-replay).
#[pallet::storage]
pub type RotationNonce<T: Config> = StorageMap<_, Blake2_128Concat, Did, u32, ValueQuery>;
/// Events emisión canonical (CHN-7 + Inmutable #41).
#[pallet::event]
#[pallet::generate_deposit(pub(super) fn deposit_event)]
@@ -249,6 +386,23 @@ pub mod pallet {
/// T::AccountId del agent canonical.
account: T::AccountId,
},
/// La autoridad sobre un DID pasó a otra llave. El DID no cambió.
///
/// Va AL FINAL del enum a propósito: insertarlo en medio corría el índice SCALE de
/// `AgentUpdated` de 2 a 3, y un decodificador estático generado con la metadata de
/// RV13 leería un `KeyRotated` como `AgentUpdated`.
KeyRotated {
/// Cuenta titular de la identidad.
account: T::AccountId,
/// El DID, idéntico antes y después.
did: Did,
/// Llave que tenía la autoridad y queda quemada.
old_key: [u8; 32],
/// Llave que la tiene ahora.
new_key: [u8; 32],
/// Nonce consumido por esta rotación.
nonce: u32,
},
}
/// Pallet errors canonical.
@@ -269,6 +423,25 @@ pub mod pallet {
/// debe ser `AGENT_DID_REGISTER_DOMAIN || who.encode()`, firmado con la
/// private key de `public_key`.
InvalidSignature,
/// La cuenta no tiene identidad registrada: no hay nada que rotar.
NotRegistered,
/// La llave nueva es la vigente. Rotar a la misma llave no cambia nada y
/// consumiría un nonce en vano.
SameKey,
/// La llave nueva ya tuvo autoridad sobre algún DID —éste u otro, vigente o
/// retirada—, o es la llave de nacimiento de otro DID. Una llave manda sobre
/// un solo DID en toda la vida de la cadena.
KeyAlreadyUsed,
/// La firma de la llave VIGENTE no verifica: quien pide rotar no tiene la
/// autoridad actual sobre el DID.
InvalidCurrentKeySignature,
/// La firma de la llave NUEVA no verifica: no se puede ceder la autoridad a
/// una llave cuyo dueño no ha probado controlarla.
InvalidNewKeySignature,
/// La llave no es una llave con la que solo su dueño pueda firmar: no está en la
/// curva, su codificación no es canónica o es de orden pequeño. Con una llave así
/// cualquiera produce firmas válidas en nombre del DID.
WeakKey,
}
/// Dispatchables canonical (real impl v0.0.26).
@@ -309,6 +482,13 @@ pub mod pallet {
Error::<T>::AlreadyRegistered
);
// Step 2.2: la llave tiene que ser una con la que solo su dueño firme. Sin esto
// una llave de orden pequeño supera la verificación con una firma trivial.
ensure!(
crate::is_strong_ed25519_key(&public_key),
Error::<T>::WeakKey
);
// Step 2.5: FIX-3 (V3-H1) DID BINDING — prueba de control de public_key.
// El registrante presenta una firma Ed25519 (por la private key de
// public_key) sobre AGENT_DID_REGISTER_DOMAIN || who.encode(). Cierra el
@@ -333,6 +513,21 @@ pub mod pallet {
Error::<T>::DidAlreadyTaken
);
// Step 4.5: una llave manda sobre un solo DID en toda la vida de la cadena.
// Faltaba: el registro ESCRIBÍA en `KeyToDid` pero no lo CONSULTABA, así que
// quien tuviera una llave a la que otro DID ya había rotado —vigente o
// retirada— podía fundar con ella un DID nuevo y sobrescribir el índice.
// Encontrado cinco veces, por separado, en la revisión adversarial de RV14.
//
// Va DETRÁS de la colisión de DID a propósito: registrar dos veces la misma
// llave desde dos cuentas sigue devolviendo `DidAlreadyTaken`, el error
// documentado de ese caso (scenario_12). Este chequeo atrapa lo que aquel no
// ve: llaves que entraron por ROTACIÓN, cuyo `derive_did` nunca se registró.
ensure!(
!KeyToDid::<T>::contains_key(public_key),
Error::<T>::KeyAlreadyUsed
);
// Step 5: build AgentChain canonical BoundedVec<AccountId, ConstU32<10>>
let mut agent_chain = AgentChain::default();
for raw in co_authors_raw {
@@ -364,6 +559,9 @@ pub mod pallet {
Identities::<T>::insert(&who, identity);
DidToAccount::<T>::insert(did, &who);
// La llave de nacimiento entra en el registro de llaves usadas desde el
// primer bloque: ninguna rotación posterior de nadie podrá apuntar a ella.
KeyToDid::<T>::insert(public_key, did);
// Step 8: emit events canonical
Self::deposit_event(Event::AgentRegistered {
@@ -381,6 +579,116 @@ pub mod pallet {
Ok(())
}
/// Rota la llave con autoridad sobre el DID del firmante. **El DID no cambia.**
///
/// Existe porque sin ella una identidad no sobrevive a perder o filtrar su
/// llave: la única salida era registrarse otra vez, que genera OTRO DID, y con
/// él se pierden credenciales, créditos e historial. Es la misma ley que cerró
/// la condición #4 del DC de identidad: una identidad tiene que sobrevivir a
/// aquello en lo que se le permite convertirse.
///
/// Exige DOS firmas sobre `agent_key_rotate_message`:
/// - la de la llave **vigente**, que autoriza ceder la autoridad;
/// - la de la llave **nueva**, que prueba que su dueño la controla.
///
/// Sin la segunda, un titular podría «rotar» su DID a la llave pública de un
/// tercero, que pasaría a figurar como autoridad de una identidad que no es suya.
///
/// **Hacen falta LAS DOS cosas: la cuenta titular Y la llave vigente.** Son dos
/// secretos distintos (sr25519 para firmar la extrinsic, Ed25519 para la autoridad
/// sobre el DID) y perder CUALQUIERA de los dos deja la rotación imposible:
/// - sin la llave vigente, nadie produce la primera firma;
/// - sin la cuenta, `Identities[who]` no existe para la cuenta nueva
/// (`NotRegistered`), aunque la llave esté a salvo.
///
/// La versión anterior de este comentario decía que el único caso sin cubrir era
/// perder la llave. Era falso a medias, y la revisión adversarial lo midió. Los dos
/// casos son la RECUPERACIÓN por guardianes, que va aparte porque tiene su propio
/// ataque (robar la cuenta y retirar a los guardianes antes de que el dueño
/// reaccione).
#[pallet::call_index(1)]
#[pallet::weight(T::WeightInfo::rotate_agent_key())]
pub fn rotate_agent_key(
origin: OriginFor<T>,
new_public_key: [u8; 32],
current_key_signature: [u8; 64],
new_key_signature: [u8; 64],
) -> DispatchResult {
let who = ensure_signed(origin)?;
let identity = Identities::<T>::get(&who).ok_or(Error::<T>::NotRegistered)?;
let did = identity.did;
let current_key = CurrentKey::<T>::get(did).unwrap_or(identity.creator.0);
ensure!(new_public_key != current_key, Error::<T>::SameKey);
ensure!(
crate::is_strong_ed25519_key(&new_public_key),
Error::<T>::WeakKey
);
// Quemada si alguna vez mandó sobre un DID, o si es la llave de nacimiento
// de un DID registrado (la segunda condición cubre registros que
// precedieran al índice `KeyToDid`).
ensure!(
!KeyToDid::<T>::contains_key(new_public_key)
&& !DidToAccount::<T>::contains_key(crate::derive_did(&new_public_key)),
Error::<T>::KeyAlreadyUsed
);
let nonce = RotationNonce::<T>::get(did);
let genesis_full = <frame_system::Pallet<T>>::block_hash(BlockNumberFor::<T>::zero());
let genesis_ref: &[u8] = genesis_full.as_ref();
let mut genesis_hash = [0u8; 32];
let n = genesis_ref.len().min(32);
genesis_hash[..n].copy_from_slice(&genesis_ref[..n]);
let message = crate::agent_key_rotate_message(
&genesis_hash,
did.as_bytes(),
&current_key,
&new_public_key,
nonce,
&who.encode(),
);
ensure!(
sp_io::crypto::ed25519_verify(
&ed25519::Signature::from_raw(current_key_signature),
&message,
&ed25519::Public::from_raw(current_key),
),
Error::<T>::InvalidCurrentKeySignature
);
ensure!(
sp_io::crypto::ed25519_verify(
&ed25519::Signature::from_raw(new_key_signature),
&message,
&ed25519::Public::from_raw(new_public_key),
),
Error::<T>::InvalidNewKeySignature
);
CurrentKey::<T>::insert(did, new_public_key);
// La vieja ya está en el índice en todo camino actual (el registro la escribe y
// cada rotación escribe la nueva). Solo se escribe si falta, y NUNCA se
// sobrescribe: antes era un insert incondicional que podía pisar una entrada
// que apuntaba a OTRO DID.
if !KeyToDid::<T>::contains_key(current_key) {
KeyToDid::<T>::insert(current_key, did);
}
KeyToDid::<T>::insert(new_public_key, did);
RotationNonce::<T>::insert(did, nonce.saturating_add(1));
Self::deposit_event(Event::KeyRotated {
account: who,
did,
old_key: current_key,
new_key: new_public_key,
nonce,
});
Ok(())
}
}
/// Pure helper canonical para Runtime API v0.0.27 consumer.
@@ -389,6 +697,16 @@ pub mod pallet {
pub fn query_identity_of(account: &T::AccountId) -> Option<AgentIdentity> {
Identities::<T>::get(account)
}
/// Llave Ed25519 con autoridad HOY sobre `did`: la rotada si hubo rotación, la
/// de nacimiento si no. `None` si el DID no está registrado.
pub fn current_key_of(did: &Did) -> Option<[u8; 32]> {
if let Some(k) = CurrentKey::<T>::get(did) {
return Some(k);
}
let account = DidToAccount::<T>::get(did)?;
Identities::<T>::get(account).map(|id| id.creator.0)
}
}
}
@@ -402,6 +720,22 @@ pub mod mock;
#[cfg(test)]
mod tests_integration;
#[cfg(test)]
mod tests_rotation;
/// Vectores dorados de la rotación para el benchmark (que no puede firmar en no_std).
/// Los fija `tests_rotation::vectores_dorados_del_benchmark_de_rotacion`.
#[cfg(any(test, feature = "runtime-benchmarks"))]
pub mod benchmark_golden {
/// Llave pública de DAVE (seed `[0x44;32]`), destino de la rotación.
pub const ROT_DAVE_PUBKEY: &str =
"d759793bbc13a2819a827c76adb6fba8a49aee007f49f2d0992d99b825ad2c48";
/// ALICE (vigente) firma la rotación ALICE→DAVE, génesis `0xAB×32`, nonce 0, who=`[1u8;32]`.
pub const ROT_SIG_CURRENT: &str = "4492d319dfbd4c2e4701b3b0b3e4391f269b96b8c85f3bd388c8275a8d0c5ef0c2af41d47d422db53571f2f93ad7abb9b0b2603813169223d3a0b756202afb05";
/// DAVE (nueva) firma la misma rotación.
pub const ROT_SIG_NEW: &str = "122f3d749410f0029610c402e9096d0709f570fbd1819db057fac2d698654019ed0766c3ff08959be7a14ecf65c47fb46c93aa3361bc102dcf639975f154e60b";
}
#[cfg(test)]
mod tests {
use super::*;
@@ -547,4 +881,3 @@ mod tests {
assert_eq!(core::mem::size_of::<Did>(), 32);
}
}
@@ -40,10 +40,17 @@ pub fn new_test_ext() -> sp_io::TestExternalities {
.into();
ext.execute_with(|| {
System::set_block_number(1);
// El mensaje de rotación lleva el hash de génesis (anti-replay entre cadenas).
// Se siembra un valor fijo para que las firmas de los tests sean deterministas,
// igual que `origin-chain-provenance`.
frame_system::BlockHash::<Test>::insert(0u64, sp_core::H256(GENESIS));
});
ext
}
/// Génesis de los tests de rotación. Mismo valor que usa `origin-chain-provenance`.
pub const GENESIS: [u8; 32] = [0xABu8; 32];
/// Test origin canonical ALICE.
pub fn alice_origin() -> RuntimeOrigin {
RuntimeOrigin::signed(sp_runtime::AccountId32::new([1u8; 32]))
@@ -103,3 +110,50 @@ pub fn sign_registration(pair: &ed25519::Pair, who: &sp_runtime::AccountId32) ->
let msg = origin_chain_agent_identity::agent_did_register_message(&who.encode());
pair.sign(&msg).0
}
// ─── Rotación de llave ────────────────────────────────────────────────────
/// Ed25519 pair DAVE — llave NUEVA a la que rota ALICE en los tests.
pub fn dave_pair() -> ed25519::Pair {
ed25519::Pair::from_seed(&[0x44u8; 32])
}
/// Ed25519 pair EVE — segunda rotación / atacante.
pub fn eve_pair() -> ed25519::Pair {
ed25519::Pair::from_seed(&[0x55u8; 32])
}
/// Firma de rotación: `pair` firma el mensaje de rotación con EL MISMO builder que
/// el pallet. Se usa dos veces por rotación —llave vigente y llave nueva—.
pub fn sign_rotation(
pair: &ed25519::Pair,
did: &[u8; 32],
current_key: &[u8; 32],
new_key: &[u8; 32],
nonce: u32,
who: &sp_runtime::AccountId32,
) -> [u8; 64] {
sign_rotation_on(&GENESIS, pair, did, current_key, new_key, nonce, who)
}
/// Igual que `sign_rotation` pero firmando para OTRA cadena (otro génesis). Existe para
/// probar que una rotación firmada en una cadena no vale en otra.
pub fn sign_rotation_on(
genesis: &[u8; 32],
pair: &ed25519::Pair,
did: &[u8; 32],
current_key: &[u8; 32],
new_key: &[u8; 32],
nonce: u32,
who: &sp_runtime::AccountId32,
) -> [u8; 64] {
use parity_scale_codec::Encode;
let msg = origin_chain_agent_identity::agent_key_rotate_message(
genesis,
did,
current_key,
new_key,
nonce,
&who.encode(),
);
pair.sign(&msg).0
}
@@ -0,0 +1,533 @@
//! # Rotación de llave — la identidad sobrevive a perder o filtrar su llave
//!
//! Estos tests fijan tres propiedades, en orden de gravedad:
//!
//! 1. **Tras rotar, la llave vieja pierde la autoridad.** Es la razón de existir de
//! la rotación: se rota porque la llave se filtró, y si la vieja siguiera
//! mandando, rotar no protegería de nada.
//! 2. **No se puede ceder la autoridad a una llave ajena.** Hace falta la firma de
//! la llave nueva; sin ella un titular podría poner como autoridad de su DID la
//! llave pública de un tercero.
//! 3. **El DID no cambia.** Es el identificador de por vida; lo que cambia es qué
//! llave manda sobre él.
#![cfg(test)]
use crate::mock::{
alice_account, alice_origin, alice_pair, alice_pubkey, bob_account, bob_origin, bob_pair,
bob_pubkey, dave_pair, eve_pair, new_test_ext, sign_registration, sign_rotation,
sign_rotation_on, AgentIdentity as Pallet, RuntimeEvent, System, Test, GENESIS,
};
use crate::{
derive_did, CurrentKey, DidToAccount, Error, Event, Identities, KeyToDid, RotationNonce,
};
use frame_support::{assert_noop, assert_ok};
use origin_chain_types::AgentKind;
use sp_core::Pair;
/// Registra a ALICE con su llave de nacimiento y devuelve su DID.
fn registrar_alice() -> [u8; 32] {
let sig = sign_registration(&alice_pair(), &alice_account());
assert_ok!(Pallet::register_agent_did(
alice_origin(),
alice_pubkey(),
AgentKind::Human,
alloc::vec::Vec::new(),
sig,
));
*derive_did(&alice_pubkey()).as_bytes()
}
/// Rota la autoridad del DID de ALICE desde `actual` hacia `nueva`, firmando bien.
fn rotar_alice(actual: &sp_core::ed25519::Pair, nueva: &sp_core::ed25519::Pair) {
let did = *derive_did(&alice_pubkey()).as_bytes();
let nonce = RotationNonce::<Test>::get(derive_did(&alice_pubkey()));
let (k_act, k_new) = (actual.public().0, nueva.public().0);
let s_act = sign_rotation(actual, &did, &k_act, &k_new, nonce, &alice_account());
let s_new = sign_rotation(nueva, &did, &k_act, &k_new, nonce, &alice_account());
assert_ok!(Pallet::rotate_agent_key(
alice_origin(),
k_new,
s_act,
s_new
));
}
extern crate alloc;
#[test]
fn sin_rotacion_la_llave_vigente_es_la_de_nacimiento() {
new_test_ext().execute_with(|| {
registrar_alice();
let did = derive_did(&alice_pubkey());
assert_eq!(Pallet::current_key_of(&did), Some(alice_pubkey()));
// Fija lo que `creator` guarda de verdad: la llave de nacimiento, no la cuenta.
let id = Identities::<Test>::get(alice_account()).expect("registrada");
assert_eq!(id.creator.0, alice_pubkey());
// Y ningún CurrentKey escrito: el resolutor cae a `creator` sin migración.
assert!(CurrentKey::<Test>::get(did).is_none());
});
}
#[test]
fn el_registro_quema_la_llave_de_nacimiento() {
new_test_ext().execute_with(|| {
registrar_alice();
assert_eq!(
KeyToDid::<Test>::get(alice_pubkey()),
Some(derive_did(&alice_pubkey()))
);
});
}
#[test]
fn rotar_conserva_el_did_y_cambia_la_autoridad() {
new_test_ext().execute_with(|| {
registrar_alice();
let did = derive_did(&alice_pubkey());
rotar_alice(&alice_pair(), &dave_pair());
// El DID es el mismo en los dos índices.
assert_eq!(Identities::<Test>::get(alice_account()).unwrap().did, did);
assert_eq!(DidToAccount::<Test>::get(did), Some(alice_account()));
// La autoridad es la nueva.
assert_eq!(Pallet::current_key_of(&did), Some(dave_pair().public().0));
System::assert_last_event(RuntimeEvent::AgentIdentity(Event::KeyRotated {
account: alice_account(),
did,
old_key: alice_pubkey(),
new_key: dave_pair().public().0,
nonce: 0,
}));
});
}
/// LA PROPIEDAD QUE JUSTIFICA LA ROTACIÓN. Si la llave vieja conservara la autoridad,
/// rotar tras una filtración no protegería de nada: el ladrón seguiría mandando.
#[test]
fn tras_rotar_la_llave_vieja_pierde_la_autoridad() {
new_test_ext().execute_with(|| {
registrar_alice();
rotar_alice(&alice_pair(), &dave_pair());
let did = *derive_did(&alice_pubkey()).as_bytes();
let (dave, eve) = (dave_pair().public().0, eve_pair().public().0);
// El ladrón solo tiene la llave VIEJA (y la cuenta). Firma lo único que puede
// firmar: una rotación que declara como vigente a la vieja.
//
// ⚠️ La primera versión de este test firmaba con `current = dave`, que es algo que
// un ladrón real no conoce como suyo, y el test pasaba por la razón equivocada. Lo
// midió el crítico de la revisión adversarial: con el pallet roto para ignorar
// `CurrentKey`, esta mitad seguía verde y el ataque real devolvía `Ok`. El test
// solo se ponía rojo por su mitad POSITIVA. Ahora el ladrón firma con la vieja.
let alice = alice_pubkey();
let robada = sign_rotation(&alice_pair(), &did, &alice, &eve, 1, &alice_account());
let de_eve = sign_rotation(&eve_pair(), &did, &alice, &eve, 1, &alice_account());
assert_noop!(
Pallet::rotate_agent_key(alice_origin(), eve, robada, de_eve),
Error::<Test>::InvalidCurrentKeySignature
);
// Y la llave vigente sí puede.
rotar_alice(&dave_pair(), &eve_pair());
assert_eq!(
Pallet::current_key_of(&derive_did(&alice_pubkey())),
Some(eve)
);
});
}
#[test]
fn sin_firma_de_la_llave_vigente_no_se_rota() {
new_test_ext().execute_with(|| {
let did = registrar_alice();
let (a, d) = (alice_pubkey(), dave_pair().public().0);
// La "firma de la vigente" la produce otra llave.
let falsa = sign_rotation(&eve_pair(), &did, &a, &d, 0, &alice_account());
let nueva = sign_rotation(&dave_pair(), &did, &a, &d, 0, &alice_account());
assert_noop!(
Pallet::rotate_agent_key(alice_origin(), d, falsa, nueva),
Error::<Test>::InvalidCurrentKeySignature
);
});
}
/// Sin la firma de la llave nueva, un titular podría poner como autoridad de su DID
/// la llave pública de un tercero — que figuraría como dueña de una identidad ajena.
#[test]
fn no_se_puede_ceder_la_autoridad_a_una_llave_ajena() {
new_test_ext().execute_with(|| {
let did = registrar_alice();
let (a, e) = (alice_pubkey(), eve_pair().public().0);
let vigente = sign_rotation(&alice_pair(), &did, &a, &e, 0, &alice_account());
// Alice "firma" también por Eve, cuya llave privada no tiene.
let suplantada = sign_rotation(&alice_pair(), &did, &a, &e, 0, &alice_account());
assert_noop!(
Pallet::rotate_agent_key(alice_origin(), e, vigente, suplantada),
Error::<Test>::InvalidNewKeySignature
);
});
}
#[test]
fn la_misma_rotacion_no_puede_reenviarse() {
new_test_ext().execute_with(|| {
let did = registrar_alice();
let (a, d) = (alice_pubkey(), dave_pair().public().0);
let s_a = sign_rotation(&alice_pair(), &did, &a, &d, 0, &alice_account());
let s_d = sign_rotation(&dave_pair(), &did, &a, &d, 0, &alice_account());
assert_ok!(Pallet::rotate_agent_key(alice_origin(), d, s_a, s_d));
// Reenviada tal cual: ahora rotar a `d` es rotar a la vigente.
assert_noop!(
Pallet::rotate_agent_key(alice_origin(), d, s_a, s_d),
Error::<Test>::SameKey
);
});
}
/// Una llave retirada queda quemada para siempre.
///
/// ⚠️ La primera versión de este test volvía a la llave de NACIMIENTO, y pasaba por
/// la razón equivocada: esa la atrapa la cláusula de `derive_did`, no el índice
/// `KeyToDid`. Lo destapó el contrafactual —se borró el chequeo de `KeyToDid` y el
/// test siguió verde—. Por eso aquí se rota a una llave retirada que NO es de
/// nacimiento de ningún DID (dave): solo el índice puede rechazarla.
#[test]
fn una_llave_retirada_queda_quemada() {
new_test_ext().execute_with(|| {
let did = registrar_alice();
rotar_alice(&alice_pair(), &dave_pair()); // dave manda
rotar_alice(&dave_pair(), &eve_pair()); // dave queda retirada, eve manda
let (e, d) = (eve_pair().public().0, dave_pair().public().0);
// dave no es llave de nacimiento de nadie: si se rechaza, es por el índice.
assert!(!DidToAccount::<Test>::contains_key(derive_did(&d)));
let s_e = sign_rotation(&eve_pair(), &did, &e, &d, 2, &alice_account());
let s_d = sign_rotation(&dave_pair(), &did, &e, &d, 2, &alice_account());
assert_noop!(
Pallet::rotate_agent_key(alice_origin(), d, s_e, s_d),
Error::<Test>::KeyAlreadyUsed
);
});
}
/// La llave de nacimiento propia también está quemada una vez retirada. La atrapa
/// una cláusula distinta (`derive_did`), así que se prueba aparte.
#[test]
fn no_se_vuelve_a_la_llave_de_nacimiento_propia() {
new_test_ext().execute_with(|| {
let did = registrar_alice();
rotar_alice(&alice_pair(), &dave_pair());
let (d, a) = (dave_pair().public().0, alice_pubkey());
let s_d = sign_rotation(&dave_pair(), &did, &d, &a, 1, &alice_account());
let s_a = sign_rotation(&alice_pair(), &did, &d, &a, 1, &alice_account());
assert_noop!(
Pallet::rotate_agent_key(alice_origin(), a, s_d, s_a),
Error::<Test>::KeyAlreadyUsed
);
});
}
#[test]
fn no_se_rota_a_la_llave_de_nacimiento_de_otro_did() {
new_test_ext().execute_with(|| {
let did = registrar_alice();
let sig_bob = sign_registration(&bob_pair(), &bob_account());
assert_ok!(Pallet::register_agent_did(
bob_origin(),
bob_pubkey(),
AgentKind::Human,
alloc::vec::Vec::new(),
sig_bob
));
let (a, b) = (alice_pubkey(), bob_pubkey());
let s_a = sign_rotation(&alice_pair(), &did, &a, &b, 0, &alice_account());
let s_b = sign_rotation(&bob_pair(), &did, &a, &b, 0, &alice_account());
assert_noop!(
Pallet::rotate_agent_key(alice_origin(), b, s_a, s_b),
Error::<Test>::KeyAlreadyUsed
);
});
}
#[test]
fn no_se_rota_a_la_misma_llave() {
new_test_ext().execute_with(|| {
let did = registrar_alice();
let a = alice_pubkey();
let s = sign_rotation(&alice_pair(), &did, &a, &a, 0, &alice_account());
assert_noop!(
Pallet::rotate_agent_key(alice_origin(), a, s, s),
Error::<Test>::SameKey
);
});
}
#[test]
fn sin_identidad_no_hay_nada_que_rotar() {
new_test_ext().execute_with(|| {
let (d, e) = (dave_pair().public().0, eve_pair().public().0);
let s = sign_rotation(&dave_pair(), &[0u8; 32], &d, &e, 0, &bob_account());
assert_noop!(
Pallet::rotate_agent_key(bob_origin(), e, s, s),
Error::<Test>::NotRegistered
);
});
}
/// La firma queda atada a la cuenta que la envía. Todo lo demás es correcto —mismo
/// DID, mismas llaves, mismo nonce— salvo `who`, y basta para que no verifique.
#[test]
fn la_firma_de_rotacion_esta_atada_a_la_cuenta() {
new_test_ext().execute_with(|| {
let did = registrar_alice();
let (a, d) = (alice_pubkey(), dave_pair().public().0);
let s_a = sign_rotation(&alice_pair(), &did, &a, &d, 0, &bob_account());
let s_d = sign_rotation(&dave_pair(), &did, &a, &d, 0, &bob_account());
assert_noop!(
Pallet::rotate_agent_key(alice_origin(), d, s_a, s_d),
Error::<Test>::InvalidCurrentKeySignature
);
});
}
#[test]
fn el_nonce_avanza_con_cada_rotacion() {
new_test_ext().execute_with(|| {
registrar_alice();
let did = derive_did(&alice_pubkey());
assert_eq!(RotationNonce::<Test>::get(did), 0);
rotar_alice(&alice_pair(), &dave_pair());
assert_eq!(RotationNonce::<Test>::get(did), 1);
rotar_alice(&dave_pair(), &eve_pair());
assert_eq!(RotationNonce::<Test>::get(did), 2);
});
}
/// Imprime y FIJA los vectores dorados que usa el benchmark de `rotate_agent_key`.
///
/// El benchmark corre en `no_std` y no puede firmar, así que lleva las firmas
/// precomputadas. Este test las recalcula con el mismo builder y falla si alguien
/// cambia el mensaje de rotación sin regenerarlas: un benchmark con firmas viejas
/// mediría el camino de ERROR (la verificación falla pronto) y daría un peso falso.
#[test]
fn vectores_dorados_del_benchmark_de_rotacion() {
use crate::benchmark_golden::{ROT_DAVE_PUBKEY, ROT_SIG_CURRENT, ROT_SIG_NEW};
let did = *derive_did(&alice_pubkey()).as_bytes();
let (a, d) = (alice_pubkey(), dave_pair().public().0);
let s_a = sign_rotation(&alice_pair(), &did, &a, &d, 0, &alice_account());
let s_d = sign_rotation(&dave_pair(), &did, &a, &d, 0, &alice_account());
let hex = |b: &[u8]| {
b.iter()
.map(|x| alloc::format!("{x:02x}"))
.collect::<alloc::string::String>()
};
std::println!("ROT_DAVE_PUBKEY={}", hex(&d));
std::println!("ROT_SIG_CURRENT={}", hex(&s_a));
std::println!("ROT_SIG_NEW={}", hex(&s_d));
assert_eq!(hex(&d), ROT_DAVE_PUBKEY, "pubkey de dave cambió");
assert_eq!(
hex(&s_a),
ROT_SIG_CURRENT,
"el mensaje de rotación cambió: regenera la firma vigente"
);
assert_eq!(
hex(&s_d),
ROT_SIG_NEW,
"el mensaje de rotación cambió: regenera la firma nueva"
);
}
// ─── Defectos encontrados por la revisión adversarial de RV14 ───────────────
/// Firma trivial `R = identidad, s = 0`. Con una llave de orden pequeño, el verificador
/// ZIP215 del host la da por buena para CUALQUIER mensaje.
fn firma_trivial() -> [u8; 64] {
let mut s = [0u8; 64];
s[0] = 1; // R = punto identidad comprimido (01 00..00)
s
}
fn hex32(h: &str) -> [u8; 32] {
let mut out = [0u8; 32];
for i in 0..32 {
out[i] = u8::from_str_radix(&h[2 * i..2 * i + 2], 16).unwrap();
}
out
}
/// Las tres llaves débiles medidas por la sonda: identidad canónica, orden 2, e
/// identidad con codificación NO canónica (y = p + 1).
fn llaves_debiles() -> [[u8; 32]; 3] {
[
hex32("0100000000000000000000000000000000000000000000000000000000000000"),
hex32("ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"),
hex32("eeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff7f"),
]
}
/// CONTROL POSITIVO del verificador: sin él, un `is_strong` que devolviera siempre
/// `false` haría verdes todos los tests de rechazo y bloquearía a todo el mundo.
#[test]
fn las_llaves_reales_son_fuertes_y_las_debiles_no() {
for p in [alice_pair(), bob_pair(), dave_pair(), eve_pair()] {
assert!(
crate::is_strong_ed25519_key(&p.public().0),
"llave real rechazada"
);
}
for k in llaves_debiles() {
assert!(
!crate::is_strong_ed25519_key(&k),
"llave débil aceptada: {k:02x?}"
);
}
}
/// El ataque que hace grave el hallazgo: rotar a una llave de orden pequeño con una
/// firma trivial en lugar de la prueba de control. Antes daba `Ok` y dejaba el DID bajo
/// una llave con la que cualquiera firma en su nombre.
#[test]
fn no_se_rota_a_una_llave_debil() {
new_test_ext().execute_with(|| {
let did = registrar_alice();
let a = alice_pubkey();
for debil in llaves_debiles() {
let vigente = sign_rotation(&alice_pair(), &did, &a, &debil, 0, &alice_account());
assert_noop!(
Pallet::rotate_agent_key(alice_origin(), debil, vigente, firma_trivial()),
Error::<Test>::WeakKey
);
}
});
}
/// El registro tenía el mismo agujero: cualquiera fundaba un DID que nadie controla.
#[test]
fn no_se_registra_con_una_llave_debil() {
new_test_ext().execute_with(|| {
for debil in llaves_debiles() {
assert_noop!(
Pallet::register_agent_did(
bob_origin(),
debil,
AgentKind::Human,
alloc::vec::Vec::new(),
firma_trivial()
),
Error::<Test>::WeakKey
);
}
});
}
/// El hallazgo que salió CINCO veces: el registro escribía en `KeyToDid` pero no lo
/// consultaba. Una llave retirada de un DID fundaba otro DID y pisaba el índice.
#[test]
fn no_se_registra_con_una_llave_retirada_de_otro_did() {
new_test_ext().execute_with(|| {
registrar_alice();
rotar_alice(&alice_pair(), &dave_pair());
rotar_alice(&dave_pair(), &eve_pair()); // dave retirada: el caso típico de una filtrada
let d = dave_pair().public().0;
let sig = sign_registration(&dave_pair(), &bob_account());
assert_noop!(
Pallet::register_agent_did(
bob_origin(),
d,
AgentKind::Human,
alloc::vec::Vec::new(),
sig
),
Error::<Test>::KeyAlreadyUsed
);
// Y el índice sigue apuntando al DID de alice.
assert_eq!(KeyToDid::<Test>::get(d), Some(derive_did(&alice_pubkey())));
});
}
#[test]
fn no_se_registra_con_la_llave_vigente_de_otro_did() {
new_test_ext().execute_with(|| {
registrar_alice();
rotar_alice(&alice_pair(), &dave_pair()); // dave manda sobre el DID de alice
let d = dave_pair().public().0;
let sig = sign_registration(&dave_pair(), &bob_account());
assert_noop!(
Pallet::register_agent_did(
bob_origin(),
d,
AgentKind::Human,
alloc::vec::Vec::new(),
sig
),
Error::<Test>::KeyAlreadyUsed
);
});
}
/// Hueco medido por el crítico: borrando `KeyToDid::insert(new_public_key, …)` pasaban
/// los 38 tests. Faltaba el caso «rotar a la llave vigente, NO de nacimiento, de otro DID».
#[test]
fn no_se_rota_a_la_llave_vigente_no_natal_de_otro_did() {
new_test_ext().execute_with(|| {
registrar_alice();
rotar_alice(&alice_pair(), &dave_pair()); // dave: vigente de alice, no natal de nadie
let sig_bob = sign_registration(&bob_pair(), &bob_account());
assert_ok!(Pallet::register_agent_did(
bob_origin(),
bob_pubkey(),
AgentKind::Human,
alloc::vec::Vec::new(),
sig_bob
));
let did_bob = *derive_did(&bob_pubkey()).as_bytes();
let (b, d) = (bob_pubkey(), dave_pair().public().0);
assert!(
!DidToAccount::<Test>::contains_key(derive_did(&d)),
"dave no es natal"
);
let s_b = sign_rotation(&bob_pair(), &did_bob, &b, &d, 0, &bob_account());
let s_d = sign_rotation(&dave_pair(), &did_bob, &b, &d, 0, &bob_account());
assert_noop!(
Pallet::rotate_agent_key(bob_origin(), d, s_b, s_d),
Error::<Test>::KeyAlreadyUsed
);
});
}
/// Replay entre cadenas: una rotación firmada con OTRO génesis no vale aquí. Todo lo
/// demás es idéntico —cuenta, DID, llaves, nonce—.
#[test]
fn una_rotacion_firmada_para_otra_cadena_no_vale() {
new_test_ext().execute_with(|| {
let did = registrar_alice();
let otra_cadena = [0xCDu8; 32];
assert_ne!(otra_cadena, GENESIS);
let (a, d) = (alice_pubkey(), dave_pair().public().0);
let s_a = sign_rotation_on(
&otra_cadena,
&alice_pair(),
&did,
&a,
&d,
0,
&alice_account(),
);
let s_d = sign_rotation_on(
&otra_cadena,
&dave_pair(),
&did,
&a,
&d,
0,
&alice_account(),
);
assert_noop!(
Pallet::rotate_agent_key(alice_origin(), d, s_a, s_d),
Error::<Test>::InvalidCurrentKeySignature
);
});
}
@@ -14,7 +14,7 @@
//! procedimiento completo y el hardware al que hay que creerle).
//!
//! GENERADO POR SUBSTRATE BENCHMARK CLI VERSION 51.0.0
//! FECHA: 2026-09-09, STEPS: `50`, REPEAT: `20`
//! FECHA: 2026-09-16, STEPS: `50`, REPEAT: `20`
//! HOSTNAME: `MacBookProM3Max.local`, CPU: `<UNKNOWN>`
//! WASM-EXECUTION: `Compiled`, DB CACHE: 1024
@@ -24,7 +24,7 @@
// benchmark
// pallet
// --runtime
// /Users/davidherran/originmetaverse/artifacts/origin-chain/origin_chain_runtime_rv12_bench.compact.compressed.wasm
// target/release/wbuild/origin-chain-runtime/origin_chain_runtime.compact.compressed.wasm
// --genesis-builder-preset
// development
// --pallet
@@ -36,11 +36,11 @@
// --repeat
// 20
// --template
// /Users/davidherran/originmetaverse/external/arkhe-origin-chain/scripts/bench/origin-weights-template.hbs
// scripts/bench/origin-weights-template.hbs
// --header
// /Users/davidherran/originmetaverse/external/arkhe-origin-chain/scripts/bench/HEADER.txt
// scripts/bench/HEADER.txt
// --output
// /Users/davidherran/originmetaverse/external/arkhe-origin-chain/crates/origin-chain-agent-identity/src/weights.rs
// crates/origin-chain-agent-identity/src/weights.rs
#![cfg_attr(rustfmt, rustfmt_skip)]
#![allow(unused_parens)]
@@ -54,6 +54,8 @@ use frame_support::{traits::Get, weights::constants::RocksDbWeight, weights::Wei
pub trait WeightInfo {
/// Peso de `register_agent_did` — MEDIDO sobre el WASM, no estimado a mano.
fn register_agent_did() -> Weight;
/// Peso de `rotate_agent_key` — MEDIDO sobre el WASM, no estimado a mano.
fn rotate_agent_key() -> Weight;
}
/// Weights parametrizados por el `DbWeight` real del runtime.
@@ -63,6 +65,8 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
/// Proof: `AgentIdentity::Identities` (`max_values`: None, `max_size`: Some(475), added: 2950, mode: `MaxEncodedLen`)
/// Storage: `AgentIdentity::DidToAccount` (r:1 w:1)
/// Proof: `AgentIdentity::DidToAccount` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
/// Storage: `AgentIdentity::KeyToDid` (r:1 w:1)
/// Proof: `AgentIdentity::KeyToDid` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
/// Storage: `System::Number` (r:1 w:0)
/// Proof: `System::Number` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
/// Storage: `System::ExecutionPhase` (r:1 w:0)
@@ -75,21 +79,57 @@ impl<T: frame_system::Config> WeightInfo for SubstrateWeight<T> {
// Proof size en bytes:
// Medido: `25`
// Estimado: `3940`
// Tiempo minimo de ejecucion: 41_000_000 picosegundos.
Weight::from_parts(43_000_000, 0)
// Tiempo minimo de ejecucion: 54_000_000 picosegundos.
Weight::from_parts(55_000_000, 0)
.saturating_add(Weight::from_parts(0, 3940))
.saturating_add(T::DbWeight::get().reads(6_u64))
.saturating_add(T::DbWeight::get().writes(4_u64))
.saturating_add(T::DbWeight::get().reads(7_u64))
.saturating_add(T::DbWeight::get().writes(5_u64))
}
/// Storage: `AgentIdentity::Identities` (r:1 w:0)
/// Proof: `AgentIdentity::Identities` (`max_values`: None, `max_size`: Some(475), added: 2950, mode: `MaxEncodedLen`)
/// Storage: `AgentIdentity::CurrentKey` (r:1 w:1)
/// Proof: `AgentIdentity::CurrentKey` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
/// Storage: `AgentIdentity::KeyToDid` (r:2 w:1)
/// Proof: `AgentIdentity::KeyToDid` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
/// Storage: `AgentIdentity::DidToAccount` (r:1 w:0)
/// Proof: `AgentIdentity::DidToAccount` (`max_values`: None, `max_size`: Some(80), added: 2555, mode: `MaxEncodedLen`)
/// Storage: `AgentIdentity::RotationNonce` (r:1 w:1)
/// Proof: `AgentIdentity::RotationNonce` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`)
/// Storage: `System::BlockHash` (r:1 w:0)
/// Proof: `System::BlockHash` (`max_values`: None, `max_size`: Some(44), added: 2519, mode: `MaxEncodedLen`)
/// Storage: `System::Number` (r:1 w:0)
/// Proof: `System::Number` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
/// Storage: `System::ExecutionPhase` (r:1 w:0)
/// Proof: `System::ExecutionPhase` (`max_values`: Some(1), `max_size`: Some(5), added: 500, mode: `MaxEncodedLen`)
/// Storage: `System::EventCount` (r:1 w:1)
/// Proof: `System::EventCount` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`)
/// Storage: `System::Events` (r:1 w:1)
/// Proof: `System::Events` (`max_values`: Some(1), `max_size`: None, mode: `Measured`)
fn rotate_agent_key() -> Weight {
// Proof size en bytes:
// Medido: `434`
// Estimado: `6100`
// Tiempo minimo de ejecucion: 93_000_000 picosegundos.
Weight::from_parts(97_000_000, 0)
.saturating_add(Weight::from_parts(0, 6100))
.saturating_add(T::DbWeight::get().reads(11_u64))
.saturating_add(T::DbWeight::get().writes(5_u64))
}
}
/// Impl para mocks y tests — `RocksDbWeight` directo, sin necesitar `T`.
impl WeightInfo for () {
fn register_agent_did() -> Weight {
Weight::from_parts(43_000_000, 0)
Weight::from_parts(55_000_000, 0)
.saturating_add(Weight::from_parts(0, 3940))
.saturating_add(RocksDbWeight::get().reads(6_u64))
.saturating_add(RocksDbWeight::get().writes(4_u64))
.saturating_add(RocksDbWeight::get().reads(7_u64))
.saturating_add(RocksDbWeight::get().writes(5_u64))
}
fn rotate_agent_key() -> Weight {
Weight::from_parts(97_000_000, 0)
.saturating_add(Weight::from_parts(0, 6100))
.saturating_add(RocksDbWeight::get().reads(11_u64))
.saturating_add(RocksDbWeight::get().writes(5_u64))
}
}
@@ -119,6 +159,7 @@ mod guard {
fn ningun_weight_declara_proof_size_cero() {
let casos: &[(&str, Weight)] = &[
("register_agent_did", <() as WeightInfo>::register_agent_did()),
("rotate_agent_key", <() as WeightInfo>::rotate_agent_key()),
];
for (nombre, w) in casos {
assert!(