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:
1 parent
7f68c5b656
commit
defd3fa580
12 files changed
+1146
-20
No files matched your search
Generated
+1
@@ -6383,6 +6383,7 @@ name = "origin-chain-agent-identity"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"blake3",
|
||||
"curve25519-dalek",
|
||||
"frame-benchmarking",
|
||||
"frame-support",
|
||||
"frame-system",
|
||||
|
||||
@@ -188,6 +188,9 @@ scale-info = { version = "2.11", default-features = false, features = ["derive"]
|
||||
# ============================================================
|
||||
ed25519-dalek = { version = "2", default-features = false }
|
||||
blake3 = { version = "1", default-features = false }
|
||||
# Rechazo de llaves Ed25519 débiles (orden pequeño / codificación no canónica) en
|
||||
# agent-identity. Ya estaba en el grafo del WASM vía schnorrkel y ed25519-zebra.
|
||||
curve25519-dalek = { version = "4.1.3", default-features = false }
|
||||
|
||||
# ============================================================
|
||||
# Error handling + serialization canonical
|
||||
|
||||
@@ -23,6 +23,12 @@ sp-api = { workspace = true }
|
||||
# Crypto canonical (BLAKE3 hash derivation para DID)
|
||||
blake3 = { workspace = true }
|
||||
|
||||
# Rechazo de llaves débiles: descomprimir el punto, exigir codificación canónica y
|
||||
# que no sea de orden pequeño. El verificador del host (ed25519-zebra, reglas ZIP215)
|
||||
# ACEPTA esas llaves por compatibilidad de consenso, así que el pallet tiene que
|
||||
# rechazarlas antes de que una firma trivial «pruebe» control sin llave privada.
|
||||
curve25519-dalek = { workspace = true }
|
||||
|
||||
# Substrate primitives canonical (BoundedVec consumer)
|
||||
sp-core = { workspace = true }
|
||||
sp-runtime = { workspace = true }
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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(),
|
||||
¤t_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!(
|
||||
|
||||
@@ -224,7 +224,7 @@ fn print_info() {
|
||||
println!(" • Index 9 AgentIdentity (pallet_agent_identity AGI DIDs Inmutable #41) [Phase 2.7 COMPLETE v0.0.27]");
|
||||
println!(" • Index 10 Governance (origin_chain_governance Treasury+Council+Advisors+Emergency+Disbursement CHN-4) [Phase 5 FULLY COMPLETE v0.0.34]");
|
||||
println!();
|
||||
println!("Runtime APIs canonical (15):");
|
||||
println!("Runtime APIs canonical (16):");
|
||||
println!(" • sp_api::Core (version + execute_block)");
|
||||
println!(" • sp_api::Metadata (runtime metadata)");
|
||||
println!(" • sp_block_builder::BlockBuilder (block production)");
|
||||
@@ -239,6 +239,7 @@ fn print_info() {
|
||||
println!(" • origin_chain_provenance::ProvenanceApi (verify_provenance RPC) [Phase 2.1 v0.0.17]");
|
||||
println!(" • origin_chain_fees::FeesApi (query_fee_for_action RPC) [Phase 2.4 COMPLETE v0.0.23]");
|
||||
println!(" • origin_chain_agent_identity::AgentIdentityApi (query_identity RPC) [Phase 2.7 COMPLETE v0.0.27]");
|
||||
println!(" • origin_chain_agent_identity::AgentKeyApi (current_key RPC: DID → llave vigente) [RV14]");
|
||||
println!(" • origin_chain_governance::GovernanceApi (query_proposal + Council + Advisor + Treasury RPCs) [Phase 5 FULLY COMPLETE v0.0.34]");
|
||||
println!();
|
||||
println!("Genesis presets canonical (3):");
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
//!
|
||||
//! 1. `did`: BLAKE3 32-byte hash canonical (deterministic via public_key derivation)
|
||||
//! 2. `kind`: AgentKind enum Human/AGI/Hybrid canonical
|
||||
//! 3. `creator`: AccountId32 que registró el identity canonical
|
||||
//! 3. `creator`: la llave Ed25519 de NACIMIENTO (no la cuenta firmante; ver el campo)
|
||||
//! 4. `registered_at_block`: u64 block number canonical sealed by runtime
|
||||
//! 5. `provenance_proof`: Option<AssetHash> linking to chain attestation canonical
|
||||
//! 6. `agent_chain`: BoundedVec<AccountId, ConstU32<10>> co-authorship chain
|
||||
@@ -50,6 +50,21 @@ use crate::{AccountId, AgentChain, AssetHash};
|
||||
/// cross-runtime garantizado (Pi 5 · Desktop · M4 Ultra ⇒ mismo DID, Inmutable
|
||||
/// #16 byte-determinism).
|
||||
///
|
||||
/// # ⚠️ Desde RV14 esto vale para la llave de NACIMIENTO, no para la vigente
|
||||
///
|
||||
/// La rotación de llave (`rotate_agent_key`) cambia qué llave manda sobre el DID y
|
||||
/// **deja el DID igual**, que es su razón de ser: una identidad no puede morir por
|
||||
/// perder o filtrar su llave. Consecuencia que hay que tener clara, y que midió la
|
||||
/// revisión adversarial de RV14: tras rotar, **`derive(llave_vigente) != did`**.
|
||||
///
|
||||
/// Por eso la promesa del DC de unificación se cumple completa solo para identidades
|
||||
/// que nunca rotaron. Para cualquier llave, la forma correcta de obtener el DID es
|
||||
/// RESOLVERLO contra la cadena (`KeyToDid[llave]`), no derivarlo. Una capa que siga
|
||||
/// derivando —OCF con `OitFingerprint::from_ed25519_pubkey`, Palace con su wallet—
|
||||
/// verá a un usuario que rotó como una identidad nueva y vacía. Eso es una enmienda
|
||||
/// pendiente del DC, con las firmantes de la unificación, y no se resuelve en este
|
||||
/// repo.
|
||||
///
|
||||
/// # Por qué es un NEWTYPE y no un alias de [`AssetHash`]
|
||||
///
|
||||
/// Condición vinculante **#3 de Mathi** en la firma que ratificó el DC: *"un
|
||||
@@ -180,8 +195,19 @@ pub struct AgentIdentity {
|
||||
pub did: Did,
|
||||
/// Agent kind canonical (Human + AGI + Hybrid per Inmutable #41).
|
||||
pub kind: AgentKind,
|
||||
/// AccountId que registró el identity on-chain canonical. Para Human +
|
||||
/// AGI = self-registration. Para Hybrid = canonical Human que coordinates.
|
||||
/// **La llave pública Ed25519 de NACIMIENTO** de esta identidad, guardada en un
|
||||
/// `AccountId` porque ambos son 32 bytes.
|
||||
///
|
||||
/// ⚠️ El nombre engaña y el comentario anterior también: decía que era «el
|
||||
/// AccountId que registró», pero `register_agent_did` guarda aquí
|
||||
/// `AccountId::new(public_key)` — la llave Ed25519 de la que se derivó el DID, no
|
||||
/// la cuenta firmante. Corregido el 2026-09-16 al construir la rotación, que
|
||||
/// depende exactamente de esto: si no ha habido rotación, ésta es la llave con
|
||||
/// autoridad sobre el DID.
|
||||
///
|
||||
/// **Para verificar una firma del titular NO uses este campo**: usa la llave
|
||||
/// VIGENTE (`AgentKeyApi::current_key`). Tras una rotación son distintas, y este
|
||||
/// campo nunca cambia.
|
||||
pub creator: AccountId,
|
||||
/// Block number canonical at which identity was registered (sealed by runtime).
|
||||
pub registered_at_block: u64,
|
||||
|
||||
Reference in new issue
Block a user