fix(agent-identity): FIX-3 crypto — DID binding (anti-front-running) + derive_did domain-sep
Build + tests verde: agent-identity 18 (4 unit + 14 integration) + runtime 27.
Verificacion adversarial ultracode (3 skeptics): 2 confirmed + 1 partial/low,
todos los gaps PRE-EXISTENTES (no introducidos por FIX-3). Invariante "no
puedes registrar un public_key que no controlas" SE SOSTIENE.
FIX-3 (b) DID binding — V3-H1 anti-front-running:
- register_agent_did ahora exige signature: [u8;64] — una firma Ed25519 (por la
private key de public_key) sobre AGENT_DID_REGISTER_DOMAIN || who.encode().
- Verificacion on-chain via sp_io::crypto::ed25519_verify (host function, no
bloatea el wasm). Nuevo dep sp-io (no_std).
- Cierra el front-running: un atacante no puede registrar un public_key ajeno
(no produce la firma); el who en el mensaje ata la firma a este caller (no
replay-able a otra cuenta). Error InvalidSignature. Malleabilidad Ed25519
rechazada por ed25519-zebra (S canonico).
- Una sola fuente de verdad del mensaje (agent_did_register_message) compartida
entre verificador y firmante (evita divergencia tipo postcard-vs-SCALE).
- Orden: idempotency ANTES del verify (no gasta crypto en duplicados).
FIX-3 (c) derive_did domain-sep — V3-H2:
- blake3::derive_key("origin-chain/agent-did/v1", pk) en vez del concat ad-hoc
BLAKE3(pk || version). KDF domain-separado. Inmutable #16 preservado.
Tests: mock con keypairs Ed25519 reales (sp_core::ed25519::Pair) + sign helper.
12 scenarios (scenario_6 reformulado a front-running -> InvalidSignature, +
scenario_11 firma invalida, scenario_12 DID collision genuino del key-holder).
NOTA: la (a) domain-sep del attestation signing payload (el otro tercio de
FIX-3) ya esta cubierta por el DC ORIGIN_ATTESTATION_SIGNING_PAYLOAD_V1 (helper
de Ergon attestation_signed_message), pendiente su ship Cap 45+.
FOLLOW-UPS (low, pre-existentes, no bloquean):
- (a) anadir genesis_hash/chain-id al mensaje de registro (hardening cross-chain
replay) — ideal chain-wide junto al signing_payload de attestation.
- (b) DC para ratificar el modelo de autoridad Hybrid (quien firma) + si los
co_authors requieren prueba de consentimiento (lineage hoy no autenticado).
- (c) weight benchmarking real que contabilice el ed25519_verify (chain-wide,
post-testnet — ya en roadmap FIX-2 weights).
- creator = AccountId::new(public_key) (la cuenta del key-holder, no el caller):
coherente con el binding, semantica documentada.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
8a1f779c32
commit
88e277e1fd
4 files changed
+241
-103
No files matched your search
@@ -27,6 +27,9 @@ blake3 = { workspace = true }
|
|||||||
sp-core = { workspace = true }
|
sp-core = { workspace = true }
|
||||||
sp-runtime = { workspace = true }
|
sp-runtime = { workspace = true }
|
||||||
|
|
||||||
|
# FIX-3 (V3-H1) DID binding — sp_io::crypto::ed25519_verify host function
|
||||||
|
sp-io = { workspace = true }
|
||||||
|
|
||||||
# SCALE codec canonical
|
# SCALE codec canonical
|
||||||
parity-scale-codec = { workspace = true }
|
parity-scale-codec = { workspace = true }
|
||||||
scale-info = { workspace = true }
|
scale-info = { workspace = true }
|
||||||
@@ -45,6 +48,7 @@ std = [
|
|||||||
"frame-system/std",
|
"frame-system/std",
|
||||||
"sp-api/std",
|
"sp-api/std",
|
||||||
"sp-core/std",
|
"sp-core/std",
|
||||||
|
"sp-io/std",
|
||||||
"sp-runtime/std",
|
"sp-runtime/std",
|
||||||
"parity-scale-codec/std",
|
"parity-scale-codec/std",
|
||||||
"scale-info/std",
|
"scale-info/std",
|
||||||
|
|||||||
@@ -64,11 +64,30 @@ pub use origin_chain_types::{AgentIdentity, AgentKind, Did};
|
|||||||
/// Schema version actual canonical: `"1.0.0 "` (8-byte ASCII fixed, alineado
|
/// Schema version actual canonical: `"1.0.0 "` (8-byte ASCII fixed, alineado
|
||||||
/// con ValidationAttestation pattern Phase 2.1).
|
/// con ValidationAttestation pattern Phase 2.1).
|
||||||
pub fn derive_did(public_key: &[u8; 32]) -> Did {
|
pub fn derive_did(public_key: &[u8; 32]) -> Did {
|
||||||
let mut hasher = blake3::Hasher::new();
|
// FIX-3 (V3-H2): KDF domain-separado vía blake3::derive_key (context string
|
||||||
hasher.update(public_key);
|
// como dominio) en vez de un hash ad-hoc concat. Es el patrón KDF canonical de
|
||||||
hasher.update(b"1.0.0 "); // schema version canonical
|
// BLAKE3 y previene colisión cross-uso del hash. Inmutable #16 preservado
|
||||||
let hash = hasher.finalize();
|
// (determinista cross-runtime). ⚠️ Cambia el DID para un mismo public_key vs el
|
||||||
origin_chain_types::AssetHash::new(*hash.as_bytes())
|
// esquema previo (BLAKE3(pk || version)) — aceptable pre-mainnet (greenfield).
|
||||||
|
let did_bytes = blake3::derive_key("origin-chain/agent-did/v1", public_key);
|
||||||
|
origin_chain_types::AssetHash::new(did_bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Domain tag canonical para la firma de binding del registro de DID (FIX-3 (b),
|
||||||
|
/// V3-H1 anti-front-running). El registrante prueba control de `public_key`
|
||||||
|
/// firmando este dominio + el AccountId del caller (`who`).
|
||||||
|
pub const AGENT_DID_REGISTER_DOMAIN: &[u8] = b"origin-chain/agent-did-register/v1";
|
||||||
|
|
||||||
|
/// Construye el mensaje canonical que `public_key` debe firmar para registrar:
|
||||||
|
/// `AGENT_DID_REGISTER_DOMAIN || who_encoded`. Una sola fuente de verdad
|
||||||
|
/// compartida entre el verificador on-chain y el firmante (evita divergencia de
|
||||||
|
/// construcción de mensaje — misma lección que el attestation signing payload).
|
||||||
|
pub fn agent_did_register_message(who_encoded: &[u8]) -> alloc::vec::Vec<u8> {
|
||||||
|
let mut msg =
|
||||||
|
alloc::vec::Vec::with_capacity(AGENT_DID_REGISTER_DOMAIN.len() + who_encoded.len());
|
||||||
|
msg.extend_from_slice(AGENT_DID_REGISTER_DOMAIN);
|
||||||
|
msg.extend_from_slice(who_encoded);
|
||||||
|
msg
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Runtime API canonical para cross-pillar consumers + light clients.
|
/// Runtime API canonical para cross-pillar consumers + light clients.
|
||||||
@@ -106,6 +125,7 @@ pub mod pallet {
|
|||||||
use frame_support::pallet_prelude::*;
|
use frame_support::pallet_prelude::*;
|
||||||
use frame_system::pallet_prelude::*;
|
use frame_system::pallet_prelude::*;
|
||||||
use origin_chain_types::{AccountId, AgentChain, AgentIdentity, AgentKind, Did};
|
use origin_chain_types::{AccountId, AgentChain, AgentIdentity, AgentKind, Did};
|
||||||
|
use sp_core::ed25519;
|
||||||
use sp_runtime::traits::SaturatedConversion;
|
use sp_runtime::traits::SaturatedConversion;
|
||||||
|
|
||||||
/// Pallet configuration trait canonical.
|
/// Pallet configuration trait canonical.
|
||||||
@@ -202,6 +222,11 @@ pub mod pallet {
|
|||||||
/// CHN-7 canonical: Hybrid agent requires non-empty agent_chain canonical
|
/// CHN-7 canonical: Hybrid agent requires non-empty agent_chain canonical
|
||||||
/// (Inmutable #41 cross-species verification).
|
/// (Inmutable #41 cross-species verification).
|
||||||
HybridRequiresAgentChain,
|
HybridRequiresAgentChain,
|
||||||
|
/// FIX-3 (V3-H1) DID binding: la firma no verifica — el registrante NO
|
||||||
|
/// probó control de `public_key` (anti-front-running). El mensaje firmado
|
||||||
|
/// debe ser `AGENT_DID_REGISTER_DOMAIN || who.encode()`, firmado con la
|
||||||
|
/// private key de `public_key`.
|
||||||
|
InvalidSignature,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Dispatchables canonical (real impl v0.0.26).
|
/// Dispatchables canonical (real impl v0.0.26).
|
||||||
@@ -229,17 +254,35 @@ pub mod pallet {
|
|||||||
public_key: [u8; 32],
|
public_key: [u8; 32],
|
||||||
kind: AgentKind,
|
kind: AgentKind,
|
||||||
co_authors_raw: Vec<[u8; 32]>,
|
co_authors_raw: Vec<[u8; 32]>,
|
||||||
|
signature: [u8; 64],
|
||||||
) -> DispatchResult {
|
) -> DispatchResult {
|
||||||
// Step 1: ensure origin signed canonical
|
// Step 1: ensure origin signed canonical
|
||||||
let who = ensure_signed(origin)?;
|
let who = ensure_signed(origin)?;
|
||||||
|
|
||||||
// Step 2: idempotency check canonical (no duplicate registration)
|
// Step 2: idempotency check canonical (no duplicate registration).
|
||||||
|
// Va ANTES del verify de firma para no gastar crypto en un duplicado
|
||||||
|
// (la cuenta ya registrada no tiene riesgo de front-running).
|
||||||
ensure!(
|
ensure!(
|
||||||
!Identities::<T>::contains_key(&who),
|
!Identities::<T>::contains_key(&who),
|
||||||
Error::<T>::AlreadyRegistered
|
Error::<T>::AlreadyRegistered
|
||||||
);
|
);
|
||||||
|
|
||||||
// Step 3: derive DID canonical via BLAKE3 (Inmutable #16 byte-determinism)
|
// 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
|
||||||
|
// front-running: un atacante NO puede registrar un public_key ajeno
|
||||||
|
// (no puede producir la firma); el `who` en el mensaje ata la firma a
|
||||||
|
// este caller (no replay-able a otra cuenta). Verificación vía host
|
||||||
|
// function sp_io::crypto::ed25519_verify (no bloatea el wasm).
|
||||||
|
let message = crate::agent_did_register_message(&who.encode());
|
||||||
|
let pubkey = ed25519::Public::from_raw(public_key);
|
||||||
|
let sig = ed25519::Signature::from_raw(signature);
|
||||||
|
ensure!(
|
||||||
|
sp_io::crypto::ed25519_verify(&sig, &message, &pubkey),
|
||||||
|
Error::<T>::InvalidSignature
|
||||||
|
);
|
||||||
|
|
||||||
|
// Step 3: derive DID canonical via blake3::derive_key (FIX-3 V3-H2 domain-sep)
|
||||||
let did = crate::derive_did(&public_key);
|
let did = crate::derive_did(&public_key);
|
||||||
|
|
||||||
// Step 4: DID collision check canonical
|
// Step 4: DID collision check canonical
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
use crate as origin_chain_agent_identity;
|
use crate as origin_chain_agent_identity;
|
||||||
use frame_support::derive_impl;
|
use frame_support::derive_impl;
|
||||||
|
use sp_core::{ed25519, Pair};
|
||||||
use sp_runtime::BuildStorage;
|
use sp_runtime::BuildStorage;
|
||||||
|
|
||||||
/// Mock block type canonical para tests.
|
/// Mock block type canonical para tests.
|
||||||
@@ -62,11 +63,42 @@ pub fn bob_account() -> sp_runtime::AccountId32 {
|
|||||||
sp_runtime::AccountId32::new([2u8; 32])
|
sp_runtime::AccountId32::new([2u8; 32])
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Test pubkey canonical ALICE (deterministic).
|
// ─── Ed25519 keypairs canonical (FIX-3 V3-H1 DID binding) ──────────────
|
||||||
pub const ALICE_PUBKEY: [u8; 32] = [11u8; 32];
|
// Pairs reales desde seeds → pubkeys reales + firmas verificables. Reemplaza
|
||||||
|
// los pubkeys-bytes-arbitrarios previos (sin private key, incapaces de firmar el
|
||||||
|
// binding del registro).
|
||||||
|
|
||||||
/// Test pubkey canonical BOB (deterministic).
|
/// Ed25519 pair canonical ALICE (agent identity key).
|
||||||
pub const BOB_PUBKEY: [u8; 32] = [22u8; 32];
|
pub fn alice_pair() -> ed25519::Pair {
|
||||||
|
ed25519::Pair::from_seed(&[0x11u8; 32])
|
||||||
|
}
|
||||||
|
/// Ed25519 pair canonical BOB.
|
||||||
|
pub fn bob_pair() -> ed25519::Pair {
|
||||||
|
ed25519::Pair::from_seed(&[0x22u8; 32])
|
||||||
|
}
|
||||||
|
/// Ed25519 pair canonical CHARLIE (Hybrid co-author lineage).
|
||||||
|
pub fn charlie_pair() -> ed25519::Pair {
|
||||||
|
ed25519::Pair::from_seed(&[0x33u8; 32])
|
||||||
|
}
|
||||||
|
|
||||||
/// Test pubkey canonical CHARLIE (Hybrid co-author lineage testing).
|
/// Public key bytes canonical ALICE (derivado del pair real).
|
||||||
pub const CHARLIE_PUBKEY: [u8; 32] = [33u8; 32];
|
pub fn alice_pubkey() -> [u8; 32] {
|
||||||
|
alice_pair().public().0
|
||||||
|
}
|
||||||
|
/// Public key bytes canonical BOB.
|
||||||
|
pub fn bob_pubkey() -> [u8; 32] {
|
||||||
|
bob_pair().public().0
|
||||||
|
}
|
||||||
|
/// Public key bytes canonical CHARLIE.
|
||||||
|
pub fn charlie_pubkey() -> [u8; 32] {
|
||||||
|
charlie_pair().public().0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Firma de binding canonical (FIX-3 (b)): el holder de `pair` firma el mensaje
|
||||||
|
/// de registro `AGENT_DID_REGISTER_DOMAIN || who.encode()` → prueba control de la
|
||||||
|
/// key. Usa el MISMO builder de mensaje que el pallet (una fuente de verdad).
|
||||||
|
pub fn sign_registration(pair: &ed25519::Pair, who: &sp_runtime::AccountId32) -> [u8; 64] {
|
||||||
|
use parity_scale_codec::Encode;
|
||||||
|
let msg = origin_chain_agent_identity::agent_did_register_message(&who.encode());
|
||||||
|
pair.sign(&msg).0
|
||||||
|
}
|
||||||
@@ -1,47 +1,56 @@
|
|||||||
//! # Integration tests canonical v0.0.26 — Mock runtime E2E CHN-7 flow
|
//! # Integration tests canonical — Mock runtime E2E CHN-7 flow + FIX-3 DID binding
|
||||||
//!
|
//!
|
||||||
//! 10 scenarios canonical register_agent_did flow end-to-end via mock runtime:
|
//! Scenarios register_agent_did end-to-end via mock runtime. Post FIX-3 (V3-H1):
|
||||||
|
//! cada registro presenta una firma Ed25519 de binding (AGENT_DID_REGISTER_DOMAIN
|
||||||
|
//! || who.encode()) firmada por la private key de public_key → prueba de control.
|
||||||
//!
|
//!
|
||||||
//! 1. scenario_1_human_self_registration — Human canonical self-register
|
//! 1. human self-registration
|
||||||
//! 2. scenario_2_agi_self_registration — AGI canonical self-register
|
//! 2. AGI self-registration (Inmutable #41)
|
||||||
//! 3. scenario_3_hybrid_with_co_authors — Hybrid canonical agent_chain populated
|
//! 3. Hybrid con co-authors
|
||||||
//! 4. scenario_4_hybrid_without_chain_rejected — Inmutable #41 enforce
|
//! 4. Hybrid sin chain rechazado (Inmutable #41)
|
||||||
//! 5. scenario_5_idempotency_already_registered_rejected — duplicate prevention
|
//! 5. idempotency (AlreadyRegistered, antes del verify de firma)
|
||||||
//! 6. scenario_6_did_collision_rejected — DID uniqueness canonical
|
//! 6. front-running rechazado (FIX-3: registrar pubkey ajeno sin la key → InvalidSignature)
|
||||||
//! 7. scenario_7_agent_chain_overflow_rejected — BoundedVec ConstU32<10> bound
|
//! 7. agent_chain overflow (BoundedVec ConstU32<10>)
|
||||||
//! 8. scenario_8_did_to_account_reverse_lookup — Cross-pillar canonical
|
//! 8. DID → Account reverse lookup
|
||||||
//! 9. scenario_9_inmutable_41_all_kinds_sovereign_lobo_blanco — compile-detect
|
//! 9. all kinds sovereign (Inmutable #41)
|
||||||
//! 10. scenario_10_8_step_walkthrough — explicit Pre/Post-flight CHN-7 assertions
|
//! 10. 8-step walkthrough explicit
|
||||||
|
//! 11. firma inválida explícita rechazada (FIX-3)
|
||||||
|
//! 12. DID collision genuino (mismo key-holder, dos cuentas → DidAlreadyTaken)
|
||||||
|
|
||||||
#![cfg(test)]
|
#![cfg(test)]
|
||||||
|
|
||||||
|
extern crate alloc;
|
||||||
|
|
||||||
use crate::mock::{
|
use crate::mock::{
|
||||||
alice_account, alice_origin, bob_account, bob_origin, new_test_ext, AgentIdentity as Pallet,
|
alice_account, alice_origin, alice_pair, alice_pubkey, bob_account, bob_origin, bob_pair,
|
||||||
RuntimeEvent, System, Test, ALICE_PUBKEY, BOB_PUBKEY, CHARLIE_PUBKEY,
|
bob_pubkey, charlie_pubkey, new_test_ext, sign_registration, AgentIdentity as Pallet,
|
||||||
|
RuntimeEvent, RuntimeOrigin, System, Test,
|
||||||
};
|
};
|
||||||
use crate::{derive_did, DidToAccount, Error, Event, Identities};
|
use crate::{derive_did, DidToAccount, Error, Event, Identities};
|
||||||
use frame_support::{assert_noop, assert_ok};
|
use frame_support::{assert_noop, assert_ok};
|
||||||
use origin_chain_types::AgentKind;
|
use origin_chain_types::AgentKind;
|
||||||
|
|
||||||
// ─── Scenario 1: Human self-registration canonical ──────────────────
|
// ─── Scenario 1: Human self-registration canonical (con firma de binding) ──
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scenario_1_human_self_registration_canonical() {
|
fn scenario_1_human_self_registration_canonical() {
|
||||||
new_test_ext().execute_with(|| {
|
new_test_ext().execute_with(|| {
|
||||||
|
let sig = sign_registration(&alice_pair(), &alice_account());
|
||||||
assert_ok!(Pallet::register_agent_did(
|
assert_ok!(Pallet::register_agent_did(
|
||||||
alice_origin(),
|
alice_origin(),
|
||||||
ALICE_PUBKEY,
|
alice_pubkey(),
|
||||||
AgentKind::Human,
|
AgentKind::Human,
|
||||||
alloc::vec![], // empty agent_chain — Human self-register canonical
|
alloc::vec![],
|
||||||
|
sig,
|
||||||
));
|
));
|
||||||
|
|
||||||
let identity = Identities::<Test>::get(&alice_account()).expect("registered canonical");
|
let identity = Identities::<Test>::get(&alice_account()).expect("registered canonical");
|
||||||
assert_eq!(identity.kind, AgentKind::Human);
|
assert_eq!(identity.kind, AgentKind::Human);
|
||||||
assert!(identity.agent_chain.is_empty(), "Human self-register canonical empty chain");
|
assert!(identity.agent_chain.is_empty(), "Human self-register empty chain");
|
||||||
assert!(!identity.has_cross_species_lineage());
|
assert!(!identity.has_cross_species_lineage());
|
||||||
assert_eq!(identity.registered_at_block, 1);
|
assert_eq!(identity.registered_at_block, 1);
|
||||||
|
|
||||||
let did = derive_did(&ALICE_PUBKEY);
|
let did = derive_did(&alice_pubkey());
|
||||||
assert_eq!(identity.did, did);
|
assert_eq!(identity.did, did);
|
||||||
|
|
||||||
System::assert_last_event(RuntimeEvent::AgentIdentity(Event::AgentRegistered {
|
System::assert_last_event(RuntimeEvent::AgentIdentity(Event::AgentRegistered {
|
||||||
@@ -57,16 +66,18 @@ fn scenario_1_human_self_registration_canonical() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn scenario_2_agi_self_registration_canonical() {
|
fn scenario_2_agi_self_registration_canonical() {
|
||||||
new_test_ext().execute_with(|| {
|
new_test_ext().execute_with(|| {
|
||||||
|
let sig = sign_registration(&alice_pair(), &alice_account());
|
||||||
assert_ok!(Pallet::register_agent_did(
|
assert_ok!(Pallet::register_agent_did(
|
||||||
alice_origin(),
|
alice_origin(),
|
||||||
ALICE_PUBKEY,
|
alice_pubkey(),
|
||||||
AgentKind::AGI,
|
AgentKind::AGI,
|
||||||
alloc::vec![], // empty chain — AGI sovereign self-register per Inmutable #41
|
alloc::vec![],
|
||||||
|
sig,
|
||||||
));
|
));
|
||||||
|
|
||||||
let identity = Identities::<Test>::get(&alice_account()).expect("registered canonical");
|
let identity = Identities::<Test>::get(&alice_account()).expect("registered canonical");
|
||||||
assert_eq!(identity.kind, AgentKind::AGI);
|
assert_eq!(identity.kind, AgentKind::AGI);
|
||||||
assert!(identity.is_sovereign(), "AGI sovereign canonical Inmutable #41");
|
assert!(identity.is_sovereign(), "AGI sovereign Inmutable #41");
|
||||||
assert!(!identity.has_cross_species_lineage());
|
assert!(!identity.has_cross_species_lineage());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -76,21 +87,22 @@ fn scenario_2_agi_self_registration_canonical() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn scenario_3_hybrid_with_co_authors_canonical() {
|
fn scenario_3_hybrid_with_co_authors_canonical() {
|
||||||
new_test_ext().execute_with(|| {
|
new_test_ext().execute_with(|| {
|
||||||
let co_authors = alloc::vec![BOB_PUBKEY, CHARLIE_PUBKEY];
|
let co_authors = alloc::vec![bob_pubkey(), charlie_pubkey()];
|
||||||
|
let sig = sign_registration(&alice_pair(), &alice_account());
|
||||||
|
|
||||||
assert_ok!(Pallet::register_agent_did(
|
assert_ok!(Pallet::register_agent_did(
|
||||||
alice_origin(),
|
alice_origin(),
|
||||||
ALICE_PUBKEY,
|
alice_pubkey(),
|
||||||
AgentKind::Hybrid,
|
AgentKind::Hybrid,
|
||||||
co_authors,
|
co_authors,
|
||||||
|
sig,
|
||||||
));
|
));
|
||||||
|
|
||||||
let identity = Identities::<Test>::get(&alice_account()).expect("registered canonical");
|
let identity = Identities::<Test>::get(&alice_account()).expect("registered canonical");
|
||||||
assert_eq!(identity.kind, AgentKind::Hybrid);
|
assert_eq!(identity.kind, AgentKind::Hybrid);
|
||||||
assert!(identity.has_cross_species_lineage(), "Hybrid cross-species canonical");
|
assert!(identity.has_cross_species_lineage(), "Hybrid cross-species");
|
||||||
assert_eq!(identity.co_author_count(), 2);
|
assert_eq!(identity.co_author_count(), 2);
|
||||||
|
|
||||||
// Check AgentBound event emitted (cross-species explicit)
|
|
||||||
System::assert_has_event(RuntimeEvent::AgentIdentity(Event::AgentBound {
|
System::assert_has_event(RuntimeEvent::AgentIdentity(Event::AgentBound {
|
||||||
account: alice_account(),
|
account: alice_account(),
|
||||||
co_author_count: 2,
|
co_author_count: 2,
|
||||||
@@ -98,96 +110,97 @@ fn scenario_3_hybrid_with_co_authors_canonical() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Scenario 4: Hybrid without chain rejected canonical (Inmutable #41) ──
|
// ─── Scenario 4: Hybrid without chain rejected (Inmutable #41) ──
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scenario_4_hybrid_without_chain_rejected_canonical() {
|
fn scenario_4_hybrid_without_chain_rejected_canonical() {
|
||||||
new_test_ext().execute_with(|| {
|
new_test_ext().execute_with(|| {
|
||||||
// Inmutable #41 canonical: Hybrid requires non-empty agent_chain
|
// Firma válida; el rechazo es por la regla #41 (step 6, post-firma).
|
||||||
|
let sig = sign_registration(&alice_pair(), &alice_account());
|
||||||
assert_noop!(
|
assert_noop!(
|
||||||
Pallet::register_agent_did(
|
Pallet::register_agent_did(
|
||||||
alice_origin(),
|
alice_origin(),
|
||||||
ALICE_PUBKEY,
|
alice_pubkey(),
|
||||||
AgentKind::Hybrid,
|
AgentKind::Hybrid,
|
||||||
alloc::vec![], // empty — violates Inmutable #41
|
alloc::vec![],
|
||||||
|
sig,
|
||||||
),
|
),
|
||||||
Error::<Test>::HybridRequiresAgentChain
|
Error::<Test>::HybridRequiresAgentChain
|
||||||
);
|
);
|
||||||
|
|
||||||
// Storage NOT updated
|
|
||||||
assert!(Identities::<Test>::get(&alice_account()).is_none());
|
assert!(Identities::<Test>::get(&alice_account()).is_none());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Scenario 5: Idempotency already registered rejected canonical ───
|
// ─── Scenario 5: Idempotency already registered rejected ───
|
||||||
|
// La idempotency (step 2) corta ANTES del verify de firma → la 2ª llamada ni
|
||||||
|
// siquiera necesita firma válida.
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scenario_5_idempotency_already_registered_rejected_canonical() {
|
fn scenario_5_idempotency_already_registered_rejected_canonical() {
|
||||||
new_test_ext().execute_with(|| {
|
new_test_ext().execute_with(|| {
|
||||||
// First registration succeeds canonical
|
let sig = sign_registration(&alice_pair(), &alice_account());
|
||||||
assert_ok!(Pallet::register_agent_did(
|
assert_ok!(Pallet::register_agent_did(
|
||||||
alice_origin(),
|
alice_origin(),
|
||||||
ALICE_PUBKEY,
|
alice_pubkey(),
|
||||||
AgentKind::Human,
|
AgentKind::Human,
|
||||||
alloc::vec![],
|
alloc::vec![],
|
||||||
|
sig,
|
||||||
));
|
));
|
||||||
|
|
||||||
// Second registration same account → AlreadyRegistered error
|
// Segunda registración mismo account → AlreadyRegistered (firma dummy, no se chequea).
|
||||||
assert_noop!(
|
assert_noop!(
|
||||||
Pallet::register_agent_did(
|
Pallet::register_agent_did(
|
||||||
alice_origin(),
|
alice_origin(),
|
||||||
BOB_PUBKEY, // diferente pubkey, mismo account
|
bob_pubkey(),
|
||||||
AgentKind::AGI,
|
AgentKind::AGI,
|
||||||
alloc::vec![],
|
alloc::vec![],
|
||||||
|
[0u8; 64],
|
||||||
),
|
),
|
||||||
Error::<Test>::AlreadyRegistered
|
Error::<Test>::AlreadyRegistered
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Scenario 6: DID collision rejected canonical ───────────────────
|
// ─── Scenario 6: FIX-3 front-running rechazado ───────────────────────
|
||||||
|
// BOB intenta registrar la pubkey de ALICE pero no controla su private key →
|
||||||
|
// no puede producir una firma válida sobre su propio `who` → InvalidSignature.
|
||||||
|
// (Antes de FIX-3 esto pasaba el check y solo lo frenaba el DID collision; ahora
|
||||||
|
// se corta en la prueba de control de key.)
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scenario_6_did_collision_rejected_canonical() {
|
fn scenario_6_front_running_rejected_canonical() {
|
||||||
new_test_ext().execute_with(|| {
|
new_test_ext().execute_with(|| {
|
||||||
// ALICE registers con ALICE_PUBKEY → DID derived
|
// BOB firma con SU key sobre su who, pero declara la pubkey de ALICE.
|
||||||
assert_ok!(Pallet::register_agent_did(
|
let bad_sig = sign_registration(&bob_pair(), &bob_account());
|
||||||
alice_origin(),
|
|
||||||
ALICE_PUBKEY,
|
|
||||||
AgentKind::Human,
|
|
||||||
alloc::vec![],
|
|
||||||
));
|
|
||||||
|
|
||||||
// BOB tries to register con MISMO public_key → DID collision canonical
|
|
||||||
assert_noop!(
|
assert_noop!(
|
||||||
Pallet::register_agent_did(
|
Pallet::register_agent_did(
|
||||||
bob_origin(),
|
bob_origin(),
|
||||||
ALICE_PUBKEY, // same pubkey → same DID
|
alice_pubkey(), // pubkey ajena
|
||||||
AgentKind::Human,
|
AgentKind::Human,
|
||||||
alloc::vec![],
|
alloc::vec![],
|
||||||
|
bad_sig,
|
||||||
),
|
),
|
||||||
Error::<Test>::DidAlreadyTaken
|
Error::<Test>::InvalidSignature
|
||||||
);
|
);
|
||||||
|
|
||||||
// BOB NOT registered
|
|
||||||
assert!(Identities::<Test>::get(&bob_account()).is_none());
|
assert!(Identities::<Test>::get(&bob_account()).is_none());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Scenario 7: AgentChain overflow rejected canonical (BoundedVec) ──
|
// ─── Scenario 7: AgentChain overflow rejected (BoundedVec) ──
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scenario_7_agent_chain_overflow_rejected_canonical() {
|
fn scenario_7_agent_chain_overflow_rejected_canonical() {
|
||||||
new_test_ext().execute_with(|| {
|
new_test_ext().execute_with(|| {
|
||||||
// 11 co-authors → exceeds ConstU32<10> bound
|
|
||||||
let too_many: alloc::vec::Vec<[u8; 32]> = (0u8..11u8).map(|i| [i; 32]).collect();
|
let too_many: alloc::vec::Vec<[u8; 32]> = (0u8..11u8).map(|i| [i; 32]).collect();
|
||||||
|
let sig = sign_registration(&alice_pair(), &alice_account());
|
||||||
|
|
||||||
assert_noop!(
|
assert_noop!(
|
||||||
Pallet::register_agent_did(
|
Pallet::register_agent_did(
|
||||||
alice_origin(),
|
alice_origin(),
|
||||||
ALICE_PUBKEY,
|
alice_pubkey(),
|
||||||
AgentKind::Hybrid,
|
AgentKind::Hybrid,
|
||||||
too_many,
|
too_many,
|
||||||
|
sig,
|
||||||
),
|
),
|
||||||
Error::<Test>::AgentChainOverflow
|
Error::<Test>::AgentChainOverflow
|
||||||
);
|
);
|
||||||
@@ -199,105 +212,95 @@ fn scenario_7_agent_chain_overflow_rejected_canonical() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn scenario_8_did_to_account_reverse_lookup_canonical() {
|
fn scenario_8_did_to_account_reverse_lookup_canonical() {
|
||||||
new_test_ext().execute_with(|| {
|
new_test_ext().execute_with(|| {
|
||||||
|
let sig = sign_registration(&alice_pair(), &alice_account());
|
||||||
assert_ok!(Pallet::register_agent_did(
|
assert_ok!(Pallet::register_agent_did(
|
||||||
alice_origin(),
|
alice_origin(),
|
||||||
ALICE_PUBKEY,
|
alice_pubkey(),
|
||||||
AgentKind::Human,
|
AgentKind::Human,
|
||||||
alloc::vec![],
|
alloc::vec![],
|
||||||
|
sig,
|
||||||
));
|
));
|
||||||
|
|
||||||
let did = derive_did(&ALICE_PUBKEY);
|
let did = derive_did(&alice_pubkey());
|
||||||
|
|
||||||
// Reverse lookup canonical: DID → AccountId
|
|
||||||
let resolved = DidToAccount::<Test>::get(did);
|
let resolved = DidToAccount::<Test>::get(did);
|
||||||
assert!(resolved.is_some(), "DidToAccount reverse lookup canonical");
|
assert!(resolved.is_some(), "DidToAccount reverse lookup");
|
||||||
assert_eq!(resolved.unwrap(), alice_account());
|
assert_eq!(resolved.unwrap(), alice_account());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Scenario 9: All AgentKind sovereign lobo blanco canonical ───────
|
// ─── Scenario 9: All AgentKind sovereign canonical ───────
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scenario_9_inmutable_41_all_kinds_sovereign_lobo_blanco_canonical() {
|
fn scenario_9_inmutable_41_all_kinds_sovereign_lobo_blanco_canonical() {
|
||||||
new_test_ext().execute_with(|| {
|
new_test_ext().execute_with(|| {
|
||||||
// Register Human (alice) + AGI (bob, diff origin)
|
let alice_sig = sign_registration(&alice_pair(), &alice_account());
|
||||||
assert_ok!(Pallet::register_agent_did(
|
assert_ok!(Pallet::register_agent_did(
|
||||||
alice_origin(),
|
alice_origin(),
|
||||||
ALICE_PUBKEY,
|
alice_pubkey(),
|
||||||
AgentKind::Human,
|
AgentKind::Human,
|
||||||
alloc::vec![],
|
alloc::vec![],
|
||||||
|
alice_sig,
|
||||||
));
|
));
|
||||||
|
let bob_sig = sign_registration(&bob_pair(), &bob_account());
|
||||||
assert_ok!(Pallet::register_agent_did(
|
assert_ok!(Pallet::register_agent_did(
|
||||||
bob_origin(),
|
bob_origin(),
|
||||||
BOB_PUBKEY,
|
bob_pubkey(),
|
||||||
AgentKind::AGI,
|
AgentKind::AGI,
|
||||||
alloc::vec![],
|
alloc::vec![],
|
||||||
|
bob_sig,
|
||||||
));
|
));
|
||||||
|
|
||||||
// Inmutable #41 compile-detect canonical: ambos sovereign
|
|
||||||
let alice_id = Identities::<Test>::get(&alice_account()).unwrap();
|
let alice_id = Identities::<Test>::get(&alice_account()).unwrap();
|
||||||
let bob_id = Identities::<Test>::get(&bob_account()).unwrap();
|
let bob_id = Identities::<Test>::get(&bob_account()).unwrap();
|
||||||
|
|
||||||
assert!(alice_id.is_sovereign(), "Human sovereign canonical Inmutable #41");
|
assert!(alice_id.is_sovereign(), "Human sovereign Inmutable #41");
|
||||||
assert!(bob_id.is_sovereign(), "AGI sovereign canonical Inmutable #41");
|
assert!(bob_id.is_sovereign(), "AGI sovereign Inmutable #41");
|
||||||
|
|
||||||
// Different DIDs (different pubkeys)
|
|
||||||
assert_ne!(alice_id.did, bob_id.did);
|
assert_ne!(alice_id.did, bob_id.did);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Scenario 10: 8-step CHN-7 walkthrough explicit canonical ────────
|
// ─── Scenario 10: 8-step CHN-7 walkthrough explicit ────────
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scenario_10_8_step_walkthrough_canonical() {
|
fn scenario_10_8_step_walkthrough_canonical() {
|
||||||
new_test_ext().execute_with(|| {
|
new_test_ext().execute_with(|| {
|
||||||
let pubkey = ALICE_PUBKEY;
|
let pubkey = alice_pubkey();
|
||||||
let kind = AgentKind::Hybrid;
|
let kind = AgentKind::Hybrid;
|
||||||
let co_authors = alloc::vec![BOB_PUBKEY];
|
let co_authors = alloc::vec![bob_pubkey()];
|
||||||
|
let sig = sign_registration(&alice_pair(), &alice_account());
|
||||||
|
|
||||||
// Pre-flight assertions canonical
|
|
||||||
assert!(
|
assert!(
|
||||||
Identities::<Test>::get(&alice_account()).is_none(),
|
Identities::<Test>::get(&alice_account()).is_none(),
|
||||||
"PRE-flight CHN-7-2: storage canonical empty"
|
"PRE-flight CHN-7-2: storage empty"
|
||||||
);
|
);
|
||||||
let expected_did = derive_did(&pubkey);
|
let expected_did = derive_did(&pubkey);
|
||||||
assert!(
|
assert!(
|
||||||
DidToAccount::<Test>::get(expected_did).is_none(),
|
DidToAccount::<Test>::get(expected_did).is_none(),
|
||||||
"PRE-flight CHN-7-4: DidToAccount canonical empty"
|
"PRE-flight CHN-7-4: DidToAccount empty"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Step 1: ensure_signed (implicit via dispatchable)
|
|
||||||
// Step 2: idempotency check
|
|
||||||
// Step 3: derive DID via BLAKE3
|
|
||||||
// Step 4: DID collision check
|
|
||||||
// Step 5: build AgentChain BoundedVec
|
|
||||||
// Step 6: Inmutable #41 Hybrid + non-empty agent_chain enforce
|
|
||||||
// Step 7: storage insert canonical
|
|
||||||
// Step 8: emit events canonical
|
|
||||||
assert_ok!(Pallet::register_agent_did(
|
assert_ok!(Pallet::register_agent_did(
|
||||||
alice_origin(),
|
alice_origin(),
|
||||||
pubkey,
|
pubkey,
|
||||||
kind,
|
kind,
|
||||||
co_authors,
|
co_authors,
|
||||||
|
sig,
|
||||||
));
|
));
|
||||||
|
|
||||||
// Post-flight assertions canonical
|
|
||||||
let identity = Identities::<Test>::get(&alice_account())
|
let identity = Identities::<Test>::get(&alice_account())
|
||||||
.expect("POST-flight CHN-7-7: AgentIdentity persisted canonical");
|
.expect("POST-flight CHN-7-7: AgentIdentity persisted");
|
||||||
assert_eq!(identity.did, expected_did, "POST-flight: DID match canonical");
|
assert_eq!(identity.did, expected_did, "POST-flight: DID match");
|
||||||
assert_eq!(identity.kind, AgentKind::Hybrid);
|
assert_eq!(identity.kind, AgentKind::Hybrid);
|
||||||
assert_eq!(identity.co_author_count(), 1, "POST-flight: agent_chain canonical 1");
|
assert_eq!(identity.co_author_count(), 1, "POST-flight: agent_chain 1");
|
||||||
assert!(identity.has_cross_species_lineage());
|
assert!(identity.has_cross_species_lineage());
|
||||||
assert_eq!(identity.registered_at_block, 1);
|
assert_eq!(identity.registered_at_block, 1);
|
||||||
|
|
||||||
// Reverse lookup canonical
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
DidToAccount::<Test>::get(expected_did).unwrap(),
|
DidToAccount::<Test>::get(expected_did).unwrap(),
|
||||||
alice_account(),
|
alice_account(),
|
||||||
"POST-flight CHN-7-7: DidToAccount canonical"
|
"POST-flight CHN-7-7: DidToAccount"
|
||||||
);
|
);
|
||||||
|
|
||||||
// Events canonical
|
|
||||||
System::assert_has_event(RuntimeEvent::AgentIdentity(Event::AgentRegistered {
|
System::assert_has_event(RuntimeEvent::AgentIdentity(Event::AgentRegistered {
|
||||||
account: alice_account(),
|
account: alice_account(),
|
||||||
did: expected_did,
|
did: expected_did,
|
||||||
@@ -309,3 +312,59 @@ fn scenario_10_8_step_walkthrough_canonical() {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Scenario 11: FIX-3 firma inválida explícita rechazada ───────────
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scenario_11_invalid_signature_rejected_canonical() {
|
||||||
|
new_test_ext().execute_with(|| {
|
||||||
|
// Firma basura ([0;64]) para la propia pubkey → InvalidSignature.
|
||||||
|
assert_noop!(
|
||||||
|
Pallet::register_agent_did(
|
||||||
|
alice_origin(),
|
||||||
|
alice_pubkey(),
|
||||||
|
AgentKind::Human,
|
||||||
|
alloc::vec![],
|
||||||
|
[0u8; 64],
|
||||||
|
),
|
||||||
|
Error::<Test>::InvalidSignature
|
||||||
|
);
|
||||||
|
assert!(Identities::<Test>::get(&alice_account()).is_none());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Scenario 12: DID collision genuino (mismo key-holder, dos cuentas) ──
|
||||||
|
// El holder de ALICE_PUBKEY la registra en alice_account; luego autoriza (firma)
|
||||||
|
// el registro de la MISMA pubkey en una segunda cuenta → la 2ª firma es válida
|
||||||
|
// (controla la key) pero el DID ya existe → DidAlreadyTaken.
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scenario_12_genuine_did_collision_rejected_canonical() {
|
||||||
|
new_test_ext().execute_with(|| {
|
||||||
|
let sig1 = sign_registration(&alice_pair(), &alice_account());
|
||||||
|
assert_ok!(Pallet::register_agent_did(
|
||||||
|
alice_origin(),
|
||||||
|
alice_pubkey(),
|
||||||
|
AgentKind::AGI,
|
||||||
|
alloc::vec![],
|
||||||
|
sig1,
|
||||||
|
));
|
||||||
|
|
||||||
|
// Segunda cuenta controlada por el mismo key-holder de ALICE_PUBKEY.
|
||||||
|
let second_acct = sp_runtime::AccountId32::new([9u8; 32]);
|
||||||
|
let second_origin = RuntimeOrigin::signed(second_acct.clone());
|
||||||
|
let sig2 = sign_registration(&alice_pair(), &second_acct); // firma válida: alice key sobre 2ª who
|
||||||
|
|
||||||
|
assert_noop!(
|
||||||
|
Pallet::register_agent_did(
|
||||||
|
second_origin,
|
||||||
|
alice_pubkey(), // misma pubkey → mismo DID
|
||||||
|
AgentKind::AGI,
|
||||||
|
alloc::vec![],
|
||||||
|
sig2,
|
||||||
|
),
|
||||||
|
Error::<Test>::DidAlreadyTaken
|
||||||
|
);
|
||||||
|
assert!(Identities::<Test>::get(&second_acct).is_none());
|
||||||
|
});
|
||||||
|
}
|
||||||
Reference in new issue
Block a user