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

@@ -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
);
});
}