From 4df7a800676d77c999679381ec7ebb9187fbb09a Mon Sep 17 00:00:00 2001 From: nol4lej Date: Wed, 21 Oct 2026 17:11:36 -0300 Subject: [PATCH 1/5] =?UTF-8?q?fix(zk-verifier)!:=20keep=20an=20on-chain,?= =?UTF-8?q?=20append-only=20record=20of=20denied=20keys=20(deny=5Fversion,?= =?UTF-8?q?=20genesis)=20=E2=80=94=20a=20denied=20key=20is=20refused=20on?= =?UTF-8?q?=20register,=20activate=20and=20unretire=20and=20never=20verifi?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 2 + frame/zk-verifier/CHANGELOG.md | 21 ++ frame/zk-verifier/Cargo.toml | 3 + frame/zk-verifier/src/benchmarking.rs | 16 ++ frame/zk-verifier/src/lib.rs | 77 +++++++- frame/zk-verifier/src/mock.rs | 26 +++ frame/zk-verifier/src/port.rs | 13 +- frame/zk-verifier/src/tests.rs | 273 +++++++++++++++++++++++++- frame/zk-verifier/src/verifier.rs | 6 +- frame/zk-verifier/src/weights.rs | 11 ++ 10 files changed, 432 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4b9a5d98..3d86d339 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12944,6 +12944,8 @@ dependencies = [ "parity-scale-codec", "scale-info", "serde", + "serde_json", + "sp-core", "sp-io", "sp-runtime", "sp-std", diff --git a/frame/zk-verifier/CHANGELOG.md b/frame/zk-verifier/CHANGELOG.md index 232b43ff..b02a95c7 100644 --- a/frame/zk-verifier/CHANGELOG.md +++ b/frame/zk-verifier/CHANGELOG.md @@ -6,6 +6,27 @@ All notable changes to this pallet are documented here. ## [Unreleased] +### Added + +- `DeniedVkHashes`: an on-chain, append-only record of `blake2_256` hashes of keys + found unsound. A denied key is refused by `register_verification_key`, + `batch_register_verification_keys`, `set_active_version` and `unretire_version` + (`DeniedVerificationKey`), and a stored one verifies nothing, active or not + (`UnsupportedCircuitVersion`; `is_supported_version` is false). No call removes an + entry, `remove_verification_key` and `purge_circuit` included, so a Root mistake — + unretiring, re-registering under another version — cannot bring the key back. +- `deny_version(circuit_id, version)` (Root, call index 8): retires the version and + records its key's hash (`VerificationKeyDenied`). Never the active version; a hash + already recorded is refused (`KeyAlreadyDenied`). +- `GenesisConfig::denied_vk_hashes`: the record a new chain starts with, written before + any genesis key. + +### Changed + +- `ZkVerifierPort::verification_weight` adds one storage read, the `VkHashes` lookup of + the denied-key check. +- `WeightInfo::deny_version`: estimated from `retire_version` until benchmarked. + ## [0.18.1] - 2026-10-21 ### Changed diff --git a/frame/zk-verifier/Cargo.toml b/frame/zk-verifier/Cargo.toml index dd63fb8e..f00badd4 100644 --- a/frame/zk-verifier/Cargo.toml +++ b/frame/zk-verifier/Cargo.toml @@ -21,6 +21,7 @@ serde = { version = "1.0", default-features = false, features = ["derive", "allo # Substrate primitives +sp-core = { version = "43.0.0", default-features = false, features = ["serde"] } sp-io = { version = "48.0.0", default-features = false } sp-runtime = { version = "48.0.0", default-features = false } sp-std = { version = "14.0.0", default-features = false } @@ -41,6 +42,7 @@ orbinum-zk-core = { path = "../../primitives/zk-core", default-features = false orbinum-zk-verifier = { path = "../../primitives/zk-verifier", default-features = false } [dev-dependencies] +serde_json = "1.0" orbinum-zk-core = { path = "../../primitives/zk-core", default-features = false } orbinum-zk-verifier = { path = "../../primitives/zk-verifier", default-features = false } # Build real Groth16 VKs of a given arity in tests (arity is now validated on register). @@ -54,6 +56,7 @@ std = [ "parity-scale-codec/std", "scale-info/std", "serde/std", + "sp-core/std", "sp-io/std", "sp-runtime/std", "sp-std/std", diff --git a/frame/zk-verifier/src/benchmarking.rs b/frame/zk-verifier/src/benchmarking.rs index b29a79ef..2cffdeaf 100644 --- a/frame/zk-verifier/src/benchmarking.rs +++ b/frame/zk-verifier/src/benchmarking.rs @@ -208,6 +208,22 @@ mod benchmarks { assert!(!RetiredVersions::::contains_key(circuit_id, retired)); } + #[benchmark] + fn deny_version() { + let circuit_id = CircuitId::TRANSFER; + let active = 1u32; + let target = 2u32; + + insert_sample_vk::(circuit_id, active); + insert_sample_vk::(circuit_id, target); + ActiveCircuitVersion::::insert(circuit_id, active); + + #[extrinsic_call] + _(RawOrigin::Root, circuit_id, target); + + assert!(RetiredVersions::::contains_key(circuit_id, target)); + } + /// Uses circuit id 99 rather than the retired 5: the extrinsic only accepts /// ids the runtime does not implement, and 99 stays unknown even if 5 is /// ever reassigned. diff --git a/frame/zk-verifier/src/lib.rs b/frame/zk-verifier/src/lib.rs index a7f29375..9f6d2d59 100644 --- a/frame/zk-verifier/src/lib.rs +++ b/frame/zk-verifier/src/lib.rs @@ -146,6 +146,13 @@ pub mod pallet { ValueQuery, >; + /// `blake2_256` of every key denied on this chain (`deny_version`, genesis): the + /// record of circuits found unsound. Such a key is refused on register, activate + /// and unretire, and verifies nothing if stored. Append-only: no call removes an + /// entry, `remove_verification_key` and `purge_circuit` included. + #[pallet::storage] + pub type DeniedVkHashes = StorageMap<_, Identity, [u8; 32], (), OptionQuery>; + // ── Genesis ─────────────────────────────────────────────────────────────── #[pallet::genesis_config] @@ -153,6 +160,8 @@ pub mod pallet { pub struct GenesisConfig { /// One key per circuit, registered as version 1 and activated. pub verification_keys: Vec<(CircuitId, Vec)>, + /// `DeniedVkHashes` a new chain starts with, recorded before any key. + pub denied_vk_hashes: Vec, #[serde(skip)] pub _phantom: PhantomData, } @@ -160,6 +169,9 @@ pub mod pallet { #[pallet::genesis_build] impl BuildGenesisConfig for GenesisConfig { fn build(&self) { + for hash in &self.denied_vk_hashes { + DeniedVkHashes::::insert(hash.0, ()); + } for (circuit_id, vk_bytes) in &self.verification_keys { let key_data: VkBytes = vk_bytes .clone() @@ -218,6 +230,12 @@ pub mod pallet { BatchVerificationKeysRegistered { count: u32 }, /// Every version of a retired circuit was purged from storage. CircuitPurged { circuit_id: CircuitId, removed: u32 }, + /// A version was retired for good and its key's hash recorded as denied. + VerificationKeyDenied { + circuit_id: CircuitId, + version: u32, + vk_hash: [u8; 32], + }, } // ── Errors ──────────────────────────────────────────────────────────────── @@ -260,6 +278,10 @@ pub mod pallet { // Purge CircuitStillInUse, CircuitHasNoStorage, + /// The key's hash is in `DeniedVkHashes`. + DeniedVerificationKey, + /// `deny_version`: the key's hash is already denied. + KeyAlreadyDenied, } // ── Extrinsics ──────────────────────────────────────────────────────────── @@ -310,7 +332,7 @@ pub mod pallet { !RetiredVersions::::contains_key(circuit_id, version), Error::::UnsupportedCircuitVersion ); - Self::ensure_vk_arity(circuit_id, &info.key_data)?; + Self::ensure_vk_admitted(circuit_id, &info.key_data)?; Self::activate(circuit_id, version); Ok(()) @@ -457,7 +479,7 @@ pub mod pallet { ); let info = VerificationKeys::::get(circuit_id, version) .ok_or(Error::::VerificationKeyNotFound)?; - Self::ensure_vk_arity(circuit_id, &info.key_data)?; + Self::ensure_vk_admitted(circuit_id, &info.key_data)?; RetiredVersions::::remove(circuit_id, version); Self::deposit_event(Event::VersionUnretired { circuit_id, @@ -466,6 +488,40 @@ pub mod pallet { Ok(()) } + /// Deny a version for good (Root only): it is retired and its key's hash joins + /// `DeniedVkHashes`, so that key cannot be registered, activated, unretired or + /// verified again under any circuit or version. Never the active one, and no call + /// undoes it. + #[pallet::call_index(8)] + #[pallet::weight(T::WeightInfo::deny_version())] + pub fn deny_version( + origin: OriginFor, + circuit_id: CircuitId, + version: u32, + ) -> DispatchResult { + ensure_root(origin)?; + let info = VerificationKeys::::get(circuit_id, version) + .ok_or(Error::::VerificationKeyNotFound)?; + ensure!( + ActiveCircuitVersion::::get(circuit_id) != Some(version), + Error::::CannotRetireActiveVersion + ); + let vk_hash = sp_io::hashing::blake2_256(&info.key_data); + ensure!( + !DeniedVkHashes::::contains_key(vk_hash), + Error::::KeyAlreadyDenied + ); + + DeniedVkHashes::::insert(vk_hash, ()); + RetiredVersions::::insert(circuit_id, version, ()); + Self::deposit_event(Event::VerificationKeyDenied { + circuit_id, + version, + vk_hash, + }); + Ok(()) + } + /// Erase every trace of a circuit the runtime no longer implements (Root /// only). /// @@ -549,7 +605,7 @@ pub mod pallet { set_active: bool, ) -> DispatchResult { ensure!(!key_data.is_empty(), Error::::EmptyVerificationKey); - Self::ensure_vk_arity(circuit_id, &key_data)?; + Self::ensure_vk_admitted(circuit_id, &key_data)?; // Never overwrite: a replaced key would silently change what verifies. ensure!( !VerificationKeys::::contains_key(circuit_id, version), @@ -575,11 +631,20 @@ pub mod pallet { }); } - /// The rule every stored key is held to: it deserializes as a BN254 Groth16 - /// key and its arity gives an admitted layout + /// Whether `hash` is in `DeniedVkHashes`. + pub(crate) fn is_denied(hash: &[u8; 32]) -> bool { + DeniedVkHashes::::contains_key(hash) + } + + /// The rule every stored key is held to: it is not denied, it deserializes as + /// a BN254 Groth16 key, and its arity gives an admitted layout /// ([`verifier::admitted_layout`]). Ids outside the known table are only /// checked to deserialize. - fn ensure_vk_arity(circuit_id: CircuitId, key_data: &[u8]) -> DispatchResult { + fn ensure_vk_admitted(circuit_id: CircuitId, key_data: &[u8]) -> DispatchResult { + ensure!( + !Self::is_denied(&sp_io::hashing::blake2_256(key_data)), + Error::::DeniedVerificationKey + ); let arity = orbinum_zk_verifier::VerifyingKey::new(key_data.to_vec()) .num_public_inputs() .map_err(|_| Error::::InvalidVerificationKey)?; diff --git a/frame/zk-verifier/src/mock.rs b/frame/zk-verifier/src/mock.rs index 1d967d09..7eeadf8e 100644 --- a/frame/zk-verifier/src/mock.rs +++ b/frame/zk-verifier/src/mock.rs @@ -65,6 +65,32 @@ pub fn real_vk(arity: usize) -> VkBytes { .expect("serialized VK fits in MAX_VK_BYTES") } +/// A well-formed key of `arity` inputs, distinct from [`real_vk`]. +pub fn denied_vk_of(arity: usize) -> VkBytes { + use ark_bn254::{Bn254, G1Affine, G2Affine}; + use ark_ec::{AffineRepr, CurveGroup}; + use ark_groth16::VerifyingKey as ArkVk; + + let vk = ArkVk:: { + alpha_g1: (G1Affine::generator() + G1Affine::generator() + G1Affine::generator()) + .into_affine(), + beta_g2: G2Affine::generator(), + gamma_g2: G2Affine::generator(), + delta_g2: G2Affine::generator(), + gamma_abc_g1: (0..=arity).map(|_| G1Affine::generator()).collect(), + }; + orbinum_zk_verifier::VerifyingKey::from_ark_vk(&vk) + .unwrap() + .bytes + .try_into() + .expect("serialized VK fits in MAX_VK_BYTES") +} + +/// A well-formed unshield-shaped (8-input) key the tests deny. +pub fn denied_vk() -> VkBytes { + denied_vk_of(8) +} + /// Store `key_data` for `(circuit_id, version)` directly, skipping registration checks. pub fn insert_key(circuit_id: CircuitId, version: u32, key_data: VkBytes) { VerificationKeys::::insert( diff --git a/frame/zk-verifier/src/port.rs b/frame/zk-verifier/src/port.rs index b64dee99..a757e7eb 100644 --- a/frame/zk-verifier/src/port.rs +++ b/frame/zk-verifier/src/port.rs @@ -7,7 +7,7 @@ use crate::{ Pallet, encoding, - pallet::{Config, Error, RetiredVersions, VerificationKeys}, + pallet::{Config, Error, RetiredVersions, VerificationKeys, VkHashes}, types::CircuitId, verifier, }; @@ -165,11 +165,15 @@ impl ZkVerifierPort for Pallet { let cid = CircuitId(circuit_id); VerificationKeys::::contains_key(cid, version) && !RetiredVersions::::contains_key(cid, version) + && !VkHashes::::get(cid, version).is_some_and(|h| Pallet::::is_denied(&h)) } fn verification_weight() -> frame_support::weights::Weight { use crate::weights::WeightInfo; + use frame_support::traits::Get; + // Plus the `VkHashes` read of the denied-key check. T::WeightInfo::verify_proof(MAX_SPEND_PUBLIC_INPUTS) + .saturating_add(T::DbWeight::get().reads(1)) } } @@ -528,9 +532,16 @@ mod tests { fn verification_weight_covers_the_largest_spend_layout() { use crate::weights::WeightInfo; assert_eq!(MAX_SPEND_PUBLIC_INPUTS, 9); + use frame_support::traits::Get; assert_eq!( as ZkVerifierPort>::verification_weight(), ::WeightInfo::verify_proof(MAX_SPEND_PUBLIC_INPUTS) + .saturating_add( + <::DbWeight as Get< + frame_support::weights::RuntimeDbWeight, + >>::get() + .reads(1) + ) ); } } diff --git a/frame/zk-verifier/src/tests.rs b/frame/zk-verifier/src/tests.rs index 9b953624..a38162b8 100644 --- a/frame/zk-verifier/src/tests.rs +++ b/frame/zk-verifier/src/tests.rs @@ -7,8 +7,8 @@ use crate::{ new_test_ext, real_vk, }, pallet::{ - ActiveCircuitVersion, Event, PreparedKeys, RetiredVersions, VerificationKeys, - VerificationStats, VkHashes, + ActiveCircuitVersion, DeniedVkHashes, Event, PreparedKeys, RetiredVersions, + VerificationKeys, VerificationStats, VkHashes, }, types::VkEntry, }; @@ -1496,7 +1496,7 @@ fn genesis_registers_vk_at_version_1_and_activates_it() { CircuitId::TRANSFER, real_vk(TRANSFER_PUBLIC_INPUTS + MEMO_HASH_INPUTS).into_inner(), )], - _phantom: Default::default(), + ..Default::default() } .build_storage() .unwrap(); @@ -1530,7 +1530,7 @@ fn genesis_multiple_circuits_are_all_registered() { real_vk(UNSHIELD_PUBLIC_INPUTS + MEMO_HASH_INPUTS).into_inner(), ), ], - _phantom: Default::default(), + ..Default::default() } .build_storage() .unwrap(); @@ -1565,7 +1565,7 @@ fn genesis_multiple_circuits_are_all_registered() { fn genesis_rejects_a_key_that_only_passes_the_length_check() { let _ = pallet::GenesisConfig:: { verification_keys: vec![(CircuitId::TRANSFER, vec![0xCCu8; 300])], - _phantom: Default::default(), + ..Default::default() } .build_storage(); } @@ -1580,7 +1580,7 @@ fn genesis_rejects_a_valid_key_with_the_wrong_arity() { CircuitId::TRANSFER, real_vk(TRANSFER_PUBLIC_INPUTS + 3).into_inner(), )], - _phantom: Default::default(), + ..Default::default() } .build_storage(); } @@ -1589,7 +1589,7 @@ fn genesis_rejects_a_valid_key_with_the_wrong_arity() { fn genesis_with(circuit_id: CircuitId, arity: usize) { let _ = pallet::GenesisConfig:: { verification_keys: vec![(circuit_id, real_vk(arity).into_inner())], - _phantom: Default::default(), + ..Default::default() } .build_storage(); } @@ -1928,3 +1928,262 @@ mod migration_to_v2 { }); } } + +// ── Denied keys ─────────────────────────────────────────────────────────────── + +/// Store `key` for `(circuit_id, version)` with its hash, as a registration would. +fn insert_with_hash(circuit_id: CircuitId, version: u32, key: VkBytes) { + crate::mock::insert_key(circuit_id, version, key.clone()); + VkHashes::::insert(circuit_id, version, sp_io::hashing::blake2_256(&key)); +} + +fn deny_hash_of(key: &VkBytes) { + DeniedVkHashes::::insert(sp_io::hashing::blake2_256(key), ()); +} + +#[test] +fn deny_version_retires_and_records_the_hash() { + new_test_ext().execute_with(|| { + let key = crate::mock::denied_vk(); + insert_with_hash(CircuitId::UNSHIELD, 2, key.clone()); + insert_vk( + CircuitId::UNSHIELD, + 3, + UNSHIELD_PUBLIC_INPUTS + MEMO_HASH_INPUTS, + ); + activate(CircuitId::UNSHIELD, 3); + + assert_ok!(ZkVerifier::deny_version( + root().into(), + CircuitId::UNSHIELD, + 2 + )); + let vk_hash = sp_io::hashing::blake2_256(&key); + assert!(DeniedVkHashes::::contains_key(vk_hash)); + assert!(RetiredVersions::::contains_key( + CircuitId::UNSHIELD, + 2 + )); + frame_system::Pallet::::assert_last_event( + Event::VerificationKeyDenied { + circuit_id: CircuitId::UNSHIELD, + version: 2, + vk_hash, + } + .into(), + ); + }); +} + +#[test] +fn deny_version_requires_root() { + new_test_ext().execute_with(|| { + insert_with_hash(CircuitId::UNSHIELD, 2, crate::mock::denied_vk()); + assert_noop!( + ZkVerifier::deny_version(signed().into(), CircuitId::UNSHIELD, 2), + sp_runtime::DispatchError::BadOrigin + ); + }); +} + +#[test] +fn deny_version_refuses_the_active_version_and_a_missing_one() { + new_test_ext().execute_with(|| { + insert_with_hash(CircuitId::UNSHIELD, 3, crate::mock::denied_vk()); + activate(CircuitId::UNSHIELD, 3); + assert_noop!( + ZkVerifier::deny_version(root().into(), CircuitId::UNSHIELD, 3), + Error::::CannotRetireActiveVersion + ); + assert_noop!( + ZkVerifier::deny_version(root().into(), CircuitId::UNSHIELD, 9), + Error::::VerificationKeyNotFound + ); + }); +} + +#[test] +fn a_denied_key_cannot_be_denied_twice() { + new_test_ext().execute_with(|| { + insert_with_hash(CircuitId::UNSHIELD, 2, crate::mock::denied_vk()); + insert_with_hash(CircuitId::UNSHIELD, 4, crate::mock::denied_vk()); + assert_ok!(ZkVerifier::deny_version( + root().into(), + CircuitId::UNSHIELD, + 2 + )); + // The same key under another version is the same record. + assert_noop!( + ZkVerifier::deny_version(root().into(), CircuitId::UNSHIELD, 4), + Error::::KeyAlreadyDenied + ); + }); +} + +#[test] +fn register_refuses_a_denied_key() { + new_test_ext().execute_with(|| { + deny_hash_of(&crate::mock::denied_vk()); + assert_noop!( + register(CircuitId::UNSHIELD, 2, crate::mock::denied_vk()), + Error::::DeniedVerificationKey + ); + // Under any id or version: the hash decides, not the slot. + assert_noop!( + register(CircuitId::UNSHIELD, 9, crate::mock::denied_vk()), + Error::::DeniedVerificationKey + ); + }); +} + +#[test] +fn batch_register_refuses_a_denied_key() { + new_test_ext().execute_with(|| { + deny_hash_of(&crate::mock::denied_vk()); + let entries: BoundedVec> = vec![VkEntry { + circuit_id: CircuitId::UNSHIELD, + version: 2, + verification_key: crate::mock::denied_vk(), + set_active: true, + }] + .try_into() + .unwrap(); + assert_noop!( + ZkVerifier::batch_register_verification_keys(root().into(), entries), + Error::::DeniedVerificationKey + ); + }); +} + +#[test] +fn unretire_refuses_a_denied_key() { + new_test_ext().execute_with(|| { + insert_with_hash(CircuitId::UNSHIELD, 2, crate::mock::denied_vk()); + insert_vk( + CircuitId::UNSHIELD, + 3, + UNSHIELD_PUBLIC_INPUTS + MEMO_HASH_INPUTS, + ); + activate(CircuitId::UNSHIELD, 3); + assert_ok!(ZkVerifier::deny_version( + root().into(), + CircuitId::UNSHIELD, + 2 + )); + assert_noop!( + ZkVerifier::unretire_version(root().into(), CircuitId::UNSHIELD, 2), + Error::::DeniedVerificationKey + ); + }); +} + +#[test] +fn set_active_refuses_a_denied_key() { + new_test_ext().execute_with(|| { + insert_with_hash(CircuitId::UNSHIELD, 2, crate::mock::denied_vk()); + deny_hash_of(&crate::mock::denied_vk()); + assert_noop!( + ZkVerifier::set_active_version(root().into(), CircuitId::UNSHIELD, 2), + Error::::DeniedVerificationKey + ); + }); +} + +/// The record outlives the key: removing the version, or purging, does not let the +/// same key back in. +#[test] +fn the_denial_survives_removing_the_version() { + new_test_ext().execute_with(|| { + let key = crate::mock::denied_vk(); + insert_with_hash(CircuitId::UNSHIELD, 2, key.clone()); + insert_vk( + CircuitId::UNSHIELD, + 3, + UNSHIELD_PUBLIC_INPUTS + MEMO_HASH_INPUTS, + ); + activate(CircuitId::UNSHIELD, 3); + assert_ok!(ZkVerifier::deny_version( + root().into(), + CircuitId::UNSHIELD, + 2 + )); + assert_ok!(ZkVerifier::remove_verification_key( + root().into(), + CircuitId::UNSHIELD, + 2 + )); + assert!(DeniedVkHashes::::contains_key( + sp_io::hashing::blake2_256(&key) + )); + assert_noop!( + register(CircuitId::UNSHIELD, 2, key), + Error::::DeniedVerificationKey + ); + }); +} + +/// A denied key already in storage verifies nothing, even active and never retired. +#[test] +fn a_stored_denied_key_verifies_nothing() { + use crate::port::ZkVerifierPort; + new_test_ext().execute_with(|| { + insert_with_hash(CircuitId::UNSHIELD, 2, crate::mock::denied_vk()); + deny_hash_of(&crate::mock::denied_vk()); + activate(CircuitId::UNSHIELD, 2); + assert!(!::is_supported_version( + CircuitId::UNSHIELD.0, + 2 + )); + assert_err!( + ZkVerifier::verify_proof( + signed().into(), + CircuitId::UNSHIELD, + proof_bytes(), + one_public_input() + ), + Error::::UnsupportedCircuitVersion + ); + + // The same slot with an admitted key is supported: the hash decides. + insert_with_hash( + CircuitId::UNSHIELD, + 2, + real_vk(UNSHIELD_PUBLIC_INPUTS + MEMO_HASH_INPUTS), + ); + assert!(::is_supported_version( + CircuitId::UNSHIELD.0, + 2 + )); + }); +} + +#[test] +fn genesis_records_denied_hashes_before_any_key() { + let key = crate::mock::denied_vk(); + let storage = crate::mock::RuntimeGenesisConfig { + zk_verifier: crate::GenesisConfig { + denied_vk_hashes: vec![sp_core::H256(sp_io::hashing::blake2_256(&key))], + ..Default::default() + }, + ..Default::default() + } + .build_storage() + .unwrap(); + TestExternalities::new(storage).execute_with(|| { + frame_system::Pallet::::set_block_number(1); + assert_noop!( + register(CircuitId::UNSHIELD, 2, key), + Error::::DeniedVerificationKey + ); + }); +} + +/// A chain spec writes the hashes as `0x…` strings, like every other hash in it. +#[test] +fn genesis_denied_hashes_read_as_hex_strings() { + let config: crate::GenesisConfig = serde_json::from_str( + r#"{"verificationKeys":[],"deniedVkHashes":["0x8eb9dbb295ce10498fbe7088a4b0f327a675094d657170725d02770711634525"]}"#, + ) + .unwrap(); + assert_eq!(config.denied_vk_hashes[0].0[..4], [0x8e, 0xb9, 0xdb, 0xb2]); +} diff --git a/frame/zk-verifier/src/verifier.rs b/frame/zk-verifier/src/verifier.rs index de6b5f84..321a7aa2 100644 --- a/frame/zk-verifier/src/verifier.rs +++ b/frame/zk-verifier/src/verifier.rs @@ -8,7 +8,7 @@ use crate::{ Error, Pallet, keys::StoredKey, - pallet::{ActiveCircuitVersion, Config, RetiredVersions, VerificationStats}, + pallet::{ActiveCircuitVersion, Config, RetiredVersions, VerificationStats, VkHashes}, types::CircuitId, }; use alloc::vec::Vec; @@ -103,7 +103,9 @@ fn resolve_key( .or_else(|| ActiveCircuitVersion::::get(circuit_id)) .ok_or(Error::::CircuitNotFound)?; frame_support::ensure!( - !RetiredVersions::::contains_key(circuit_id, resolved), + !RetiredVersions::::contains_key(circuit_id, resolved) + && !VkHashes::::get(circuit_id, resolved) + .is_some_and(|h| Pallet::::is_denied(&h)), Error::::UnsupportedCircuitVersion ); let key = Pallet::::load_vk(circuit_id, resolved).ok_or(if version.is_some() { diff --git a/frame/zk-verifier/src/weights.rs b/frame/zk-verifier/src/weights.rs index 6d0d19b2..90194475 100644 --- a/frame/zk-verifier/src/weights.rs +++ b/frame/zk-verifier/src/weights.rs @@ -49,6 +49,7 @@ pub trait WeightInfo { fn unretire_version() -> Weight; fn purge_circuit(v: u32, ) -> Weight; fn batch_register_verification_keys(n: u32, ) -> Weight; + fn deny_version() -> Weight; } /// Weights for pallet_zk_verifier using the Substrate node and recommended hardware. @@ -260,6 +261,11 @@ impl WeightInfo for SubstrateWeight { .saturating_add(T::DbWeight::get().writes((3_u64).saturating_mul(n.into()))) .saturating_add(Weight::from_parts(0, 4950).saturating_mul(n.into())) } + /// Not benchmarked yet: `retire_version` plus the key read and the `DeniedVkHashes` + /// read and write. + fn deny_version() -> Weight { + Self::retire_version().saturating_add(T::DbWeight::get().reads_writes(2, 1)) + } } // For backwards compatibility and tests @@ -470,4 +476,9 @@ impl WeightInfo for () { .saturating_add(RocksDbWeight::get().writes((3_u64).saturating_mul(n.into()))) .saturating_add(Weight::from_parts(0, 4950).saturating_mul(n.into())) } + /// Not benchmarked yet: `retire_version` plus the key read and the `DeniedVkHashes` + /// read and write. + fn deny_version() -> Weight { + Self::retire_version().saturating_add(RocksDbWeight::get().reads_writes(2, 1)) + } } From a8a108ed4871ff10408c140fe5a94f9a9134ac31 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Wed, 21 Oct 2026 17:51:42 -0300 Subject: [PATCH 2/5] =?UTF-8?q?fix(zk-verifier)!:=20refuse=20a=20verifying?= =?UTF-8?q?=20key=20whose=20delta=20is=20=C2=B1gamma=20or=20=C2=B1G2=20(a?= =?UTF-8?q?=20snarkjs=20key=20without=20phase=202),=20with=20which=20anyon?= =?UTF-8?q?e=20could=20forge=20proofs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frame/zk-verifier/CHANGELOG.md | 3 + frame/zk-verifier/src/benchmarking.rs | 4 +- frame/zk-verifier/src/mock.rs | 6 +- frame/zk-verifier/src/tests.rs | 34 ++++++++++- primitives/zk-verifier/CHANGELOG.md | 10 ++++ primitives/zk-verifier/src/prepared.rs | 4 +- primitives/zk-verifier/src/types.rs | 81 +++++++++++++++++++++++++- primitives/zk-verifier/src/verifier.rs | 4 +- 8 files changed, 132 insertions(+), 14 deletions(-) diff --git a/frame/zk-verifier/CHANGELOG.md b/frame/zk-verifier/CHANGELOG.md index b02a95c7..b9c0c454 100644 --- a/frame/zk-verifier/CHANGELOG.md +++ b/frame/zk-verifier/CHANGELOG.md @@ -26,6 +26,9 @@ All notable changes to this pallet are documented here. - `ZkVerifierPort::verification_weight` adds one storage read, the `VkHashes` lookup of the denied-key check. - `WeightInfo::deny_version`: estimated from `retire_version` until benchmarked. +- Registration refuses a key without an independent `delta` (`delta = ±gamma` or + `± G2`, a phase-2-less snarkjs key), with which anyone could forge proofs + (`InvalidVerificationKey`, through `orbinum-zk-verifier`). ## [0.18.1] - 2026-10-21 diff --git a/frame/zk-verifier/src/benchmarking.rs b/frame/zk-verifier/src/benchmarking.rs index 2cffdeaf..466ffced 100644 --- a/frame/zk-verifier/src/benchmarking.rs +++ b/frame/zk-verifier/src/benchmarking.rs @@ -41,7 +41,7 @@ mod benchmarks { /// relies on `skip-proof-verification` to record weight despite the failed pairing. fn synthetic_vk(arity: usize) -> Vec { use ark_bn254::{Bn254, G1Affine, G2Affine}; - use ark_ec::AffineRepr; + use ark_ec::{AffineRepr, CurveGroup}; use ark_groth16::VerifyingKey as ArkVk; use orbinum_zk_verifier::VerifyingKey; @@ -49,7 +49,7 @@ mod benchmarks { alpha_g1: G1Affine::generator(), beta_g2: G2Affine::generator(), gamma_g2: G2Affine::generator(), - delta_g2: G2Affine::generator(), + delta_g2: (G2Affine::generator() + G2Affine::generator()).into_affine(), gamma_abc_g1: (0..=arity).map(|_| G1Affine::generator()).collect(), }; VerifyingKey::from_ark_vk(&vk) diff --git a/frame/zk-verifier/src/mock.rs b/frame/zk-verifier/src/mock.rs index 7eeadf8e..c88e54b6 100644 --- a/frame/zk-verifier/src/mock.rs +++ b/frame/zk-verifier/src/mock.rs @@ -48,14 +48,14 @@ pub fn new_test_ext() -> sp_io::TestExternalities { /// like a real one; tests never check a pairing (`do_verify` is stubbed). pub fn real_vk(arity: usize) -> VkBytes { use ark_bn254::{Bn254, G1Affine, G2Affine}; - use ark_ec::AffineRepr; + use ark_ec::{AffineRepr, CurveGroup}; use ark_groth16::VerifyingKey as ArkVk; let vk = ArkVk:: { alpha_g1: G1Affine::generator(), beta_g2: G2Affine::generator(), gamma_g2: G2Affine::generator(), - delta_g2: G2Affine::generator(), + delta_g2: (G2Affine::generator() + G2Affine::generator()).into_affine(), gamma_abc_g1: (0..=arity).map(|_| G1Affine::generator()).collect(), }; orbinum_zk_verifier::VerifyingKey::from_ark_vk(&vk) @@ -76,7 +76,7 @@ pub fn denied_vk_of(arity: usize) -> VkBytes { .into_affine(), beta_g2: G2Affine::generator(), gamma_g2: G2Affine::generator(), - delta_g2: G2Affine::generator(), + delta_g2: (G2Affine::generator() + G2Affine::generator()).into_affine(), gamma_abc_g1: (0..=arity).map(|_| G1Affine::generator()).collect(), }; orbinum_zk_verifier::VerifyingKey::from_ark_vk(&vk) diff --git a/frame/zk-verifier/src/tests.rs b/frame/zk-verifier/src/tests.rs index a38162b8..d05c99b4 100644 --- a/frame/zk-verifier/src/tests.rs +++ b/frame/zk-verifier/src/tests.rs @@ -239,7 +239,7 @@ fn register_vk_accepts_a_memo_bound_rotation_at_the_same_arity() { #[test] fn register_vk_rejects_a_point_at_infinity() { use ark_bn254::{Bn254, G1Affine, G2Affine}; - use ark_ec::AffineRepr; + use ark_ec::{AffineRepr, CurveGroup}; new_test_ext().execute_with(|| { let mut abc: Vec = (0..=TRANSFER_PUBLIC_INPUTS) .map(|_| G1Affine::generator()) @@ -249,7 +249,7 @@ fn register_vk_rejects_a_point_at_infinity() { alpha_g1: G1Affine::generator(), beta_g2: G2Affine::generator(), gamma_g2: G2Affine::generator(), - delta_g2: G2Affine::generator(), + delta_g2: (G2Affine::generator() + G2Affine::generator()).into_affine(), gamma_abc_g1: abc, }; let bytes = orbinum_zk_verifier::VerifyingKey::from_ark_vk(&vk) @@ -262,6 +262,34 @@ fn register_vk_rejects_a_point_at_infinity() { }); } +/// A key whose `delta` is `gamma` (snarkjs without phase 2) lets anyone forge proofs. +#[test] +fn register_vk_rejects_a_key_without_phase_2() { + use ark_bn254::{Bn254, G1Affine, G2Affine}; + use ark_ec::AffineRepr; + new_test_ext().execute_with(|| { + let vk = ark_groth16::VerifyingKey:: { + alpha_g1: G1Affine::generator(), + beta_g2: G2Affine::generator(), + gamma_g2: G2Affine::generator(), + delta_g2: G2Affine::generator(), + gamma_abc_g1: vec![ + G1Affine::generator(); + TRANSFER_PUBLIC_INPUTS + MEMO_HASH_INPUTS + 1 + ], + }; + let bytes = orbinum_zk_verifier::VerifyingKey::from_ark_vk(&vk) + .unwrap() + .bytes; + assert_noop!( + register(CircuitId::TRANSFER, 2, bytes.try_into().unwrap()), + Error::::InvalidVerificationKey + ); + // The same key with an independent delta registers. + assert_ok!(register(CircuitId::TRANSFER, 2, vk_bytes())); + }); +} + /// Shield has no memo-bound layout: every version takes the base arity, and one /// input more — memo-bound for a spend circuit — is refused. #[test] @@ -1671,7 +1699,7 @@ fn other_transfer_key() -> VkBytes { alpha_g1: (g + g).into_affine(), beta_g2: G2Affine::generator(), gamma_g2: G2Affine::generator(), - delta_g2: G2Affine::generator(), + delta_g2: (G2Affine::generator() + G2Affine::generator()).into_affine(), gamma_abc_g1: vec![g; TRANSFER_PUBLIC_INPUTS + MEMO_HASH_INPUTS + 1], }; orbinum_zk_verifier::VerifyingKey::from_ark_vk(&vk) diff --git a/primitives/zk-verifier/CHANGELOG.md b/primitives/zk-verifier/CHANGELOG.md index 765f058f..19551325 100644 --- a/primitives/zk-verifier/CHANGELOG.md +++ b/primitives/zk-verifier/CHANGELOG.md @@ -6,6 +6,16 @@ All notable changes to this crate are documented here. ## [Unreleased] +### Changed + +- `VerifyingKey::to_ark_vk` refuses a key whose `delta_g2` is `±gamma_g2` or `±` the G2 + generator — what snarkjs leaves when a circuit's phase 2 never ran. With such a + `delta` the pairing check reduces to `e(A,B) = e(α,β)·e(L(x) ± C, γ)`, which + `(α, β, ∓L(x))` satisfies for any statement from public key data alone. Every + path that validates a raw key (registration, `prepared_bytes`, `Groth16Verifier`) + refuses it with `InvalidVerifyingKey`. Keys already stored in prepared form are not + re-checked. + ## [3.3.1] - 2026-10-21 ### Changed diff --git a/primitives/zk-verifier/src/prepared.rs b/primitives/zk-verifier/src/prepared.rs index a7da5bb2..563b8750 100644 --- a/primitives/zk-verifier/src/prepared.rs +++ b/primitives/zk-verifier/src/prepared.rs @@ -155,7 +155,7 @@ mod tests { use super::{layout::*, *}; use alloc::vec; use ark_bn254::{G1Affine, G2Affine}; - use ark_ec::AffineRepr; + use ark_ec::{AffineRepr, CurveGroup}; use ark_groth16::VerifyingKey as ArkVK; /// A genuine key of `arity` inputs, as registration stores it. @@ -164,7 +164,7 @@ mod tests { alpha_g1: G1Affine::generator(), beta_g2: G2Affine::generator(), gamma_g2: G2Affine::generator(), - delta_g2: G2Affine::generator(), + delta_g2: (G2Affine::generator() + G2Affine::generator()).into_affine(), gamma_abc_g1: (0..=arity).map(|_| G1Affine::generator()).collect(), }; VerifyingKey::from_ark_vk(&vk).expect("serializes") diff --git a/primitives/zk-verifier/src/types.rs b/primitives/zk-verifier/src/types.rs index d04e69bf..b1433768 100644 --- a/primitives/zk-verifier/src/types.rs +++ b/primitives/zk-verifier/src/types.rs @@ -147,6 +147,11 @@ impl VerifyingKey { /// declaring 2^25 points would ask for gigabytes. Points at infinity are /// refused too — an identity `gamma_abc` entry would leave its public input /// out of the verification equation entirely. + /// + /// So is a `delta` that is `±gamma` or `±` the G2 generator — what snarkjs leaves + /// when the circuit's phase 2 never ran. With `delta = ±gamma` the check reduces to + /// `e(A,B) = e(α,β)·e(L(x) ± C, γ)`, which `(α, β, ∓L(x))` satisfies for any + /// statement from public data alone. pub fn to_ark_vk(&self) -> Result, VerifierError> { use ark_ec::AffineRepr; // alpha (G1) + beta, gamma, delta (G2), compressed; then the u64 length @@ -179,6 +184,11 @@ impl VerifyingKey { if identity { return Err(VerifierError::InvalidVerifyingKey); } + let g2 = ark_bn254::G2Affine::generator(); + let degenerate = [vk.gamma_g2, -vk.gamma_g2, g2, -g2].contains(&vk.delta_g2); + if degenerate { + return Err(VerifierError::InvalidVerifyingKey); + } Ok(vk) } @@ -287,12 +297,12 @@ mod tests { /// the guard exists. fn vk_bytes(arity: usize, tweak: impl FnOnce(&mut ArkVK)) -> Vec { use ark_bn254::{G1Affine, G2Affine}; - use ark_ec::AffineRepr; + use ark_ec::{AffineRepr, CurveGroup}; let mut vk = ArkVK:: { alpha_g1: G1Affine::generator(), beta_g2: G2Affine::generator(), gamma_g2: G2Affine::generator(), - delta_g2: G2Affine::generator(), + delta_g2: (G2Affine::generator() + G2Affine::generator()).into_affine(), gamma_abc_g1: (0..=arity).map(|_| G1Affine::generator()).collect(), }; tweak(&mut vk); @@ -446,6 +456,73 @@ mod tests { assert!(VerifyingKey::new(zero_delta).to_ark_vk().is_err()); } + /// A `delta` tied to `gamma` (or the generator, snarkjs's phase-2-less default). + #[test] + fn to_ark_vk_refuses_a_delta_tied_to_gamma() { + use ark_bn254::G2Affine; + use ark_ec::{AffineRepr, CurveGroup}; + let g2 = G2Affine::generator(); + let three = (g2 + g2 + g2).into_affine(); + for (name, key) in [ + ("delta = gamma", vk_bytes(3, |vk| vk.delta_g2 = vk.gamma_g2)), + ( + "delta = -gamma", + vk_bytes(3, |vk| vk.delta_g2 = -vk.gamma_g2), + ), + ("delta = G2", vk_bytes(3, |vk| vk.delta_g2 = g2)), + ("delta = -G2", vk_bytes(3, |vk| vk.delta_g2 = -g2)), + // gamma moved off the generator: still refused when delta follows it. + ( + "delta = gamma = 3·G2", + vk_bytes(3, |vk| { + vk.gamma_g2 = three; + vk.delta_g2 = three; + }), + ), + ] { + assert!( + VerifyingKey::new(key).to_ark_vk().is_err(), + "{name} accepted" + ); + } + assert!(VerifyingKey::new(well_formed_vk(3)).to_ark_vk().is_ok()); + } + + /// With `delta = gamma`, `(A, B, C) = (α, β, −L(x))` satisfies the pairing check for + /// any statement. arkworks accepts it; the key is refused before it gets there. + #[test] + fn a_phase2_less_key_cannot_forge_a_proof() { + use ark_bn254::{G1Projective, G2Affine}; + use ark_ec::{AffineRepr, CurveGroup}; + use ark_groth16::{prepare_verifying_key, Groth16}; + + let g2 = G2Affine::generator(); + let key = vk_bytes(3, |vk| vk.delta_g2 = g2); + let vk = ArkVK::::deserialize_compressed(&key[..]).unwrap(); + let inputs: Vec = (1..=3u64).map(Bn254Fr::from).collect(); + let mut l = G1Projective::from(vk.gamma_abc_g1[0]); + for (x, p) in inputs.iter().zip(vk.gamma_abc_g1.iter().skip(1)) { + l += *p * x; + } + let forged = ArkProof:: { + a: vk.alpha_g1, + b: vk.beta_g2, + c: (-l).into_affine(), + }; + assert!( + Groth16::::verify_proof(&prepare_verifying_key(&vk), &forged, &inputs).unwrap(), + "the forgery must hold against the bare key, or this test proves nothing" + ); + + let proof = Proof::from_ark_proof(&forged).unwrap(); + let public = PublicInputs::from_field_elements(&inputs); + assert!( + crate::Groth16Verifier::verify(&VerifyingKey::new(key.clone()), &public, &proof) + .is_err() + ); + assert!(VerifyingKey::new(key).prepared_bytes().is_err()); + } + #[test] fn test_vk_prepare_invalid() { assert!(VerifyingKey::new(vec![0u8; 10]).prepare().is_err()); diff --git a/primitives/zk-verifier/src/verifier.rs b/primitives/zk-verifier/src/verifier.rs index d05ba556..4b8e380b 100644 --- a/primitives/zk-verifier/src/verifier.rs +++ b/primitives/zk-verifier/src/verifier.rs @@ -58,7 +58,7 @@ mod tests { use super::*; use crate::{BASE_VERIFICATION_COST, PER_INPUT_COST}; use ark_bn254::{Bn254, Fr as Bn254Fr, G1Affine, G2Affine}; - use ark_ec::AffineRepr; + use ark_ec::{AffineRepr, CurveGroup}; use ark_ff::{BigInteger, PrimeField}; use ark_groth16::VerifyingKey as ArkVerifyingKey; use ark_serialize::CanonicalSerialize; @@ -92,7 +92,7 @@ mod tests { alpha_g1: G1Affine::generator(), beta_g2: G2Affine::generator(), gamma_g2: G2Affine::generator(), - delta_g2: G2Affine::generator(), + delta_g2: (G2Affine::generator() + G2Affine::generator()).into_affine(), gamma_abc_g1: (0..=expected_inputs) .map(|_| G1Affine::generator()) .collect(), From b4f63798e3e29330a2aa76297391274a64ffc3a9 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Wed, 21 Oct 2026 18:15:04 -0300 Subject: [PATCH 3/5] =?UTF-8?q?fix(zk-verifier)!:=20a=20removed=20version?= =?UTF-8?q?=20keeps=20its=20key's=20hash=20and=20can=20only=20take=20that?= =?UTF-8?q?=20key=20again=20=E2=80=94=20a=20client=20that=20cached=20what?= =?UTF-8?q?=20it=20means=20is=20never=20served=20another=20key=20under=20i?= =?UTF-8?q?t?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frame/zk-verifier/CHANGELOG.md | 7 ++++ frame/zk-verifier/src/lib.rs | 34 +++++++++++++-- frame/zk-verifier/src/tests.rs | 77 ++++++++++++++++++++++++++++------ 3 files changed, 103 insertions(+), 15 deletions(-) diff --git a/frame/zk-verifier/CHANGELOG.md b/frame/zk-verifier/CHANGELOG.md index b9c0c454..7d397a11 100644 --- a/frame/zk-verifier/CHANGELOG.md +++ b/frame/zk-verifier/CHANGELOG.md @@ -20,12 +20,19 @@ All notable changes to this pallet are documented here. already recorded is refused (`KeyAlreadyDenied`). - `GenesisConfig::denied_vk_hashes`: the record a new chain starts with, written before any genesis key. +- `RemovedVkHashes`: `remove_verification_key` records the hash of the key the version + held, and that version can then only take the same key again (`VersionReused` + otherwise, also in `batch_register_verification_keys`). Before, removing a version + and registering it with another key changed its meaning for any client that cached + it. Append-only: `purge_circuit` does not clear it. ### Changed - `ZkVerifierPort::verification_weight` adds one storage read, the `VkHashes` lookup of the denied-key check. - `WeightInfo::deny_version`: estimated from `retire_version` until benchmarked. +- `register_verification_key` and `batch_register_verification_keys` add one read per + key (`RemovedVkHashes`), `remove_verification_key` one read and one write. - Registration refuses a key without an independent `delta` (`delta = ±gamma` or `± G2`, a phase-2-less snarkjs key), with which anyone could forge proofs (`InvalidVerificationKey`, through `orbinum-zk-verifier`). diff --git a/frame/zk-verifier/src/lib.rs b/frame/zk-verifier/src/lib.rs index 9f6d2d59..586650af 100644 --- a/frame/zk-verifier/src/lib.rs +++ b/frame/zk-verifier/src/lib.rs @@ -153,6 +153,20 @@ pub mod pallet { #[pallet::storage] pub type DeniedVkHashes = StorageMap<_, Identity, [u8; 32], (), OptionQuery>; + /// `blake2_256` of the key a removed version held. That version can hold only the + /// same key again, so a client that cached what it means is never served another + /// key under it. Append-only: nothing clears it, `purge_circuit` included. + #[pallet::storage] + pub type RemovedVkHashes = StorageDoubleMap< + _, + Blake2_128Concat, + CircuitId, + Blake2_128Concat, + u32, + [u8; 32], + OptionQuery, + >; + // ── Genesis ─────────────────────────────────────────────────────────────── #[pallet::genesis_config] @@ -282,6 +296,8 @@ pub mod pallet { DeniedVerificationKey, /// `deny_version`: the key's hash is already denied. KeyAlreadyDenied, + /// The version was removed while holding another key; it can hold only that one. + VersionReused, } // ── Extrinsics ──────────────────────────────────────────────────────────── @@ -302,7 +318,8 @@ pub mod pallet { /// semantics: any other change takes a new circuit id, and a superseded /// version is retired with `retire_version`. #[pallet::call_index(0)] - #[pallet::weight(T::WeightInfo::register_verification_key())] + #[pallet::weight(T::WeightInfo::register_verification_key() + .saturating_add(T::DbWeight::get().reads(1)))] pub fn register_verification_key( origin: OriginFor, circuit_id: CircuitId, @@ -340,7 +357,8 @@ pub mod pallet { /// Remove a version and all its data (Root only); never the active one. #[pallet::call_index(2)] - #[pallet::weight(T::WeightInfo::remove_verification_key())] + #[pallet::weight(T::WeightInfo::remove_verification_key() + .saturating_add(T::DbWeight::get().reads_writes(1, 1)))] pub fn remove_verification_key( origin: OriginFor, circuit_id: CircuitId, @@ -356,6 +374,9 @@ pub mod pallet { .ok_or(Error::::ActiveVersionNotSet)?; ensure!(active != version, Error::::CannotRemoveActiveVersion); + if let Some(hash) = VkHashes::::get(circuit_id, version) { + RemovedVkHashes::::insert(circuit_id, version, hash); + } Self::remove_vk(circuit_id, version); Self::deposit_event(Event::VerificationKeyRemoved { circuit_id, @@ -408,7 +429,8 @@ pub mod pallet { /// Atomically register (and optionally activate) up to 10 VKs (Root only). #[pallet::call_index(4)] - #[pallet::weight(T::WeightInfo::batch_register_verification_keys(entries.len() as u32))] + #[pallet::weight(T::WeightInfo::batch_register_verification_keys(entries.len() as u32) + .saturating_add(T::DbWeight::get().reads(entries.len() as u64)))] pub fn batch_register_verification_keys( origin: OriginFor, entries: BoundedVec>, @@ -611,6 +633,12 @@ pub mod pallet { !VerificationKeys::::contains_key(circuit_id, version), Error::::CircuitAlreadyExists ); + // Nor reuse a removed version for another key. + ensure!( + RemovedVkHashes::::get(circuit_id, version) + .is_none_or(|h| h == sp_io::hashing::blake2_256(&key_data)), + Error::::VersionReused + ); Self::store_vk(circuit_id, version, key_data)?; Self::deposit_event(Event::VerificationKeyRegistered { circuit_id, diff --git a/frame/zk-verifier/src/tests.rs b/frame/zk-verifier/src/tests.rs index d05c99b4..26229e48 100644 --- a/frame/zk-verifier/src/tests.rs +++ b/frame/zk-verifier/src/tests.rs @@ -7,8 +7,8 @@ use crate::{ new_test_ext, real_vk, }, pallet::{ - ActiveCircuitVersion, DeniedVkHashes, Event, PreparedKeys, RetiredVersions, - VerificationKeys, VerificationStats, VkHashes, + ActiveCircuitVersion, DeniedVkHashes, Event, PreparedKeys, RemovedVkHashes, + RetiredVersions, VerificationKeys, VerificationStats, VkHashes, }, types::VkEntry, }; @@ -1717,25 +1717,78 @@ fn entries_of(circuit_id: CircuitId) -> usize { + usize::from(ActiveCircuitVersion::::contains_key(circuit_id)) } -/// A version removed and registered again with another key verifies under the -/// new key's prepared form, never a stale one. +/// Register `first` as transfer v2 next to an active v3, then remove v2. +fn remove_transfer_v2(first: VkBytes) { + assert_ok!(register(CircuitId::TRANSFER, 2, first)); + assert_ok!(register(CircuitId::TRANSFER, 3, memo_bound_transfer_key())); + activate(CircuitId::TRANSFER, 3); + assert_ok!(ZkVerifier::remove_verification_key( + root().into(), + CircuitId::TRANSFER, + 2 + )); +} + +/// A removed version cannot take another key: a client that cached what v2 means +/// would be served a different circuit under the same number. #[test] -fn a_reregistered_version_gets_the_new_keys_prepared_form() { +fn a_removed_version_cannot_hold_another_key() { new_test_ext().execute_with(|| { let (first, second) = (memo_bound_transfer_key(), other_transfer_key()); assert_ne!(first, second); - assert_ok!(register(CircuitId::TRANSFER, 2, first)); - assert_ok!(register(CircuitId::TRANSFER, 3, memo_bound_transfer_key())); - activate(CircuitId::TRANSFER, 3); + remove_transfer_v2(first.clone()); + assert_eq!( + RemovedVkHashes::::get(CircuitId::TRANSFER, 2), + Some(sp_io::hashing::blake2_256(&first)) + ); + assert_noop!( + register(CircuitId::TRANSFER, 2, second.clone()), + Error::::VersionReused + ); + // Another version number is free. + assert_ok!(register(CircuitId::TRANSFER, 4, second)); + }); +} + +/// The same key may come back under its old number, with a fresh prepared form. +#[test] +fn a_removed_version_takes_its_own_key_back() { + new_test_ext().execute_with(|| { + let first = memo_bound_transfer_key(); + remove_transfer_v2(first.clone()); + assert_ok!(register(CircuitId::TRANSFER, 2, first.clone())); + assert_eq!( + PreparedKeys::::get(CircuitId::TRANSFER, 2), + Some(ZkVerifier::prepare_vk(&first).unwrap()) + ); + // Removed again, it still remembers the same key. assert_ok!(ZkVerifier::remove_verification_key( root().into(), CircuitId::TRANSFER, 2 )); - assert_ok!(register(CircuitId::TRANSFER, 2, second.clone())); - assert_eq!( - PreparedKeys::::get(CircuitId::TRANSFER, 2), - Some(ZkVerifier::prepare_vk(&second).unwrap()) + assert_noop!( + register(CircuitId::TRANSFER, 2, other_transfer_key()), + Error::::VersionReused + ); + }); +} + +#[test] +fn batch_register_refuses_a_reused_version() { + new_test_ext().execute_with(|| { + remove_transfer_v2(memo_bound_transfer_key()); + let entries: BoundedVec> = vec![VkEntry { + circuit_id: CircuitId::TRANSFER, + version: 2, + verification_key: other_transfer_key(), + set_active: false, + }] + .try_into() + .unwrap(); + assert_noop!( + ZkVerifier::batch_register_verification_keys(root().into(), entries), + Error::::VersionReused ); }); } From 86943c9be4f3b78d214d2c045171645027c05336 Mon Sep 17 00:00:00 2001 From: nol4lej Date: Wed, 21 Oct 2026 18:26:02 -0300 Subject: [PATCH 4/5] =?UTF-8?q?test(zk-verifier):=20pin=20the=20frozen=20b?= =?UTF-8?q?n254=5Fgroth16=5Fverify=20(v1)=20to=20golden=20vectors=20?= =?UTF-8?q?=E2=80=94=20a=20production=20key,=20its=20stored=20prepared=20f?= =?UTF-8?q?orm=20and=20a=20real=20proof,=20plus=20the=20refusals=20and=20c?= =?UTF-8?q?onstants=20v1=20depends=20on?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- primitives/zk-verifier/CHANGELOG.md | 10 + .../fixtures/host_v1_unshield_v3.prepared.hex | 1 + .../tests/fixtures/host_v1_unshield_v3.txt | 3 + .../zk-verifier/tests/host_function_v1.rs | 203 ++++++++++++++++++ 4 files changed, 217 insertions(+) create mode 100644 primitives/zk-verifier/tests/fixtures/host_v1_unshield_v3.prepared.hex create mode 100644 primitives/zk-verifier/tests/fixtures/host_v1_unshield_v3.txt create mode 100644 primitives/zk-verifier/tests/host_function_v1.rs diff --git a/primitives/zk-verifier/CHANGELOG.md b/primitives/zk-verifier/CHANGELOG.md index 19551325..84ebdf7e 100644 --- a/primitives/zk-verifier/CHANGELOG.md +++ b/primitives/zk-verifier/CHANGELOG.md @@ -6,6 +6,16 @@ All notable changes to this crate are documented here. ## [Unreleased] +### Added + +- Golden vectors for the frozen `bn254_groth16_verify` (version 1) + (`tests/host_function_v1.rs`): a production key (unshield v3), its prepared form as + stored on chain, and a real proof. They pin that the host function and + `verify_prepared` accept it, refuse tampered, non-canonical and malformed input, and + that preparing the key still gives the stored bytes, along with the constants + version 1 depends on. A failure means a change alters what version 1 answers: add a + `#[version(N)]` instead of updating the fixtures. + ### Changed - `VerifyingKey::to_ark_vk` refuses a key whose `delta_g2` is `±gamma_g2` or `±` the G2 diff --git a/primitives/zk-verifier/tests/fixtures/host_v1_unshield_v3.prepared.hex b/primitives/zk-verifier/tests/fixtures/host_v1_unshield_v3.prepared.hex new file mode 100644 index 00000000..fad7fd70 --- /dev/null +++ b/primitives/zk-verifier/tests/fixtures/host_v1_unshield_v3.prepared.hex @@ -0,0 +1 @@ +0xe2f26dbea299f5223b646cb1fb33eadb059d9407559d7441dfd902e3a79a4d2d26194d00ffca76f0010323190a8389ce45e39f2060ecd861b0ce373c50ddbe14abb73dc17fbc13021e2471e0c08bd67d8401f52b73d6d07483794cad4778180e0c06f33bbc4c79a9cadef253a68084d382f17788f885c9afd176f7cb2f036709c8ced07a54067fd5a905ea3ec6b796f892912f4dd2233131c7a857a4b1c13917a74623114d9aa69d370d7a6bc4defdaa3c8c3fd947e8f5994a708ae0d1fb4cb0edf692d95cbdde46ddda5ef7d422436779445c5e66006a42761e1f12efde0018c212f3aeb785e49712e7a9353349aaf1255dfb31b7bf60723a480d9293938e19aa7dfa6601cce64c7bd3430c69e7d1e38f40cb8d8071ab4aeb6d8cdba55ec8125b9722d1dcdaac55f38eb37033314bbc95330c69ad999eec75f05f58d0890609cc102854217a33c345729d6a9a2b47d1395afcc90dcac89781b3a21eb60f9e062bcea02635700239f739426700403fcb3e6bd3510e0ee3564fe2092c798e53239d23387e1e17993fbde04a299cfaff0c943069dfc92c6f6afe19894803d72014eccdcf12409523296f10500c71f8fffe0d038e663e667834a6c176eb9195460109000000000000007bdcfd16ff3e36485a7b3be77f01e7c58ce58bb067a48b315e432b9fba39ac22a2672b98b753c5f9b635bf17ee1f0127e078aa0d8573b809855837c5c628fb0222ac2412d3ffd196d309f9639192745b07b2f186e12402038cd0ba981ac5581cc21d01a8b20a5e70a60008a7e12f3cfa5235037b8e2425e0e5ec7cc6abd8a1a27dabcbfbc1285c5fd1933e8832698b1348db41f16c0636a22791e2b6b3796a2de85ff18f6c686f07bf07c785283281a7cfca7fddabf7c28783ee4671f3916006f61a01607e955c6417c4877ef2de7df33c3a0c6e0207c780fa9ec87de947fe1194326ae84894cb10ff1fe5cb82b397bf1cc13a9f1d43462c58fcd01248bcd7aa95120e87d825d3827b145b41bddcbcd7ffef75fb7c1d7b38e585410be4ab112c79de21bb832ae4b2181a380f2d1fa1ab4c6ec08f1332407e86e979889468da996a5e7f60390ddf3b826ac0d01bc5addf5b0b7882b1388d66465f657bf5e5381ad7b0937d6c3c2af76322be8352c2ac4ba9e9cf9a8dad2c334a5cf9492a6aada8557b509428fddbf54bc2bda2b450d4ff7b501f4305ec9f57c04189a25fb67f24726b53fbf10de692f9e01e9fc6238ceed9d04f84833919b8ec9a8e6115546317f48f23f8985dc86b5b936756e41e0694381a73ee558adafcc8307fe9a690a42d50c369bfd7a9979eda4136621bce229315dc2da1746b65d31f61071f77191b01943dd0b8ae18c48b6d55c36fef36cffcb40b890cee5d9211aaf0e3d09ba21a0d3b8e7ea0f31a02ba97652fa40f9fd6a909bde720318c59c9a47f800aa66468a00d14dc30b678357d988b3eb0e8ada11bc7b2b5d2cf0c1fe27522cb2a819b7c044a601bd9302a94a80677a9f72ebeaada131e9bfba30621c8f038b547beb9962e182089742c1d388436771390c6af9937729c39e6414746ee5636c4741d1f220df45c80a7cded5ad653bc4f8b201c94054918dee160e1dc90cea027d4ec69e01a6df0f8d739d7911aa63b6b6923f10cdce763de1046fe0f91d590f5f510397611c57ac6daac2f9222d8cc3130e57f99dcd2edecb3e1d11b860c0d9d64a5dda30f42b8a8e513c9d5983486332c3ecd1236192b988666c15818838559bc27b6a50f49fee88fd43ac88dc3e75419bfd451374e25b8c4845b4bfcbd460ad48bb55016e4e1edf293696a43b76c8a5feffe6cfddb59cd7bc0246c1784061ed3eff2280a74e56fcabc5b93d84d72e10a0ad78079b02d06cc8c3ae435936ce2e85722d82b9480a46f039988a80cf4ca669c026100f8ce3fbc5298180dec08d2055d7a621ad3cc4fdd99242a86d7c80528d3b438c4669288c56b77abb8367528a31f01c511570000000000000054fbf4cd0298cd99f6a68718d2cea3c71f81961b01e35695d6db18b74bbd9025b62e45a2b9b559abe61d67e1666296782b6718d25a333dd9ebe0bfb0a0130d127da146d10b1a8c024cd77adb7dcf2abfd65d15c25e4786de92f87ba87b436a2f5ab3b320d63f46b79611f5107326f37ebae67ff96f482a9fa8d0f18bddc42f0c5ef31ec21e29621cb0335adfbb38b4dd84fa2d9c9e71e1c29d3744f4ff6fdb2f32fa77e3c3232d5c375a8c6cd05432d7be10cf3c3c520f355d1f92054ba0280896a3dd28571611496ace8f852922a884d7393d0b27641e55c20005a368a05a1ecb5c05fa027874513f255d976134ab9c95c152523b8690acd235115c60a1911a15cf7239bcc35c28714e1a9650cad19782071329a6127d9a7974ee3bce916b104e6ac92a775152ec753ea724f9827194f053ee1ea877bee8c243e219e0e8ba0919b2a001f10764c82be4486fe0092f3c280e252550c4e5d8d2cba54cab4c53242c9cc5ce6961305aae2b2bf73240548103bc72fd51f0a781152ae882dbaf9801fcfc9d67840b7883616cc675df96f686d366815fe73f30fff77809b1e550c209633f4aed40112f5d9f267adbddb6d1a4eaf265fe6fb28daf92547c65ba90b314bdafaf36d07e87f945efe3dc5945286ce0c7fad2f39a0a291d7503277237cf23f73cf960b757391385146d4d647dba66e239d844b3d1240a6778bb0b56694c301e79a5e8178c4f769ff80279608e99f6d8168f8a4b63c604ff4414de31160f0d00dffea23723a52c205fe6b75e529f8945319f73f9401ef42c33307ee2ed672176bae3507716d40a4a8ff28da44c8d1a131a7a2c6f11f6467f0d646010bc2b1163d1ec13a24552894cd20caae2f6f7f77d944ae30a937990f281240f0288db21666745a9842a6e1e53a0f8d0ddd38a49db1799e41e4cf91dee98c71f303f6329e61b682318a121416db5891b62a477b937e89976410ad494a1db3ac8a552f419c92882a1c80061827c11d230370c231a1f329f6c565de6641427d50d6290a415d8f196e6c7ccf3738f72a33ab5d5e2d222e61753ef4d138af22b52e83fdfe91b4fe82154f63268cd054ed7107c06fbb800dd3b1ece8d625565a2dad7260dfd17d5e3fc06e1a76f8c7476014155f477dcde193508dd245b5030cb838e58fcbe0cad221905ba7c655ab214ea45db4a448c4c7532410ee49b38b8ce8e96d232fb01aa53fa8f9450de7b21471be18ac9564ce597d6dd06e063f39e619c94cbf0a609d65fd45f2a00ae46174db6a84f345cca7c7ce155e7ba34380b4e429c5c934926bfbf3459b0eab1254dc652c597fff9d4e465c8b9db1aff40e9344302f0f1db16c2f4ddd0e0df24e7acc02b0fc20f3bd8a8a59600d8d4653fe6c1576b5eb3690f0af08f47c6f4e1dc6bf326c2c85e3ee1f325b113bae686f4936748efc341c8043d98609feac2527672078e9ba1dc3fbf7e85571d341c281ec9d754369ad9902e3e877d0c7a01889ac63534019d5f9ac5bbe0973bb81a3693fa757aa717cb4e26cc0f5334d1f8cfbc9addce908ec7734d6e51f121fc91a3e790c007a5016d4b01eabb25a7538c3a6ee1a4c98a69022e9d0194f986681eb9c4c64988d6f758a91f99dc2b4fec0b0ac1ef5736cfe757fb46a8e0ebabaa39da7f0aa1048c80275a2050f15485c0c9383d4d1486aab016e2c3cc62c54fecd4d44559b49ec9b845302667960556d860ad50a894e531f59cd513218e8cc210f908b55806079f57343c01b6625ce3f537581b776ac985ea0a6a72086d2eb494a00a4764f1cfc797e5cf26b8fac7e5acb08b6bd8736d9d249baa9454bfe94277286efdbc8376147aa95501a76ff2b8c781b320546f24941d005a0868d7925aee8e6d4b88a34a2b8618d92268da649deb8fdf86e70cbf8850bd5d928ef9323ba49eaefda17f093e2ba29507090771e0b5c930c5d0fb9dd1175a973acb7ae7f9ec758fd7e702dc852564f307e994d0dc73f021320ffaa5edaf163c50c01a9b25f7cc9af45335b3b99b9b4d1de17eb39895a94e5656422a336aaa66bb6ef2870b8405d357edbc3967c583100d46036d7f1a5351bee152a769c2f39f377b19896522fa93ad75f611c6e893b90c586cee599d056b480e11be1cde5f5e4eba12c5b0c820bf34159c1480b9e7e32447480a657ac9b2b35366bd2784c2e0e3f63bf253a8d7b99894e86525b20aab264fc1b2c32d307ca0e043e40f8438b3ffbf4c1726eb41985236d8b4d50c40b52dda493b5900b0a2a186351075f1893448891db4b97c98db85f405245b2111c6063c31cc811c2d7f11abf9bc5caf9a66233cfada52990cc20fc1a8d044c2591318d2b638586736ce8b32785029f285babc514b85efff8a34da5f248f09f98c7b0b4ad746efb29494a371cd50df5e17ad8741211a148693027068a81a542cf264232d9811fc7d4cb14dbc6732548aa0d5c7dbdf6dfa016bf969169f6c47939ba92c84e13a8c00ba67ba5cb20ef60c59fbf83c62c19a11dcf00a34e66b4a6b0dea0cccbdd318dce3422348c669995cd8fc108cc3da34038dee8d720e51dc0f888d1074506e509df5964d98926847b7a0b575c15beff4794eaedd68d8f9fee8e4461eddd76c0a06e49e70279a19b4aabe22fb2e9781242d94343dc95d73cbb1eb481d76fb9d94ba72a7cc01e92e8667cdf0befa2c36521fca3615c8d798b3d05b812fbd3145532fa0eecb3171d1b6f5a91bad7f8b1c5fdfb7bf9a1d61752bd3d9f9185161c6b195730826242e0d5f4f912bfa85c33e2f878c050dde749baa74830f2239ca5e41be4551a8290f348611850d769a74f1d37d06fd8650856465d7a7681c69227d5ac23a87efa92a49e54e86d78fc66c1ab8adc6de9eb11c25bf5413451b787479e0c87289dfbaa5ef8f261f4ea61ed96bcbb2bc289a5f6ab1bce23cd416f6da76bb4bd48ce0457b57e7989a92c5df6ef8b8919ff905fbbd5f5e87769e294f6720bc43d5db3c71b566677ea532e5e3507ef27ac798a8b41188d994ea7a12fe9f7b521dd83966a0585b2d8594f158c1f360a396cbae0948e3228dd671cb1cb674c26d372c6ad1b7273455aaa278d08cbc0edb1433d4aabb66f3bec137a419506fb83b0088ba02c112548e8fcf8ebb90d6fe03801596136305700315eedf178237bf3e9dca30719ad8eef841083411f56dd476e23fd49680c63f0e7de5b21704ef42183b88ccfe10f122597569e45ec94d7a436b7f3677fe02fba75c86342bfc823d387f3c167408a6f704743c65232acfc694614faba7ab6750580372122e33b4b513ee56d2384645467195d4e950a16ffaee0e19df764d4c1358446e2414f2413b60e9130e31c02d235c748c0e047bc42b939c025972b111f70e23c4b70237e9af827c70a03d7cfdc3624d8773e0854dbdc8fe4d7e46c1dc554f12985f19c7ac5b9642226598bb26539f67b37d49d756fbcbf90c6c5f9b3deab4db776b00a6988258afdfa7820d81e287a5e784da4fb6c0f190cbf5b44991b3f585b5e7187ec527af8df7a43604ac299a46a4d1b930588ab3941e1a06962771c3da05701c0df89b1d4d01b314e78904959ccff5c37d02b17ef63f5f783111323f3aa40108fdcca83246355da750695cf3e78bacfc2af94a81f8a6597a292723334cd63d059f717e1cab9060797ba14eb1116c007b05d13a6a82918a0436e0f6b4f44f0030c620196131bc75542167ba4c595e4cf20f472283fa0e04e48e21d23b18c3960d984a75872fa2747ade55574d3031827e642df6cb7137351b518420ccf705cf2d0e184e324e59da18059d9ce0de5b865428587b3227a8989e6a425853ce6acf1de2e58e10dde8e4c32765a3fda3761a7aecdf54a8fc51ae47d25af419fad0a60b19ca957b4f505b799100133120347deafd56b1b8dc14c273e4de02327c4e2126648f2cf40e2e1420ce44086479b8f886a0c15f26c8dffaff783dee7d1970e304d12eca28a0f2f00a3f58cc86dc6e28639f9d7a10a547e7e6d97b9ae58832a62c2cc74571334f3b2b49e8d421ff287e055238af67943ec159e750390512c6e128dcdbc2c38569866978388be90b3872bf9121c4ad055723bbddf881c5f694c21f238a88e7a2dcaea7a45a21dd43e8a76ccc34b2316aa48f2bf50815735ee786139c4dcc1181b148eb24ea13a2d4a62f08b3c6fa279e77304dfabb67636fc3a11061bcc55f8f555cf960f47a074b67ca677235e3c4b00d9ed5998451b830e73d0e84554c3a1c9a45156f693ff81e38358e2f04ccc722b474d527c844e082870e092c1b4edd59e9874e44fa6605557af20b74d187df60a89a4357db3d2ccc7d5c085c686fd299a05dbc73e21757d786152fd7d03f3309948474db778af96b44fe26f2253087b725ee7c6cac8d6078e58ed313b1720cc71da964bb4661720be3bc0cd79dd7e56706625bce3a56f25e03e8af1d0bbd3159fb40f205ffe3094ecd1f05287257d948ae84a9e5029a3c19ec9cfc7fb4e9fd986dc2aee7a2829a23ed45003f7606d69e620a77b0cd8e7409dd3becc870351fa74c5b2f41d89c6e70e19b112e052294377c4ffe5620b957ac107878485d81acd0bee81bd2804624f19e7b0caccec2016ec8ece8d23f75d307cdd658e20956c80ab97b735094ce37078f001a91a39e486793e4874c1f9463bf116b6187bb742f5b90b479b6918483ca91942d7d01935ec860dfa1cef71353fe101300f91c9b124d8adb01cf5eadd26553c710c9e4af004305510b05c0fa2fa6e9b6057dcb58eac087a6f8590aac1a8169ae19ad3fbb7325456ee38519c9522b88ed4193a0c7dfce00ee8b14995835f8a09020ddea2b4d9848aeb4bbeb4ba4ffda4fd0e4701140679733042a8273f14ee3b229a4c14aa5440a7bef511036e299920d5a9fed0cdfff058a12370d6b7e9286192c399f1119662a12c5ac2f54cac6b6e0f94646486af872acfcc733664c6f43571b722b573464cbf434cffa0d58407b26e782d4212f0415fe5198b819352802a214f5ebc4dfa2a60a5893a359967c8949ee4e50fd93e94ffa9f13615581f2893d1408d13d0590bd6ca24b6c29863bb8ef2c038eabe67ba9efdc128048f68f9beb1d570b9022dabd79c41dfd0a9a9bda6b6ec1e19435eb26b725edcbf44e7a927421ac166af035457644c5ba4083951db4c364b26fd422d15f30ae7caea181a18b05a1545c4043f2e29e59e77ae7485e1c5d713dd6f8021863fb188499435e24c70f2f936a795d0f686bf3d13d7d95f85179fdac619d640227bf9ebf80ba6227b825882b59a26c86ae68fc1e9313d0064eac5e793d6910b2aed145186e459f91e91936225b4d5eb1684dfe704bfe71a479dd67fffdfbb0775426fa57d7e419fbfa2653288a4311cef2383739c9ab119f72f4f198fbf10884a791ebcd1731b69b66260466b7cc7c6686299dc633b8467e8525aa56fcf73fcecdcea9bb1a2cc00a0b2257bd391f1f55d5341bd0d2f42c128fdf925585cf99d86cf2123172ff88837a0dde3692298d56ea9a026c4b1a41954efc347fefbb3af68d4fe78f37714e474c2708560de805b647c54d35ab3a9089ffd961e4be402819a3e310d11d73960b0004882e11bd842c72eae9d4ca138ea3459f6849c4fe361e0297551015ee878a400be564fd2159b1e4272955f8cd2837d6e3381926f866e7a0a23f0986dafcd19d292e8a813c2618ee93271d0ab01fcf61232204b3192b8dc4368e53ba49673fae1dc85af7bcbf290171d5b04a7bb777d022c4c49c71deb799f0d67009ef59ffaf2e831f6d28df9e97ae15d873c26ea2deb004c70c988ecb6d0eb89dd86bc1f41c127ca00438b7c4f0fb90c8d10f6b55cc9d6b360ab0b5ee4a89ade2d69d3a54881eb463a11d17d382681d04d66ec4c2c5e226ef9bd68e49171c933ed8aeb5e31b0ac51dc0e364c6ab480e746fe7054dcda944d73bb746473eade1b0b1a42426401aac52e1ed2496c749d8abba42f9d0ff8207ec2420a750b14e4ed30fd1c3e08e1fed9faa0df7e0ebe3694f345b01b6f24fc12e41df111c297e10f9148747067a0421773ad6fa82873109da88ac36d1b54e70935ebef624ff7133a76cbe774d0327da5d5d16bac3f2e7d57a0e657ec7c5ed4c2ad98a4f215688744614b43fe8cb22bb8d36b1f230e3db185394c552ff667876cbd4ecec1a9cd03b9567b52b154a03b3a8f96037898fe599fb7765bca94f07fc5d11d5d02ad51e8a2152340dc5fb1639c9f11e9a4c620073bdd20f7b62b116dcdf98d3df207cd450e78bc8012ba90b447cfb0f1ffa64ebb01a21b76e5415a0a4decf97e885a2361b1e66a1ed9a491bd2517129927247bdccc2d787297d4a732bc93c7e6470f05b9acd276923fc470ebc33b666a49745d99f7e9d041df8f65e014c6a5f7459a2e3f4a74ee79565d027094c36655f9cc71c08284a68963f21f85649acf0fd01db3b46b4207fe8acd5011a329444b1329ffa1284b35a9ea5ae8ba72bba53da9158765dea0a57d15a9c20d0986461e15e13482eb705f93b3c88013043cfc06fd211fb032b73cff7084312523a9ee5a577116a6f448154b971acd3e2875fddd6d2fa4be816050419b44f061623c582918f93b30004c7b08977a0063d21f065be15a52d47aca22cdac3ba1d7e7b7e4c203e6a827ff8c26488f3689d4116e792a2788390b4e180445396a62744999e176422ba01b4930d720506c40893998d504de14dd4cc13b80038e87907769acb9d2b6f66456cb4eb383abb3b4508d21eaa6bd433a75cb6b5e511b2a428191657826ec6f5f49f9b678a3c8fd57a86b30c25fdf65b77673a9523781dc81a0d1c1d5ff32e2da32d62e7a1bdef2f10415aec3f8c7569bc81441db2651a602d4f54e8ace2d51e784d1014836c06d52b858d5d198c299d0a0d8c70bb5eed58035918278d593f25d44a5dc0397b48465f554407507d7699b799e6abb891484d13eec5d4790ad591a2b30e416835cdd59ee4d13b4a8bb45d0ffc120f4c1f473126a7a2771f53268fdd525e54b02765d02e055d20dd4399d6a12118305bdeccd824a61873bfc0711329d34b23e2ebd564143d2a611ac79207df12c7ceea0556ba1e710fba33718936f16f4ff17f9bec77aeb54c1a18cd34f840f2227ec9c3cfd10cbd5b7a56fe30aad5627993f999a5f127f826e5226b7ca017ae3b73d4cccda716f34e365e2d8ab12ad153395dad5d688908c1d7a683edcfc5399501acd4fce2062faa7dc5ab70699bdfad0d55b801748d034a50db494efe97ca6978b92e945927f9cb8255ea84e1bccfbcc6b5ac433c79b08600e8b7042da89e36b800c143961e8eca2fdf74e85c49209abe7a3dbd5346a6d42d00d7235e521b60fedc793fc010256c555a0b59840d1073d131bb2e97875c7b4f7d8c6435664419ace8349230019819fcba412ae734a496d64d19a31cc2956f3f10728aa02103f10352e3c22d0042947f6445eca2a0581977bc81453b12ac89e21a1038c7a37cf31a10ebe6320e3ac548ff8864d65c03807e7e0817eeeb2b0556e6089e7b72dc0690226531951f2605dccf39a63b4deef655166d7cf08b2a5e2359d2dcb89798ccadc08671100ac84afa3c1a23ffa3af8ccbbf8f1885e46fea45ef94ac3212c9cc0278af206c1f7d39c6ab05340959de44b74df73b859231ef991dcc535ff8f34b7bdf16980d27744fa33e0bb8148822e52b6789110745b785e178cb73ce1a654d93640d17af29945bb290dccd3ae5df6597f6ceb1ba41945fde14fd9a96f7069ac666f28d4121b0e5d8d701a6985692caed3765cb88cecfe1597a9d3f6239831e7a95c942a11d5a04ed604242f9ed503ac642c10f3e33b26b3e4bb1a558a93e613c8c875a6b170f7c83bc11799f11491394edda89adc4ac93fd3c966871e1ed95a51909ccae1494b62a2dab67e5bae66ed8d223622d051e6beabc684295ed12768bdc28aec5139bec759d610b6f1961df336fd7bdbc61a99fa320fb3c1e9c824199da7b04be02950eeadf99615ef73daa39a493890bf5b10b1daf344c99775867beae19bff21dbea37dc556d67a9b7ba8543f9ef31a378da9ec494225d64cae3e3039184ef10ef5ced8260967897397ba1000c787dcfe0ed4e245fcd0bda3812f690efddff404514a8f4b1b72656533141aaede71ccc78f1d6edc1ff402a6f3167eac8167800d61bc6c707fbd10f86516394168a9be81824d804da256c9d0a0c5116c125365184f2d593ab77b88c968c2d789fa9a8c7d5d2419fa1029da40166d7b6f1310361e804d8e1cd9b825ed8d8c51dbe74e859d9b336ab4e39f84cd2b78910970cd9f247deabfb91e5915dfed0f31bdd557a93ffc051a94ec888d8c2d3957aadb44d30f1ac212ce476a48401fb93b4ada3b8467f3fd87e7a1bdcde3239e157876f9150b76086050199ccd4f7c5505c3f2a40debcf7226d447a23226cd59d1386565ce155d6429be88229504f121b52332b83f4f50433303168087818a261b9521394c122bfa82e25a7fc00ab31d603e5220f7f4d06f82b16cb8cb556732a85ad2f852196a54bcdbcdf11832de14278f9ac5de62ee96c55184e32558c05c643b472b8e0666b320cbee97d4cd02830b56b32a5602aa903a0f2190da96343d7f02a2122620ace9bd49850996967f0b63d65594079858bb0d9bae9f03ddb7c0b9e95b7032084d1d5609c1683f94186a1e8298aaccb07f51332d004bc13cb4a1c21047ec5f2e8945c93c9c9a16eae3932a68616cd1420390236bcce6a2249e8869ec1722c11d9836c4440f9314335758a2134d2b2abdb893830bd6439c022ecbbc4ca826b710d8ea67cf0e2a498403d7500ec6767d7fce81cedc640fee49c99d6d8533913529ae5e61125fbf578f4500e7b36c9220525eae32102f5defacbccefc6dd11e441358dc0dbe5efd892a04fceed2eb4524919d6522be862b8d192c05d8ccf6c1d418d1c48cd86b15e8220de0f8569c1ba721334b3aca777070c2416623df4cf31b1c9cccc56c3e779885f9cf4a2176c32bdfe89820ec6392e751429723fe8726f80ceddda8bf11d7dd6ab6899af8b832e188dd9abb59fe5ce7defefd421cfc33f02ae3f33e38defb47e704c670d6677dcd1182bdfaef24539c5f18fd5e3b923e7411e54e3005d3e594c05f8fe4852b7977df647a44a8dd6b56e06f2a9cd0d90b5106eea06c823d33777213991219b9a85eac3793da4cc390e77d3beead669134d92d7d85cdaf34436ac2deea96c240ddd92a7f6ddafcc49297c38a780a981d001e022bee5b7e1f5d31c7164efde9998526ab81ecd790c3dee0bd7504a8bc1f20331fa1f4505ad844cae8a5ab9de74e4d4e3d233c11bf09b0f97b6c7f3152d616b418a290076110c61aefee4dd064d52bd5662e72d5e547d78c62a3f970505eb1ab150134bba21cbd309ef5a02074533fa28fbafde8907e5281ccb7818cba12088007eeac055cb11ac8aac2de3f954fc88ebca77d62d1ab000ca22882024ed81c6824f84d8d1158319d617c2173092e350ee3f85e7497e57cc1a2f3cf6f78e0a93f161ad28198bedb874b5949b67f7325b1ffa3644ebf617626dac163bb29359fa32ec63a9dcd60b1ffcc3f048aa9064ce20ab3691fee3f53243cc8fa074477f4dd1331a69686b2661efc96dc317589ef130d05d239104554a054dd18b64683bd6026d120c36c2fbe036414b70af9660c910d4e3cef7cade34e56c1788d07befe6007ef48057cd4056444c16c793ddf7515422d9f306a7c6414c283959734be674b01abaf993158560d0b337f2505f3884e8d18399316b1941defd645507ee1cde72595f18155511a21255edbeb0640a4aa29cf8471c347d0f21671b079f6274ac7193ea007d7d26502d808cde308d42357f43b6723a75d961d48e5fb94edd833771e473f9a92f67df6c1db76e64261c4290d78db8d863244b64e626fcd79aa9c0f11d2758321249522717f6e1abc6728acebdf043c9fe94b8edc7c0ca72a9e591c29fd0e97024c10ac289e876930de566dee453c0163237f2a01d2d61ac858368a2dbb145b202e90f797c9406948e1e34cd983c148e195fcff431f37b517b2291a1638fb1f11699274c69e194ca2dfd4cedcc3db426bf6d141436bec1761a13cb81513f52e4f93e8eaa488b667adeaeeb42fbc818486a3414fbfb8ebb666e341881e9343e608ccbb3fa67eb3a63927b3f9a950f3ac7d8c6a922d1336e41c01263420aaee7283ab1229fee43a6f0f231cb153855ba2121809fde687c5ab99c32fdd21279040937ce3b6ba040baaaa61b06dffc25dcee1753eb20d2ce54e8811d3480eb2fd3dab20010585775bab87a57bb082c3d981a79d8bd46179f1c368c484cc2c3e2ddbb9f22802c755a4fff57fbde60b820603213dad9d2c4a6457964973bb095a7563f7d20ed178ff2687715f5f2541f5995a9daf1db7fd73b1ae572ed3882492c425f8030298ab7c07f8c2bde72f0b81f347fd4945b0cbd0d2203c17888d1beb58d102952ef15e19b1d022ab7d314981fcc0e343eb35a43ee49ab40703292b25e2baad3970186c4516446ed64f128dee882e893327cfafe7fb4cbe843b4c201e673402b4ed1f78aefcf875fa4002c51b18e753fe9b587d00cec51cbec9db1b1ced106cbde7f837363a24fc1960fa174e90ab089486c048dbe8afed08ecaa01654643d51e037ca30675c9d868945e942eed9d2dbb4bad6f672bc38eb16d9a255f172886b8319c9a80a4d50ffbf8cef7d04f3be695a50979df0ed58784e63f14956247e8bf8f995ccc32fb1ed81094d5cd184c31e3d8f5bf6a1545e730e15d17f2d877cc5d6c725dfa7a18f65197c2cbc50f9b1bd891c95d4e9c3886ce8b812f1dbc2d1fb5cf2527d5ba390119ccf4cdb509d379f13bf07df8f291c5e7758a004909f41c6ece67fc6e7701558bfecf527e557958edfc94473bee231b116c562ec96cd06d3d4eb29f7722fdf9790ad8731937303b96d87c0010fcff6ce622c919d3a8ecc90971d2528260719fd28f31a10162c18cf27b653716c6f564635e3624f24171476a1f9e4f0bf656cfcfbfb4b82c74f80f49f617f35e9e955b48cc8c23680225687a49bd53da3dc7221fcf1b2445b1baec7b2d849fab2015d5a469bc0ceb22c79c3ae6388bdd98d5a5f518ff1eab37b07d93391050e214a1065806912ab0cf8153beebe4cde99373d52ab9aa24aff251ad8afbf0456be6c2c8df4d6a10506607c9dcefd6bfaa39b2cc2f12cada4417f8f0683aa4d5f2e0d26a90f3bd141ac969f73c8a93988b74d1e5ca0505c5bfbcc45b13257e7fd59284cd48321e1c0198576659edc33e37502a532309bb26738edd0f14027da3ec62d53b91d10d2bce5252ee55a842ca199089c6600eae0002bcce794c4a8ee067921a7d165a1306b00ede2b4e26aaf9d5f0cc9e63086dc296c7b16752f08dfe0b2a5efcec335008a358e4654cb7e54fb56d1f2db8fc3434f175f87994de78dda2264fa801c26b1a794ddbcf5c58cbfa1c3a53af6ba4d36d85e7f21bc786ed9e4eff587207d6e0182d2a7fdb302ab30cd328920ec199c268ea2717d5e2348bab9699a0d5e29f0f183ee936e95feb989b531bdaa889bfbd18b32b6f17e29829cdc6ceaae704ecfa145a1a3edab54c3f2d657192fe02deb28bf47f5b9b5683827a71ea12b2d6ebc425c70d674d25ec7aa956d473595c0c852a5677b24f8baf2f5853e4b5205331241299742b4b9184591e6eec8cc938b31baba0b0aa7d85c42cdc0163335aef1f9a0e20c470120d5346fab0e89319403737337bf9eda9ea4d7b8540c1de0909db60292624bb8fa4fa9d4454eec65f238a75b05d50ed63363613edbd0e9cc7529317016f09b1d06ce06eb6abc24991e834a005145129eb285ffb3b7a7e42e8be886524e979f2913f10e9e21ad841f12b8224a16dc335223f8ccd46a79234a272686810205416e002a114187be5668c9b18e52db43909a4be425c84307200c98f4f85250de7b5d8cb4b7c8b18231d90ead88ee023df38e8e5368df392adc1a3450053298bc6c94f18e05601c32776da82ce4f550a373c61dc02cde6f68e5ef1cd12f40734fde29dd43812cb742257ac73bfe0cc9505a82cecfd8e43cd4d4a042050640bffc558f533f358ec0e05e00da6dc62c5ac8aff3fa6a79f1284d7273cfcb8010e1457f42a1419e06bf496ab6ea7c787babab07149f80f012cd22e04e68c94382454a108fbc59232e886a121d49bbc6ed3a2c5e6a28ffcbf828bd9765804b89e2522391d1315b58a1f3abb318bb531f48542c23bfebbe3663fb476fcdc272fa812cb9391c15794fdeb4ab2dad8dbf7aae97f1826f93a8860f6c83ffdd5ffc712290ffef24a67d52d477111e74f0963c11394b6565443a7f8334417e0b1476947275d2b75b9e229aa76a78d7abfcb959bcb6b5beaaa919f816b1c26d3d1ea5c63144ef90ef39110d0b0b3aebfbb90639ec3cde0059a41fa04503185ea49e417c60160d7419b8b18eba1d2368d8e71cb98cb296e55c0e13ee35f767bf26cf9c6592319ec17f5e76a481415cfdab74bd24e2ddca97b712eaf9c4da0b848562b606b2161ab179fe1399cd07b619bd2549bfcc3190ebeefb4605e1910a285108952e9133fe26f4e19c7be47341911bb145c433ce444cf6cbf78a3aae56b09a7a12b3c2dd1c467d4fb1cab519dd580c29c45c16ccfd299bd1941921123a278c73543f416260f476691d193f2953a7f972bb442ed5ae2a5b88a88d95f7f213c08ff011c1ed06460e871cd2b6379dc463f99c712559369c570addb5d09d100b0b5a09c3317a3a666e7753f47154612e81f244dea3e288bf5d042bffa36411220b3c1d6c507a1a9f94fa45346ca4e134ea59321cc5a5a7ee797f13ba9bec8f715ef45927e1cb3b96797a099a2ee02bafec54eeb0e51a31121a4540b7734356bac3eb5dca90940c74cfdbfc144bb4afaa40a1135c344ec5c9b8c96b68bf31eee6e694a6b52007ec0261c739d074562fedc16a4516a00e08acd5778f09bd63aef499e9bb68a22712572faf163cbcbe043ebff8bc1fe88db1f188d95c5e11a638f1797cbce1018ffabe6dbfa1b549f24ed9e94aaa66396528ed017bb1613f7dcd2607af2ea0402fc8629b31be024ec624a68e69f20e819302c7ef510acd540e52302b434c60f169bb33602e3902a8065d9efc4e681f2816b73ed06cd4969a0b31ddb6533bcbb1c86848d699a38117d6a3c8290d73fe178125968c33292f7cf28b347735c4518092f02787d8d4ba1573b3d86b9c67fc9291dc9c9b67798e699c126e146ad2dbd0fb2a8e24548f9adb044b61b99dc2c39ce93ed294903f966a4794b28d7d4114d087f97b2ef7b1c935ef8bbc5e723c3463f0020f05847cff8d3fe88a9e4e9817506127aa8fcfe0788933e9a66c52bc6a844b157e1babc2158a0fd57c65e018a3915014144a755701e8c7e933799228a0e73125984584b15e6cdc47fb1acdd2f5a1b7a5ecf67e967e063253c0e0db5d106f246ab11ecb1a0085abf38b3855a73262f053a399f41d0a3788af57570fa9d9b93469b2590f717e57b3148bf938783031cb03be9e68980acaf2c641ba1b86c962b83e8e91d2fb25c45edeef26d1caf9b0b6eedc7b0bc98bfaa95580b6e4e2ebf96bc6c5cb74b7beaf26270ba0355445315c1de670f6ca72797d3631f96edd2c8e25756cf3d97b6d3fa899b808cfc5e351f12ddf7bce649a409216e8f59e38e8278f6bc045b0c053bd5b162c9381c70ef1a7c24025923f03a4cf6f54a0f900358bf9d5f19981d53fae7985108f36e428c0d08ee83735a01583ccdc45bae2b260142fb34e625918162e8ff36dfb7fd2b0f113f8955d419ff1b32dab10edba00dc967efbbb1753b72c61677890b538c96ae2fe83c31f4194fc2bb9ee62a7a90fcd8c00856ff82aab96a531872905ffc8f3a1838af7f01a97ca33641ea184b9846fa850a742c7b3a228d6ddada92eb1cdcf40f02ccd1c2259fcb11662cf2389d5743729a2612b9c17ef1a95687916eda4f6d002d80e03f86cb65a5e11434956439bf2468e7ab21a0ee3e5f29c568bba1cbb90aae3198c8cd3475f502e971fe684a26c572ea65b0b91846ab9b6cf0971740091dd26a6fd99dc361dd44361b3073928daaeb1953dfb11c4ac022b5ef97b269ac27e63c5efe0ca522f6a6b8de0305c45df40b7287e195e5698fde418d14ec8c30237078e33b00acf2dd4836f65d0c2eb2cb4969e6126a4c6ff2f4f571c44aadbc252a355ac38e33aacfbdf16ed83c4a73c120cd21fe2e73bfa843869fcb26e9fa2a6811643bf97a7498aa8a1cf4e024c38d1d159181f5f9d402dafd630cd96d6f0419a5109f52070ffe015eb772436cb3221f1f43721df8613eca20b2ee67af9821cd445f58bbb5fad8715ee3c125835de63c990f0d809b9a9d579ca81ca9bcdf18e2cbe4cc028c4902d7208d5cc054b71ff7944cb75b1823dd35704c21ad486911cf5cf2d93a9805bc16f9109c02ee7f715ce5f6eba517556af9072da7ae580b194cec269815d37d53cb69fd36d4383b7d77e63c86d0500e7c3e07e0d730a85925ec682caa7374eb38baf59e181a73744b2220aaf725563a81e944adeb98284d22b45c561ade7b6695933b41e9ba161472cf97b7b53ffd65bb73923479f45169160c89570f799cb230ac1e31b1de9717a17fc2ff582e73d9429a60eb2c3345450a0cefcd4ac5635ed1276eefa26ad7993c0d5c143286284443aa3c93b054dbd205d98ebe99ca97a5f092e794f80b070ec15d02a0b7db291ca04467f0355491b32814998a3bc56182174cfecea12699b884307aaa2ad032e080271614e9d3653511b41515178ebd82a10872275afa2d8da6eca90f74da993d0ddee354e5585c431ec297b88232ea4fcc1c45869fc5496fe70a79f27a08bb70feb3780acf6d66271ecbeb90737e57019f2c1ad391d2c74b080addccf1f913e45fc2cb4c9c14131204f9136c5b204db4e752edc47072962ff2121b4f028e850c2f4cbc97c77db6f50c918ca90eaaef8b8d4665684b6f09bb45b87537d7469fdece7c5cc6e1fe213a1ecbfbdc2726dcdea71ee587b2577f0be63a85678e7ee95919af79536af2794c0bbb808c969330ced903e36aaa713310460f1d501eddf6a7bf9dfaf7e6ca39d509416911781d8a948584e45762f6cf7baf1adcc500ab26ef8500576f273606fa133bea9c2953ca13880375457d6ddd657c8f2698c0a4368db75f4f050b37750407a09e0a0757e2e3328bfae527689e52933795eeb798bd0048355eec0e72663117cb3c04d1eb3a5332b6cba6535a4667971fd5a28e46b7a301a279ea79cc9d9920e2d655cf6ea0e0e3266844bbe2eff774bda89ecad86b0c4ebaac3b02ec45ea05172848071bd0c2f34060f5e48cad7c987798ad35b47546f58b61dbc71beee40dbfae6ae92bfbeb84363c6071f81fef4c5b71bf8c453147becd12ff1bef1b701f2242ae947e1d35918bb8eb95cb4389d58dbe4dfd3d9a723542c03a227321a6093b95de07dd9841b05e1421e172896bb916d7fbb034b5f6648cf4b25fdae1f81b813f3534aefd049f6390698994235fde229e2de04b1e5a12f001154e54c1521135997f05a571d291992d2a33f9743dc41d6600b4b6d20943c7366070ddab011a98440b8d3f36399f9dc82d6a8ae3c6832b07d8ec8b687f1eabf811fde8adec17f4587900cf79f55e93bd03be632174a88b1e3db738c169fb76a957ef46682602d9bd7e01a5a1f5762b0ace911d17ca0a44c3e4b5718cf192736dfc77c262972c39633d562d840aede605d96cd39d63f8aa3e97178d528a4f6346c316affb25183b255df4e497b3c51da1058f82d833756bc1589ebf7b93728359f2c1cf488802c89fc6881522725def51c4606accdc65cff1c3dc75e6796e693886ca51d629163231935fe90fde1ab0a2cf346573e5dd612769056ffe6ea401c8658f60e18806a5308d7a9777ceb3306dce556a97cb5b3309be6bad0ce0d577c8cce08b0d410099c7ba3cf2ca16720d533b5fd25fcc7622a4336e95e2b98f6552ca7ad404862f4c24eed9f28247493ff90231a6ae9bc2e56773a715a376967c9622d06a8b740bfbe8548af1252af208f4bced406f33ff2a5d0ee23afa98bc02d89fbbbd32ee06a15074e856ce3509791b06cfa7bb04823701405dd5d6b4e4b9e3d4955d84e623cb89f9db081b7cfeee1b6bdf64d7e40b108857784c90ee5f901d30c48f3a2b0ea15c25723937a8b8f0cdb2141db7658c4adb1ada053ac5a925d245930d94270e46fe76d187bb6b75396051e833972399f5ef9f2698e1be837515da65aecbb52674c147f1f7df37302e71db9da511d5fa4cdf531fdf3cc8ab0fe2f7c4fe693429a74645fe5995fcb8b5cb1538ee5b0701a4ae2ed64e27ca9b52c927cfb870e60416a03ac7c2d12f183f6dc02b72d01c0276b243ea3bcf95a99566f9d1821daa26fb28816e79a93760711cb30b42997a8f3401e5c2d3f66551c5be43a87aa2021737cf65b121e4c171214dc2b39880cd52e64de31399cad73f777fe1b2398534025835cdbca3c99870a583f6803cb2842d5bb0807cd8c8ae4b1864cc40ebf78a2b25fdc7a6e09f6a8c20b14762db2712e2ae7b9065d9f3d60b850fea4e462d97291e5d42bc5d2950794db400703a67745a58b2eba168a26777ee1f41a8d1b4ec0fec2cd94f832a8703bcfd3dbb01a92f208d4ed3a56f65fba399e4fb83ae2a4920bff6e30cdfc8fb2c02e15cd707a9958f24a95394b85f764ef85d1b53d89ca32000240a6fa07be0f8ecd2b1fdb315eae350bde9b82953290d10223e2462940e113bb36f5f3035c71b55b4683f8d9f22f36c896a0ee72f8ae4598c993496193130c8ce0c5bedbe761fc24f6dc41ec1be69589d48dafee22d007c51a541237fc5279cddb12f2b04668f8c3b251693ae7177f732fe73683e92acf4ece24600c98a27e37469d46ca3ff1fd8e3c6ff1a258c4a4285615b65b50cd57403702951678e1b1de74aadc305434e7db5a129321adf38b8ef2aaa5b0925fd78aa16f11736ef2a5305b219bb3ac2a775015335335e10fb5037a1fee380ec26c3d693e74bf62a155e64c2b8a6c419c207e5b69c634f6d0f1a558607e230ae435e3d232ebe380b1c4af586b116260897e3371cb54e27cdf8eacdb123c69feeed2f038242358c3a2cf548dd18ac66acf1c3b591781c81296289cfb4b62c96dc8ee82feaf708819d28daef3cae76d429007f69ed52599623e31cbb4f9945614cadd3e64ff5a1c87309bd897ef095671eeee300a319f86f1c258716977539bba13644ea6eef292c8d04a7c33872e94fa05c85b4a6c7f7a6682153583d084bae67f6fa0d0ad63bec282ab75977421bd2ea996116bfe6258000a03ea760d5d8770f6c5a8e962bd97e600113ce5c21c5e9310464ce68ca771d468f8d3ec6d104d72f81b788f25b4444470848b87d850421652e1e5790ad60d7fb79ec2057dd05a7cb0bd13404746c451527ef34df6eac33e527342591a917c9541ae708ec1d0f2a55182565ec56dc6ca018dadac3e2f4dffc4f2b092b9aed73363081804659f19e7622dad373bc6d13972f13e966b75982269174a6e426c5c184098495a07d3e3d40ed53b307c48e4e0801a61002450b1c77d02449c31df358215a8546f9e94996fcf995cf601521ef5b2daf08fcf22962fab44b31ee78ebcfa01c32ba64b056062f968728cb8a38350621a0730de004b2cfbc5d9ce7daaecb6326c48fbbc83e3d1a66844a622aea030f2de9d284072da5734f854a4ca22b6f7ba0330531b0eab847eb3b1299cfdf898715a470099babf5b7f4ee6bd17835eb6b3f668404be51c6c7e336dbc57d1958fe0bf57fc58f1abd2c3d76bfec20952aea2b08c71cd70342924f607b2ec4ab784f10eb7bdbd0cfcd5a2b14dc04e43a2546a01d55c4e329db281f83f49ef9aaf1721cfa13176d90b457d55df2bd6623881237b21f322b37550b70d2bf60812d354228ec4675db54c1e01d9351086d8fbddfcfbf0ea8879231f994bb2676ab4bc35404c387684e8d67f373efc43b071491e531a1c7dd3359d4ff28bf4c9b7b54f3b809a08c979ccc9544b8d6c242e883802fc723bd790bde23316394a7f7a8392405176496ed0f158831e8995d9f71c0a3d2e937bde0247588c8b5812da7af4e9cc528296f87458490b40a97d3c7ed3e2ba75a9b5379d8b29431709dd914bbcd9d7b20e9b0cd5b0c1b048c50ca584847274ad3dc15d3e88f9159032472f88544b7dc172a211233ab2e803378750d7b9f852e0e7ebe111013b467193e706ec5f97c1e05603359eb7b4922445faccf0f976a551121b4ce68e885e27307788ea6d655342b96e7da3b889395374b2d451e59ccfb5dfb020369720beafbb3491e08487b0e0c5591b2a74c642c7b788a103b5dae3ab74407230725fbc586b1f7ec78a7484e1c166d18dc502fb65ab79eb6f0ee5ce943a9955e1caceadfbf0dd660f787ba941c205045e13861de9b18cf4e10626a82113a40f9838c7924fec17cf6dd8a7bd70182179706d8e5c613ed6abeed8a4a0741d68d57fab5ca40bf61108e7b39395d063e4a0278648d1cae64bfdb37a2e86cdf0da295fb3769297b1a8508ebef307424b6d28b0087d5177a91ba6b416552d50fdb545dbf7611a886c5b753c19aa0a425527f8e1b49cbeb929555d81ed9b7d0bfe53141130d8979f0543a4ba28d54c718f77eaee28ca1849204efa5d01874633a3c15d0b83941b265931be52900285a244138ff8f273652bf73d693720511392cea29e23cf5f519f143da44b6df6fe105a86c70a963ed423b66d163971cd4fd349adb5447288327c4d21fb4d49362ab1697a1f80331f3e987723252a7bf8564fb436bca7d2e9eb467a5084d555b89892743c922e323498de7d0a53063c069444db2af930a7486bc0c8d05e5481a68b9261644e2a86ef3aefb3dfdda70095443cd42a9ad36a49e09f43e5e1652d35ba1120f9dde7482228d2ca8d9235266c92d66b525b12aadb087074a5f1664f6046f255120bbe9f6fdc7a9f1634b2c18e9fc6671baf0025d3e1da304c0f6cb6e20a202e994d40a2a7e63ae9bc20814ebcb1647c18bc4ae2ec49bc2294b45f1eb0ad80026d6923b50bc217fa467a1aca05c62cce34b0e55f4b8e53a64b2d50a4fec340a9451a67d0c9e7c42cb1ee3c8524fb987212476176b07377a05bae6814a19e217aacceef4ce3625eb3621d667d50d88ca09b7eb0a558ee9339e7abdf9ddc81920d04cc4e81d85d8d50b1c8cdbe531137aeb4e35ec7b2374789e648c094d73e720ef7d4708437b96791cd4c49d8e77ae07dc49d54ddff850aa1db48edb2b8ac025f1c467e1ee11be4bcc6a28f0c1dcd11768e16fd2fab43e6cf26e797d7dd72522bef03f6627ace90f5125621f3c5dfe346504e6d5267d56cc5242e3d6ec26cc26ee61c68e3a02d731b40d2179fb04bfc05f720dae3e99873a96045a9971a4a404783bcbbe3b7e6fb76fa94899731538bf58b440062bc2b7721a6f55cf6026d205e6eebd1cc0af7a4f768bdf97ba2d3b9f693d6bff8669d7e2dc09273f064024162fc8739f627e2acfe016ce219f0a37e423ea485cd540dbd63ebfe3bc919c711272104115c9f75cc920147b7896c470c70a2a11f469a8769bb687a367e33f941919a21fdc1016e608a68611df0790b41f496897633057b2afc2083510ba48e810f123e43eba661def175e65f689d4cb3ccd895e5846dff3c6518fd14ad05c051d5930821030a160cff1d21cef494bb4431c2139e4933613700ffb6069d3cd0122fdd08d511cba8d74d0649c302140c48c9e4204a408044fdc580f2db90d0fe2192147752c66c76c35c257908285672e5be1d4e58aad77bed92c2215887fe5910cda150c1520e5eff7d7f67c70f39d6761d5787c636ae5befc4b9228a59830e014fb7c355eb807735dae88ac87dd775f4516277542eb7614e802d0b89d5f58021155d8dc82d0fc9e35ce635806a2de361e62fbb80dd2ba23c1d944f604d1b1261e1f77c5ad8b4482534397dbcea6ac6235b9e9a2c96e0ab6093af9a6ccd602ce2b2254f8baae417a44bff8bf4ad957406efdfdfcda736e4633db31276b19407019f96655801d083c1492baa01adea739ccb263007dab5779f1cffe2a41b569f60c011329ad0a64d4ec51777f4cbddbee44891df86eb084bc162d0b012c8d1b0c2e25c0d9bd945b32a8e021c68e1e99fce77c30210792573ab28619cabb8250630c977d8675f0a9cf42993d0a7314509654fcbeb6e655306aa7521eab6600fa0827f9bf9aa3d33669262722ce026081c5cc678fbfe285dcc9ff5d83cc5301be9e1c185aa1082096878f2cecdf83acb76edb1c0890cb678dc45d0794302b325924094762f2e0d2d72b42b42e10a2b8f85ef2d1085c3bf20cb898cd9a451c33e95f19dd744a2bccffa93b942957c439738f9227d9173f1eacff1fac46d601371fd21cd3acc3cd0ff8fdd6015b0b04c8237f953c04438d8a0b410771c107002ad6a5028e88c07b9326432ce5e9f405ef627a0115afad2b152035867ec80c402a6b6912245778a4ff93a199ed09ffefde490c081344d02a8828c3ae56454de2394be91dd22cab8da147fd556dbb517f45be32d5ef89495aa5cc7e8fc94b2cb23eb4072f84f3f628b16ff31e411e6ad3d79c4f6990da83053bf096ce9c878968bc383e0092565714afb7d29ad7f4143b037703a0e990eecf93a13da91c44b0b8cc6e770a16caa27907b72eaf520cacbfe4dc75f37b2409c5bd3e5a1b88adec0f9a6f2c1b8257bf78651595ed07eb93908ce8fa6c56a375ab0158726f3e9f3962c6c968040dbe39bc1f63eecf7ebebe49af674ff9df0b19e19729e572036359bbd9fc271b2652fa52072e6d9b6dce2cfaf021c2abc4781178a43b35dc924301454ae1242ca697836b38fbe3d1395f9b924239e7715514ff2101f01a001abeda41c303e32cf9cfae968ab69eca23cf32043a2626ed26e4e8556f34710392388d301d274e2d867fc1fc742544f932aecfb32968f91a6b5da5ddab610db9b6ba5bf503d5a40c7a1281d5023979b25e9946bd0e317adee5e3d0d4b25791c47730f5c451c3a30a64c309c63b7d8a4213cb81ecb4416aaa7ae4ff6b0f1bbcae03f2c9ce349a5d2c8688c44b5e1a3e7b4375c7e09ed7db49fe660288713e5161fb66d88560c10b0d108773715f568c2230f488348a6f1f0108f967ed8b86fe51d9aa4f2154261e0e4017955c8fd63fc3c95c2cd9c4275fef09559f483836d28cff512f621656ef13919d2564c40f3e2d2711f6f020eced063240c2d121af46df85014dc5f6fce4008af97e4a3761b9bd2c8067d4234fc7f26b05abbff957d102b1018a5f75a0511d02a70a0b1adc9b594cd9f8639f1b4adb9d5551471c083118390429d036c4440ff1538534b341bef9d76453a333a03fe6a93a9f8b7acfd6fe8dfd4af088012f1fdec88c0ce1745052fb41fdcb0d0bdc2a44ea675554db9724f80cdc587c23c904db399f19523a40dc3ecd55904a27100d971b354ff1ba6884c9c61abe0151132f0b7aea57fa59c86257e45435cebf27a239ae839251f51d576beea2704400081af5276d0ab038181bce6524f7aebc7b81afd7892a672190143dbf83fb222e93020c334f2c6fb55efcb2e22c3ebaec5fc31e2ff0d03e94661a17f57ad79e03411b1e7554c88869a8ddb7c8e38d5fcf00633f1cd387eb6c684ffeebdc43cf0fdd0c13ff8f26673c76b365389b05c3cb20860a393c22323534b10fd1de4c31584318f47c627d004c4d4ca0bb9bb727e5ad00e2fb77624e2cac4934689c826dca9019f919f1014dd9aaca8fb14ce71ca212e9bf980d18ebc4c529bcb9f1fde124e52a6517675b2197ad6fa3e3262968bfa244b71b4bdad6e313107abcd0eca469c500356b7e5ff2534e25ce5ea40506664058eabf3e285381aa0d696edae1948c8810071e4013629ab339bb3d42de6ff3a38b721ddb5e59014111522a940c8e5c700bac5504146f443f588b8191cfa57cf010b3faf06e462d73026e67fe6e1a4fb31652076490ca9c85b906a4ad3bdf7433483dad55af3b1b71c2f3d9941126e42418c83b11c67168c1d0ffa255c209abdcb7de591a23cf5b8ebce99c10a60bc3580ca773cb4dd1ef9ef1bd4f9622fea155f7736323591df6d8f382078353da59030a5302ea3b4a7aa35e50e6956c3f8a26a6b2fc0ca7ec04189479e60040f8e2ea048cd2e57e07030d18f1b7ff2ec262817eb86665c02dd50a78d2bf2ffe9036e91acd0c7be8f8b06432b2a9d6224f16c1b2c89d787228db8a093d83b684ab943e232f99066c9702dbcd0058f0edf14a3d8180840cc8c9190bd6ad21d06ab8db55293a80ef915adcd3a16f83ee490fd352c07248231126a992763d8ecd8f82644a0dfa4dadfdc4c9438853d180a86c509bfe9519d79f3bacc0a687cf13d5f4b9d31449969d0ecccca004ccd33658fdc62e6a1cea652c07a4260f6857f9b065084802b21f709463a53db25a93e6d5727a97cb92241ab8f2d31dfbc112d6ee3a961f1fc6c42b414123db434e283b72fd71502953d1bc7caac7c14058522d54bd65db0124027eda9c1c0135cf49e359296c85cd5bf5983e79f1ce0c5ba1d85cd79e3b2e200de96d320877cf600be346030ca98e72dcee6cad6eb10334ca88f54fd20b085147e0227fa4b82b08952bc7a437555cfd58a6e1a9f7a074a38c69f6b1e1f12feaa52fd40fc1edf0c89db503c13e4073f21c3d34d70809c573cc0c87315bac14e78f23159ec3e703ff499a1343d341d0112603d3d71f5a2f391c6719146e7506a3b9c81ec1c0986c7d2d4138e5c8421eb53bfc9742801496f18c0c98aee64025b03389b2d5aa9549ef099f2047c8ab0fb1e1f898569090ba67c59fbcf568400b9717bf3cdda7a8c6e7b1eee8710915ea08da66f98d92c50c302f01de823f18217815f1df08112e5462fc757e8deb9bafd1a90eaae25fa490decb4574f659962103539b98401d3107577c42f755e3aa0797d85df3e32bf02b7124455c1c119917bbf04fb72d8ecdc831108c172060b834d42b3a6648a51b736ec7a05a98567d2991e5cd42499c9c44adaf79e6c4c048f92311763eddd0536873819f681e5a5911eec08096b42426cc8acf42483573339575e48681ecb9fb5267b18296d7c40025ba08f333afe11f8319b7711a6237a4c28f9e46152a004c65715053b70bfc651d82bc39a8c052294a8b55ccfe7a30d183ed109501ef2b567d2e5634c8ac6465175a7cb95b2a0ebdab4a0ad5fbf3a2399b82309044b67c75f7461f7ef0f8c98205bc0a2300de416ae06c2c87620e88605ae423c67cab628da3b557dc89b1ef1a2d504121aebb86e12fc7e92383d9cb11b224abccd9481f21e22b0becc1a58c0e2908fe58350ad3267adcafbc4cbe299312e9196f943f231f6c4533fa6ddc71bb15041743de245b026df17f5b7904012d7a65d8a275d24439a286fb52da848747232a35cdd3660dc8c8b91fc9a16a1045f2373d5e52da8a4f87e7eeb6cc811d691a533abc0078cedc3a41ef9646c6c0396551ef5bb1de025934494e14b478cae8070057000000000000003a4770fc3c2e327f7ac1955238f5ff192861d2be9359ded4fc33129106ae4128d89b9f25802a4752de20a018e2f0fffd1b061ccd7cccf0684c83edd6232b8d020cd957dd0bdf01ae6e57445224540227bf4314a8a849d7d3f8d44536c7df1612e4a6acca59e97eef73c152cbb8aa15e15393998550beccbf602469877034582bcbd87f0aae34eabd60f791bfc707f7a701dca2b4472d14f70a87e13a9394b814228e0056491377bf76328ebbd9d54dd48bbd4f0a13df462a8700b41964afb426b529ddc245317548bb11563ed23067ee7714cc02f1cedf477df6ed24401c1313fb9fc399be202573a92cc4f3923097a919f7e9dafc245aff893be38ee9f6eb22ae9ba0ce85b04f215ef6ac6dc931b26af7470888666cf4028f6853a1fc89800aab436f52bdf08e76ed5ff5b8b1e9b9208b0e79ccd6d2e5e403e4cd9e1f7ec0269407fa72bbf89a2b02d67e1808a2935cc46c75b63e1a0555a653ad5ca2167819dad4c393d0407f424ccc550e2250e8cfb0a83f2dec63741d491757b59f4d280a81f4bcf28c5225d546eb4d9c0db7470086feb317aed50765bb6140e0627e17182eedc529f79aa0a1ca8e869b9043fc50b120947425ed69e7caead2a653e9070eaa1724113cdbe05d2f36c7a91955d7a1ba6761ecf7af876c00a77dd543e90509d488adf5a8f7ef2dd9835135153e4dbf97ca9a04058228644656ba753926c912dbcdf8d44a11bd18188e3470d8f3f79cf61c1dfbf05b480d06e7f2101953c1276270dd45075d6bbd7d2ea777f48f75315d3a3ac4ff4ddf3bedf5a8f6a20f720980dabe53ff866b6473a3712dbaf125d759d742dc36fa908c03a3c5efe066b21c861452dade35213f4b9b50e66b6cb67e67765dfdb7db71e8fadf88b6fc21a3180eaea099635d5f8ea7565a639177829d0f821f52ff6b2993e0fab582dc24ed071ca51a1a978c526f07b3d5e6514e1d89869e8399219153394d9c66c4230c9405529dc90f460f2cfb4d2d753814e7c94a3a0f6734080d68a40dfc25cf702fae0835382c4d5a7180cdfe175f7a93efadb5eeae27b166469331d89180e202db4e2c5ae6ae8f112a308ee445da4ff5a535cfecaa8e0930d261eb3dca4d29677c351cd3e7bab8d86c27de0fc4917fdcfab376903b326063eef9fe50e587a425f02f1e70b219e3fa8c514ac483b86204dabeba28d1f8266062dc1a48627213f661d8024263c105e02b809ff5d81f97e69245945ff3924cf2206e06b6ce9d90761f551f8b2bd37f5d292ec4196d05f9a9599fbacc6e60aeb200f43c1c7570dd1b98070ce8d08e69af0ec33faf3b6ca67785e0c7a0648f6d43c2b94d57afb827cc53c215c95a33d306a55e4e735eae20936dc407df89e00d30113ab2b27e093fa9643d30b52525f28ed9f4f56213e80f4924f01e0df48a5786f8ce4084685930090468146e2cf0f9d56cdbf2fabd8a550808f64127693eb23dec33518889c5db6046d1032a21b7baa0688a6df254e9010297ecb141c27c8f6fbcd225a9163b1e81c1f02fceaa2da6c0ce03ff77136d5aebe12b93bcc2fa4f2776bc4fbcb0f4910643f2074a5205333084d85d7545677e6b5f588fd9929b613b1128bb55a8f557d5a5cb0efd53a66505bddba3d0a11b50293b372b77272f93ba0d11d6ffebaa7a6f0521129cd0859f766918196fdfa151dddd59f4ad4d40c5fdc0cf0b164f6c20a3fa552df2528bd3e7e188ab5d111b70707c7d72ff0f6f5a9316d4cd0b2f87bb6e01f21b93204541c4e57311a63083f72ba824cadc1d8caae341e6cfc726a943488ed417ae6f93176664e314d8d6c8dcb11f3e95031c670e6f63406db9d9dc020d2eb4084955cc505d7549a607fb24fe98ce2264d576d181849228d784cfe05fc9109c041dd32dde264ab37ec64ca278edd59f2d252aa228e32eb288a823924d5b0fb82d019fa77152506750a99031851fc91177bb0f864528b8097a004f9e6e58f0071842a881f4de09722fbad364cfa8f05110132873e826fa608d46cf5fb470a59f0a617b456ee6d45f7e47dd00020759320ae2ff241c757a485438251a44885c6e1b86d87905bfab895a600668266a81ca6211883fd9e5dba548ebc3b3f429cfe60b57dc9bee62bf302f1b77078fe02ece691159a38f622ce4f455235695c941c911d901f3228be38a9ee6b1f1ba043829113ebdda770470e6d7d90863e47ed9030d0c5a25b9d9cb48aabe2438e0eaf7ba9c1e153203b3bfbc88c8c60de9f615e1256eb5b99ee455a7cbddd139fc73e54087a72500e28ea9de0b9a42b4aee5b0870e8a1762a4c941fb853e8428736b7be24c436653265c3aaa0cdae10fb1737403092ca4940fe4ed26e7de8f009afafe1a66cfe0eaed22c5bdb802f6edf1a93c220407174f70ac5be6ed6d1dbb99c25180b8d83d670683f11e26d69bb50a12999f116c7c5659101062b805bfd6b35b7662bdcd9b8dcfac5f4c105a29bef37222082c33a4b0220ecc5e445c40f01d8c051c78887c3a7144c5d5e8d09b55f96f3e382085c880ea4ef5fb01c669c90ca7f52ffaf0a6f9f069f5bd5cef2ebeea4fea1a039b3ec19c80502fd9d5232a9b042d0b4c4af52cb9512f7fb05564de3a19756b2c6b584696df7d2f01d746a8d34985fbeba9e5f4cc08b2705b637c552635bdd222a47957186b6fb6a925d76ea44d6a85647347fad6d176eedd1f87ece96560a919960894be58d9bb07d88443c1f549595724eb6a9cc6c5bf08602998a6b577770375fa1cf23206c67c992a05d8d5281c5011c9cb08d0c5b827cbc5245604a4c92215ac10623fe6a6ff362f9b392a03fc16620c73153ff0178ec73050a56ff38c0dff7d2776583e2d8c29bf2b40a3923f1e46a66d959651a93303e6302fe222272031928c9c15735a3cde834bd99fa921509ca35f77d942ee974a774238fb8b5709c58251a2fe48e2cfd67922b5ff1ad27537d9ea2144d8977f65631dd8e28f141a5e514ab3a56456d537d515bcf4e5daf71381be84cfa5ef19ebebf125e790a20a585e64d3c27fd818000cef783056a02453cb09c185bc272aa53cc63c18744e22fac2873336a5c001cb6571caaa06675a16204cd9d126a1273897de4aab62f41a866eec9a76334f6fbfaba7ece638dd9a3a2e762a57cda4e47423d3ea4286f516f6359fc138db872dbdff901a07fed013f97bf732e9c49452bb5b878a060896201f009ca90f758ac9a5edda7a7462e6538a3feea839978c61666f516f34291e02c64ec1ff53e8632b33a97a70cfec87476e9eed10b2601fa912fa5c9556dcf7085299616046e8666328a3c76d78ab5fb0b6cf3cbef99cef3eb6134c24f4c1a01f5c725323d05cf1e44eade6755814e99f5f417bc0992d328b5cede6e0fd9e56240eb20143b8f4675a1a686643470add23b1ebe050d780cdd43701e3a47020fa2dfe4ebfb0c3818f0bddae7c3723222e94fc1f845e3dc53827207fc22d8391203033e57d5cc197512873a161ee5ca42803dcea1beebe43a56976c4ddc31fa1f11be6e3058add663ddc1db5bc5983a1fbecbac68aa1206ba7f873fab066b33c4904f1cf7a21f0f0efc1ebf9d8aa978398e632d224e271a91f3f37b72bd5b63e420ab8f2311c42e270652edc9bfa503d6ebed7449b16720482a85ed1894d66f69709a1b3a582610921c5ce2fd1b16ea0004dbb985ebb92da7645cbadb797bf978b1486c8120009332fa3e44ac185041df995f3ab6b38871c11268022bf3cef02ae0ee910831c4a3e6ef591e909db55a8ee8993b8886397309d37a7fc55384603e31eb8aa700ff632f32be4477fa86dd525c44c35a94b6065c1282d3b4188228c64003e67b816779b4dce58be39e4dff70c089fdae7edd30dc461b061443dddf6932e79126faf2352ee17bbd03ec0b448d8c44928fb8c8e42e000488e26c0cc35161138d1cc6cdf6ed6000e2f64de6525a6c54239c4bf63ad03adf755f92cb6d1411e46e6fa4018c05a0ab170483fcef36b173e94d9ffb8c181a9db404b02c5ec28139abceb1ce44c1671ff8c551214f9bdc372a287119fc674ee0842527ef18e2219058048eafdeeb41dfefa01a1b486ca31e0651dbd1fcbe9315b26efa246c780086255939890af834f8a4eba84d6b06542ab097ebe22ee15e0f554cf548dcc7e2fdda25af1645be3fc379adf07022fb045a6a312661ffa1d399071b5d3956296227629728b30ab6e5b48a083cfcf72377dabb06486e0593853ae6adeff91244c058943f6b61350de45b57e3ec7f8b774811f01e3808c4ddef7638563eb9fb795068888039827c29674b2cda50d7e6a0451badf739ab03f9dadd12c3bd157df9b0ad98917fd3a3341aa0745199c2c2dffd4dd8feb2ee705873217170fff06d32c16e6d6be1a1786e0097c330e8e36b70be485cb44e61dda92214b5071548982880715705d80b2d26ab9a9ccbb02ca0340e851463092c211e7c1401b7deef646db021842024b5be021c0f9bcc3511d32a5e6abc3c4f03044bf833f7be095f15a660febf17fae3c4e5c87d548ad45be9baa5eb0734d213890e2a1e5ccbcc03ae57312260041138f301b743b9498ecdde11bf0242f433d03862f291d19fa2ce75aff18c2b6954912a1e804e381d63ca1a5cffb45df6955d09433721589b7a52f1f050391abc7a72483c22ef21bc1efd4acc9100795fb3591ffa4c2e4b7c3ca02620006e8e92f33e098652cff9929ade6fedf1127bf66513688a6ff2e9b81cc862fa60378c7a01fcab3adeb60e5b735bcea040c4b24da2f71085d1801797032b397b82076196cd77fbf0698e02ad6c6a550fdc6bafde7bbafc26f52f8ca635cf3197122f8566c8e9234b066df78957d76a5a8bf8846729d7abca08213e774f96e68ea20caeb8a40876f944c3ff34e940985befb7786bffb0677a0538d31426febc6141aa5eec9389393239332252f001d59a87f54b504d20485e950b48fa359b0a6c1073fa0100d9fb6e1f416038f38bb24b8ac404e06c05046a7646663fcb1bfa30f1649d621d6bb74b33e4ffe0753f15fff613e5488ced761c0e8adeccbf01581cd2463d07fb57794120a78737bcce6ab0813093c7194604eef6da5cea0bef9606d058394121e37a55260d86c86e777805b2e28ff5be43bcbf1cf7d1937f272ef8028f2ad5e2879b99ce7cfd57dab0e767428764b72af81257a2f67169b15fed22621b6da3a1a1642f4d74ef4d464ec26c4b379788664d6b21e7f207eba72782a0d2f498e683bbe8b4ba9dc01e4aab86da15a354aff34ebc87533ab66fc20ff5a24103d7e20db011b308414e09c0ee6b415bd515d2e3fd6a41126399b4f1f1df32928fb69d255d2e1ba8d13aec7de2ee5e2802d4d565cab1712327fcc68aa793ff71ad62b37f0ab88a15e676197f9f3901b5fbc87f52d67a7dfb4c1eb2477039a3c11a3b6e7863481e1893b533fc0efdc6bef91ee0d84754b73d86850b6a32467270470f83a5bbd9ee5ba34a98a53db13754ae763ffcfa5c1f478136c530efb3ea021477026297ae48ddb3f41a59e2fd2313189d8c6c96a9581824106228adb359e05caf6295101b1141cc38926a4be6e5ed103ab85c36743e69381a8b66e8d292214bd596195d8faf3e8bc392b12dbb3425dca918e236ed735b492a1d4ab700cef102908e9e596969502f5da8a8c23f86eab92dbd21e38ceb9a61dbb449a31e5e82d79a0ebf390b0f81e1d24d0acd2b989e0495a1b83b039355748cc5481a1dd2b01ca0a80fe7830bdeb3da47a50281391cfd69a842d33151baed8219baab95b5511ce172a0306c412cc29f8d5ef03f3d2758ef9ab5e8165b1993548147ddda99913cb0c93a84b6be923a76d049538bcea983c4528f42408bedfe4cfe8b3e46cfb0a3d040be11950b432c0c5f064a3f1f348db6eee8836e0c606098d7aa7bd3b660711c5c4e81c44dcf057175378fa5718c123b2b513768ace32439dbb00469834081b48cd7809e8418a3c051d9b83ebf1afdd0191de1873879402faca8691486a29efca022e6a3a6f0604274f4e4b81dbb3ef73330d50307d548b9ce880847a33092c8e505cf48010c1cb977338f21f78bfa691fa00f028129e22ac1fc9953072150711f248a58ff9a52947015d4f381225e3f2504c363f6d1e396700966be455064a2f4eced0d5f38a24146710bf0a6f625f16147cdb44a5b8f359d1a8ce5f8f1911dca8339b5b92b43efd5534c738e413b851dad2c98fffe372c09c39e62d9d01db97af19fe26b327c91de9efc26cce4d22b943f0c7b48029ae69c6891b9a5505ac0c31c8f04e55f6d9cd4b29e8e58a4e5cac74ad6303a325163e763577157217e03ee66fc8f78390b793fbf2f2a9695f0e3546ee79f4032155c10a65a144dc163eac0d0fa0c89fb0a75d72f1ebe34a2ddc7f9d910e5006a45608499138fa2d012f3c32671042a0b651f3d6ea15c4ce2578d4978e76bdd4e1b152409edf0f011d2dce87fcea0e3a6af299bdd52905fc82dcd3a03df71426144384fa46cc2634140b4a4e411221c00a437d3a4e9abfa02529edadb8c50f0ac119d9e08becb50929510ceaf6bfa1a65553aa2ad2d09c39a7f3d4c95f51545c73c375354ad73ba01b43d3c6c2b29cb9c6064112b6ba974879a07d6aa70b167d505e932a81461ee00663e298bbdfce59530dd76df4333b8835f51931634e3aac49377bffbf59a4d618dc2cd73c60fe6f9ae1c7afb67f82c5d3800849740825a1447264450811f669119ec41930065ac2d1a32fc07593f37ccf7beaa7743679f7316d38428136638826f98281da06f848236e8988f136b45b861d6f91cbf48b1ec65a1c69c9fbb7cb093b0f40ffca96370463a80e8c02ea4a8c0878eb701c149b2f61647e3205cc9f0f975802d0dc8b356b756788b571d7feb5e3c2f26796236b4a0c8a988e3c7b982394638e0df8c0c8b26c30435e510a1e8bbfc7d26f52573a78669ccb1577e39726136379add6d5547f91255990fc5934cf370daa0486c0ab661f823d7a290b2e1593488ebc7199818204eee1ae5907979b21b3b822daf6cedcba107956fe604a09124a2d133e188f0fe0ef7d98a48766d5fe223a1721db38c487802e34595afe24b859a2447411040846fc226ffa952ceaabff7aa205f28e55fd21cb44c134f60fc0443f6e1ab783538d710b59858b103ee80219e6397ddb9e7b60f03ef0cd16171e91555f7bab1352aa22c63726aba080e8e1c5c987a5adae261d1a8bf8f9112bb799cdb72f9b44428ab1338a0d4f7e33d9d44563cf099baeeeef8d52541b690bb38ac99bd6929de6d91b2ab026e3dbb1d840682610a6d77b75408c55443f470d92eb8effa606483e98b49f5b2a96eeab50cd0e9625291c07430e474c71db8b29dd2868fd7fe8086529889ab43b9ba70db8d9224dd7dffb81ce807c3cb85a222a2885a10923b3e6a330af2d762ae926b27528760157950665c6ed04f0076f4519c36963864ab5dc79bd320ab3276a089b9693dfe42b3b28af210f98a8a7300e0d85115eb159f0d7fb92880da77ffb2580fb598a3ecd223efdc03a1d720528d72feb6346737b4da99aaad0f7c78bb63fe88d36c16b6686bd316f1c49aaf4880018dcda90697511104f31c4043c65dde6cdcab58f83f5f3586892224628606eaa169d86c4b0a1ee8a90d754dca2986780f35df1cb98c0b27db2fea9fe1c039bed1012ab179e2f668810d6e77a4486b9c51d20d2b6ef89003b049f91dbb659ab5218daf49e59e1fd9b6f94e26c0ea5a24eb3f46ca222a3859683a0ce1ff91462971cf3d0c19992a60c81be891168116ecb05d3ecee12c325d6b6f50262ca372ed1110d6e3d6b7a157e0b4f83ba8b74f4a1473a15f0d6a0f862b88f49a63f8a35300b4ae9a33093a3d048cc10fff40ac62572363788dd5bd2eeab14ed794608fc430544b130b72d33c2960f116589afef2a785e374fe8e9e7203637fed3b9d2f32e2c28329265705fb9724c64e74c75bdc586be7b7d77ceff059837481114b12f6b152077bc4119e5d45bbb17f4acc4d3b6af5e30aca85e1b48e49dbca487cf4f402af842a81050e8ace1a869f3dd88c56979bfe8ba8bb8408f82444858d7824add27e2747fbbee8db4f31d4de2209ee404834deb981d20f3bc26fef6563d3bb50312519586596c1ba8fd927d2a57386ff5c678bd7fcee1eb34c15a8c49d747f060283b39b12278ac040fed5dcf1f0db49c70b5f57bccbff92a900acda8746c647703a7c84ba107f90918d8305c1a68855a1c17f25e312f482d15551fce83b44ae9101ee315256b3ac359df2db941fbf3142e7a69b2f93b5e22ff9c3f7cdb09dc5d09a770d64d13b3c595e42114084f8ff740ce2dcfccece34e9cfb3a4c263d4c0604fb75501c37b6b0e464a31e61d0b139168dfa6ba4a9ced82c0a69c120ff6e742d00c2afe18c10b154da4195e965db6751c25a9fcd5cc7680364368be836a33b2c4c6013e7bb93306406931b533d3e1e8da008a57222de02df41165c09c9aae22808d3dbfcec2fd4d81bdb485274d423003dbe872d2eb42f1b86c626b742f3e3179d753b57bab4dee80c13db0155c35cf56b20ecb32b430d0abd0842bd7c76cc081bc3b4ff3e2c81978bc026446853c14b1f81da9e3e099023ca63d70e12bd41093bd70251ebe6eae6e79442ce7c8a7eb580229be462a13450f8acd6d398459f2bcb312e72960b106f825a48df35c2dffa046ddd9c8e332f96a724c703f263262131ff63fe06430d1c983eb979389c116ca137076b8ad0d00a5ec80edcc86123225fdbde2fd2d82c0bb888427f0e1c527ffaf00257ffb7ee51ef8b008966666413226e3c049ee5913d2db3f9cc6f5ba4af42591e370141301a5cf1c7a692625c1263010cd67e8952d95a05b1c81df393eb60073595667825b5f0cc0e127297c8179d7cde0484f2dbd6f261c7028da10a85a901bbfd4f9f9b2ff7005e43dce3c1160813f74d30acde6ccfc3b93fab53f7c867808fe97da0b593024662ef3eff2a026cb04d1be9cc174e25784956b2e00a4b4e16a449aed0c4c0e9e8a826093a751578ad311a00508205d129344bde76c815e82213bf21c9b6a394c19448b76845298999968dcb13d2dfb4645b5c80a5e3ebd39e858bd10c0923d57f0fcf7e21b901b455d73fe3bbbb8de63a638245be66c86c78a31cdd4912dbc01159595360a21f9a3190c6243d11bb96b8deb3d42050388cbdfe3a1bf846c064500c24b8c98a073a02ba17971b455c96932b312741d3cef9783ca86babdab0364fff267e02802ad620e8e0f1127aae5f356cb42748dfbb3ad087dcb54503f4e9e0074a9572b90dd1b9c367157aaa3d73a4ff6362f70daaa6f455e94d24cee04061591a24b0201e17fd576c1792f854f8f5966a93df69bff783cd16a90f1d09142fc1954edad82ad3f32aaa6181cee5807e263cd03dfcf436856ab3cf3688b88cd727b4ebc757305104a209ba3bb124a70741d7d2fa1109b161fe5ba9db6ced08dda025e3c6192c1e748ce535713a9a07a0cedaaaded6f339c8391a457862c29d3f42159e6aab154b374d0f463f0cef9f4896b96e90f66462f2787ff38e6fb0ec15732ac8f5d92f77beaf8493b88b892a6e4bbea3ef042da88f5797e2abb113cc66f7350463be10a970c1ed0f7ec93d42876930f2bb00f4da2ce4825820779bd79ae6e05e2bb00098395180900f99f11526d111e76fa2c339151526609dd93f1cb8b5f04366f220398eb387e06e20fb6519e9425c7214af638a0e48f6bbc96a4fbb04c6db70f32ba6ed107cc3cf62fbdb88eaf493b4622beef744f57525c82352bc5b02a202bb22d76bd27128509b2cf5b1aa499de13ff9651cdc77919330bec207e2f55e0a681035ee3c422b0760c6a5468f71790aa77edf8087167c1024f5c2233eae6ae58d15f0f85c3807b4ec3d6e9de0ce33373fc0ddfd58882aa94c618b28aa6a75f11d199c1e71d314f66696a871e4fe2c08c1635a9daf031471e92bacedbbaa6887c503a659f77553d85de2cb4b3d506ba3bfcf005372d4e8b29b75663cf578c72e8e2a82ffad3113f5dd25ca8c311706352f34f890055402035d3289e78bca07775f14fc54b14ac74c76c1a9495af80a1e9076657ebb0b0d0b940e8c04290b6508f622e17b0e6ba828e253e6006430f46ec7744227042cabcb4bdcf6c380a66bd0840f27012ff4022eee26b546cf14e351b01ecc1d6916d0645c2b2e8c757410589e03267adb4e8b95edeb629844d6945702b94488ce53a853e4a6b8b1567d1704da23dd660962ce0bbe4ec466a5cab16459504254a74b77604f4470840faf6989c62023c46f7524e4c0bafc47e20f39b804deede6cd6b861e014c897ae281cc74371387bb87c4c821e982b648a695d3c5e69a8bbf5db93aa8342bcb72e047cdfe4d07225d309f31d9de2204293ad9b46dc1a01506691bb5d0674f20e64640ef7f5415dce858a5739045a7059192f4f83caff1c752c7038068fe2c7a057d55eb58640d89d1b98ecd69cb4fd455540530ce8c1269d22d04b942c740c307b8c359131b28373b0d1323503d1bf23a0f1c9864f2e33963e9b756efab0bd284b65f6ae4c80d22ed7255614f38c6c75c79e982cc6bd8be465a328e1be1228a496f0111fa392a75f9ba9b1af6369c7694deaa7d60712b3db02ec1ee646f570ec000b93c4b58241ca89a1b528a896fd354c0f6cff2f813d2e93831edefdfe828dddbb003b7511508f2d8ffa83e88412e62131ddc2e5aba55e6f55d73c2d4860eafc973512c901bb2fa26058497d20c586b3cf47165a547568939687f34fd19a80401649b82bd1579176a843eee7a6d9d796b2e2e7974a840d217a3d6d58349505fbe74467fb51f207baf8b502ba881b9179f4dabca0c1bf9f924bd663cbeac50672e620d412c2169485665c08e0c9ff827174ee7b50bc4f7249aab8222f68962ec30dfd341601ca57966b639a4494358124c88d2a96e2fefe3ac36e390cc95fb990b8aa89e2a1d1417644c5f868603e8ed4090ae52fd74771638fdd9f63e8cf60edfaa51ee752ccb69f5695aaaa35a7cc201536a537093afd47b28b39b0d260fe0e965d94deb1f8ca7a57a02a776d0d44ee2940304c525119df6f25e7ece35d26578be2b8e0902d28eaae5837b8959737191ee9055514c1b7c3dbf2a2874aa796afd2b520f0b0521dc562ed38d0fba5fdca05a6525d43958039370b6ecea6c2a1708435041eb012a45067a416744ae4f84e5e0951219584c0623e7e04306297a0ad73564546d056c3842a4034403bfed7c16c5553064f99b4462ba213ec569508d4fe275b55605e750b9904b5a8adf38c3cc867b78dff3b1e69ec6a9585faf28376fce93c619145ffe86bf9826b264ced464b6bb08d7d111ee4c69e0623e6603eaf814ca0ed3130b6654b8b4bd3d7770311f84d7f5868ee8d06e8485e5064d955bfeccf6cf290adb4ba3155327227a0944c1a79f9330a3a8a04e3916065ad4820671a15e5e901feb21a90dca00117638a52deb20e15972bad06a9a8eb5c3757814a4ba4cd441091cf87ff0924c94d2db4035570ca4c79d4fa25b4bd469ca40dcf7eadbf5351815c5775cc8c616d22386c77fcc2e7dee997f59d12ac57168766ef97d6951ee7121a663f3ffd789afd6fab2d1dd2f5009a55dd14e29123c1dc97ef4137cfd0ab7265230345709160ccb5a4d83792748091b3af89525b3ee8b4806108c94ab16b41844536c838f44d20b12efe54ee33449942f15d8d0a5cc27fda9caa708f0c09a01ce3acd80d561b4617bfc8c72a78081776e3852e50bcc8ce177f7792a408b4901d19744246f0aa8db9b37831818624ef83e2fb08c0301e2cec92e0ca046fc69188e1e2ac4e21c30a454e940abf182410d65df0078346e67dfb4120299598da3073c23b45b0f480c7fdd1d85e03ad0fc6246650c3202908f47444aa9001252751d54f4e9e07eb6b095f7e9c8d20bd32b1bf6fdd14f1bd13b74808a69fd87bae40b7f1519f8a3731ee9db4c799e9b2aa0f6abd6e630bd1d75061fbb40a29628dc2ce263e9457eb4af5ab3370bf8c8dfb476c33c180e6528e26f53a21e6751a3ca18925971834ac07791322617772d48a9446c87fe4d0b9e676ee69bcfc48be7e2275502674e932c7a91bb3c24debed288ac6ce9225eb7190614fd2c1b950a74a01913482181c5c8bd99587944f31f811638cce968ef2158ec725b6a06e506a347282752b2da594f1eab83cb23c0f4d233b237fbb392b65c4cb6fb544e8bf42ab012ace26487f6fa01dc718a6ef0a0bb704f2cb49c83ca4d7cf16d28d7168fd45d07960daaca35f470912575cece446d775455998e751ef0e458a4be833978bc600debe945cec0f8b4bc65ef4884fea828377e4fd012c1f141fdd829b17d74ae2e26d890ff416319bc3b731f08896273e0dfd7f6533a91b94af6156f062a51e90b2c71f415ca86e71be8d5b82fe494a5389176186b83816e8f53b24f245385a4081ec8c371d79c8b15c0d1953057ab1f0f77e50e30f70598665fe100e4c2f7b3fd27036c0a5e3dc63125de7b378333c48ad5d870e9cf2cc9d6217f0c2018e316c423eeb09d87b8a7e20fcf12478c687adb2897a1162fe01244c0a3c861104214ea15afeafa6ff335ce367e634b11c6a614d8d539244d0d295c89bbd1a3073296f112005796b95940e1e205b7766db30b54253e7ae416b5a79e1e1a1fa2785be5d0293f071b82f9c361dc0b428e22debc6372c1fab0142ddc1c1749097342570e22296b25f35702f4d8e12a8889359adc84e953eb31167a0127064c0ab20777d3ba1f5f0739e18aa32951255ccb8c8693a32ae2400dea3dc0b1e501d6aac58a4b911416da366478aeed42d139f3236f5212e30007e5d9f31e5788301df610040d0613235c82a600d4ed309695ab81046b703391c9e3e7a99dde37362f741890792327ae18b1a3b834aa69c6d4dccaf86651d8fbac78848f51acab542b0e2bd1e9c9266b9cbc7f729f610a529f91eeb0d33713f007190a799f846a53c14fbd32d0c527cc2e3f1cc1616ae4c93b6ef0aa87acaf4ff571c6dd942a3f9afb4787f7618002f4e110775a1d3fcbf83b8e3383805ddde52f6ab3f542f1b64b98e82831cce02863a0cf4072b7f434253f880c2efedc1672b6dd1d53e35637a6606f9f3fc43e0a49062dea4c85413ce3ae12d8d96a2217554e8e1e5236b2c247ea5e106860112e56b1c2dafe5a390a27efca669e689a07737fb7f426e0e7bf7b8c6e3d41012c09b869ff0417d3fe026fc10cd82cb17e6673636f6f603f22b9a0a00f59e275d60199a027ef317f0744ac4cc89fd913ca130328fde8092cc45039954cfc3aa3a9015af0969cfc56c65e8014005959631d2d38af789a2290d2d56da737a5df18172ce2901202435984d5519c7da021185e878e21f43da6c58224ee1f8fea7ab56b00e44a22fddd75cdb11f1ef42d5b6645012fe1b61d7b3bce0070048aeea99a94263cae6778a65fce7b623fdb3bb399c30cd3933e51c1d255be977aa89493339828d828ea8a40e3a3d99d13878ee92df9b2fd2d8619c7107bca5b1f4a0bc215fa014ed4c2322db81204c52a6ddcf8a382bced9b6b2836303d73c1ef433812bf8f27d7902f9fb44ea5fb47c12884f37d20ab75ce84cd254c3e9c87d9aa4b9b743912f055bd623571938edad6487febd3cc92ceaf9d21f37d95304debf75766e90615ee023e4e834a838ff717bf264b3ba0520b21200ff9f4365948a64ac18bac341426336a07ea68754866bd610ce9177034a5f6b070160efbd73a82572e348d3b2a68fee84a64dbc79a15c06e575a9af55ec925cd41818f950efec7dbbff5a3d80fa9ba39bd2d77e33237b9faa1d8a76d05a3b82f4cf0bb51840e36434a1b0a1b09c4ebe7c8ee49a3612fdc309c020c107abb4c5c86101b679e1bff3594e7b436221f48098c11754ac213af9f8451a2ee52962eb7d3a55012d1efd326f43782d81d9d5fbf9dce8f7f79a8228e668d84e1548a80d96fd498a8f27d9c6c3acebb5a266f24ad78032db283b049a190670d09ec9968d1e387786b7d0bd44a2af6e4af246d23edd4e5d34405d359f27a86e6a6339b8a8536519776f8cdef7d253469772af2fab2cce63ba73eb5b9e5a906be41199c89a3e8da7423d79e0feff22a84760b112d906f054ff001875bdb89f499d89bb19b179000af815e83922fb9db517318ad3f72ef432924b269cfa978c1f27e5ede2aeece85ab3e19cefaa3e89cc45a18fcd62a33e6fe08c46a646c600cc0de88c228e382d573ebb64c6b1ad367725720a61548a2bf763050f740f01cfb3a44a01ffb3e6341eb956c0fc8265c05856c1f2ff9021b1493cc4b070a566ce70a18c5fcf55f39cdb56a6992a8520898de5023b92ccd15314aaca50860a0ac9705eb8498eaa20882e88e55d350b9259b5e542d58ea269c9bccec0cc243ddecad7caa05cb70ee16af7d10faea3a0dfb4d548812e4758894d110ee422e090e6bf18ee523a36403017c05f17d6dcff4e94ccb640d63b5a0528813b614ca310c3475394036b7167040cf51d612f0f1fba5fee058086e087946eae4aec7a4a4167e0c33000f0564e4f9515dec1166a198405264ae0edc9fedb7abcb71c4290485d8a96ba583961fcdbd2b3e981c77428ed93857e601c518b9f3bba6f3a9988904a95777574e7e2f81e212bd6a2136c1362960ff861ea4449297baad38dfc6c7db208f13bb08d3f84d05649f276e4090f8d26aee1a20d2a3ad5648229ea5ee2c4843a2824179d0dc493a8a9ee86d46264614ddc0dd1c8bb3fc9476357de3c6fec307241906d97e52b1dc72609e77d51848b85d935713f7617433dea840cb3be94507d2695878c995f36bed232024c8e91577c0c90c0317ed28a06142d2fe1c2b98b9860434e5383cd338540ed5e992e4e6a5f0ef3b1e3ca9bd060c52024cd1e209a68c83e120a9405261a7f7e21fb2e882c1d046a62dbe4472bb99313653b1868e9fff8ceb08343c73101e6e9a3a3f60fa8d39b7b916c16b6d937b47fccd3bac2f013c587c868c50039c004767798ff1c418191d9218d883294ba8b65358fd87ac753112022168349a34fa3d154495c673667ed7380ae814f3ef91b42f76d03437d65eeffb822ba3ae43a2c4db9520b06cd25abc640012c43f5161881a109bccb0bb7f9b24dccfa71042a5183ea4427fb0da4317910f82e949a3b995a2d703ba920463db1833dec02ad2fc2866ad7503e59ed407a80991b09ad76ca8e9d048609c5a2c88dcd09d9b100f609897bca38c4153715dfd0482bd77cdf3eeb8f82b2a8f9f7e76765bfecd4ed58661b3c193d3db4bf3f2b52324d385d256719cd0c323a116f0ce68a5975e66690828a5298b1e2acfa9dc2318af4ba6f2654acea8ab00d4b68969fc79b4db8999698bfd5cdadd931c84d4e91580c61d5a19a802777bf9b2e579446ae69018453fb6712b77eccf74457b029b1efee0b214f75ce9eff7cfb59482e44750e784c51869fd4a1ef744a6b6d04e5221065e90de05290bff1580464ddb83cbbf393c1a1283117e86d0d3e00c9eacab29330ae4e7443d4a61f51e54be0b7eb9d3465c8098ac67e2c5be1c40eda332910106d747fa2d4281f789e35337791a54e1d4dedf478680f4da619c58a739b27b14ef4f8bbd489d463a37f2308dcf66ed1bdf1e4d9c7afde84110108cbea1ad690c0f78d2483b68caab88d2c54fa132d9253e01eebc0ecfcbef0ecdb647b8ead2223a9f8a020ede21d21493c1708d8b2c411ef85ecaf2a7221c6f9af69b7ce5990a7bb3437d2e3ca88e0812d7a6cd884893fa70dd996641bd99c3228c182474da143884a4f84ec24645344eb25f5ddf2e768bdfa1cda784e9ff06f2cb9cf62a4d306c2b5be8c6a44941d947a33ac8b97687c364a13537d185e8c9e32e4f46d8f20c08e592588e86c51a94fa559a28e75163e53cf3e188988971b2c8834eb4ef862c8eab775a5028658464d239b40200bd082b4f1fb8a506db225ef388a53f06ce14ea875128c65dd4bebb627f2d61f608faa9b8b7bee45bd60c31082c05ac9dce20ad4ca5667eb0e47bc6acc72238fc8b277cc8e449c084ce909bff0d2285ea72199890f12959e3b454fc13bb6245f0df5b7e0e869e1025e65fa3f608aebe7a6b29c7ca1d60d0a6ed01b9a83bcb0fba91542db35ecaf6deade84cef0e9a9ad64613f6f45c6979b07071574ad99d28efebea79790e57ba15c6b1878a75173e68c3189b7cbd0fd51f9c2aca367ab12fb9c649a32cd8e5e71b4ee1407fb12350b512093347702b91d793c220fdf0a70b5bca60bf19a24e5de6b03edc4dfb82cf173a0ac30557ed4db93dfa7a12144463c8b21cdc2f8b31d2377232911e5544bf0be01221f7700bda5872e41916ca85a32c6738ab248c3bf40d40bfd2c89378752bdb0657b13fbc947a0a85fba188e1e6b0b151f612df4ab2ccd41910ea67bbabc58828dc3056c82bbfafe6e53c3ce732a6bf3ba609e3162067ee6c0dc9b2496f7207252726afe820ec7ba2aba146c0b4727c2d2cece0a5da7b45cb6800b5cdbb92a52f445b9947a4ddd2cde8e90b7c62bee8275120b6ee4a674d375566207da8b22d1f85735fbfa912aba54087bb3259f10f985160ad687737aa0134f854ec88c9782a2525ff100247ff11b0c5535c5d642e2c1a0165d9931864a87ed007cca4cf1202b5f0d2c068159cf3631d205be762d684d98ba6511765695939bf2a1af728fa25f53d1e05c9beabf57e0f2f7e6fd6201b86d12bac931ffcf33503dfcf5f744b296a5cd88d7da80aaf7ebdc1dcac3d2beb949f42f3242ba3264a9511528d23ce05eff24eea8ea84ec63a796f76489d64bea34343dcf69312f86023f46652f63c153490a078c3849624c1ff501dcd2b730fb44f2b210b93466fa575a30004222f2da8ad66767280fd0b895ab49ea94420fe438af1fb9a1bc3bdf1f8a428af2a94288cdbc0b5bb8aef4475003108c42d41e32e16161ec8b88d3bbf8a60179dce4c14c21a10ae1c4f128b5307d633394ad1fc45024c8f31563245c0fa5f8609411e2e222f776186cdf007861a7e7bfdddb73878bbbc0fd202f8ae13ce0159c9595b24d3dfe75bc300f75b8bafe7344a85da4d6cceb955c92441f672fd586e6db84e10ca20b6fb68b62cf6cb191a3e4ce507f7e409f2ac1af63c78f14c8951c203d502d3375b3709fc4840275d4266b36f2853dbad14e6aa2ad0acb52e2f65f4a464015e9c957cfbcea4ca73ba0788d26c61dfaffea1b09c092b77a7131b9217a62911f910b21cfbc0941afdbd58902be744fb0b4995498dc51d28884011f59d871010297fe2b0144b83d25de6831d30b2700fc9850b3d13ebb1ccf50dd4deeee00d24f7f1dbb193dfc1ba7cc8456957f33c4a81d684507f1d9f625352084642a1a603a911f616cf7abd9a2ba3e1fa37e1d5cc9235440b4f479328ed80d905115d17306ab97d5656e24e42a155779b45d26af991c84a2c04555453d823d60b18c6e921df024054df4b000621c79553a6f0da912562fa57dbc127fa4f9e30024823d12e1bb01354d89442eadb7991b134148dcf7674e11f316914f53b32f2d8c63ba11a07c58966883cc8bf79b497c1173c257f117974adacf520ffff367d288becac2bbfacf1e77a4fc00033f2f7472eab668cd48c2492a644802bd737dfc37fc13e0b43375b87cc14f424979ba0a5a1583626c9bffdde54ab08546bcdafcc3c089c2505ada2fea8559d26b0b40a70e589bae83dad27827250314ba0bdae91078f1518601239280979f965ae75da2d503a2a4ec04c77d5292725e9f8755a36dbde0b0567c3b2bfecdcfa38ad23eb1358a910cfcd1b8e12fead3f90c2b6d5dcc86842039da471b5ca80398b3fee9d9772ca7d5528a65d438fa2a935d647e88e00c42e1a214b253c034c6717e086eb7afcc14c951fdc9de418023ff2fcbd4c2d9508332ab193cffc4d8e86bcc26996c7929bdad197a81865af7d1d3be3203073b5029705ad70c1c16b3db18b094f19a72b72151a1e0a6cec3bede80a0a028067e3c8b616845255847f4b8877675cad09e49861dc19825f28e6629d89be300c65d2c8ee1c613cd4aad8b826fbd5f2a4676d1f87895e832f1c3ad83e70929c75e480146f2266915bfde4f20943aaafa9fd18e16095e5cd2d7ef07b9691c7600d25a2ebe800885859bb2fddb21559259cce87dcffa2b4361311eb8fc42e8a991508ed5e1a157482cb1ed7daf9c790733288348a3b4ad66dfab3bc652aea2dbcf0fec556640709e04f05eb48852adcbc6de3d4c6a0ed037236893824804204f14ee82908db0759e1754521ae4b5e0f9b78972af39033083b9b5bcbb624bc2042b600d862fd00b30e4dcc61e8d6b36d4f5714622cef8932d8af28bc3165a8c0a14a2ece721418122aeed8241f5824b460bf7c74987123a82cbe495b01d673ba910b5be2456a29955e1b55103e3a020154eeb3826f03a637afb0f16807207c78654dd85a7b8629c93dc4273caa555f5d30b7317a52d990b43fb2ba473e7424dce7979a8e76841bb4fdc1fb2421e64637455412f01af46b0306ff544a67c2349e63744a1e6e3b095e55306fa60ee834435fee8a7040201814e4f7cd93ad5eda44c060ae78a82e110538ecae1a0ea38380c5a48c63322f02ee2eb880c4f3a18b0e95d2002596ed04d0afce03ec2dc446ea0d4186b76e78023518705be240dd94b72e7186eadd272077eecd0b2b314b3bf2a40396f89a8e66e8295da903d41272622e683731bbaf290b7b91f1ccb5bf5fb070846bde2755fb1240a1d549d391ef3d78dfb614397a2e9b7c26cfa39f5654ef1d36f5ee6b5fb9f8aad994d1fdfdbe00c535a0a87f3204be9f356461b3d17dc665944ffb2c9db6672be37ca1df67d7ef9378fa0c25d11593332cc1bc7571578cea17976f4806ba27b9c76cbc991c43e6be7c49d450f3299966043a1aceb7fb0e2eac74c1f2228c87626868fe0b59f0ae1c098fa0649519ae39246d2e7478f549a85989447889ad49a27b8e6c75abf01aa19e348921c71996d8629e327490e67a99c607f754b75a536e4e96fb4df631fb4a012fd3a01f1ff807bfa5cd68ebdfab8a8be5c12e1346f482cb74d80a4f2fdf4d7972a4275a271bd8323ec92d658cf6cfae5d4c0c90b3b9ed2e61ff512b4848da37bb98acab18c9efc00c327046b6d412cadfa4fa9602dd42f5367b4e3647e8c2b9dd4f765122d5acf29ef36b8cf7b2c7221bcbb36a4393d0558d5b461b2cac68ecb6ef42431c16faa18c8aedca3752f36a5efe881bd11222d5b3a8f4922d72cbadc66abbc42c4392a2ee0014ac0a80f57d48b11c119d03f8fae4d6992e53e9a768585b09bb15219e6edf84c94ffa40608dbfd6adcba570a0f0e919cbfa5d1422824fc715fb00d861718c3ffc1f318a83da302674456d79df7b3fdb2911cff120775950355b0a86116b18c995ad56dfab51a755f9cb7bfc49beed9a18d1699f51db6f54668024a8229c3df8aca38729006b9e60ba8019dd34a8a367d934a0a1b47f2760e27e23d10f7e5dfa95faff5dc3be7bf866ddb55b330a76135e1a02a687f8110023c5201681c139c58a2dec206acabcd39c29dacc749b231a2d1da5582a337825ebc80c4d71b33974269d2a9b9806532064ffb68e78ce900f1dc37e7d0826b257a08c19226b1406010505dec6d1ab95a1e9d334143b82b02b25b87c37b250270699171627a8ec110047efcac4c4b38b51d5491933973845ff5401e045ffbfe3e6525a290ec6d6bf9ac24a0a2d73289c4b3436d3ce946b0882879cca6f08222660196625ceb5ddd357d38da0587de903998d9ae053b9e299a6a5eed814bb2090dbbde4061ce2b7eb846bfa464805a6742148ec3daa125c1659d9893e82860879377ee805acd379c8b539f5d027d69b5eb72cba3fd46c652f81c0e3b456736eec764a192338419b7467fadc99278a1759e34fc8213fe251317e79c9fc1e4d2bf58490490c7b9f04df55702abed139b6d55dc5277a2e53ebb7b935a10deeb7aedb9e6fd40cb1e28238f1458a48cf61ead022c8c0010359a3dae4272c11fede7371f7a93730239825aa22beb9805b07dd6c82a68a991b91b73c78c2c69c63a48786bcd865207a3f4ff03933078a3c59ed7b38600cea4608141fd61b844d2137ade74aab88005d36a579878c0104210d856240369a7091b0b34f75341101f15ffc15fc75c90c27552f1ccd19edd7d409dbd48d4b9a09c8d2d8a02675a565d8870ee09b032409456b65e7e30d303a6e20f491e112590e16bc169a912d4b10fa6e9aff72669400663ea6ec11e36a01b9b449a01cb400e286a4584ee425541d60412a818db6ad0e94d9178cb5625f7d87904c2b463bef53fa2ecaf00799ca8b3abfdcb72faeee03b1ecb080416d795cf47146f82cf100687bfe0fe1f76804bf0aa9e22449184f28cf5d6c40adf04e8a3cb71e8db6d4ff2baf889aaa3c4dfda7a3f73aee692ce30ae272e550fe53faaa7d6d85c16c4dec2ec23679ac3cc9c22f51a39c4cfc4f0f1f6f78bce88bd39d0deabc896eddc54f0a6eaa8ca6f7f347dda234b9a59eca401a9b6a2e7e12f0c26c7e37265f0c59ab132b1674b2b3ab50b095dd8a4e9b20bf22cd49d8b7e2d75a32c53734cd4237ac5b60b37a3587cbc106201ea3eba12acc0e36972a80f23a5f38c9a358cb9c7dca472e798b6c8166a6b181600fd8d111141ab0c8d611a2c2867d25d22d0109e501380d8791ce7927c59f2c22f5fc2fc0dd02ed3533c8e892aaeecefb9a370268d42dd2f7612a40c627dfa0a12909271b8d2e973ea8a4bf11052cfcbd62b5e42fb1e6d67b1fc9114e89d26566b9068c15a82d1c93cc20db8176649e132f9c78bb352f63380e35060b5e999f2cfe731a00f10c4b8dfcbcec85875979f44d3cf56fc02e9038185b350df897f26aa0a97ad80016d60dc6e264ab95ec09004b116db1d501c6965113b2e2ccedfe16caa390f49f21336bf9b6c3f158577ab100b6990a57f6c727957badcc7c130b4558c79b5067004bbdf04e65a3c3c1a112147eb7bcbf0f023abd43ecda2c83306be1c4f50c3a2518adce98fce5c5d47d927ec0a645e9d780ccd75f7ef04d71a65534a15233a318c2b155d3a4d8c75e4fb0c5357d2bb2ebd6be1f0762889a798aec20eaeb55692bed84ba401742e8ab29246fbea5a87c2aaf58d7ccaa997dca223dcdeae30679072f42407bf1554bc1afbb07a71aac897815f105e53cd9c71b6ab3e7ec842e8c29ff80db706480f0bab8e163e04024622942f63d9e29a7a534cfa30300e353cb21dd57724c282e1a3606f72db3b3f633b4f3010fc59e87dae61b49aac1d7cb611a3302fb947d8fcd9eec3d4adae9e4e35f2a14d3611e0f866c1fe4a75927f6601e92ac973e97e04528ffef66ff68bb7ecd7641582a5ee62a6bb7e6f403c903f0024251bf3375679c5904a5a133d463bf103a9039196de480437ddfdd9ed0be9d1858154b0bfea2a7bd9852d80a6690f94b066511e11780a08b40efc16dc6394914a561208ff33545cb483283e76fce80daaba2f3397a6ba34f6df51be15d445c00e4e1b50d10d67f87d38e423bd50b1b92e131b3699b2407b6fa3e49b462a9111bc1127b5e6c7564342b6b44ace59acd26513569ee5276198bcdb94516f3c8290292561fe7eb451c6c68e5aeb1e03cf7a2600355a518842ee6431b1f5da4fbb02de8d55e5e8037c3b17ceb2ccf0294e8270b0a418dc31050eb50e09903ea54db0b167da60694b01b1668a41d0c204c46b54a6f375875821d62813d1b50e7666d2f5839a001a4eea95859636b3ade294d3be08d2cf94eda48afb85898465a6daa22664b8dd353646c2c92819a5e47762e460d0602d8a9329d831691e2c88747e20f26474eec316fff1f8feb7265b7d1f8f1996e4727b3737df23fe3cfc91e69621cf75963c9a9934fdf4d71529eb7613e67e20496b7050c98a7fa114a4cbc742609043bf42ec4701ed7706fee623041c46430e09b27986e5616467167d795a67e250cd23cca61b277eb92cff7a90d4490301daec10df982f05abf424a5f99e8d021e65071d6f4fe373d6dec117c58f7d2d25203d374572baab5f551050f90103d20476a42984de6e9566136594df18ce70520a332ea254faf19f21f43ddf122bd0e8bdb770b2b0171aff0c95f70b6c209b959cac651b33e78d4a9ca735901059019a2e81e7c76a59709a52662de8953dbce00b8a7354d72e6226053c146d947870ddecb66135410f4a708d63998430535117b9eb440ef8ec7777fa579c0d813e11e916017f4c9a9cba9cceff9b7ccac1c4ca25c00534cd88f0d745d9e2e0c9418257505e01ccd05fa3dd33107346792a66f0f6081f2953f010a2284814cefb1ea2fb0e03cf3137358ed3a80466dbd39c7db442df292366728f7f746c3309c9bbc08bec27f210ae297b1211b33b98a4c5d5394101eb0572572d3b81f994c440c242c0734ba4a41e381fe1b42046fb06149a4e9930f541842e0fcb6b0575a01c45c2de6af17a739e39eb9658aa4424e83856dd9741ff6564ccffff14a364c3136c70497822c7bebc10abee94c845d69121c75e6db653306a982d78b06bd539d948f2cb91514144ceb09797b604705b8ae26ad10a2b6f5dae93fc63b2a16a08dd5132c2d39789b23321089743d2b15ec6c45b2b037a88b9f2daf9bb157b4a69534cc1f14a01577500ed6f65534af0e601cb0b046a7fd5d0d7b35fc00f66b5861eb1625cdfcf18ee6a5020eb9a0b9cf3741b2879f4040aece7106766021ca59c9a0912f5d0caa2444a08e0871606a95eaaed79a23e572898968b7963a0f9a851383fc021af53678dd1703f3511b1431f51bdb96c13c16c9658cec5e0929cdcd21e67127f46bb07549e5dff73e8d0bd8f64381751843647716fa2751d2c79075c783a5293f377f0f3f517837d0de4453fc30e1c97548b77d5a6928ae8648adbc621a1904a81785aa3e963abe8eb73548f2eda74e3073e7acb740418b97c3bedc7a01c61f36d0aa6398f6a0af5ece12c7e47f8a338f87e9314a28dfedd49bfe87a45508051f7377e9c936e85c68d4783cdbab179be6b7e71566e86fde2fc3c3ed71325411d27a67d1f6a273fa3e729738b9dbf91ba83b08e4e0d98cb926adf8fa9e7ac116e2e6f398be4f0798ccae0b418d2eab3b657634c8f41a91db96eb5994e84d0114ca39526f2bc6adc2779c1dcae4772e4a0a29c588c394c79aef2e58c36a22880032e91fcfa21f7bc7f8ff784ee6e7b0014ceeee3b2febf81e19bb1c63d9f18d0aaf8309e40f28246d238149b9fdb44b1abd28fd3b5e5b51173ce50378fd025d15fa2228a8cc4c36c8ad66579f2eb6aabbf1e8c8206f4c3636ef867554c1e8b203ed858f8e946147354a073827819cdd3c22e107165657c0b8d8faf6d0859d97072e7293affa092d1e435f211e34bcb71b0b424fdef537484709cee5bec62bf021a99fa7db57c8c42e5750f3f295b6cbd088bef9e89a6af0ca4bd4c50a4205b1151cc4e871cdbf611f13eb3fe608d015903dfd5a3633b1b72fabe4b47ce97a6d27778c1710371df7e65b27371cebc71514899455619bbd6e3c3ad032097d15612d00 diff --git a/primitives/zk-verifier/tests/fixtures/host_v1_unshield_v3.txt b/primitives/zk-verifier/tests/fixtures/host_v1_unshield_v3.txt new file mode 100644 index 00000000..65a8f829 --- /dev/null +++ b/primitives/zk-verifier/tests/fixtures/host_v1_unshield_v3.txt @@ -0,0 +1,3 @@ +vk=0xe2f26dbea299f5223b646cb1fb33eadb059d9407559d7441dfd902e3a79a4d2dabb73dc17fbc13021e2471e0c08bd67d8401f52b73d6d07483794cad4778180e0c06f33bbc4c79a9cadef253a68084d382f17788f885c9afd176f7cb2f036789edf692d95cbdde46ddda5ef7d422436779445c5e66006a42761e1f12efde0018c212f3aeb785e49712e7a9353349aaf1255dfb31b7bf60723a480d9293938e19cc102854217a33c345729d6a9a2b47d1395afcc90dcac89781b3a21eb60f9e062bcea02635700239f739426700403fcb3e6bd3510e0ee3564fe2092c798e532309000000000000007bdcfd16ff3e36485a7b3be77f01e7c58ce58bb067a48b315e432b9fba39ac2222ac2412d3ffd196d309f9639192745b07b2f186e12402038cd0ba981ac5589c7dabcbfbc1285c5fd1933e8832698b1348db41f16c0636a22791e2b6b3796a2df61a01607e955c6417c4877ef2de7df33c3a0c6e0207c780fa9ec87de947fe9195120e87d825d3827b145b41bddcbcd7ffef75fb7c1d7b38e585410be4ab11ac6a5e7f60390ddf3b826ac0d01bc5addf5b0b7882b1388d66465f657bf5e5389a557b509428fddbf54bc2bda2b450d4ff7b501f4305ec9f57c04189a25fb67f24f48f23f8985dc86b5b936756e41e0694381a73ee558adafcc8307fe9a690a42d943dd0b8ae18c48b6d55c36fef36cffcb40b890cee5d9211aaf0e3d09ba21a8d +proof=0x42872e04e14b38349f7e12abbffa59e0ead0d9d891725ae8f3b1eb1cfffb962d89e76e1d4ce4d53810c20ae6cb9d2930106e7d7c8c4e19eca4e8a75f33c8602e74bb942dc3fdf07b38f71a18e7c760de41e471ec44eceb9e331418cafc2c419ba0eaf3a26185daf076007e556b19c2a1b97775e7e47b81cb045164c36b2f3e07 +inputs=0xf333acc3646658a64d7d850d1c00d45de0cb054621ac1573b8895d34cb0e342a00ecaead242e8f081e1b5928284545ba382d33534958f61e2f13c9a5e95f7e100000a337c12fbd0d00000000000000000000000000000000000000000000000015cdc7dc7b07dd35842bfaf1289f1a53a888b14871aed9c70e95c6f7a5a0b30100000000000000000000000000000000000000000000000000000000000000000000c16ff28623000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000505e8bdcf453a9a8503ed771c10ed7dfe65cfad2a858ef471023a64084ba7223 diff --git a/primitives/zk-verifier/tests/host_function_v1.rs b/primitives/zk-verifier/tests/host_function_v1.rs new file mode 100644 index 00000000..2efe58aa --- /dev/null +++ b/primitives/zk-verifier/tests/host_function_v1.rs @@ -0,0 +1,203 @@ +//! Golden vectors for the frozen `bn254_groth16_verify` host function (version 1). +//! +//! Blocks already executed called version 1 with prepared keys written by earlier +//! runtimes, and syncing nodes re-run them, so a node release must return exactly +//! what version 1 returned for the same bytes. These vectors pin that: a real +//! production key (unshield v3, `@orbinum/circuits` 0.17.1), its prepared form as +//! stored on chain, and a real proof. +//! +//! A failure here means a change (an arkworks upgrade, a constant, the parser) alters +//! what version 1 answers. Do not update the fixtures to make it pass: add a new +//! `#[version(N)]` of the host function and leave version 1 as it was. +//! +//! The fixtures were generated once (`regenerate_prepared_fixture`, ignored). + +use orbinum_zk_verifier::{ + host_interface::groth_16_host_interface::bn254_groth16_verify, verify_prepared, VerifyingKey, + MAX_PREPARED_VK_BYTES, MAX_PUBLIC_INPUTS, PROOF_BYTES, +}; + +const VECTORS: &str = include_str!("fixtures/host_v1_unshield_v3.txt"); +const PREPARED: &str = include_str!("fixtures/host_v1_unshield_v3.prepared.hex"); + +/// r, the BN254 scalar field order, little-endian. +const R_LE: [u8; 32] = [ + 0x01, 0x00, 0x00, 0xf0, 0x93, 0xf5, 0xe1, 0x43, 0x91, 0x70, 0xb9, 0x79, 0x48, 0xe8, 0x33, 0x28, + 0x5d, 0x58, 0x81, 0x81, 0xb6, 0x45, 0x50, 0xb8, 0x29, 0xa0, 0x31, 0xe1, 0x72, 0x4e, 0x64, 0x30, +]; + +fn unhex(s: &str) -> Vec { + let s = s.trim().trim_start_matches("0x"); + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap()) + .collect() +} + +fn vector(name: &str) -> Vec { + let line = VECTORS + .lines() + .find_map(|l| l.strip_prefix(&format!("{name}="))) + .unwrap_or_else(|| panic!("fixture has no `{name}`")); + unhex(line) +} + +struct Golden { + vk: Vec, + prepared: Vec, + proof: Vec, + inputs: Vec, +} + +fn golden() -> Golden { + Golden { + vk: vector("vk"), + prepared: unhex(PREPARED), + proof: vector("proof"), + inputs: vector("inputs"), + } +} + +/// Both entry points version 1 is built on, which must always agree. +fn verifies(prepared: &[u8], proof: &[u8], inputs: &[u8]) -> bool { + let library = verify_prepared(prepared, proof, inputs); + let host = bn254_groth16_verify(prepared, proof, inputs); + assert_eq!(library, host, "host function and verify_prepared disagree"); + host +} + +// ─── The accepted vector ────────────────────────────────────────────────────── + +#[test] +fn the_pinned_prepared_key_verifies_the_real_proof() { + let g = golden(); + assert_eq!(g.proof.len(), 128); + assert_eq!(g.inputs.len(), 8 * 32); + assert!(verifies(&g.prepared, &g.proof, &g.inputs)); +} + +/// Preparing the key today gives the bytes chains store: keys registered by any +/// runtime and read back by any node mean the same thing. +#[test] +fn preparing_the_key_gives_the_pinned_bytes() { + let g = golden(); + let prepared = VerifyingKey::new(g.vk).prepared_bytes().unwrap(); + assert_eq!( + prepared.len(), + g.prepared.len(), + "prepared layout size changed" + ); + assert!(prepared == g.prepared, "prepared bytes changed"); +} + +// ─── Refused vectors: false, never a panic ──────────────────────────────────── + +#[test] +fn a_tampered_proof_is_refused() { + let g = golden(); + for i in [0, 31, 32, 64, 95, 96, 127] { + let mut proof = g.proof.clone(); + proof[i] ^= 0x01; + assert!(!verifies(&g.prepared, &proof, &g.inputs), "byte {i}"); + } +} + +#[test] +fn a_changed_input_is_refused() { + let g = golden(); + for k in 0..8 { + let mut inputs = g.inputs.clone(); + inputs[k * 32] ^= 0x01; + assert!(!verifies(&g.prepared, &g.proof, &inputs), "input {k}"); + } +} + +/// `x + r` is the same field element under another encoding: refused, not reduced. +#[test] +fn a_non_canonical_input_is_refused() { + let g = golden(); + let mut inputs = g.inputs.clone(); + let mut carry = 0u16; + for (byte, r) in inputs[..32].iter_mut().zip(R_LE) { + let sum = *byte as u16 + r as u16 + carry; + *byte = sum as u8; + carry = sum >> 8; + } + assert_eq!(carry, 0, "the first input plus r must still fit 32 bytes"); + assert!(!verifies(&g.prepared, &g.proof, &inputs)); +} + +#[test] +fn the_wrong_number_of_inputs_is_refused() { + let g = golden(); + assert!(!verifies(&g.prepared, &g.proof, &g.inputs[..7 * 32])); + let mut more = g.inputs.clone(); + more.extend_from_slice(&[0u8; 32]); + assert!(!verifies(&g.prepared, &g.proof, &more)); + assert!(!verifies(&g.prepared, &g.proof, &[])); + assert!(!verifies( + &g.prepared, + &g.proof, + &vec![0u8; (MAX_PUBLIC_INPUTS + 1) * 32] + )); +} + +#[test] +fn malformed_lengths_are_refused() { + let g = golden(); + assert!(!verifies(&g.prepared, &g.proof[..127], &g.inputs)); + let mut long = g.proof.clone(); + long.push(0); + assert!(!verifies(&g.prepared, &long, &g.inputs)); + assert!(!verifies(&g.prepared, &g.proof, &g.inputs[..8 * 32 - 1])); +} + +#[test] +fn malformed_prepared_keys_are_refused() { + let g = golden(); + assert!(!verifies(&[], &g.proof, &g.inputs)); + assert!(!verifies( + &g.prepared[..g.prepared.len() - 1], + &g.proof, + &g.inputs + )); + let mut longer = g.prepared.clone(); + longer.push(0); + assert!(!verifies(&longer, &g.proof, &g.inputs)); + assert!(!verifies(&vec![0u8; g.prepared.len()], &g.proof, &g.inputs)); + assert!(!verifies( + &vec![0xffu8; MAX_PREPARED_VK_BYTES + 1], + &g.proof, + &g.inputs + )); + // The raw key is not a prepared one. + assert!(!verifies(&g.vk, &g.proof, &g.inputs)); +} + +// ─── The constants version 1 depends on ─────────────────────────────────────── + +/// Changing any of these changes what version 1 accepts. +#[test] +fn the_constants_version_1_depends_on_are_unchanged() { + assert_eq!(MAX_PUBLIC_INPUTS, 32); + assert_eq!(PROOF_BYTES, 128); + assert_eq!(MAX_PREPARED_VK_BYTES, 36_378); + assert_eq!(golden().prepared.len(), 34_842); +} + +/// Writes the prepared fixture from the raw key. Only for new vectors: never to +/// make the tests above pass. +#[test] +#[ignore] +fn regenerate_prepared_fixture() { + let prepared = VerifyingKey::new(vector("vk")).prepared_bytes().unwrap(); + let hex: String = prepared.iter().map(|b| format!("{b:02x}")).collect(); + std::fs::write( + concat!( + env!("CARGO_MANIFEST_DIR"), + "/tests/fixtures/host_v1_unshield_v3.prepared.hex" + ), + format!("0x{hex}\n"), + ) + .unwrap(); +} From c9a44d2f836cd49fef0c61c935717752171b32bd Mon Sep 17 00:00:00 2001 From: nol4lej Date: Wed, 21 Oct 2026 18:28:35 -0300 Subject: [PATCH 5/5] =?UTF-8?q?chore(zk-verifier):=20release=20pallet-zk-v?= =?UTF-8?q?erifier=200.19.0=20and=20orbinum-zk-verifier=203.4.0=20?= =?UTF-8?q?=E2=80=94=20on-chain=20denied=20keys,=20removed=20versions=20ke?= =?UTF-8?q?ep=20their=20key,=20keys=20without=20phase=202=20refused,=20hos?= =?UTF-8?q?t-function=20golden=20vectors=20(spec=2020)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cargo.lock | 4 ++-- frame/zk-verifier/CHANGELOG.md | 2 ++ frame/zk-verifier/Cargo.toml | 4 ++-- primitives/zk-verifier/CHANGELOG.md | 2 ++ primitives/zk-verifier/Cargo.toml | 2 +- template/runtime/RUNTIME_VERSIONS.md | 29 ++++++++++++++++++++++++++-- 6 files changed, 36 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3d86d339..b127c06d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10075,7 +10075,7 @@ dependencies = [ [[package]] name = "orbinum-zk-verifier" -version = "3.3.1" +version = "3.4.0" dependencies = [ "ark-bn254", "ark-ec 0.5.0", @@ -12930,7 +12930,7 @@ dependencies = [ [[package]] name = "pallet-zk-verifier" -version = "0.18.1" +version = "0.19.0" dependencies = [ "ark-bn254", "ark-ec 0.5.0", diff --git a/frame/zk-verifier/CHANGELOG.md b/frame/zk-verifier/CHANGELOG.md index 7d397a11..34d47c37 100644 --- a/frame/zk-verifier/CHANGELOG.md +++ b/frame/zk-verifier/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to this pallet are documented here. ## [Unreleased] +## [0.19.0] - 2026-10-21 + ### Added - `DeniedVkHashes`: an on-chain, append-only record of `blake2_256` hashes of keys diff --git a/frame/zk-verifier/Cargo.toml b/frame/zk-verifier/Cargo.toml index f00badd4..01b88276 100644 --- a/frame/zk-verifier/Cargo.toml +++ b/frame/zk-verifier/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "pallet-zk-verifier" -version = "0.18.1" +version = "0.19.0" description = "Zero-Knowledge proof verification pallet for Orbinum" authors = { workspace = true } license = "GPL-3.0-or-later" @@ -42,9 +42,9 @@ orbinum-zk-core = { path = "../../primitives/zk-core", default-features = false orbinum-zk-verifier = { path = "../../primitives/zk-verifier", default-features = false } [dev-dependencies] -serde_json = "1.0" orbinum-zk-core = { path = "../../primitives/zk-core", default-features = false } orbinum-zk-verifier = { path = "../../primitives/zk-verifier", default-features = false } +serde_json = "1.0" # Build real Groth16 VKs of a given arity in tests (arity is now validated on register). ark-bn254 = { version = "0.5.0", default-features = false, features = ["curve"] } ark-ec = { version = "0.5.0", default-features = false } diff --git a/primitives/zk-verifier/CHANGELOG.md b/primitives/zk-verifier/CHANGELOG.md index 84ebdf7e..f4a3596c 100644 --- a/primitives/zk-verifier/CHANGELOG.md +++ b/primitives/zk-verifier/CHANGELOG.md @@ -6,6 +6,8 @@ All notable changes to this crate are documented here. ## [Unreleased] +## [3.4.0] - 2026-10-21 + ### Added - Golden vectors for the frozen `bn254_groth16_verify` (version 1) diff --git a/primitives/zk-verifier/Cargo.toml b/primitives/zk-verifier/Cargo.toml index 0cb6b0b9..943c4063 100644 --- a/primitives/zk-verifier/Cargo.toml +++ b/primitives/zk-verifier/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "orbinum-zk-verifier" -version = "3.3.1" +version = "3.4.0" authors = { workspace = true } edition = "2021" publish = false diff --git a/template/runtime/RUNTIME_VERSIONS.md b/template/runtime/RUNTIME_VERSIONS.md index 6764e5eb..25e2d9b8 100644 --- a/template/runtime/RUNTIME_VERSIONS.md +++ b/template/runtime/RUNTIME_VERSIONS.md @@ -28,8 +28,11 @@ high-level calls reach them. EVM transaction validation and the modexp precompile follow upstream Frontier (polkadot-evm/frontier, changes after #1790). Ed25519 keys sign only text that names an Orbinum transaction: raw `Ed25519` (`0x00`) never verifies, and Solana wallets and Substrate extensions -sign through the new `Ed25519Text` (`0x03`) and `Ed25519WrappedText` (`0x04`). `transaction_version` is unchanged. Ships with -`pallet-evm-precompile-balances` 0.2.0. +sign through the new `Ed25519Text` (`0x03`) and `Ed25519WrappedText` (`0x04`). +Circuit keys: unsound ones are recorded on chain and never verify again, a +removed version keeps its key, and a key without phase 2 is refused. +`transaction_version` is unchanged. Ships with `pallet-evm-precompile-balances` +0.2.0, `pallet-zk-verifier` 0.19.0 and `orbinum-zk-verifier` 3.4.0. - **Balances precompile (`0x…0802`)** refuses static calls, delegated calls (DELEGATECALL, CALLCODE) and calls carrying value. Under DELEGATECALL the @@ -102,6 +105,28 @@ sign through the new `Ed25519Text` (`0x03`) and `Ed25519WrappedText` (`0x04`). ` polkadot-js and noble already sign this way. - The module is split by responsibility under `src/orbinum_signature/` (`signer`, `ed25519`, `sr25519`, `ecdsa`, `text`). +- **Denied circuit keys.** `pallet_zk_verifier` keeps an on-chain, append-only + record of unsound keys (`DeniedVkHashes`), filled by `deny_version` (Root, call + index 8) or the chain spec (`denied_vk_hashes`, `0x…` hashes). A denied key is + refused on register, activate and unretire, and verifies nothing if stored; no + call removes an entry. Transfer and unshield v1/v2 let `k + i·l` spending-key + aliases give one note several nullifiers, and a spend chooses its circuit + version, so retiring them was the only guard: `unretire_version` checked arity + alone, and the same key could be registered again under another version. + **After the upgrade**, Root records them on testnet with `deny_version` for + `(1,1)`, `(1,2)`, `(2,1)` and `(2,2)`, all already retired. +- **Removed versions keep their key.** `remove_verification_key` records the + key's hash (`RemovedVkHashes`), and the version can then take only that key + again (`VersionReused`). Before, a version could be removed and registered + with another key, changing its meaning for any client that cached it. +- **Keys without phase 2 refused.** Registration refuses a key whose `delta` is + `±gamma` or `±` the G2 generator — what snarkjs leaves when phase 2 never ran. + With it `(α, β, −L(x))` verifies any statement from public key data alone: a + forged unshield drained the pool on a dev node before the check. Keys already + stored are not re-checked; the seven on testnet have an independent `delta`. +- **Weights.** Every proof verification adds one read (the denied-key check); + `deny_version` is estimated from `retire_version`, and registration and + removal add the `RemovedVkHashes` read/write, until re-benchmarked. ### spec 19 — tx 7 — 2026-10-13 (`v0.5.0`)