diff --git a/deny.toml b/deny.toml index 8518d2d3..7bd66c27 100644 --- a/deny.toml +++ b/deny.toml @@ -17,6 +17,23 @@ ignore = [ "RUSTSEC-2025-0141", # bincode 1.3.3 unmaintained (from wasmtime-jit) "RUSTSEC-2026-0020", # wasmtime 8.0.1 - guest-controlled resource exhaustion (from Polkadot SDK sc-executor) "RUSTSEC-2026-0021", # wasmtime 8.0.1 - panic on excessive headers (from Polkadot SDK sc-executor) + "RUSTSEC-2026-0085", # wasmtime 8.0.1 - panic lifting flags component value (from Polkadot SDK sc-executor) + "RUSTSEC-2026-0086", # wasmtime 8.0.1 - host data leakage with 64-bit tables and Winch (from Polkadot SDK) + "RUSTSEC-2026-0087", # wasmtime 8.0.1 - segfault f64x2.splat on x86-64 Cranelift (from Polkadot SDK) + "RUSTSEC-2026-0088", # wasmtime 8.0.1 - data leakage between pooling allocator instances (from Polkadot SDK) + "RUSTSEC-2026-0089", # wasmtime 8.0.1 - host panic on table.fill with Winch (from Polkadot SDK) + "RUSTSEC-2026-0091", # wasmtime 8.0.1 - OOB write transcoding component model strings (from Polkadot SDK) + "RUSTSEC-2026-0092", # wasmtime 8.0.1 - panic transcoding misaligned UTF-16 strings (from Polkadot SDK) + "RUSTSEC-2026-0093", # wasmtime 8.0.1 - heap OOB read UTF-16 to latin1+utf16 transcoding (from Polkadot SDK) + "RUSTSEC-2026-0094", # wasmtime 8.0.1 - improperly masked return from table.grow with Winch (from Polkadot SDK) + "RUSTSEC-2026-0095", # wasmtime 8.0.1 - sandbox-escaping memory access with Winch (from Polkadot SDK) + "RUSTSEC-2026-0096", # wasmtime 8.0.1 - miscompiled heap access enables sandbox escape on aarch64 (from Polkadot SDK) + + # Unsound (transitive, no upgrade path within current SDK pinning) + "RUSTSEC-2026-0097", # rand 0.8.5 / 0.9.2 - unsound with custom logger (from ark-std, fc-rpc, litep2p) + + # Unmaintained (transitive) + "RUSTSEC-2025-0161", # libsecp256k1 0.7.2 unmaintained (from fc-rpc, fp-account/pallet-evm) # Allowed warnings (8 total) "RUSTSEC-2024-0388", # derivative 2.2.0 unmaintained (from ark-r1cs-std) diff --git a/frame/zk-verifier/Cargo.toml b/frame/zk-verifier/Cargo.toml index 53695077..e4a54c0c 100644 --- a/frame/zk-verifier/Cargo.toml +++ b/frame/zk-verifier/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-zk-verifier" -version = "0.4.0" +version = "0.4.1" description = "Zero-Knowledge proof verification pallet for Orbinum" authors = ["Orbinum Team"] license = "GPL-3.0-or-later" diff --git a/frame/zk-verifier/src/benchmarking.rs b/frame/zk-verifier/src/benchmarking.rs index cda3e8d0..3f963ab2 100644 --- a/frame/zk-verifier/src/benchmarking.rs +++ b/frame/zk-verifier/src/benchmarking.rs @@ -194,5 +194,44 @@ mod benchmarks { )); } + #[benchmark] + fn batch_register_verification_keys(n: Linear<1, 10>) { + let vk_bytes = sample_verification_key(); + + let entries: Vec = (0..n) + .map(|i| crate::types::VkEntry { + circuit_id: CircuitId(100 + i), + version: 1, + verification_key: vk_bytes + .clone() + .try_into() + .expect("benchmark vk bytes must fit BoundedVec"), + set_active: true, + }) + .collect(); + + let bounded_entries: frame_support::BoundedVec< + crate::types::VkEntry, + frame_support::traits::ConstU32<10>, + > = entries + .try_into() + .expect("n <= 10, bounded vec debe admitir la entrada"); + + #[extrinsic_call] + _(RawOrigin::Root, bounded_entries); + + for i in 0..n { + assert!( + VerificationKeys::::contains_key(CircuitId(100 + i), 1u32), + "entrada {i} debe estar en VerificationKeys" + ); + assert_eq!( + crate::pallet::ActiveCircuitVersion::::get(CircuitId(100 + i)), + Some(1u32), + "entrada {i} debe tener versión activa = 1" + ); + } + } + impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test); } diff --git a/frame/zk-verifier/src/lib.rs b/frame/zk-verifier/src/lib.rs index c8112108..457f2ff9 100644 --- a/frame/zk-verifier/src/lib.rs +++ b/frame/zk-verifier/src/lib.rs @@ -57,7 +57,7 @@ mod benchmarking; /// Domain port for ZK verification (the ONLY public contract) pub use domain::services::ZkVerifierPort; -pub use types::{CircuitId, ProofSystem, VerificationKeyInfo, VerificationStatistics}; +pub use types::{CircuitId, ProofSystem, VerificationKeyInfo, VerificationStatistics, VkEntry}; pub use weights::WeightInfo; #[derive( @@ -206,6 +206,8 @@ pub mod pallet { ProofVerified { circuit_id: CircuitId, version: u32 }, /// Proof verification failed ProofVerificationFailed { circuit_id: CircuitId, version: u32 }, + /// All VKs in a batch were registered (and optionally activated) atomically + BatchVerificationKeysRegistered { count: u32 }, } // ======================================================================== @@ -377,6 +379,69 @@ pub mod pallet { ) -> DispatchResult { Self::execute_verify_proof(origin, circuit_id.0, None, proof, public_inputs) } + + /// Atomically register (and optionally activate) up to 10 verification keys. + /// + /// All entries are validated and written in the same block. If any entry is + /// invalid the whole call reverts — no partial state is committed. + /// + /// `set_active: true` in an entry forces that version to become the active one + /// for its circuit. If no active version exists yet the entry is activated + /// regardless of the flag. + /// + /// Origin must be Root (sudo/governance). + #[pallet::call_index(4)] + #[pallet::weight(T::WeightInfo::batch_register_verification_keys(entries.len() as u32))] + pub fn batch_register_verification_keys( + origin: OriginFor, + entries: BoundedVec>, + ) -> DispatchResult { + ensure_root(origin)?; + ensure!(!entries.is_empty(), Error::::InvalidBatchSize); + + for entry in entries.iter() { + let domain_vk = crate::domain::entities::VerificationKey::new( + entry.verification_key.to_vec(), + crate::domain::value_objects::ProofSystem::Groth16, + ) + .map_err(|err| { + Self::map_application_error( + crate::application::errors::ApplicationError::Domain(err), + ) + })?; + + let vk_info = VerificationKeyInfo { + key_data: domain_vk + .data() + .to_vec() + .try_into() + .map_err(|_| Error::::VerificationKeyTooLarge)?, + system: ProofSystem::Groth16, + registered_at: frame_system::Pallet::::block_number(), + }; + + VerificationKeys::::insert(entry.circuit_id, entry.version, vk_info); + + let auto_activate = ActiveCircuitVersion::::get(entry.circuit_id).is_none(); + if entry.set_active || auto_activate { + ActiveCircuitVersion::::insert(entry.circuit_id, entry.version); + Self::deposit_event(Event::ActiveVersionSet { + circuit_id: entry.circuit_id, + version: entry.version, + }); + } + + Self::deposit_event(Event::VerificationKeyRegistered { + circuit_id: entry.circuit_id, + version: entry.version, + }); + } + + Self::deposit_event(Event::BatchVerificationKeysRegistered { + count: entries.len() as u32, + }); + Ok(()) + } } } diff --git a/frame/zk-verifier/src/tests/e2e/batch_tests.rs b/frame/zk-verifier/src/tests/e2e/batch_tests.rs new file mode 100644 index 00000000..bdbb9b28 --- /dev/null +++ b/frame/zk-verifier/src/tests/e2e/batch_tests.rs @@ -0,0 +1,627 @@ +//! Tests para batch_register_verification_keys +//! +//! Cubre toda la funcionalidad de la extrinsic call_index(4): +//! +//! - Happy paths: registro de 1 a 10 entradas válidas +//! - Activación automática (auto_activate) cuando no existe versión activa +//! - set_active: true fuerza la activación aunque ya exista versión activa +//! - set_active: false no sobreescribe una versión activa existente +//! - Registro de múltiples versiones del mismo circuito +//! - Registro de múltiples circuitos distintos en un solo batch +//! - Eventos emitidos: uno por entrada + BatchVerificationKeysRegistered al final +//! - Errores de guardiana: batch vacío, origen no-root, VK vacía +//! +//! NOTA sobre atomicidad: +//! La extrinsic garantiza atomicidad a nivel de Executive (el runtime envuelve +//! cada extrinsic en una transacción de storage). En tests que invocan la función +//! directamente (sin Executive), los writes anteriores al error SÍ persisten. +//! Por eso los tests de error solo cubren casos en los que el fallo ocurre +//! ANTES de cualquier escritura (guardias iniciales). + +use crate::{ + ActiveCircuitVersion, Error, VerificationKeys, + mock::{RuntimeOrigin, Test, ZkVerifier}, + pallet::Event, + types::{CircuitId, VkEntry}, +}; +use frame_support::{BoundedVec, assert_noop, assert_ok, pallet_prelude::ConstU32}; +use sp_io::TestExternalities; +use sp_runtime::BuildStorage; + +// ============================================================================ +// Helpers +// ============================================================================ + +/// Crea un VK válido de 512 bytes (suficiente para Groth16 / longitud aceptada +/// por VerificationKey::new con el rango esperado del proof system). +fn sample_vk() -> BoundedVec> { + let mut vk = Vec::with_capacity(512); + vk.extend_from_slice(&[1u8; 64]); + vk.extend_from_slice(&[2u8; 128]); + vk.extend_from_slice(&[3u8; 128]); + vk.extend_from_slice(&[4u8; 128]); + vk.extend_from_slice(&[5u8; 64]); + vk.try_into().unwrap() +} + +/// Crea un VK válido de tamaño diferente para distinguir datos entre entradas. +fn sample_vk_alt() -> BoundedVec> { + let mut vk = Vec::with_capacity(512); + vk.extend_from_slice(&[10u8; 64]); + vk.extend_from_slice(&[20u8; 128]); + vk.extend_from_slice(&[30u8; 128]); + vk.extend_from_slice(&[40u8; 128]); + vk.extend_from_slice(&[50u8; 64]); + vk.try_into().unwrap() +} + +fn make_entry(circuit_id: CircuitId, version: u32, set_active: bool) -> VkEntry { + VkEntry { + circuit_id, + version, + verification_key: sample_vk(), + set_active, + } +} + +fn bounded_entries(entries: Vec) -> BoundedVec> { + entries + .try_into() + .expect("batch no puede superar 10 entradas") +} + +fn new_ext() -> TestExternalities { + let storage = frame_system::GenesisConfig::::default() + .build_storage() + .unwrap(); + TestExternalities::new(storage) +} + +// ============================================================================ +// Happy paths: registro exitoso +// ============================================================================ + +#[test] +fn batch_with_single_entry_works() { + new_ext().execute_with(|| { + let entries = bounded_entries(vec![make_entry(CircuitId::TRANSFER, 1, true)]); + assert_ok!(ZkVerifier::batch_register_verification_keys( + RuntimeOrigin::root(), + entries + )); + + assert!(VerificationKeys::::contains_key( + CircuitId::TRANSFER, + 1 + )); + assert_eq!( + ActiveCircuitVersion::::get(CircuitId::TRANSFER), + Some(1) + ); + }); +} + +#[test] +fn batch_with_multiple_distinct_circuits_works() { + new_ext().execute_with(|| { + let entries = bounded_entries(vec![ + make_entry(CircuitId::TRANSFER, 1, true), + make_entry(CircuitId::UNSHIELD, 1, true), + make_entry(CircuitId::DISCLOSURE, 1, true), + ]); + assert_ok!(ZkVerifier::batch_register_verification_keys( + RuntimeOrigin::root(), + entries + )); + + assert!(VerificationKeys::::contains_key( + CircuitId::TRANSFER, + 1 + )); + assert!(VerificationKeys::::contains_key( + CircuitId::UNSHIELD, + 1 + )); + assert!(VerificationKeys::::contains_key( + CircuitId::DISCLOSURE, + 1 + )); + }); +} + +#[test] +fn batch_preserves_key_data_correctly() { + new_ext().execute_with(|| { + let vk_data = sample_vk(); + let entry = VkEntry { + circuit_id: CircuitId::TRANSFER, + version: 3, + verification_key: vk_data.clone(), + set_active: true, + }; + + assert_ok!(ZkVerifier::batch_register_verification_keys( + RuntimeOrigin::root(), + bounded_entries(vec![entry]) + )); + + let stored = VerificationKeys::::get(CircuitId::TRANSFER, 3).unwrap(); + assert_eq!(stored.key_data.to_vec(), vk_data.to_vec()); + }); +} + +#[test] +fn batch_up_to_max_size_ten_works() { + new_ext().execute_with(|| { + let entries: Vec = (0u32..10) + .map(|i| make_entry(CircuitId(100 + i), 1, true)) + .collect(); + + assert_ok!(ZkVerifier::batch_register_verification_keys( + RuntimeOrigin::root(), + bounded_entries(entries) + )); + + for i in 0..10u32 { + assert!(VerificationKeys::::contains_key( + CircuitId(100 + i), + 1 + )); + assert_eq!( + ActiveCircuitVersion::::get(CircuitId(100 + i)), + Some(1) + ); + } + }); +} + +// ============================================================================ +// Lógica de activación +// ============================================================================ + +#[test] +fn batch_auto_activates_when_no_active_version_exists() { + new_ext().execute_with(|| { + // set_active: false, pero no hay versión activa → debe activarse igualmente + let entries = bounded_entries(vec![make_entry(CircuitId::TRANSFER, 1, false)]); + assert_ok!(ZkVerifier::batch_register_verification_keys( + RuntimeOrigin::root(), + entries + )); + + assert_eq!( + ActiveCircuitVersion::::get(CircuitId::TRANSFER), + Some(1) + ); + }); +} + +#[test] +fn batch_set_active_false_does_not_override_existing_active() { + new_ext().execute_with(|| { + // Registrar versión 1 como activa + let first = bounded_entries(vec![make_entry(CircuitId::TRANSFER, 1, true)]); + assert_ok!(ZkVerifier::batch_register_verification_keys( + RuntimeOrigin::root(), + first + )); + assert_eq!( + ActiveCircuitVersion::::get(CircuitId::TRANSFER), + Some(1) + ); + + // Registrar versión 2 con set_active: false → versión activa no debe cambiar + let second = bounded_entries(vec![make_entry(CircuitId::TRANSFER, 2, false)]); + assert_ok!(ZkVerifier::batch_register_verification_keys( + RuntimeOrigin::root(), + second + )); + + assert!(VerificationKeys::::contains_key( + CircuitId::TRANSFER, + 2 + )); + assert_eq!( + ActiveCircuitVersion::::get(CircuitId::TRANSFER), + Some(1), // sigue siendo 1 + ); + }); +} + +#[test] +fn batch_set_active_true_overrides_existing_active() { + new_ext().execute_with(|| { + // Registrar versión 1 como activa + let first = bounded_entries(vec![make_entry(CircuitId::TRANSFER, 1, true)]); + assert_ok!(ZkVerifier::batch_register_verification_keys( + RuntimeOrigin::root(), + first + )); + + // Registrar versión 2 con set_active: true → debe convertirse en activa + let second = bounded_entries(vec![make_entry(CircuitId::TRANSFER, 2, true)]); + assert_ok!(ZkVerifier::batch_register_verification_keys( + RuntimeOrigin::root(), + second + )); + + assert_eq!( + ActiveCircuitVersion::::get(CircuitId::TRANSFER), + Some(2) + ); + }); +} + +#[test] +fn batch_same_circuit_two_entries_first_auto_activates_second_does_not() { + new_ext().execute_with(|| { + // Primera entrada: no hay activo → auto_activate + // Segunda entrada: ya existe activo (puesto por la primera) y set_active: false → sin cambio + let entries = bounded_entries(vec![ + make_entry(CircuitId::TRANSFER, 1, false), + make_entry(CircuitId::TRANSFER, 2, false), + ]); + assert_ok!(ZkVerifier::batch_register_verification_keys( + RuntimeOrigin::root(), + entries + )); + + assert!(VerificationKeys::::contains_key( + CircuitId::TRANSFER, + 1 + )); + assert!(VerificationKeys::::contains_key( + CircuitId::TRANSFER, + 2 + )); + // Activo es la versión 1 (la que fue auto-activada) + assert_eq!( + ActiveCircuitVersion::::get(CircuitId::TRANSFER), + Some(1) + ); + }); +} + +#[test] +fn batch_same_circuit_two_entries_second_set_active_true_overrides() { + new_ext().execute_with(|| { + // Primera entrada: auto_activate → version 1 activa + // Segunda entrada: set_active: true → version 2 pasa a activa + let entries = bounded_entries(vec![ + make_entry(CircuitId::TRANSFER, 1, false), + make_entry(CircuitId::TRANSFER, 2, true), + ]); + assert_ok!(ZkVerifier::batch_register_verification_keys( + RuntimeOrigin::root(), + entries + )); + + assert_eq!( + ActiveCircuitVersion::::get(CircuitId::TRANSFER), + Some(2) + ); + }); +} + +#[test] +fn batch_each_circuit_activates_independently() { + new_ext().execute_with(|| { + // Dos circuitos distintos, ninguno con versión activa previa → ambos auto-activan + let entries = bounded_entries(vec![ + make_entry(CircuitId::TRANSFER, 5, false), + make_entry(CircuitId::UNSHIELD, 3, false), + ]); + assert_ok!(ZkVerifier::batch_register_verification_keys( + RuntimeOrigin::root(), + entries + )); + + assert_eq!( + ActiveCircuitVersion::::get(CircuitId::TRANSFER), + Some(5) + ); + assert_eq!( + ActiveCircuitVersion::::get(CircuitId::UNSHIELD), + Some(3) + ); + }); +} + +// ============================================================================ +// Múltiples versiones del mismo circuito +// ============================================================================ + +#[test] +fn batch_registers_multiple_versions_for_same_circuit() { + new_ext().execute_with(|| { + let entries = bounded_entries(vec![ + make_entry(CircuitId::TRANSFER, 1, true), + make_entry(CircuitId::TRANSFER, 2, false), + make_entry(CircuitId::TRANSFER, 3, false), + ]); + assert_ok!(ZkVerifier::batch_register_verification_keys( + RuntimeOrigin::root(), + entries + )); + + assert!(VerificationKeys::::contains_key( + CircuitId::TRANSFER, + 1 + )); + assert!(VerificationKeys::::contains_key( + CircuitId::TRANSFER, + 2 + )); + assert!(VerificationKeys::::contains_key( + CircuitId::TRANSFER, + 3 + )); + // Solo la primera es la activa + assert_eq!( + ActiveCircuitVersion::::get(CircuitId::TRANSFER), + Some(1) + ); + }); +} + +// ============================================================================ +// Eventos +// ============================================================================ + +#[test] +fn batch_emits_per_entry_events_and_summary_event() { + new_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + + let entries = bounded_entries(vec![ + make_entry(CircuitId::TRANSFER, 1, true), + make_entry(CircuitId::UNSHIELD, 1, true), + ]); + assert_ok!(ZkVerifier::batch_register_verification_keys( + RuntimeOrigin::root(), + entries + )); + + let events: Vec<_> = frame_system::Pallet::::events() + .into_iter() + .map(|r| r.event) + .collect(); + + // Debe haber ActiveVersionSet para cada entrada, VerificationKeyRegistered para + // cada entrada, y BatchVerificationKeysRegistered al final. + let active_set_count = events + .iter() + .filter(|e| { + matches!( + e, + crate::mock::RuntimeEvent::ZkVerifier(Event::ActiveVersionSet { .. }) + ) + }) + .count(); + + let vk_registered_count = events + .iter() + .filter(|e| { + matches!( + e, + crate::mock::RuntimeEvent::ZkVerifier(Event::VerificationKeyRegistered { .. }) + ) + }) + .count(); + + let batch_summary_count = events + .iter() + .filter(|e| { + matches!( + e, + crate::mock::RuntimeEvent::ZkVerifier(Event::BatchVerificationKeysRegistered { + count: 2 + }) + ) + }) + .count(); + + assert_eq!(active_set_count, 2, "debe haber 2 eventos ActiveVersionSet"); + assert_eq!( + vk_registered_count, 2, + "debe haber 2 eventos VerificationKeyRegistered" + ); + assert_eq!( + batch_summary_count, 1, + "debe haber 1 evento BatchVerificationKeysRegistered" + ); + }); +} + +#[test] +fn batch_emits_single_entry_events_correctly() { + new_ext().execute_with(|| { + frame_system::Pallet::::set_block_number(1); + + let entries = bounded_entries(vec![make_entry(CircuitId::DISCLOSURE, 7, true)]); + assert_ok!(ZkVerifier::batch_register_verification_keys( + RuntimeOrigin::root(), + entries + )); + + let events: Vec<_> = frame_system::Pallet::::events() + .into_iter() + .map(|r| r.event) + .collect(); + + // Verificar evento específico de registro + let registered = events.iter().any(|e| { + matches!( + e, + crate::mock::RuntimeEvent::ZkVerifier(Event::VerificationKeyRegistered { + circuit_id: CircuitId::DISCLOSURE, + version: 7, + }) + ) + }); + assert!( + registered, + "debe emitir VerificationKeyRegistered para DISCLOSURE v7" + ); + + // Verificar evento de resumen final + let summary = events.iter().any(|e| { + matches!( + e, + crate::mock::RuntimeEvent::ZkVerifier(Event::BatchVerificationKeysRegistered { + count: 1 + }) + ) + }); + assert!( + summary, + "debe emitir BatchVerificationKeysRegistered con count=1" + ); + }); +} + +#[test] +fn batch_no_active_version_event_when_set_active_false_with_existing_active() { + new_ext().execute_with(|| { + // Pre-registrar versión activa + let first = bounded_entries(vec![make_entry(CircuitId::TRANSFER, 1, true)]); + assert_ok!(ZkVerifier::batch_register_verification_keys( + RuntimeOrigin::root(), + first + )); + + frame_system::Pallet::::reset_events(); + + // Segunda vuelta: set_active: false con activo existente → sin evento ActiveVersionSet + let second = bounded_entries(vec![make_entry(CircuitId::TRANSFER, 2, false)]); + assert_ok!(ZkVerifier::batch_register_verification_keys( + RuntimeOrigin::root(), + second + )); + + let events: Vec<_> = frame_system::Pallet::::events() + .into_iter() + .map(|r| r.event) + .collect(); + + let active_set_count = events + .iter() + .filter(|e| { + matches!( + e, + crate::mock::RuntimeEvent::ZkVerifier(Event::ActiveVersionSet { .. }) + ) + }) + .count(); + + assert_eq!( + active_set_count, 0, + "no debe emitir ActiveVersionSet cuando set_active es false y ya existe versión activa" + ); + }); +} + +// ============================================================================ +// Errores de guardiana (ocurren antes de cualquier escritura) +// ============================================================================ + +#[test] +fn batch_empty_reverts_with_invalid_batch_size() { + new_ext().execute_with(|| { + let empty: BoundedVec> = BoundedVec::new(); + assert_noop!( + ZkVerifier::batch_register_verification_keys(RuntimeOrigin::root(), empty), + Error::::InvalidBatchSize + ); + }); +} + +#[test] +fn batch_non_root_fails_with_bad_origin() { + new_ext().execute_with(|| { + let entries = bounded_entries(vec![make_entry(CircuitId::TRANSFER, 1, true)]); + assert_noop!( + ZkVerifier::batch_register_verification_keys(RuntimeOrigin::signed(1), entries), + sp_runtime::DispatchError::BadOrigin + ); + }); +} + +#[test] +fn batch_first_entry_empty_vk_fails() { + new_ext().execute_with(|| { + let empty_vk: BoundedVec> = BoundedVec::new(); + let entry = VkEntry { + circuit_id: CircuitId::TRANSFER, + version: 1, + verification_key: empty_vk, + set_active: true, + }; + let entries = bounded_entries(vec![entry]); + + // El primer elemento falla la validación de dominio → error antes de cualquier write + assert_noop!( + ZkVerifier::batch_register_verification_keys(RuntimeOrigin::root(), entries), + Error::::EmptyVerificationKey + ); + }); +} + +// ============================================================================ +// Idempotencia / sobreescritura +// ============================================================================ + +#[test] +fn batch_overwrites_existing_key_data_for_same_version() { + new_ext().execute_with(|| { + // Registrar circuito version 1 con datos A + let entry_a = VkEntry { + circuit_id: CircuitId::TRANSFER, + version: 1, + verification_key: sample_vk(), + set_active: true, + }; + assert_ok!(ZkVerifier::batch_register_verification_keys( + RuntimeOrigin::root(), + bounded_entries(vec![entry_a]) + )); + + // Sobrescribir version 1 con datos B + let entry_b = VkEntry { + circuit_id: CircuitId::TRANSFER, + version: 1, + verification_key: sample_vk_alt(), + set_active: false, + }; + assert_ok!(ZkVerifier::batch_register_verification_keys( + RuntimeOrigin::root(), + bounded_entries(vec![entry_b]) + )); + + let stored = VerificationKeys::::get(CircuitId::TRANSFER, 1).unwrap(); + assert_eq!(stored.key_data.to_vec(), sample_vk_alt().to_vec()); + }); +} + +// ============================================================================ +// Circuitos personalizados (IDs fuera de las constantes predefinidas) +// ============================================================================ + +#[test] +fn batch_works_with_custom_circuit_ids() { + new_ext().execute_with(|| { + let entries = bounded_entries(vec![ + make_entry(CircuitId(42), 1, true), + make_entry(CircuitId(999), 2, true), + ]); + assert_ok!(ZkVerifier::batch_register_verification_keys( + RuntimeOrigin::root(), + entries + )); + + assert!(VerificationKeys::::contains_key(CircuitId(42), 1)); + assert!(VerificationKeys::::contains_key(CircuitId(999), 2)); + assert_eq!(ActiveCircuitVersion::::get(CircuitId(42)), Some(1)); + assert_eq!(ActiveCircuitVersion::::get(CircuitId(999)), Some(2)); + }); +} diff --git a/frame/zk-verifier/src/tests/e2e/mod.rs b/frame/zk-verifier/src/tests/e2e/mod.rs index 42aebf69..b0496b0a 100644 --- a/frame/zk-verifier/src/tests/e2e/mod.rs +++ b/frame/zk-verifier/src/tests/e2e/mod.rs @@ -1,3 +1,4 @@ //! End-to-end tests module +pub mod batch_tests; pub mod genesis_tests; diff --git a/frame/zk-verifier/src/types.rs b/frame/zk-verifier/src/types.rs index fadede93..bd7f4ff4 100644 --- a/frame/zk-verifier/src/types.rs +++ b/frame/zk-verifier/src/types.rs @@ -110,3 +110,30 @@ pub struct VerificationStatistics { /// Failed verifications pub failed_verifications: u64, } + +/// A single entry for batch verification key registration. +/// +/// Used by `batch_register_verification_keys` to atomically register +/// and optionally activate multiple circuits in one extrinsic. +#[derive( + Clone, + PartialEq, + Eq, + Encode, + Decode, + DecodeWithMemTracking, + MaxEncodedLen, + TypeInfo, + Debug +)] +pub struct VkEntry { + /// Circuit to register the key for. + pub circuit_id: CircuitId, + /// Version number for this key. + pub version: u32, + /// Serialized verification key data (max 8 KB). + pub verification_key: BoundedVec>, + /// Set this version as active after registration. + /// If no active version exists for the circuit, it is activated regardless. + pub set_active: bool, +} diff --git a/frame/zk-verifier/src/weights.rs b/frame/zk-verifier/src/weights.rs index 8eb7cc9e..fc514fa4 100644 --- a/frame/zk-verifier/src/weights.rs +++ b/frame/zk-verifier/src/weights.rs @@ -38,6 +38,7 @@ pub trait WeightInfo { fn set_active_version() -> Weight; fn remove_verification_key() -> Weight; fn verify_proof() -> Weight; + fn batch_register_verification_keys(n: u32) -> Weight; } /// Weight functions for `pallet_zk_verifier`. @@ -85,4 +86,33 @@ impl WeightInfo for SubstrateWeight { .saturating_add(T::DbWeight::get().reads(7)) .saturating_add(T::DbWeight::get().writes(3)) } + + /// Storage: `System::Number` (r:1 w:0) + /// Proof: `System::Number` (`max_values`: Some(1), `max_size`: Some(4), added: 499, mode: `MaxEncodedLen`) + /// Storage: `ZkVerifier::ActiveCircuitVersion` (r:10 w:10) + /// Proof: `ZkVerifier::ActiveCircuitVersion` (`max_values`: None, `max_size`: Some(24), added: 2499, 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`) + /// Storage: `ZkVerifier::VerificationKeys` (r:0 w:10) + /// Proof: `ZkVerifier::VerificationKeys` (`max_values`: None, `max_size`: Some(8239), added: 10714, mode: `MaxEncodedLen`) + /// The range of component `n` is `[1, 10]`. + fn batch_register_verification_keys(n: u32) -> Weight { + // Proof Size summary in bytes: + // Measured: `61` + // Estimated: `1546 + n * (2499 ±0)` + // Minimum execution time: 14_000_000 picoseconds. + Weight::from_parts(6_790_664, 0) + .saturating_add(Weight::from_parts(0, 1546)) + // Standard Error: 8_132 + .saturating_add(Weight::from_parts(7_990_704, 0).saturating_mul(n.into())) + .saturating_add(T::DbWeight::get().reads(4)) + .saturating_add(T::DbWeight::get().reads((1_u64).saturating_mul(n.into()))) + .saturating_add(T::DbWeight::get().writes(2)) + .saturating_add(T::DbWeight::get().writes((2_u64).saturating_mul(n.into()))) + .saturating_add(Weight::from_parts(0, 2499).saturating_mul(n.into())) + } }