From 88e277e1fdcb8192afb6c882bb0451e4c76fc7e4 Mon Sep 17 00:00:00 2001 From: DavidHerran Date: Tue, 9 Jun 2026 23:18:22 -0500 Subject: [PATCH] =?UTF-8?q?fix(agent-identity):=20FIX-3=20crypto=20?= =?UTF-8?q?=E2=80=94=20DID=20binding=20(anti-front-running)=20+=20derive?= =?UTF-8?q?=5Fdid=20domain-sep?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- crates/origin-chain-agent-identity/Cargo.toml | 4 + crates/origin-chain-agent-identity/src/lib.rs | 57 ++++- .../origin-chain-agent-identity/src/mock.rs | 44 +++- .../src/tests_integration.rs | 239 +++++++++++------- 4 files changed, 241 insertions(+), 103 deletions(-) diff --git a/crates/origin-chain-agent-identity/Cargo.toml b/crates/origin-chain-agent-identity/Cargo.toml index e8bd034..485254e 100644 --- a/crates/origin-chain-agent-identity/Cargo.toml +++ b/crates/origin-chain-agent-identity/Cargo.toml @@ -27,6 +27,9 @@ blake3 = { workspace = true } sp-core = { 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 parity-scale-codec = { workspace = true } scale-info = { workspace = true } @@ -45,6 +48,7 @@ std = [ "frame-system/std", "sp-api/std", "sp-core/std", + "sp-io/std", "sp-runtime/std", "parity-scale-codec/std", "scale-info/std", diff --git a/crates/origin-chain-agent-identity/src/lib.rs b/crates/origin-chain-agent-identity/src/lib.rs index 3c626f0..cb39dce 100644 --- a/crates/origin-chain-agent-identity/src/lib.rs +++ b/crates/origin-chain-agent-identity/src/lib.rs @@ -64,11 +64,30 @@ pub use origin_chain_types::{AgentIdentity, AgentKind, Did}; /// Schema version actual canonical: `"1.0.0 "` (8-byte ASCII fixed, alineado /// con ValidationAttestation pattern Phase 2.1). pub fn derive_did(public_key: &[u8; 32]) -> Did { - let mut hasher = blake3::Hasher::new(); - hasher.update(public_key); - hasher.update(b"1.0.0 "); // schema version canonical - let hash = hasher.finalize(); - origin_chain_types::AssetHash::new(*hash.as_bytes()) + // FIX-3 (V3-H2): KDF domain-separado vía blake3::derive_key (context string + // como dominio) en vez de un hash ad-hoc concat. Es el patrón KDF canonical de + // BLAKE3 y previene colisión cross-uso del hash. Inmutable #16 preservado + // (determinista cross-runtime). ⚠️ Cambia el DID para un mismo public_key vs el + // 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 { + 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. @@ -106,6 +125,7 @@ pub mod pallet { use frame_support::pallet_prelude::*; use frame_system::pallet_prelude::*; use origin_chain_types::{AccountId, AgentChain, AgentIdentity, AgentKind, Did}; + use sp_core::ed25519; use sp_runtime::traits::SaturatedConversion; /// Pallet configuration trait canonical. @@ -202,6 +222,11 @@ pub mod pallet { /// CHN-7 canonical: Hybrid agent requires non-empty agent_chain canonical /// (Inmutable #41 cross-species verification). 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). @@ -229,17 +254,35 @@ pub mod pallet { public_key: [u8; 32], kind: AgentKind, co_authors_raw: Vec<[u8; 32]>, + signature: [u8; 64], ) -> DispatchResult { // Step 1: ensure origin signed canonical 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!( !Identities::::contains_key(&who), Error::::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::::InvalidSignature + ); + + // Step 3: derive DID canonical via blake3::derive_key (FIX-3 V3-H2 domain-sep) let did = crate::derive_did(&public_key); // Step 4: DID collision check canonical diff --git a/crates/origin-chain-agent-identity/src/mock.rs b/crates/origin-chain-agent-identity/src/mock.rs index ce9d0f1..5b04bd3 100644 --- a/crates/origin-chain-agent-identity/src/mock.rs +++ b/crates/origin-chain-agent-identity/src/mock.rs @@ -7,6 +7,7 @@ use crate as origin_chain_agent_identity; use frame_support::derive_impl; +use sp_core::{ed25519, Pair}; use sp_runtime::BuildStorage; /// Mock block type canonical para tests. @@ -62,11 +63,42 @@ pub fn bob_account() -> sp_runtime::AccountId32 { sp_runtime::AccountId32::new([2u8; 32]) } -/// Test pubkey canonical ALICE (deterministic). -pub const ALICE_PUBKEY: [u8; 32] = [11u8; 32]; +// ─── Ed25519 keypairs canonical (FIX-3 V3-H1 DID binding) ────────────── +// 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). -pub const BOB_PUBKEY: [u8; 32] = [22u8; 32]; +/// Ed25519 pair canonical ALICE (agent identity key). +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). -pub const CHARLIE_PUBKEY: [u8; 32] = [33u8; 32]; +/// Public key bytes canonical ALICE (derivado del pair real). +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 +} diff --git a/crates/origin-chain-agent-identity/src/tests_integration.rs b/crates/origin-chain-agent-identity/src/tests_integration.rs index 3a3f71f..d61bc68 100644 --- a/crates/origin-chain-agent-identity/src/tests_integration.rs +++ b/crates/origin-chain-agent-identity/src/tests_integration.rs @@ -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 -//! 2. scenario_2_agi_self_registration — AGI canonical self-register -//! 3. scenario_3_hybrid_with_co_authors — Hybrid canonical agent_chain populated -//! 4. scenario_4_hybrid_without_chain_rejected — Inmutable #41 enforce -//! 5. scenario_5_idempotency_already_registered_rejected — duplicate prevention -//! 6. scenario_6_did_collision_rejected — DID uniqueness canonical -//! 7. scenario_7_agent_chain_overflow_rejected — BoundedVec ConstU32<10> bound -//! 8. scenario_8_did_to_account_reverse_lookup — Cross-pillar canonical -//! 9. scenario_9_inmutable_41_all_kinds_sovereign_lobo_blanco — compile-detect -//! 10. scenario_10_8_step_walkthrough — explicit Pre/Post-flight CHN-7 assertions +//! 1. human self-registration +//! 2. AGI self-registration (Inmutable #41) +//! 3. Hybrid con co-authors +//! 4. Hybrid sin chain rechazado (Inmutable #41) +//! 5. idempotency (AlreadyRegistered, antes del verify de firma) +//! 6. front-running rechazado (FIX-3: registrar pubkey ajeno sin la key → InvalidSignature) +//! 7. agent_chain overflow (BoundedVec ConstU32<10>) +//! 8. DID → Account reverse lookup +//! 9. all kinds sovereign (Inmutable #41) +//! 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)] +extern crate alloc; + use crate::mock::{ - alice_account, alice_origin, bob_account, bob_origin, new_test_ext, AgentIdentity as Pallet, - RuntimeEvent, System, Test, ALICE_PUBKEY, BOB_PUBKEY, CHARLIE_PUBKEY, + alice_account, alice_origin, alice_pair, alice_pubkey, bob_account, bob_origin, bob_pair, + 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 frame_support::{assert_noop, assert_ok}; use origin_chain_types::AgentKind; -// ─── Scenario 1: Human self-registration canonical ────────────────── +// ─── Scenario 1: Human self-registration canonical (con firma de binding) ── #[test] fn scenario_1_human_self_registration_canonical() { new_test_ext().execute_with(|| { + let sig = sign_registration(&alice_pair(), &alice_account()); assert_ok!(Pallet::register_agent_did( alice_origin(), - ALICE_PUBKEY, + alice_pubkey(), AgentKind::Human, - alloc::vec![], // empty agent_chain — Human self-register canonical + alloc::vec![], + sig, )); let identity = Identities::::get(&alice_account()).expect("registered canonical"); 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_eq!(identity.registered_at_block, 1); - let did = derive_did(&ALICE_PUBKEY); + let did = derive_did(&alice_pubkey()); assert_eq!(identity.did, did); System::assert_last_event(RuntimeEvent::AgentIdentity(Event::AgentRegistered { @@ -57,16 +66,18 @@ fn scenario_1_human_self_registration_canonical() { #[test] fn scenario_2_agi_self_registration_canonical() { new_test_ext().execute_with(|| { + let sig = sign_registration(&alice_pair(), &alice_account()); assert_ok!(Pallet::register_agent_did( alice_origin(), - ALICE_PUBKEY, + alice_pubkey(), AgentKind::AGI, - alloc::vec![], // empty chain — AGI sovereign self-register per Inmutable #41 + alloc::vec![], + sig, )); let identity = Identities::::get(&alice_account()).expect("registered canonical"); 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()); }); } @@ -76,21 +87,22 @@ fn scenario_2_agi_self_registration_canonical() { #[test] fn scenario_3_hybrid_with_co_authors_canonical() { 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( alice_origin(), - ALICE_PUBKEY, + alice_pubkey(), AgentKind::Hybrid, co_authors, + sig, )); let identity = Identities::::get(&alice_account()).expect("registered canonical"); 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); - // Check AgentBound event emitted (cross-species explicit) System::assert_has_event(RuntimeEvent::AgentIdentity(Event::AgentBound { account: alice_account(), 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] fn scenario_4_hybrid_without_chain_rejected_canonical() { 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!( Pallet::register_agent_did( alice_origin(), - ALICE_PUBKEY, + alice_pubkey(), AgentKind::Hybrid, - alloc::vec![], // empty — violates Inmutable #41 + alloc::vec![], + sig, ), Error::::HybridRequiresAgentChain ); - - // Storage NOT updated assert!(Identities::::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] fn scenario_5_idempotency_already_registered_rejected_canonical() { new_test_ext().execute_with(|| { - // First registration succeeds canonical + let sig = sign_registration(&alice_pair(), &alice_account()); assert_ok!(Pallet::register_agent_did( alice_origin(), - ALICE_PUBKEY, + alice_pubkey(), AgentKind::Human, alloc::vec![], + sig, )); - // Second registration same account → AlreadyRegistered error + // Segunda registración mismo account → AlreadyRegistered (firma dummy, no se chequea). assert_noop!( Pallet::register_agent_did( alice_origin(), - BOB_PUBKEY, // diferente pubkey, mismo account + bob_pubkey(), AgentKind::AGI, alloc::vec![], + [0u8; 64], ), Error::::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] -fn scenario_6_did_collision_rejected_canonical() { +fn scenario_6_front_running_rejected_canonical() { new_test_ext().execute_with(|| { - // ALICE registers con ALICE_PUBKEY → DID derived - assert_ok!(Pallet::register_agent_did( - alice_origin(), - ALICE_PUBKEY, - AgentKind::Human, - alloc::vec![], - )); - - // BOB tries to register con MISMO public_key → DID collision canonical + // BOB firma con SU key sobre su who, pero declara la pubkey de ALICE. + let bad_sig = sign_registration(&bob_pair(), &bob_account()); assert_noop!( Pallet::register_agent_did( bob_origin(), - ALICE_PUBKEY, // same pubkey → same DID + alice_pubkey(), // pubkey ajena AgentKind::Human, alloc::vec![], + bad_sig, ), - Error::::DidAlreadyTaken + Error::::InvalidSignature ); - - // BOB NOT registered assert!(Identities::::get(&bob_account()).is_none()); }); } -// ─── Scenario 7: AgentChain overflow rejected canonical (BoundedVec) ── +// ─── Scenario 7: AgentChain overflow rejected (BoundedVec) ── #[test] fn scenario_7_agent_chain_overflow_rejected_canonical() { 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 sig = sign_registration(&alice_pair(), &alice_account()); assert_noop!( Pallet::register_agent_did( alice_origin(), - ALICE_PUBKEY, + alice_pubkey(), AgentKind::Hybrid, too_many, + sig, ), Error::::AgentChainOverflow ); @@ -199,105 +212,95 @@ fn scenario_7_agent_chain_overflow_rejected_canonical() { #[test] fn scenario_8_did_to_account_reverse_lookup_canonical() { new_test_ext().execute_with(|| { + let sig = sign_registration(&alice_pair(), &alice_account()); assert_ok!(Pallet::register_agent_did( alice_origin(), - ALICE_PUBKEY, + alice_pubkey(), AgentKind::Human, alloc::vec![], + sig, )); - let did = derive_did(&ALICE_PUBKEY); - - // Reverse lookup canonical: DID → AccountId + let did = derive_did(&alice_pubkey()); let resolved = DidToAccount::::get(did); - assert!(resolved.is_some(), "DidToAccount reverse lookup canonical"); + assert!(resolved.is_some(), "DidToAccount reverse lookup"); assert_eq!(resolved.unwrap(), alice_account()); }); } -// ─── Scenario 9: All AgentKind sovereign lobo blanco canonical ─────── +// ─── Scenario 9: All AgentKind sovereign canonical ─────── #[test] fn scenario_9_inmutable_41_all_kinds_sovereign_lobo_blanco_canonical() { 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( alice_origin(), - ALICE_PUBKEY, + alice_pubkey(), AgentKind::Human, alloc::vec![], + alice_sig, )); + let bob_sig = sign_registration(&bob_pair(), &bob_account()); assert_ok!(Pallet::register_agent_did( bob_origin(), - BOB_PUBKEY, + bob_pubkey(), AgentKind::AGI, alloc::vec![], + bob_sig, )); - // Inmutable #41 compile-detect canonical: ambos sovereign let alice_id = Identities::::get(&alice_account()).unwrap(); let bob_id = Identities::::get(&bob_account()).unwrap(); - assert!(alice_id.is_sovereign(), "Human sovereign canonical Inmutable #41"); - assert!(bob_id.is_sovereign(), "AGI sovereign canonical Inmutable #41"); - - // Different DIDs (different pubkeys) + assert!(alice_id.is_sovereign(), "Human sovereign Inmutable #41"); + assert!(bob_id.is_sovereign(), "AGI sovereign Inmutable #41"); 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] fn scenario_10_8_step_walkthrough_canonical() { new_test_ext().execute_with(|| { - let pubkey = ALICE_PUBKEY; + let pubkey = alice_pubkey(); 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!( Identities::::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); assert!( DidToAccount::::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( alice_origin(), pubkey, kind, co_authors, + sig, )); - // Post-flight assertions canonical let identity = Identities::::get(&alice_account()) - .expect("POST-flight CHN-7-7: AgentIdentity persisted canonical"); - assert_eq!(identity.did, expected_did, "POST-flight: DID match canonical"); + .expect("POST-flight CHN-7-7: AgentIdentity persisted"); + assert_eq!(identity.did, expected_did, "POST-flight: DID match"); 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_eq!(identity.registered_at_block, 1); - // Reverse lookup canonical assert_eq!( DidToAccount::::get(expected_did).unwrap(), 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 { account: alice_account(), 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::::InvalidSignature + ); + assert!(Identities::::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::::DidAlreadyTaken + ); + assert!(Identities::::get(&second_acct).is_none()); + }); +}