diff --git a/Cargo.lock b/Cargo.lock index b988dca4092..76245d3e1be 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -131,6 +131,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", + "const-random", "getrandom", "once_cell", "version_check", @@ -3628,6 +3629,19 @@ dependencies = [ "gstd", ] +[[package]] +name = "demo-plonky2-verifier" +version = "0.1.0" +dependencies = [ + "gcore", + "gear-wasm-builder", + "gstd", + "num", + "plonky2", + "plonky2_field", + "serde", +] + [[package]] name = "demo-program-factory" version = "0.1.0" @@ -5041,6 +5055,15 @@ dependencies = [ "pin-project", ] +[[package]] +name = "fixed-hash" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcf0ed7fe52a17a03854ec54a9f76d6d84508d1c0e66bc1793301c73fc8493c" +dependencies = [ + "static_assertions", +] + [[package]] name = "fixed-hash" version = "0.8.0" @@ -6005,7 +6028,7 @@ dependencies = [ "gsys", "hex-literal", "log", - "primitive-types", + "primitive-types 0.12.2", "proptest", "sp-arithmetic 26.0.0 (git+https://github.com/gear-tech/polkadot-sdk.git?branch=gear-polkadot-stable2409)", "sp-core 34.0.0 (git+https://github.com/gear-tech/polkadot-sdk.git?branch=gear-polkadot-stable2409)", @@ -6044,7 +6067,7 @@ dependencies = [ "numerated", "parity-scale-codec", "paste", - "primitive-types", + "primitive-types 0.12.2", "proptest", "rand", "scale-info", @@ -6093,6 +6116,7 @@ dependencies = [ "gear-core-backend", "gear-core-errors", "gear-lazy-pages-common", + "gear-runtime-interface", "gear-wasm-instrument", "gsys", "log", @@ -6193,7 +6217,7 @@ dependencies = [ "gsdk", "names", "parking_lot 0.12.3", - "primitive-types", + "primitive-types 0.12.2", "rand", "reqwest", "subxt", @@ -6333,6 +6357,7 @@ dependencies = [ "gear-lazy-pages", "gear-lazy-pages-common", "gear-sandbox-interface", + "gprimitives-client", "log", "parity-scale-codec", "sha2 0.10.8", @@ -6878,11 +6903,20 @@ dependencies = [ "gear-ss58", "hex", "parity-scale-codec", - "primitive-types", + "primitive-types 0.12.2", "scale-info", "serde", ] +[[package]] +name = "gprimitives-client" +version = "1.7.0" +dependencies = [ + "num", + "rand", + "unroll", +] + [[package]] name = "gring" version = "1.7.0" @@ -8337,6 +8371,16 @@ dependencies = [ "sha3-asm", ] +[[package]] +name = "keccak-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce2bd4c29270e724d3eaadf7bdc8700af4221fc0ed771b855eadcd1b98d52851" +dependencies = [ + "primitive-types 0.10.1", + "tiny-keccak", +] + [[package]] name = "keyring" version = "1.2.1" @@ -10439,6 +10483,7 @@ dependencies = [ "autocfg", "num-integer", "num-traits", + "rand", ] [[package]] @@ -10448,6 +10493,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" dependencies = [ "num-traits", + "rand", ] [[package]] @@ -10942,6 +10988,7 @@ dependencies = [ "demo-new-meta", "demo-out-of-memory", "demo-ping", + "demo-plonky2-verifier", "demo-program-factory", "demo-program-generator", "demo-proxy", @@ -10994,7 +11041,7 @@ dependencies = [ "pallet-gear-voucher", "pallet-timestamp", "parity-scale-codec", - "primitive-types", + "primitive-types 0.12.2", "rand", "rand_pcg", "scale-info", @@ -11023,7 +11070,7 @@ dependencies = [ "pallet-authorship", "pallet-balances", "parity-scale-codec", - "primitive-types", + "primitive-types 0.12.2", "scale-info", "sp-io 38.0.0 (git+https://github.com/gear-tech/polkadot-sdk.git?branch=gear-polkadot-stable2409)", "sp-runtime 39.0.1", @@ -11059,6 +11106,7 @@ dependencies = [ "gear-core-processor", "gear-runtime-interface", "gprimitives", + "hex", "hex-literal", "impl-trait-for-tuples", "log", @@ -11075,7 +11123,7 @@ dependencies = [ "pallet-staking", "pallet-timestamp", "parity-scale-codec", - "primitive-types", + "primitive-types 0.12.2", "scale-info", "sha2 0.10.8", "sp-core 34.0.0 (git+https://github.com/gear-tech/polkadot-sdk.git?branch=gear-polkadot-stable2409)", @@ -11132,7 +11180,7 @@ dependencies = [ "pallet-gear-scheduler", "pallet-timestamp", "parity-scale-codec", - "primitive-types", + "primitive-types 0.12.2", "scale-info", "sp-core 34.0.0 (git+https://github.com/gear-tech/polkadot-sdk.git?branch=gear-polkadot-stable2409)", "sp-io 38.0.0 (git+https://github.com/gear-tech/polkadot-sdk.git?branch=gear-polkadot-stable2409)", @@ -11190,7 +11238,7 @@ version = "1.7.0" dependencies = [ "jsonrpsee 0.24.7", "pallet-gear-eth-bridge-rpc-runtime-api", - "primitive-types", + "primitive-types 0.12.2", "sp-api 34.0.0 (git+https://github.com/gear-tech/polkadot-sdk.git?branch=gear-polkadot-stable2409)", "sp-blockchain", "sp-runtime 39.0.1", @@ -11225,7 +11273,7 @@ dependencies = [ "pallet-timestamp", "parity-scale-codec", "parity-wasm", - "primitive-types", + "primitive-types 0.12.2", "scale-info", "sp-core 34.0.0 (git+https://github.com/gear-tech/polkadot-sdk.git?branch=gear-polkadot-stable2409)", "sp-io 38.0.0 (git+https://github.com/gear-tech/polkadot-sdk.git?branch=gear-polkadot-stable2409)", @@ -11250,7 +11298,7 @@ dependencies = [ "pallet-gear-gas", "pallet-timestamp", "parity-scale-codec", - "primitive-types", + "primitive-types 0.12.2", "rand", "scale-info", "sp-io 38.0.0 (git+https://github.com/gear-tech/polkadot-sdk.git?branch=gear-polkadot-stable2409)", @@ -11283,7 +11331,7 @@ dependencies = [ "pallet-transaction-payment", "parity-scale-codec", "parity-wasm", - "primitive-types", + "primitive-types 0.12.2", "scale-info", "sp-io 38.0.0 (git+https://github.com/gear-tech/polkadot-sdk.git?branch=gear-polkadot-stable2409)", "sp-runtime 39.0.1", @@ -11318,7 +11366,7 @@ dependencies = [ "pallet-timestamp", "pallet-treasury", "parity-scale-codec", - "primitive-types", + "primitive-types 0.12.2", "scale-info", "sp-core 34.0.0 (git+https://github.com/gear-tech/polkadot-sdk.git?branch=gear-polkadot-stable2409)", "sp-io 38.0.0 (git+https://github.com/gear-tech/polkadot-sdk.git?branch=gear-polkadot-stable2409)", @@ -11377,7 +11425,7 @@ dependencies = [ "pallet-gear-program", "pallet-timestamp", "parity-scale-codec", - "primitive-types", + "primitive-types 0.12.2", "scale-info", "sp-core 34.0.0 (git+https://github.com/gear-tech/polkadot-sdk.git?branch=gear-polkadot-stable2409)", "sp-io 38.0.0 (git+https://github.com/gear-tech/polkadot-sdk.git?branch=gear-polkadot-stable2409)", @@ -11408,7 +11456,7 @@ dependencies = [ "pallet-treasury", "pallet-utility", "parity-scale-codec", - "primitive-types", + "primitive-types 0.12.2", "scale-info", "serde", "sp-authority-discovery", @@ -11454,7 +11502,7 @@ dependencies = [ "pallet-balances", "parity-scale-codec", "parity-wasm", - "primitive-types", + "primitive-types 0.12.2", "scale-info", "sp-core 34.0.0 (git+https://github.com/gear-tech/polkadot-sdk.git?branch=gear-polkadot-stable2409)", "sp-io 38.0.0 (git+https://github.com/gear-tech/polkadot-sdk.git?branch=gear-polkadot-stable2409)", @@ -12191,6 +12239,53 @@ version = "0.3.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26072860ba924cbfa98ea39c8c19b4dd6a4a25423dbdf219c1eca91aa0cf6964" +[[package]] +name = "plonky2" +version = "1.0.0" +source = "git+https://github.com/gear-tech/plonky2.git?branch=fix-getrandom#ec5e33ebdff2e4bcab005fad36f10fc0c2db5172" +dependencies = [ + "ahash 0.8.11", + "anyhow", + "getrandom", + "hashbrown 0.14.5", + "itertools 0.11.0", + "keccak-hash", + "log", + "num", + "plonky2_field", + "plonky2_maybe_rayon", + "plonky2_util", + "rand", + "serde", + "static_assertions", + "unroll", +] + +[[package]] +name = "plonky2_field" +version = "1.0.0" +source = "git+https://github.com/gear-tech/plonky2.git?branch=fix-getrandom#ec5e33ebdff2e4bcab005fad36f10fc0c2db5172" +dependencies = [ + "anyhow", + "itertools 0.11.0", + "num", + "plonky2_util", + "rand", + "serde", + "static_assertions", + "unroll", +] + +[[package]] +name = "plonky2_maybe_rayon" +version = "1.0.0" +source = "git+https://github.com/gear-tech/plonky2.git?branch=fix-getrandom#ec5e33ebdff2e4bcab005fad36f10fc0c2db5172" + +[[package]] +name = "plonky2_util" +version = "1.0.0" +source = "git+https://github.com/gear-tech/plonky2.git?branch=fix-getrandom#ec5e33ebdff2e4bcab005fad36f10fc0c2db5172" + [[package]] name = "polkavm" version = "0.9.3" @@ -12438,13 +12533,23 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "primitive-types" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05e4722c697a58a99d5d06a08c30821d7c082a4632198de1eaa5a6c22ef42373" +dependencies = [ + "fixed-hash 0.7.0", + "uint", +] + [[package]] name = "primitive-types" version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" dependencies = [ - "fixed-hash", + "fixed-hash 0.8.0", "impl-codec", "impl-serde", "scale-info", @@ -13495,7 +13600,7 @@ dependencies = [ "num-bigint", "num-traits", "parity-scale-codec", - "primitive-types", + "primitive-types 0.12.2", "proptest", "rand", "rlp", @@ -15026,7 +15131,7 @@ checksum = "e98f3262c250d90e700bb802eb704e1f841e03331c2eb815e46516c4edbf5b27" dependencies = [ "derive_more 0.99.18", "parity-scale-codec", - "primitive-types", + "primitive-types 0.12.2", "scale-bits", "scale-decode-derive", "scale-type-resolver", @@ -15053,7 +15158,7 @@ checksum = "4ba0b9c48dc0eb20c60b083c29447c0c4617cb7c4a4c9fef72aa5c5bc539e15e" dependencies = [ "derive_more 0.99.18", "parity-scale-codec", - "primitive-types", + "primitive-types 0.12.2", "scale-bits", "scale-encode-derive", "scale-type-resolver", @@ -16225,7 +16330,7 @@ dependencies = [ "parity-scale-codec", "parking_lot 0.12.3", "paste", - "primitive-types", + "primitive-types 0.12.2", "rand", "scale-info", "schnorrkel", @@ -16271,7 +16376,7 @@ dependencies = [ "parity-scale-codec", "parking_lot 0.12.3", "paste", - "primitive-types", + "primitive-types 0.12.2", "rand", "scale-info", "schnorrkel", @@ -16690,7 +16795,7 @@ dependencies = [ "impl-trait-for-tuples", "parity-scale-codec", "polkavm-derive", - "primitive-types", + "primitive-types 0.12.2", "sp-externalities 0.29.0 (registry+https://github.com/rust-lang/crates.io-index)", "sp-runtime-interface-proc-macro 18.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "sp-std 14.0.0 (registry+https://github.com/rust-lang/crates.io-index)", @@ -16709,7 +16814,7 @@ dependencies = [ "impl-trait-for-tuples", "parity-scale-codec", "polkavm-derive", - "primitive-types", + "primitive-types 0.12.2", "sp-externalities 0.29.0 (git+https://github.com/gear-tech/polkadot-sdk.git?branch=gear-polkadot-stable2409)", "sp-runtime-interface-proc-macro 18.0.0 (git+https://github.com/gear-tech/polkadot-sdk.git?branch=gear-polkadot-stable2409)", "sp-std 14.0.0 (git+https://github.com/gear-tech/polkadot-sdk.git?branch=gear-polkadot-stable2409)", @@ -17533,7 +17638,7 @@ dependencies = [ "instant", "jsonrpsee 0.22.5", "parity-scale-codec", - "primitive-types", + "primitive-types 0.12.2", "reconnecting-jsonrpsee-ws-client", "scale-bits", "scale-decode", @@ -17588,7 +17693,7 @@ dependencies = [ "hex", "impl-serde", "parity-scale-codec", - "primitive-types", + "primitive-types 0.12.2", "scale-bits", "scale-decode", "scale-encode", @@ -18656,6 +18761,16 @@ dependencies = [ "subtle 2.6.1", ] +[[package]] +name = "unroll" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ad948c1cb799b1a70f836077721a92a35ac177d4daddf4c20a633786d4cf618" +dependencies = [ + "quote", + "syn 1.0.109", +] + [[package]] name = "unsafe-libyaml" version = "0.2.11" @@ -20533,18 +20648,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.7.32" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.7.32" +version = "0.7.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", diff --git a/Cargo.toml b/Cargo.toml index 72213b05ce7..2e9358b1413 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -83,6 +83,7 @@ members = [ "examples/waiter", "examples/waiting-proxy", "examples/wat", + "examples/plonky2-verifier", "galloc", "gbuiltins/*", "gcli", @@ -91,6 +92,7 @@ members = [ "gmeta", "gmeta/codegen", "gprimitives", + "gprimitives/client", "gsdk", "gsdk/codegen", "gsdk/api-gen", @@ -136,7 +138,7 @@ dirs = "4.0.0" dyn-clonable = "0.9.0" enum-iterator = "1.5.0" env_logger = "0.10" -environmental = "1.1.3" +environmental = { version = "1.1.4", default-features = false } futures = { version = "0.3", default-features = false } futures-timer = "3.0.3" futures-util = "0.3.30" @@ -148,11 +150,14 @@ impl-serde = "0.4.0" jsonrpsee = { version = "^0.24" } libc = { version = "0.2", default-features = false } log = { version = "0.4.22", default-features = false } +num = { version = "0.4", default-features = false } num_enum = { version = "0.6.1", default-features = false } parity-scale-codec = { version = "3.6.4", default-features = false } parity-wasm = "0.45.0" parking_lot = "0.12.3" path-clean = "1.0.1" +plonky2 = { git = "https://github.com/gear-tech/plonky2.git", branch = "fix-getrandom", default-features = false } +plonky2_field = { git = "https://github.com/gear-tech/plonky2.git", branch = "fix-getrandom", default-features = false } primitive-types = { version = "0.12.2", default-features = false } proc-macro2 = { version = "1", default-features = false } proptest = "1.5.0" @@ -176,6 +181,7 @@ syn = "2.0.71" thiserror = "1.0.62" tokio = { version = "1.38.0" } uluru = "3.1.0" +unroll = { version = "0.1.5", default-features = false } url = "2.5.2" # wasmer 4.3.4 for some reason have wat's version "=1.0.71" nailed down, so we have to do the same wat = "1.0.71" @@ -234,6 +240,7 @@ gtest = { path = "gtest" } gmeta = { path = "gmeta" } gmeta-codegen = { path = "gmeta/codegen" } gprimitives = { path = "gprimitives", default-features = false } +gprimitives-client = { path = "gprimitives/client", default-features = false } gear-authorship = { path = "node/authorship" } gear-core-backend = { path = "core-backend", default-features = false } gear-call-gen = { path = "utils/call-gen" } @@ -504,6 +511,7 @@ demo-wait-wake = { path = "examples/wait_wake" } demo-waiting-proxy = { path = "examples/waiting-proxy" } demo-stack-allocations = { path = "examples/stack-allocations" } demo-wat = { path = "examples/wat" } +demo-plonky2-verifier = { path = "examples/plonky2-verifier" } # Dependencies that only used in one package # diff --git a/core-backend/src/env.rs b/core-backend/src/env.rs index 72cc7342fe7..a5f448b8d95 100644 --- a/core-backend/src/env.rs +++ b/core-backend/src/env.rs @@ -219,6 +219,7 @@ where builder.add_func(ReservationReplyCommit, wrap_syscall!(reservation_reply_commit)); builder.add_func(ReservationSend, wrap_syscall!(reservation_send)); builder.add_func(ReservationSendCommit, wrap_syscall!(reservation_send_commit)); + builder.add_func(Permute, wrap_syscall!(permute)); builder.add_func(SystemBreak, wrap_syscall!(system_break)); builder.add_func(Alloc, wrap_syscall!(alloc)); diff --git a/core-backend/src/funcs.rs b/core-backend/src/funcs.rs index 9cd12764837..8a882e44b70 100644 --- a/core-backend/src/funcs.rs +++ b/core-backend/src/funcs.rs @@ -56,7 +56,7 @@ use gear_wasm_instrument::SystemBreakCode; use gsys::{ BlockNumberWithHash, ErrorBytes, ErrorWithGas, ErrorWithHandle, ErrorWithHash, ErrorWithReplyCode, ErrorWithSignalCode, ErrorWithTwoHashes, Gas, Hash, HashWithValue, - TwoHashesWithValue, + PoseidonInOut, TwoHashesWithValue, }; /// BLAKE2b-256 hasher state. @@ -1425,4 +1425,19 @@ where Err(HostError) }) } + + pub fn permute(input_ptr: u32, output_ptr: u32) -> impl Syscall { + InfallibleSyscall::new(CostToken::Permute, move |ctx: &mut CallerWrap| { + let mut registry = MemoryAccessRegistry::default(); + let input_reader = registry.register_read_decoded(input_ptr); + let output_writer = registry.register_write_as(output_ptr); + let mut io = registry.pre_process(ctx)?; + + let input_data: PoseidonInOut = io.read_decoded(ctx, input_reader)?; + let hash_out = ctx.ext_mut().permute(&input_data)?; + + io.write_as(ctx, output_writer, hash_out) + .map_err(Into::into) + }) + } } diff --git a/core-backend/src/mock.rs b/core-backend/src/mock.rs index efd6b401e7c..70c1707292d 100644 --- a/core-backend/src/mock.rs +++ b/core-backend/src/mock.rs @@ -275,6 +275,10 @@ impl Externalities for MockExt { Ok(MessageId::default()) } + fn permute(&self, _input: &[u64]) -> Result<[u64; 12], Self::UnrecoverableError> { + Ok([0u64; 12]) + } + fn signal_from(&self) -> Result { Ok(MessageId::default()) } diff --git a/core-processor/Cargo.toml b/core-processor/Cargo.toml index d3b7f41f6bc..eb8f587f9c1 100644 --- a/core-processor/Cargo.toml +++ b/core-processor/Cargo.toml @@ -18,6 +18,7 @@ gear-core-errors = { workspace = true, features = ["codec"] } gear-core-backend.workspace = true gear-wasm-instrument.workspace = true gear-lazy-pages-common.workspace = true +gear-runtime-interface.workspace = true gsys.workspace = true scale-info = { workspace = true, features = ["derive"] } @@ -32,7 +33,10 @@ enum-iterator.workspace = true [features] default = ["std"] -std = ["gear-core-backend/std"] +std = [ + "gear-core-backend/std", + "gear-runtime-interface/std", +] strict = [] mock = [] gtest = [] diff --git a/core-processor/src/ext.rs b/core-processor/src/ext.rs index 67a4985525c..3b21d535bdf 100644 --- a/core-processor/src/ext.rs +++ b/core-processor/src/ext.rs @@ -59,6 +59,7 @@ use gear_core_errors::{ ReplyCode, ReservationError, SignalCode, }; use gear_lazy_pages_common::{GlobalsAccessConfig, LazyPagesInterface, ProcessAccessError, Status}; +use gear_runtime_interface::poseidon_hash::poseidon; use gear_wasm_instrument::syscalls::SyscallName; /// Processor context. @@ -1411,6 +1412,12 @@ impl Externalities for Ext { }) } + fn permute(&self, input: &[u64]) -> Result<[u64; 12], Self::UnrecoverableError> { + Ok(poseidon(input.to_vec()) + .try_into() + .expect("poseidon always returns 12 elements")) + } + fn random(&self) -> Result<(&[u8], u32), Self::UnrecoverableError> { Ok((&self.context.random_data.0, self.context.random_data.1)) } diff --git a/core/src/costs.rs b/core/src/costs.rs index 1fcec7b2a97..de9c704692d 100644 --- a/core/src/costs.rs +++ b/core/src/costs.rs @@ -307,6 +307,9 @@ pub struct SyscallCosts { /// Cost per salt byte by `gr_create_program_wgas`. pub gr_create_program_wgas_salt_per_byte: CostOf, + + /// Cost of calling `gr_permute`. + pub gr_permute: CostOf, } /// Enumerates syscalls that can be charged by gas meter. @@ -420,6 +423,8 @@ pub enum CostToken { CreateProgram(BytesAmount, BytesAmount), /// Cost of calling `gr_create_program_wgas`, taking in account payload and salt size. CreateProgramWGas(BytesAmount, BytesAmount), + /// Cost of calling `gr_permute` + Permute, } impl SyscallCosts { @@ -498,6 +503,7 @@ impl SyscallCosts { .cost_for_with_bytes(self.gr_create_program_wgas_payload_per_byte, payload), ) .cost_for_with_bytes(self.gr_create_program_wgas_salt_per_byte, salt), + Permute => self.gr_permute.cost_for_one(), } } } diff --git a/core/src/env.rs b/core/src/env.rs index 63ab5b2d096..c2e0977bbc1 100644 --- a/core/src/env.rs +++ b/core/src/env.rs @@ -391,4 +391,7 @@ pub trait Externalities { /// Return the set of functions that are forbidden to be called. fn forbidden_funcs(&self) -> &BTreeSet; + + /// Do data permutation and return a slice of the same shape. + fn permute(&self, input: &[u64]) -> Result<[u64; 12], Self::UnrecoverableError>; } diff --git a/core/src/gas_metering/schedule.rs b/core/src/gas_metering/schedule.rs index ca7ebedb286..f65efb95e1e 100644 --- a/core/src/gas_metering/schedule.rs +++ b/core/src/gas_metering/schedule.rs @@ -525,6 +525,8 @@ pub struct SyscallWeights { pub gr_create_program_wgas_payload_per_byte: Weight, #[doc = " Weight per salt byte by `create_program_wgas`."] pub gr_create_program_wgas_salt_per_byte: Weight, + #[doc = " Weight of calling `gr_permute`."] + pub gr_permute: Weight, } impl Default for SyscallWeights { @@ -810,6 +812,10 @@ impl Default for SyscallWeights { ref_time: 1591, proof_size: 0, }, + gr_permute: Weight { + ref_time: 4901557, + proof_size: 0, + }, } } } @@ -1189,6 +1195,7 @@ impl From for SyscallCosts { .gr_create_program_wgas_salt_per_byte .ref_time() .into(), + gr_permute: val.gr_permute.ref_time().into(), } } } diff --git a/examples/plonky2-verifier/Cargo.toml b/examples/plonky2-verifier/Cargo.toml new file mode 100644 index 00000000000..baf9d8f06a7 --- /dev/null +++ b/examples/plonky2-verifier/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "demo-plonky2-verifier" +version = "0.1.0" +authors.workspace = true +edition.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[dependencies] +gcore = { workspace = true, features = ["codec"] } +gstd.workspace = true +num = { workspace = true, features = ["alloc"] } +plonky2.workspace = true +plonky2_field.workspace = true +serde = { workspace = true, features = ["derive", "alloc"] } + +[build-dependencies] +gear-wasm-builder.workspace = true + +[features] +debug = ["gstd/debug"] +std = [] +default = ["std"] diff --git a/examples/plonky2-verifier/build.rs b/examples/plonky2-verifier/build.rs new file mode 100644 index 00000000000..8b45f4b7647 --- /dev/null +++ b/examples/plonky2-verifier/build.rs @@ -0,0 +1,29 @@ +// This file is part of Gear. + +// Copyright (C) 2024 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use gear_wasm_builder::WasmBuilder; + +fn main() { + // We are forcing recommended nightly toolchain due to the need to compile this + // program with `oom-handler` feature. The WASM binary of this program is then + // used by the `oom_handler_works` pallet test. + WasmBuilder::new() + .exclude_features(vec!["std"]) + .with_forced_recommended_toolchain() // NOTE: Don't use this in production programs! + .build(); +} diff --git a/examples/plonky2-verifier/src/circuit.rs b/examples/plonky2-verifier/src/circuit.rs new file mode 100644 index 00000000000..8003cd2d179 --- /dev/null +++ b/examples/plonky2-verifier/src/circuit.rs @@ -0,0 +1,32 @@ +// This file is part of Gear. + +// Copyright (C) 2024 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Circuit config for a custom `Goldilocks` field implementation from `gstd`. + +use super::goldilocks_field::GoldilocksFieldWrapper; +use plonky2::{hash::poseidon::PoseidonHash, plonk::config::GenericConfig}; +use plonky2_field::extension::quadratic::QuadraticExtension; + +#[derive(Debug, Copy, Clone, Default, Eq, PartialEq)] +pub struct CustomPoseidonGoldilocksConfig; +impl GenericConfig<2> for CustomPoseidonGoldilocksConfig { + type F = GoldilocksFieldWrapper; + type FE = QuadraticExtension; + type Hasher = PoseidonHash; + type InnerHasher = PoseidonHash; +} diff --git a/examples/plonky2-verifier/src/goldilocks_field.rs b/examples/plonky2-verifier/src/goldilocks_field.rs new file mode 100644 index 00000000000..025e3b7359d --- /dev/null +++ b/examples/plonky2-verifier/src/goldilocks_field.rs @@ -0,0 +1,415 @@ +// This file is part of Gear. +// +// Copyright (C) 2024 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Goldilocks field wrapper gas counting. + +use core::{ + fmt::{self, Debug, Display, Formatter}, + hash::{Hash, Hasher}, + iter::{Product, Sum}, + ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}, +}; +use gcore::exec; +use num::BigUint; +use serde::{Deserialize, Serialize}; + +use plonky2::hash::{ + hash_types::RichField, + poseidon::{Poseidon, N_PARTIAL_ROUNDS}, +}; +use plonky2_field::{ + extension::{quadratic::QuadraticExtension, Extendable, Frobenius}, + goldilocks_field::GoldilocksField, + ops::Square, + types::{Field, Field64, PrimeField, PrimeField64, Sample}, +}; + +/// Goldilocks field extension degree. +pub const D: usize = 2; + +/// Goldilocks field wrapper with custom Poseidon permutation implementation. +#[derive(Copy, Clone, Serialize, Deserialize)] +pub struct GoldilocksFieldWrapper(pub GoldilocksField); + +impl Default for GoldilocksFieldWrapper { + fn default() -> Self { + Self(GoldilocksField::ZERO) + } +} + +impl PartialEq for GoldilocksFieldWrapper { + fn eq(&self, other: &Self) -> bool { + self.0.eq(&other.0) + } +} + +impl Eq for GoldilocksFieldWrapper {} + +impl Hash for GoldilocksFieldWrapper { + fn hash(&self, state: &mut H) { + self.0.hash(state); + } +} + +impl Display for GoldilocksFieldWrapper { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + Display::fmt(&self.0, f) + } +} + +impl Debug for GoldilocksFieldWrapper { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + Debug::fmt(&self.0, f) + } +} + +impl Sample for GoldilocksFieldWrapper { + fn sample(_rng: &mut R) -> Self + where + R: ?Sized, + { + unimplemented!("Not used in proofs verification") + } +} + +impl Field for GoldilocksFieldWrapper { + const ZERO: Self = Self(GoldilocksField::ZERO); + const ONE: Self = Self(GoldilocksField::ONE); + const TWO: Self = Self(GoldilocksField::TWO); + const NEG_ONE: Self = Self(GoldilocksField::NEG_ONE); + + const TWO_ADICITY: usize = GoldilocksField::TWO_ADICITY; + const CHARACTERISTIC_TWO_ADICITY: usize = GoldilocksField::TWO_ADICITY; + + // Sage: `g = GF(p).multiplicative_generator()` + const MULTIPLICATIVE_GROUP_GENERATOR: Self = + Self(GoldilocksField::MULTIPLICATIVE_GROUP_GENERATOR); + + // Sage: + // ``` + // g_2 = g^((p - 1) / 2^32) + // g_2.multiplicative_order().factor() + // ``` + const POWER_OF_TWO_GENERATOR: Self = Self(GoldilocksField::POWER_OF_TWO_GENERATOR); + + const BITS: usize = GoldilocksField::BITS; + + fn order() -> BigUint { + Self::ORDER.into() + } + fn characteristic() -> BigUint { + Self::order() + } + + /// Returns the inverse of the field element, using Fermat's little theorem. + /// The inverse of `a` is computed as `a^(p-2)`, where `p` is the prime + /// order of the field. + /// + /// Mathematically, this is equivalent to: + /// $a^(p-1) = 1 (mod p)$ + /// $a^(p-2) * a = 1 (mod p)$ + /// Therefore $a^(p-2) = a^-1 (mod p)$ + /// + /// The following code has been adapted from + /// winterfell/math/src/field/f64/mod.rs located at . + fn try_inverse(&self) -> Option { + if self.is_zero() { + return None; + } + + // compute base^(P - 2) using 72 multiplications + // The exponent P - 2 is represented in binary as: + // 0b1111111111111111111111111111111011111111111111111111111111111111 + + // compute base^11 + let t2 = self.square() * *self; + + // compute base^111 + let t3 = t2.square() * *self; + + // compute base^111111 (6 ones) + // repeatedly square t3 3 times and multiply by t3 + let t6 = exp_acc::<3>(t3, t3); + + // compute base^111111111111 (12 ones) + // repeatedly square t6 6 times and multiply by t6 + let t12 = exp_acc::<6>(t6, t6); + + // compute base^111111111111111111111111 (24 ones) + // repeatedly square t12 12 times and multiply by t12 + let t24 = exp_acc::<12>(t12, t12); + + // compute base^1111111111111111111111111111111 (31 ones) + // repeatedly square t24 6 times and multiply by t6 first. then square t30 and + // multiply by base + let t30 = exp_acc::<6>(t24, t6); + let t31 = t30.square() * *self; + + // compute base^111111111111111111111111111111101111111111111111111111111111111 + // repeatedly square t31 32 times and multiply by t31 + let t63 = exp_acc::<32>(t31, t31); + + // compute base^1111111111111111111111111111111011111111111111111111111111111111 + Some(t63.square() * *self) + } + + fn from_noncanonical_biguint(n: BigUint) -> Self { + // Biguint `mod_floor` operation - needs benchmarking + Self(GoldilocksField::from_noncanonical_biguint(n)) + } + + #[inline(always)] + fn from_canonical_u64(n: u64) -> Self { + Self(GoldilocksField::from_canonical_u64(n)) + } + + fn from_noncanonical_u96((n_lo, n_hi): (u64, u32)) -> Self { + // Contains reduction from u96 - needs benchmarking + Self(GoldilocksField::from_noncanonical_u96((n_lo, n_hi))) + } + + fn from_noncanonical_u128(n: u128) -> Self { + // Contains reduction from u128 - needs benchmarking + Self(GoldilocksField::from_noncanonical_u128(n)) + } + + #[inline] + fn from_noncanonical_u64(n: u64) -> Self { + Self(GoldilocksField::from_noncanonical_u64(n)) + } + + #[inline] + fn from_noncanonical_i64(n: i64) -> Self { + // Wrapping addition for negative numbers - needs benchmarking + Self(GoldilocksField::from_noncanonical_i64(n)) + } + + #[inline] + fn multiply_accumulate(&self, x: Self, y: Self) -> Self { + // u128 multiplication + addition, followed by reduction - needs benchmarking + Self(self.0.multiply_accumulate(x.0, y.0)) + } +} + +impl PrimeField for GoldilocksFieldWrapper { + fn to_canonical_biguint(&self) -> BigUint { + self.0.to_canonical_biguint() + } +} + +impl Field64 for GoldilocksFieldWrapper { + const ORDER: u64 = GoldilocksField::ORDER; + + #[inline] + unsafe fn add_canonical_u64(&self, rhs: u64) -> Self { + // Includes overflowing addition - needs benchmarking + Self(self.0.add_canonical_u64(rhs)) + } + + #[inline] + unsafe fn sub_canonical_u64(&self, rhs: u64) -> Self { + // Includes overflowing subtraction - needs benchmarking + Self(self.0.sub_canonical_u64(rhs)) + } +} + +impl PrimeField64 for GoldilocksFieldWrapper { + #[inline] + fn to_canonical_u64(&self) -> u64 { + self.0.to_canonical_u64() + } + + #[inline(always)] + fn to_noncanonical_u64(&self) -> u64 { + self.0.to_noncanonical_u64() + } +} + +impl Neg for GoldilocksFieldWrapper { + type Output = Self; + + #[inline] + fn neg(self) -> Self { + // Requires 1 u64 subtraction + Self(self.0.neg()) + } +} + +impl Add for GoldilocksFieldWrapper { + type Output = Self; + + #[inline] + #[allow(clippy::suspicious_arithmetic_impl)] + fn add(self, rhs: Self) -> Self { + Self(self.0 + rhs.0) + } +} + +impl AddAssign for GoldilocksFieldWrapper { + #[inline] + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } +} + +impl Sum for GoldilocksFieldWrapper { + fn sum>(iter: I) -> Self { + iter.fold(Self::ZERO, |acc, x| acc + x) + } +} + +impl Sub for GoldilocksFieldWrapper { + type Output = Self; + + #[inline] + #[allow(clippy::suspicious_arithmetic_impl)] + fn sub(self, rhs: Self) -> Self { + Self(self.0 - rhs.0) + } +} + +impl SubAssign for GoldilocksFieldWrapper { + #[inline] + fn sub_assign(&mut self, rhs: Self) { + *self = *self - rhs; + } +} + +impl Mul for GoldilocksFieldWrapper { + type Output = Self; + + #[inline] + fn mul(self, rhs: Self) -> Self { + Self(self.0 * rhs.0) + } +} + +impl MulAssign for GoldilocksFieldWrapper { + #[inline] + fn mul_assign(&mut self, rhs: Self) { + *self = *self * rhs; + } +} + +impl Product for GoldilocksFieldWrapper { + fn product>(iter: I) -> Self { + iter.fold(Self::ONE, |acc, x| acc * x) + } +} + +impl Div for GoldilocksFieldWrapper { + type Output = Self; + + #[allow(clippy::suspicious_arithmetic_impl)] + fn div(self, rhs: Self) -> Self::Output { + self * rhs.inverse() + } +} + +impl DivAssign for GoldilocksFieldWrapper { + fn div_assign(&mut self, rhs: Self) { + *self = *self / rhs; + } +} + +impl RichField for GoldilocksFieldWrapper {} + +/// Squares the base N number of times and multiplies the result by the tail +/// value. +#[inline(always)] +fn exp_acc( + base: GoldilocksFieldWrapper, + tail: GoldilocksFieldWrapper, +) -> GoldilocksFieldWrapper { + base.exp_power_of_2(N) * tail +} + +/// Goldilocks field wrapper type quadratic extension. +impl Frobenius<1> for GoldilocksFieldWrapper {} + +impl Extendable<2> for GoldilocksFieldWrapper { + type Extension = QuadraticExtension; + + // Verifiable in Sage with + // `R. = GF(p)[]; assert (x^2 - 7).is_irreducible()`. + const W: Self = Self(>::W); + + // DTH_ROOT = W^((ORDER - 1)/2) + const DTH_ROOT: Self = Self(>::DTH_ROOT); + + const EXT_MULTIPLICATIVE_GROUP_GENERATOR: [Self; 2] = [ + Self(>::EXT_MULTIPLICATIVE_GROUP_GENERATOR[0]), + Self(>::EXT_MULTIPLICATIVE_GROUP_GENERATOR[1]), + ]; + + const EXT_POWER_OF_TWO_GENERATOR: [Self; 2] = [ + Self(>::EXT_POWER_OF_TWO_GENERATOR[0]), + Self(>::EXT_POWER_OF_TWO_GENERATOR[1]), + ]; +} + +/// Poseidon hash input/output. +pub type PoseidonInOut = [u64; 12]; + +impl Poseidon for GoldilocksFieldWrapper { + const MDS_MATRIX_CIRC: [u64; 12] = GoldilocksField::MDS_MATRIX_CIRC; + const MDS_MATRIX_DIAG: [u64; 12] = GoldilocksField::MDS_MATRIX_DIAG; + + const FAST_PARTIAL_FIRST_ROUND_CONSTANT: [u64; 12] = + GoldilocksField::FAST_PARTIAL_FIRST_ROUND_CONSTANT; + + const FAST_PARTIAL_ROUND_CONSTANTS: [u64; N_PARTIAL_ROUNDS] = + GoldilocksField::FAST_PARTIAL_ROUND_CONSTANTS; + + const FAST_PARTIAL_ROUND_VS: [[u64; 12 - 1]; N_PARTIAL_ROUNDS] = + GoldilocksField::FAST_PARTIAL_ROUND_VS; + + const FAST_PARTIAL_ROUND_W_HATS: [[u64; 12 - 1]; N_PARTIAL_ROUNDS] = + GoldilocksField::FAST_PARTIAL_ROUND_W_HATS; + + // NB: This is in ROW-major order to support cache-friendly pre-multiplication. + const FAST_PARTIAL_ROUND_INITIAL_MATRIX: [[u64; 12 - 1]; 12 - 1] = + GoldilocksField::FAST_PARTIAL_ROUND_INITIAL_MATRIX; + + #[inline(always)] + fn mds_layer(state: &[Self; 12]) -> [Self; 12] { + let data: [GoldilocksField; 12] = state.map(|s| s.0); + GoldilocksField::mds_layer(&data).map(Self) + } + + #[inline(always)] + fn sbox_layer(state: &mut [Self; 12]) { + let mut data: [GoldilocksField; 12] = state.map(|s| s.0); + GoldilocksField::sbox_layer(&mut data); + // Mutate the original state based on the mutated data + for (s, d) in state.iter_mut().zip(data.iter()) { + s.0 = *d; + } + } + + #[inline] + fn poseidon(input: [Self; 12]) -> [Self; 12] { + // Using the fact that `GoldilocksFieldWrapper` is a newtype around `u64`. + let data: [u64; 12] = unsafe { core::mem::transmute(input) }; + + // Using proper conversion because not every u64 number is a valid field element. + exec::permute(data) + .expect("Error in permute") + .map(Field::from_canonical_u64) + } +} diff --git a/examples/plonky2-verifier/src/lib.rs b/examples/plonky2-verifier/src/lib.rs new file mode 100644 index 00000000000..50e4855bc83 --- /dev/null +++ b/examples/plonky2-verifier/src/lib.rs @@ -0,0 +1,42 @@ +// This file is part of Gear. +// +// Copyright (C) 2024 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(feature = "std")] +mod code { + include!(concat!(env!("OUT_DIR"), "/wasm_binary.rs")); +} + +#[cfg(feature = "std")] +pub use code::WASM_BINARY_OPT as WASM_BINARY; + +#[cfg(not(feature = "std"))] +pub const WASM_BINARY: &[u8] = &[]; + +#[cfg(not(feature = "std"))] +pub mod wasm; + +#[cfg(not(feature = "std"))] +pub mod serialize; + +#[cfg(not(feature = "std"))] +pub mod circuit; + +#[cfg(not(feature = "std"))] +pub mod goldilocks_field; diff --git a/examples/plonky2-verifier/src/serialize.rs b/examples/plonky2-verifier/src/serialize.rs new file mode 100644 index 00000000000..5f344840175 --- /dev/null +++ b/examples/plonky2-verifier/src/serialize.rs @@ -0,0 +1,49 @@ +// This file is part of Gear. +// +// Copyright (C) 2024 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Plonky2 circuit data and proof serialization. + +use plonky2::{ + field::extension::Extendable, + hash::hash_types::RichField, + plonk::{ + circuit_data::VerifierCircuitData, config::GenericConfig, proof::ProofWithPublicInputs, + }, + util::serialization::{Buffer, DefaultGateSerializer, Read}, +}; + +type CircuitDataAndProof = + (VerifierCircuitData, ProofWithPublicInputs); + +pub fn parse_circuit_data_and_proof( + data: &[u8], +) -> Result, &'static str> +where + F: RichField + Extendable, + C: GenericConfig, +{ + let gate_serializer = DefaultGateSerializer; + let mut buffer = Buffer::new(data); + let verifier_circuit_data = buffer + .read_verifier_circuit_data(&gate_serializer) + .map_err(|_| "Common circuit data parsing error")?; + let proof_with_pis = buffer + .read_proof_with_public_inputs(&verifier_circuit_data.common) + .map_err(|_| "Proof with public inputs parsing error")?; + Ok((verifier_circuit_data, proof_with_pis)) +} diff --git a/examples/plonky2-verifier/src/wasm.rs b/examples/plonky2-verifier/src/wasm.rs new file mode 100644 index 00000000000..7f32e4c9a95 --- /dev/null +++ b/examples/plonky2-verifier/src/wasm.rs @@ -0,0 +1,58 @@ +// This file is part of Gear. +// +// Copyright (C) 2024 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! A trivial Plonky2 proof verification program: +//! - the `handle` function takes a payload in a form of concatenated binary encodings of +//! `common_circuit_data` | `verifier_only_circuit_data` | `proof_with_public_inputs`, +//! parses the payload and passes the deserialized structs to the Plonky2's `verify` function. +//! Note that the payload is expected to be in the original Plonky2 binary format (not Scale). +//! The output message would either contain "Success" (as a byte array) or an error message. + +use super::{ + circuit::CustomPoseidonGoldilocksConfig as Config, + goldilocks_field::GoldilocksFieldWrapper as GF, serialize::parse_circuit_data_and_proof, +}; +use gstd::{debug, msg}; + +#[gstd::async_main] +async fn main() { + let payload = msg::load_bytes().expect("Failed to load payload"); + + let (verifier_circuit_data, proof) = + match parse_circuit_data_and_proof::(&payload) { + Ok(data) => data, + Err(e) => { + debug!("Failed to parse circuit data and proof: {}", e); + msg::reply_bytes(b"Decoding error", 0).expect("Failed to send reply"); + return; + } + }; + + match verifier_circuit_data.verify(proof) { + Ok(_) => { + msg::reply_bytes(b"Success", 0).expect("Failed to send reply"); + } + Err(e) => { + debug!("Failed to verify proof: {}", e); + msg::reply_bytes(b"Verification error", 0).expect("Failed to send reply"); + } + } +} + +#[no_mangle] +extern "C" fn init() {} diff --git a/examples/syscalls/src/lib.rs b/examples/syscalls/src/lib.rs index 6da55c28a3a..fe7e890d8f5 100644 --- a/examples/syscalls/src/lib.rs +++ b/examples/syscalls/src/lib.rs @@ -107,6 +107,8 @@ pub enum Kind { SystemReserveGas(u64), // Param(deposit amount) ReplyDeposit(u64), + // Param(input), Expected(hash) + Permute([u64; 12], [u64; 12]), } #[cfg(not(feature = "wasm-wrapper"))] diff --git a/examples/syscalls/src/wasm.rs b/examples/syscalls/src/wasm.rs index dd181b68094..dc4dc4bf485 100644 --- a/examples/syscalls/src/wasm.rs +++ b/examples/syscalls/src/wasm.rs @@ -406,6 +406,13 @@ fn process(syscall_kind: Kind) { exec::reply_deposit(mid, amount).expect("Kind::ReplyDeposit: call test failed"); } + Kind::Permute(input, expected_hash) => { + let actual_hash = exec::permute(input).expect("internal error: permute call failed"); + assert_eq!( + expected_hash, actual_hash, + "Kind::Permute: call test failed" + ); + } } } diff --git a/gcore/src/exec.rs b/gcore/src/exec.rs index 457836983ef..42705139113 100644 --- a/gcore/src/exec.rs +++ b/gcore/src/exec.rs @@ -27,7 +27,7 @@ use crate::{ ActorId, EnvVars, MessageId, }; use core::mem::MaybeUninit; -use gsys::BlockNumberWithHash; +use gsys::{BlockNumberWithHash, PoseidonInOut}; #[cfg(not(feature = "ethexe"))] use { crate::ReservationId, @@ -446,3 +446,30 @@ pub fn random(subject: [u8; 32]) -> Result<([u8; 32], u32)> { Ok((res.hash, res.bn)) } + +/// Perform input data transformation and return data in the same format. +/// Primarily designed for the Poseidon permutation of Goldilocks field arrays, +/// represented as fixed-size arrays of u64 type - the key part of the Poseidon +/// hash function. For the Goldilocks field the array size is equal to 12. +/// +/// `data` is the permutation input. +/// +/// # Examples +/// +/// ``` +/// use core::array; +/// use gcore::exec; +/// +/// #[no_mangle] +/// extern "C" fn handle() { +/// let data: [u64; 12] = array::from_fn(|i| i as u64 + 1); +/// let hash_out = exec::permute(data).expect("Error in random"); +/// } +/// ``` +pub fn permute(data: PoseidonInOut) -> Result { + let mut res = PoseidonInOut::default(); + + unsafe { gsys::gr_permute(data.as_ptr() as *const _, res.as_mut_ptr() as *mut _) }; + + Ok(res) +} diff --git a/gcore/src/utils.rs b/gcore/src/utils.rs index 9e3009f202d..544d35d8d71 100644 --- a/gcore/src/utils.rs +++ b/gcore/src/utils.rs @@ -20,21 +20,33 @@ use gprimitives::ReservationId; use gprimitives::{ActorId, CodeId, MessageId}; -pub(crate) trait AsRawPtr: AsRef<[u8]> + AsMut<[u8]> { - fn as_ptr(&self) -> *const [u8; 32] { +// pub(crate) trait AsRawPtr: AsRef<[u8]> + AsMut<[u8]> { +// fn as_ptr(&self) -> *const [u8; 32] { +// self.as_ref().as_ptr() as *const _ +// } + +// fn as_mut_ptr(&mut self) -> *mut [u8; 32] { +// self.as_mut().as_mut_ptr() as *mut _ +// } +// } +pub(crate) trait AsRawPtr: AsRef<[U]> + AsMut<[U]> +where + U: 'static + Copy, +{ + fn as_ptr(&self) -> *const [U; N] { self.as_ref().as_ptr() as *const _ } - fn as_mut_ptr(&mut self) -> *mut [u8; 32] { + fn as_mut_ptr(&mut self) -> *mut [U; N] { self.as_mut().as_mut_ptr() as *mut _ } } -impl AsRawPtr for ActorId {} -impl AsRawPtr for CodeId {} -impl AsRawPtr for MessageId {} +impl AsRawPtr for ActorId {} +impl AsRawPtr for CodeId {} +impl AsRawPtr for MessageId {} #[cfg(not(feature = "ethexe"))] -impl AsRawPtr for ReservationId {} +impl AsRawPtr for ReservationId {} /// Extensions for additional features. pub mod ext { diff --git a/gprimitives/client/Cargo.toml b/gprimitives/client/Cargo.toml new file mode 100644 index 00000000000..9acdaea430e --- /dev/null +++ b/gprimitives/client/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "gprimitives-client" +description = "Self-contained client-side primitives" +documentation = "https://docs.rs/gprimitives-client" +edition.workspace = true +version.workspace = true +authors.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true +rust-version.workspace = true + +[dependencies] +unroll.workspace = true +num = { workspace = true, features = ["alloc"] } + +[dev-dependencies] +rand = { workspace = true, features = ["std", "std_rng"] } + +[features] +default = ["std"] +std = [] diff --git a/gprimitives/client/src/field/arch/mod.rs b/gprimitives/client/src/field/arch/mod.rs new file mode 100644 index 00000000000..832557efd90 --- /dev/null +++ b/gprimitives/client/src/field/arch/mod.rs @@ -0,0 +1,2 @@ +#[cfg(target_arch = "x86_64")] +pub mod x86_64; diff --git a/gprimitives/client/src/field/arch/x86_64/avx2_goldilocks_field.rs b/gprimitives/client/src/field/arch/x86_64/avx2_goldilocks_field.rs new file mode 100644 index 00000000000..7253c51c8e4 --- /dev/null +++ b/gprimitives/client/src/field/arch/x86_64/avx2_goldilocks_field.rs @@ -0,0 +1,709 @@ +// This file is part of Gear. + +// Copyright (C) 2021-2024 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Cloned from [`plonky2`](https://github.com/0xPolygonZero/plonky2.git). + +use core::{ + arch::x86_64::*, + fmt, + fmt::{Debug, Formatter}, + iter::{Product, Sum}, + mem::transmute, + ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}, +}; + +use crate::field::{ + goldilocks_field::GoldilocksField, + ops::Square, + packed::PackedField, + types::{Field, Field64}, +}; + +/// AVX2 Goldilocks Field +/// +/// Ideally `Avx2GoldilocksField` would wrap `__m256i`. Unfortunately, `__m256i` has an alignment of +/// 32B, which would preclude us from casting `[GoldilocksField; 4]` (alignment 8B) to +/// `Avx2GoldilocksField`. We need to ensure that `Avx2GoldilocksField` has the same alignment as +/// `GoldilocksField`. Thus we wrap `[GoldilocksField; 4]` and use the `new` and `get` methods to +/// convert to and from `__m256i`. +#[derive(Copy, Clone)] +#[repr(transparent)] +pub struct Avx2GoldilocksField(pub [GoldilocksField; 4]); + +impl Avx2GoldilocksField { + #[inline] + fn new(x: __m256i) -> Self { + unsafe { transmute(x) } + } + #[inline] + fn get(&self) -> __m256i { + unsafe { transmute(*self) } + } +} + +impl Add for Avx2GoldilocksField { + type Output = Self; + #[inline] + fn add(self, rhs: Self) -> Self { + Self::new(unsafe { add(self.get(), rhs.get()) }) + } +} +impl Add for Avx2GoldilocksField { + type Output = Self; + #[inline] + fn add(self, rhs: GoldilocksField) -> Self { + self + Self::from(rhs) + } +} +impl Add for GoldilocksField { + type Output = Avx2GoldilocksField; + #[inline] + fn add(self, rhs: Self::Output) -> Self::Output { + Self::Output::from(self) + rhs + } +} +impl AddAssign for Avx2GoldilocksField { + #[inline] + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } +} +impl AddAssign for Avx2GoldilocksField { + #[inline] + fn add_assign(&mut self, rhs: GoldilocksField) { + *self = *self + rhs; + } +} + +impl Debug for Avx2GoldilocksField { + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "({:?})", self.get()) + } +} + +impl Default for Avx2GoldilocksField { + #[inline] + fn default() -> Self { + Self::ZEROS + } +} + +impl Div for Avx2GoldilocksField { + type Output = Self; + #[allow(clippy::suspicious_arithmetic_impl)] + #[inline] + fn div(self, rhs: GoldilocksField) -> Self { + self * rhs.inverse() + } +} +impl DivAssign for Avx2GoldilocksField { + #[allow(clippy::suspicious_op_assign_impl)] + #[inline] + fn div_assign(&mut self, rhs: GoldilocksField) { + *self *= rhs.inverse(); + } +} + +impl From for Avx2GoldilocksField { + fn from(x: GoldilocksField) -> Self { + Self([x; 4]) + } +} + +impl Mul for Avx2GoldilocksField { + type Output = Self; + #[inline] + fn mul(self, rhs: Self) -> Self { + Self::new(unsafe { mul(self.get(), rhs.get()) }) + } +} +impl Mul for Avx2GoldilocksField { + type Output = Self; + #[inline] + fn mul(self, rhs: GoldilocksField) -> Self { + self * Self::from(rhs) + } +} +impl Mul for GoldilocksField { + type Output = Avx2GoldilocksField; + #[inline] + fn mul(self, rhs: Avx2GoldilocksField) -> Self::Output { + Self::Output::from(self) * rhs + } +} +impl MulAssign for Avx2GoldilocksField { + #[inline] + fn mul_assign(&mut self, rhs: Self) { + *self = *self * rhs; + } +} +impl MulAssign for Avx2GoldilocksField { + #[inline] + fn mul_assign(&mut self, rhs: GoldilocksField) { + *self = *self * rhs; + } +} + +impl Neg for Avx2GoldilocksField { + type Output = Self; + #[inline] + fn neg(self) -> Self { + Self::new(unsafe { neg(self.get()) }) + } +} + +impl Product for Avx2GoldilocksField { + #[inline] + fn product>(iter: I) -> Self { + iter.reduce(|x, y| x * y).unwrap_or(Self::ONES) + } +} + +unsafe impl PackedField for Avx2GoldilocksField { + const WIDTH: usize = 4; + + type Scalar = GoldilocksField; + + const ZEROS: Self = Self([GoldilocksField::ZERO; 4]); + const ONES: Self = Self([GoldilocksField::ONE; 4]); + + #[inline] + fn from_slice(slice: &[Self::Scalar]) -> &Self { + assert_eq!(slice.len(), Self::WIDTH); + unsafe { &*slice.as_ptr().cast() } + } + #[inline] + fn from_slice_mut(slice: &mut [Self::Scalar]) -> &mut Self { + assert_eq!(slice.len(), Self::WIDTH); + unsafe { &mut *slice.as_mut_ptr().cast() } + } + #[inline] + fn as_slice(&self) -> &[Self::Scalar] { + &self.0[..] + } + #[inline] + fn as_slice_mut(&mut self) -> &mut [Self::Scalar] { + &mut self.0[..] + } + + #[inline] + fn interleave(&self, other: Self, block_len: usize) -> (Self, Self) { + let (v0, v1) = (self.get(), other.get()); + let (res0, res1) = match block_len { + 1 => unsafe { interleave1(v0, v1) }, + 2 => unsafe { interleave2(v0, v1) }, + 4 => (v0, v1), + _ => panic!("unsupported block_len"), + }; + (Self::new(res0), Self::new(res1)) + } +} + +impl Square for Avx2GoldilocksField { + #[inline] + fn square(&self) -> Self { + Self::new(unsafe { square(self.get()) }) + } +} + +impl Sub for Avx2GoldilocksField { + type Output = Self; + #[inline] + fn sub(self, rhs: Self) -> Self { + Self::new(unsafe { sub(self.get(), rhs.get()) }) + } +} +impl Sub for Avx2GoldilocksField { + type Output = Self; + #[inline] + fn sub(self, rhs: GoldilocksField) -> Self { + self - Self::from(rhs) + } +} +impl Sub for GoldilocksField { + type Output = Avx2GoldilocksField; + #[inline] + fn sub(self, rhs: Avx2GoldilocksField) -> Self::Output { + Self::Output::from(self) - rhs + } +} +impl SubAssign for Avx2GoldilocksField { + #[inline] + fn sub_assign(&mut self, rhs: Self) { + *self = *self - rhs; + } +} +impl SubAssign for Avx2GoldilocksField { + #[inline] + fn sub_assign(&mut self, rhs: GoldilocksField) { + *self = *self - rhs; + } +} + +impl Sum for Avx2GoldilocksField { + #[inline] + fn sum>(iter: I) -> Self { + iter.reduce(|x, y| x + y).unwrap_or(Self::ZEROS) + } +} + +// Resources: +// 1. Intel Intrinsics Guide for explanation of each intrinsic: +// https://software.intel.com/sites/landingpage/IntrinsicsGuide/ +// 2. uops.info lists micro-ops for each instruction: https://uops.info/table.html +// 3. Intel optimization manual for introduction to x86 vector extensions and best practices: +// https://software.intel.com/content/www/us/en/develop/download/intel-64-and-ia-32-architectures-optimization-reference-manual.html + +// Preliminary knowledge: +// 1. Vector code usually avoids branching. Instead of branches, we can do input selection with +// _mm256_blendv_epi8 or similar instruction. If all we're doing is conditionally zeroing a +// vector element then _mm256_and_si256 or _mm256_andnot_si256 may be used and are cheaper. +// +// 2. AVX does not support addition with carry but 128-bit (2-word) addition can be easily +// emulated. The method recognizes that for a + b overflowed iff (a + b) < a: +// i. res_lo = a_lo + b_lo +// ii. carry_mask = res_lo < a_lo +// iii. res_hi = a_hi + b_hi - carry_mask +// Notice that carry_mask is subtracted, not added. This is because AVX comparison instructions +// return -1 (all bits 1) for true and 0 for false. +// +// 3. AVX does not have unsigned 64-bit comparisons. Those can be emulated with signed comparisons +// by recognizing that a __m256i { + _mm256_xor_si256(x, SIGN_BIT) +} + +/// Convert to canonical representation. +/// The argument is assumed to be shifted by 1 << 63 (i.e. x_s = x + 1<<63, where x is the field +/// value). The returned value is similarly shifted by 1 << 63 (i.e. we return y_s = y + (1<<63), +/// where 0 <= y < FIELD_ORDER). +#[inline] +unsafe fn canonicalize_s(x_s: __m256i) -> __m256i { + // If x >= FIELD_ORDER then corresponding mask bits are all 0; otherwise all 1. + let mask = _mm256_cmpgt_epi64(SHIFTED_FIELD_ORDER, x_s); + // wrapback_amt is -FIELD_ORDER if mask is 0; otherwise 0. + let wrapback_amt = _mm256_andnot_si256(mask, EPSILON); + _mm256_add_epi64(x_s, wrapback_amt) +} + +/// Addition u64 + u64 -> u64. Assumes that x + y < 2^64 + FIELD_ORDER. The second argument is +/// pre-shifted by 1 << 63. The result is similarly shifted. +#[inline] +unsafe fn add_no_double_overflow_64_64s_s(x: __m256i, y_s: __m256i) -> __m256i { + let res_wrapped_s = _mm256_add_epi64(x, y_s); + let mask = _mm256_cmpgt_epi64(y_s, res_wrapped_s); // -1 if overflowed else 0. + let wrapback_amt = _mm256_srli_epi64::<32>(mask); // -FIELD_ORDER if overflowed else 0. + _mm256_add_epi64(res_wrapped_s, wrapback_amt) +} + +#[inline] +unsafe fn add(x: __m256i, y: __m256i) -> __m256i { + let y_s = shift(y); + let res_s = add_no_double_overflow_64_64s_s(x, canonicalize_s(y_s)); + shift(res_s) +} + +#[inline] +unsafe fn sub(x: __m256i, y: __m256i) -> __m256i { + let mut y_s = shift(y); + y_s = canonicalize_s(y_s); + let x_s = shift(x); + let mask = _mm256_cmpgt_epi64(y_s, x_s); // -1 if sub will underflow (y > x) else 0. + let wrapback_amt = _mm256_srli_epi64::<32>(mask); // -FIELD_ORDER if underflow else 0. + let res_wrapped = _mm256_sub_epi64(x_s, y_s); + _mm256_sub_epi64(res_wrapped, wrapback_amt) +} + +#[inline] +unsafe fn neg(y: __m256i) -> __m256i { + let y_s = shift(y); + _mm256_sub_epi64(SHIFTED_FIELD_ORDER, canonicalize_s(y_s)) +} + +/// Full 64-bit by 64-bit multiplication. This emulated multiplication is 1.33x slower than the +/// scalar instruction, but may be worth it if we want our data to live in vector registers. +#[inline] +unsafe fn mul64_64(x: __m256i, y: __m256i) -> (__m256i, __m256i) { + // We want to move the high 32 bits to the low position. The multiplication instruction ignores + // the high 32 bits, so it's ok to just duplicate it into the low position. This duplication can + // be done on port 5; bitshifts run on ports 0 and 1, competing with multiplication. + // This instruction is only provided for 32-bit floats, not integers. Idk why Intel makes the + // distinction; the casts are free and it guarantees that the exact bit pattern is preserved. + // Using a swizzle instruction of the wrong domain (float vs int) does not increase latency + // since Haswell. + let x_hi = _mm256_castps_si256(_mm256_movehdup_ps(_mm256_castsi256_ps(x))); + let y_hi = _mm256_castps_si256(_mm256_movehdup_ps(_mm256_castsi256_ps(y))); + + // All four pairwise multiplications + let mul_ll = _mm256_mul_epu32(x, y); + let mul_lh = _mm256_mul_epu32(x, y_hi); + let mul_hl = _mm256_mul_epu32(x_hi, y); + let mul_hh = _mm256_mul_epu32(x_hi, y_hi); + + // Bignum addition + // Extract high 32 bits of mul_ll and add to mul_hl. This cannot overflow. + let mul_ll_hi = _mm256_srli_epi64::<32>(mul_ll); + let t0 = _mm256_add_epi64(mul_hl, mul_ll_hi); + // Extract low 32 bits of t0 and add to mul_lh. Again, this cannot overflow. + // Also, extract high 32 bits of t0 and add to mul_hh. + let t0_lo = _mm256_and_si256(t0, EPSILON); + let t0_hi = _mm256_srli_epi64::<32>(t0); + let t1 = _mm256_add_epi64(mul_lh, t0_lo); + let t2 = _mm256_add_epi64(mul_hh, t0_hi); + // Lastly, extract the high 32 bits of t1 and add to t2. + let t1_hi = _mm256_srli_epi64::<32>(t1); + let res_hi = _mm256_add_epi64(t2, t1_hi); + + // Form res_lo by combining the low half of mul_ll with the low half of t1 (shifted into high + // position). + let t1_lo = _mm256_castps_si256(_mm256_moveldup_ps(_mm256_castsi256_ps(t1))); + let res_lo = _mm256_blend_epi32::<0xaa>(mul_ll, t1_lo); + + (res_hi, res_lo) +} + +/// Full 64-bit squaring. This routine is 1.2x faster than the scalar instruction. +#[inline] +unsafe fn square64(x: __m256i) -> (__m256i, __m256i) { + // Get high 32 bits of x. See comment in mul64_64_s. + let x_hi = _mm256_castps_si256(_mm256_movehdup_ps(_mm256_castsi256_ps(x))); + + // All pairwise multiplications. + let mul_ll = _mm256_mul_epu32(x, x); + let mul_lh = _mm256_mul_epu32(x, x_hi); + let mul_hh = _mm256_mul_epu32(x_hi, x_hi); + + // Bignum addition, but mul_lh is shifted by 33 bits (not 32). + let mul_ll_hi = _mm256_srli_epi64::<33>(mul_ll); + let t0 = _mm256_add_epi64(mul_lh, mul_ll_hi); + let t0_hi = _mm256_srli_epi64::<31>(t0); + let res_hi = _mm256_add_epi64(mul_hh, t0_hi); + + // Form low result by adding the mul_ll and the low 31 bits of mul_lh (shifted to the high + // position). + let mul_lh_lo = _mm256_slli_epi64::<33>(mul_lh); + let res_lo = _mm256_add_epi64(mul_ll, mul_lh_lo); + + (res_hi, res_lo) +} + +/// Goldilocks addition of a "small" number. `x_s` is pre-shifted by 2**63. `y` is assumed to be <= +/// `0xffffffff00000000`. The result is shifted by 2**63. +#[inline] +unsafe fn add_small_64s_64_s(x_s: __m256i, y: __m256i) -> __m256i { + let res_wrapped_s = _mm256_add_epi64(x_s, y); + // 32-bit compare is faster than 64-bit. It's safe as long as x > res_wrapped iff x >> 32 > + // res_wrapped >> 32. The case of x >> 32 > res_wrapped >> 32 is trivial and so is <. The case + // where x >> 32 = res_wrapped >> 32 remains. If x >> 32 = res_wrapped >> 32, then y >> 32 = + // 0xffffffff and the addition of the low 32 bits generated a carry. This can never occur if y + // <= 0xffffffff00000000: if y >> 32 = 0xffffffff, then no carry can occur. + let mask = _mm256_cmpgt_epi32(x_s, res_wrapped_s); // -1 if overflowed else 0. + // The mask contains 0xffffffff in the high 32 bits if wraparound occurred and 0 otherwise. + let wrapback_amt = _mm256_srli_epi64::<32>(mask); // -FIELD_ORDER if overflowed else 0. + _mm256_add_epi64(res_wrapped_s, wrapback_amt) +} + +/// Goldilocks subtraction of a "small" number. `x_s` is pre-shifted by 2**63. `y` is assumed to be +/// <= `0xffffffff00000000`. The result is shifted by 2**63. +#[inline] +unsafe fn sub_small_64s_64_s(x_s: __m256i, y: __m256i) -> __m256i { + let res_wrapped_s = _mm256_sub_epi64(x_s, y); + // 32-bit compare is faster than 64-bit. It's safe as long as res_wrapped > x iff res_wrapped >> + // 32 > x >> 32. The case of res_wrapped >> 32 > x >> 32 is trivial and so is <. The case where + // res_wrapped >> 32 = x >> 32 remains. If res_wrapped >> 32 = x >> 32, then y >> 32 = + // 0xffffffff and the subtraction of the low 32 bits generated a borrow. This can never occur if + // y <= 0xffffffff00000000: if y >> 32 = 0xffffffff, then no borrow can occur. + let mask = _mm256_cmpgt_epi32(res_wrapped_s, x_s); // -1 if underflowed else 0. + // The mask contains 0xffffffff in the high 32 bits if wraparound occurred and 0 otherwise. + let wrapback_amt = _mm256_srli_epi64::<32>(mask); // -FIELD_ORDER if underflowed else 0. + _mm256_sub_epi64(res_wrapped_s, wrapback_amt) +} + +#[inline] +unsafe fn reduce128(x: (__m256i, __m256i)) -> __m256i { + let (hi0, lo0) = x; + let lo0_s = shift(lo0); + let hi_hi0 = _mm256_srli_epi64::<32>(hi0); + let lo1_s = sub_small_64s_64_s(lo0_s, hi_hi0); + let t1 = _mm256_mul_epu32(hi0, EPSILON); + let lo2_s = add_small_64s_64_s(lo1_s, t1); + shift(lo2_s) +} + +/// Multiply two integers modulo FIELD_ORDER. +#[inline] +unsafe fn mul(x: __m256i, y: __m256i) -> __m256i { + reduce128(mul64_64(x, y)) +} + +/// Square an integer modulo FIELD_ORDER. +#[inline] +unsafe fn square(x: __m256i) -> __m256i { + reduce128(square64(x)) +} + +#[inline] +unsafe fn interleave1(x: __m256i, y: __m256i) -> (__m256i, __m256i) { + let a = _mm256_unpacklo_epi64(x, y); + let b = _mm256_unpackhi_epi64(x, y); + (a, b) +} + +#[inline] +unsafe fn interleave2(x: __m256i, y: __m256i) -> (__m256i, __m256i) { + let y_lo = _mm256_castsi256_si128(y); // This has 0 cost. + + // 1 places y_lo in the high half of x; 0 would place it in the lower half. + let a = _mm256_inserti128_si256::<1>(x, y_lo); + // NB: _mm256_permute2x128_si256 could be used here as well but _mm256_inserti128_si256 has + // lower latency on Zen 3 processors. + + // Each nibble of the constant has the following semantics: + // 0 => src1[low 128 bits] + // 1 => src1[high 128 bits] + // 2 => src2[low 128 bits] + // 3 => src2[high 128 bits] + // The low (resp. high) nibble chooses the low (resp. high) 128 bits of the result. + let b = _mm256_permute2x128_si256::<0x31>(x, y); + + (a, b) +} + +#[cfg(test)] +mod tests { + use crate::field::{ + arch::x86_64::avx2_goldilocks_field::Avx2GoldilocksField, + goldilocks_field::GoldilocksField, ops::Square, packed::PackedField, types::Field, + }; + + fn test_vals_a() -> [GoldilocksField; 4] { + [ + GoldilocksField::from_noncanonical_u64(14479013849828404771), + GoldilocksField::from_noncanonical_u64(9087029921428221768), + GoldilocksField::from_noncanonical_u64(2441288194761790662), + GoldilocksField::from_noncanonical_u64(5646033492608483824), + ] + } + fn test_vals_b() -> [GoldilocksField; 4] { + [ + GoldilocksField::from_noncanonical_u64(17891926589593242302), + GoldilocksField::from_noncanonical_u64(11009798273260028228), + GoldilocksField::from_noncanonical_u64(2028722748960791447), + GoldilocksField::from_noncanonical_u64(7929433601095175579), + ] + } + + #[test] + fn test_add() { + let a_arr = test_vals_a(); + let b_arr = test_vals_b(); + + let packed_a = *Avx2GoldilocksField::from_slice(&a_arr); + let packed_b = *Avx2GoldilocksField::from_slice(&b_arr); + let packed_res = packed_a + packed_b; + let arr_res = packed_res.as_slice(); + + let expected = a_arr.iter().zip(b_arr).map(|(&a, b)| a + b); + for (exp, &res) in expected.zip(arr_res) { + assert_eq!(res, exp); + } + } + + #[test] + fn test_mul() { + let a_arr = test_vals_a(); + let b_arr = test_vals_b(); + + let packed_a = *Avx2GoldilocksField::from_slice(&a_arr); + let packed_b = *Avx2GoldilocksField::from_slice(&b_arr); + let packed_res = packed_a * packed_b; + let arr_res = packed_res.as_slice(); + + let expected = a_arr.iter().zip(b_arr).map(|(&a, b)| a * b); + for (exp, &res) in expected.zip(arr_res) { + assert_eq!(res, exp); + } + } + + #[test] + fn test_square() { + let a_arr = test_vals_a(); + + let packed_a = *Avx2GoldilocksField::from_slice(&a_arr); + let packed_res = packed_a.square(); + let arr_res = packed_res.as_slice(); + + let expected = a_arr.iter().map(|&a| a.square()); + for (exp, &res) in expected.zip(arr_res) { + assert_eq!(res, exp); + } + } + + #[test] + fn test_neg() { + let a_arr = test_vals_a(); + + let packed_a = *Avx2GoldilocksField::from_slice(&a_arr); + let packed_res = -packed_a; + let arr_res = packed_res.as_slice(); + + let expected = a_arr.iter().map(|&a| -a); + for (exp, &res) in expected.zip(arr_res) { + assert_eq!(res, exp); + } + } + + #[test] + fn test_sub() { + let a_arr = test_vals_a(); + let b_arr = test_vals_b(); + + let packed_a = *Avx2GoldilocksField::from_slice(&a_arr); + let packed_b = *Avx2GoldilocksField::from_slice(&b_arr); + let packed_res = packed_a - packed_b; + let arr_res = packed_res.as_slice(); + + let expected = a_arr.iter().zip(b_arr).map(|(&a, b)| a - b); + for (exp, &res) in expected.zip(arr_res) { + assert_eq!(res, exp); + } + } + + #[test] + fn test_interleave_is_involution() { + let a_arr = test_vals_a(); + let b_arr = test_vals_b(); + + let packed_a = *Avx2GoldilocksField::from_slice(&a_arr); + let packed_b = *Avx2GoldilocksField::from_slice(&b_arr); + { + // Interleave, then deinterleave. + let (x, y) = packed_a.interleave(packed_b, 1); + let (res_a, res_b) = x.interleave(y, 1); + assert_eq!(res_a.as_slice(), a_arr); + assert_eq!(res_b.as_slice(), b_arr); + } + { + let (x, y) = packed_a.interleave(packed_b, 2); + let (res_a, res_b) = x.interleave(y, 2); + assert_eq!(res_a.as_slice(), a_arr); + assert_eq!(res_b.as_slice(), b_arr); + } + { + let (x, y) = packed_a.interleave(packed_b, 4); + let (res_a, res_b) = x.interleave(y, 4); + assert_eq!(res_a.as_slice(), a_arr); + assert_eq!(res_b.as_slice(), b_arr); + } + } + + #[allow(clippy::zero_prefixed_literal)] + #[test] + fn test_interleave() { + let in_a: [GoldilocksField; 4] = [ + GoldilocksField::from_noncanonical_u64(00), + GoldilocksField::from_noncanonical_u64(01), + GoldilocksField::from_noncanonical_u64(02), + GoldilocksField::from_noncanonical_u64(03), + ]; + let in_b: [GoldilocksField; 4] = [ + GoldilocksField::from_noncanonical_u64(10), + GoldilocksField::from_noncanonical_u64(11), + GoldilocksField::from_noncanonical_u64(12), + GoldilocksField::from_noncanonical_u64(13), + ]; + let int1_a: [GoldilocksField; 4] = [ + GoldilocksField::from_noncanonical_u64(00), + GoldilocksField::from_noncanonical_u64(10), + GoldilocksField::from_noncanonical_u64(02), + GoldilocksField::from_noncanonical_u64(12), + ]; + let int1_b: [GoldilocksField; 4] = [ + GoldilocksField::from_noncanonical_u64(01), + GoldilocksField::from_noncanonical_u64(11), + GoldilocksField::from_noncanonical_u64(03), + GoldilocksField::from_noncanonical_u64(13), + ]; + let int2_a: [GoldilocksField; 4] = [ + GoldilocksField::from_noncanonical_u64(00), + GoldilocksField::from_noncanonical_u64(01), + GoldilocksField::from_noncanonical_u64(10), + GoldilocksField::from_noncanonical_u64(11), + ]; + let int2_b: [GoldilocksField; 4] = [ + GoldilocksField::from_noncanonical_u64(02), + GoldilocksField::from_noncanonical_u64(03), + GoldilocksField::from_noncanonical_u64(12), + GoldilocksField::from_noncanonical_u64(13), + ]; + + let packed_a = *Avx2GoldilocksField::from_slice(&in_a); + let packed_b = *Avx2GoldilocksField::from_slice(&in_b); + { + let (x1, y1) = packed_a.interleave(packed_b, 1); + assert_eq!(x1.as_slice(), int1_a); + assert_eq!(y1.as_slice(), int1_b); + } + { + let (x2, y2) = packed_a.interleave(packed_b, 2); + assert_eq!(x2.as_slice(), int2_a); + assert_eq!(y2.as_slice(), int2_b); + } + { + let (x4, y4) = packed_a.interleave(packed_b, 4); + assert_eq!(x4.as_slice(), in_a); + assert_eq!(y4.as_slice(), in_b); + } + } +} diff --git a/gprimitives/client/src/field/arch/x86_64/mod.rs b/gprimitives/client/src/field/arch/x86_64/mod.rs new file mode 100644 index 00000000000..40e847317ee --- /dev/null +++ b/gprimitives/client/src/field/arch/x86_64/mod.rs @@ -0,0 +1,22 @@ +// This file is part of Gear. + +// Copyright (C) 2021-2024 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Modified based on [`plonky2`](https://github.com/0xPolygonZero/plonky2.git). + +#[cfg(target_feature = "avx2")] +pub mod avx2_goldilocks_field; diff --git a/gprimitives/client/src/field/extension/mod.rs b/gprimitives/client/src/field/extension/mod.rs new file mode 100644 index 00000000000..7ec6aec6afb --- /dev/null +++ b/gprimitives/client/src/field/extension/mod.rs @@ -0,0 +1,163 @@ +// This file is part of Gear. + +// Copyright (C) 2021-2024 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Modified based on the [`plonky2`](https://github.com/0xPolygonZero/plonky2.git). + +use alloc::vec::Vec; + +use crate::field::types::Field; + +pub mod quadratic; + +/// Optimal extension field trait. +/// A degree `d` field extension is optimal if there exists a base field element `W`, +/// such that the extension is `F[X]/(X^d-W)`. +#[allow(clippy::upper_case_acronyms)] +pub trait OEF: FieldExtension { + // Element W of BaseField, such that `X^d - W` is irreducible over BaseField. + const W: Self::BaseField; + + // Element of BaseField such that DTH_ROOT^D == 1. Implementers + // should set this to W^((p - 1)/D), where W is as above and p is + // the order of the BaseField. + const DTH_ROOT: Self::BaseField; +} + +impl OEF<1> for F { + const W: Self::BaseField = F::ONE; + const DTH_ROOT: Self::BaseField = F::ONE; +} + +pub trait Frobenius: OEF { + /// FrobeniusField automorphisms: x -> x^p, where p is the order of BaseField. + fn frobenius(&self) -> Self { + self.repeated_frobenius(1) + } + + /// Repeated Frobenius automorphisms: x -> x^(p^count). + /// + /// Follows precomputation suggestion in Section 11.3.3 of the + /// Handbook of Elliptic and Hyperelliptic Curve Cryptography. + fn repeated_frobenius(&self, count: usize) -> Self { + if count == 0 { + return *self; + } else if count >= D { + // x |-> x^(p^D) is the identity, so x^(p^count) == + // x^(p^(count % D)) + return self.repeated_frobenius(count % D); + } + let arr = self.to_basefield_array(); + + // z0 = DTH_ROOT^count = W^(k * count) where k = floor((p^D-1)/D) + let mut z0 = Self::DTH_ROOT; + for _ in 1..count { + z0 *= Self::DTH_ROOT; + } + + let mut res = [Self::BaseField::ZERO; D]; + for (i, z) in z0.powers().take(D).enumerate() { + res[i] = arr[i] * z; + } + + Self::from_basefield_array(res) + } +} + +pub trait Extendable: Field + Sized { + type Extension: Field + OEF + Frobenius + From; + + const W: Self; + + const DTH_ROOT: Self; + + /// Chosen so that when raised to the power `(p^D - 1) >> F::Extension::TWO_ADICITY)` + /// we obtain F::EXT_POWER_OF_TWO_GENERATOR. + const EXT_MULTIPLICATIVE_GROUP_GENERATOR: [Self; D]; + + /// Chosen so that when raised to the power `1<<(Self::TWO_ADICITY-Self::BaseField::TWO_ADICITY)`, + /// we get `Self::BaseField::POWER_OF_TWO_GENERATOR`. This makes `primitive_root_of_unity` coherent + /// with the base field which implies that the FFT commutes with field inclusion. + const EXT_POWER_OF_TWO_GENERATOR: [Self; D]; +} + +impl + FieldExtension<1, BaseField = F>> Extendable<1> for F { + type Extension = F; + const W: Self = F::ONE; + const DTH_ROOT: Self = F::ONE; + const EXT_MULTIPLICATIVE_GROUP_GENERATOR: [Self; 1] = [F::MULTIPLICATIVE_GROUP_GENERATOR]; + const EXT_POWER_OF_TWO_GENERATOR: [Self; 1] = [F::POWER_OF_TWO_GENERATOR]; +} + +pub trait FieldExtension: Field { + type BaseField: Field; + + fn to_basefield_array(&self) -> [Self::BaseField; D]; + + fn from_basefield_array(arr: [Self::BaseField; D]) -> Self; + + fn from_basefield(x: Self::BaseField) -> Self; + + fn is_in_basefield(&self) -> bool { + self.to_basefield_array()[1..].iter().all(|x| x.is_zero()) + } + + fn scalar_mul(&self, scalar: Self::BaseField) -> Self { + let mut res = self.to_basefield_array(); + res.iter_mut().for_each(|x| { + *x *= scalar; + }); + Self::from_basefield_array(res) + } +} + +impl FieldExtension<1> for F { + type BaseField = F; + + fn to_basefield_array(&self) -> [Self::BaseField; 1] { + [*self] + } + + fn from_basefield_array(arr: [Self::BaseField; 1]) -> Self { + arr[0] + } + + fn from_basefield(x: Self::BaseField) -> Self { + x + } +} + +/// Flatten the slice by sending every extension field element to its D-sized canonical representation. +pub fn flatten(l: &[F::Extension]) -> Vec +where + F: Field + Extendable, +{ + l.iter() + .flat_map(|x| x.to_basefield_array().to_vec()) + .collect() +} + +/// Batch every D-sized chunks into extension field elements. +pub fn unflatten(l: &[F]) -> Vec +where + F: Field + Extendable, +{ + debug_assert_eq!(l.len() % D, 0); + l.chunks_exact(D) + .map(|c| F::Extension::from_basefield_array(c.to_vec().try_into().unwrap())) + .collect() +} diff --git a/gprimitives/client/src/field/extension/quadratic.rs b/gprimitives/client/src/field/extension/quadratic.rs new file mode 100644 index 00000000000..125af15e04e --- /dev/null +++ b/gprimitives/client/src/field/extension/quadratic.rs @@ -0,0 +1,307 @@ +// This file is part of Gear. + +// Copyright (C) 2021-2024 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Modified based on the [`plonky2`](https://github.com/0xPolygonZero/plonky2.git). + +use core::{ + fmt::{self, Debug, Display, Formatter}, + iter::{Product, Sum}, + ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}, +}; + +use num::bigint::BigUint; + +use crate::field::{ + extension::{Extendable, FieldExtension, Frobenius, OEF}, + ops::Square, + types::Field, +}; + +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub struct QuadraticExtension>(pub [F; 2]); + +impl> Default for QuadraticExtension +where + Self: Mul, +{ + fn default() -> Self { + Self::ZERO + } +} + +impl> OEF<2> for QuadraticExtension +where + Self: Mul, +{ + const W: F = F::W; + const DTH_ROOT: F = F::DTH_ROOT; +} + +impl> Frobenius<2> for QuadraticExtension where Self: Mul {} + +impl> FieldExtension<2> for QuadraticExtension +where + Self: Mul, +{ + type BaseField = F; + + fn to_basefield_array(&self) -> [F; 2] { + self.0 + } + + fn from_basefield_array(arr: [F; 2]) -> Self { + Self(arr) + } + + fn from_basefield(x: F) -> Self { + x.into() + } +} + +impl> From for QuadraticExtension { + fn from(x: F) -> Self { + Self([x, F::ZERO]) + } +} + +impl> Field for QuadraticExtension +where + Self: Mul, +{ + const ZERO: Self = Self([F::ZERO; 2]); + const ONE: Self = Self([F::ONE, F::ZERO]); + const TWO: Self = Self([F::TWO, F::ZERO]); + const NEG_ONE: Self = Self([F::NEG_ONE, F::ZERO]); + + // `p^2 - 1 = (p - 1)(p + 1)`. The `p - 1` term has a two-adicity of `F::TWO_ADICITY`. As + // long as `F::TWO_ADICITY >= 2`, `p` can be written as `4n + 1`, so `p + 1` can be written as + // `2(2n + 1)`, which has a 2-adicity of 1. + const TWO_ADICITY: usize = F::TWO_ADICITY + 1; + const CHARACTERISTIC_TWO_ADICITY: usize = F::CHARACTERISTIC_TWO_ADICITY; + + const MULTIPLICATIVE_GROUP_GENERATOR: Self = Self(F::EXT_MULTIPLICATIVE_GROUP_GENERATOR); + const POWER_OF_TWO_GENERATOR: Self = Self(F::EXT_POWER_OF_TWO_GENERATOR); + + const BITS: usize = F::BITS * 2; + + fn order() -> BigUint { + F::order() * F::order() + } + fn characteristic() -> BigUint { + F::characteristic() + } + + // Algorithm 11.3.4 in Handbook of Elliptic and Hyperelliptic Curve Cryptography. + fn try_inverse(&self) -> Option { + if self.is_zero() { + return None; + } + + let a_pow_r_minus_1 = self.frobenius(); + let a_pow_r = a_pow_r_minus_1 * *self; + debug_assert!(FieldExtension::<2>::is_in_basefield(&a_pow_r)); + + Some(FieldExtension::<2>::scalar_mul( + &a_pow_r_minus_1, + a_pow_r.0[0].inverse(), + )) + } + + fn from_noncanonical_biguint(n: BigUint) -> Self { + F::from_noncanonical_biguint(n).into() + } + + fn from_canonical_u64(n: u64) -> Self { + F::from_canonical_u64(n).into() + } + + fn from_noncanonical_u128(n: u128) -> Self { + F::from_noncanonical_u128(n).into() + } + + fn from_noncanonical_i64(n: i64) -> Self { + F::from_noncanonical_i64(n).into() + } + + fn from_noncanonical_u64(n: u64) -> Self { + F::from_noncanonical_u64(n).into() + } +} + +impl> Display for QuadraticExtension { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "{} + {}*a", self.0[0], self.0[1]) + } +} + +impl> Debug for QuadraticExtension { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + Display::fmt(self, f) + } +} + +impl> Neg for QuadraticExtension { + type Output = Self; + + #[inline] + fn neg(self) -> Self { + Self([-self.0[0], -self.0[1]]) + } +} + +impl> Add for QuadraticExtension { + type Output = Self; + + #[inline] + fn add(self, rhs: Self) -> Self { + Self([self.0[0] + rhs.0[0], self.0[1] + rhs.0[1]]) + } +} + +impl> AddAssign for QuadraticExtension { + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } +} + +impl> Sum for QuadraticExtension +where + Self: Mul, +{ + fn sum>(iter: I) -> Self { + iter.fold(Self::ZERO, |acc, x| acc + x) + } +} + +impl> Sub for QuadraticExtension { + type Output = Self; + + #[inline] + fn sub(self, rhs: Self) -> Self { + Self([self.0[0] - rhs.0[0], self.0[1] - rhs.0[1]]) + } +} + +impl> SubAssign for QuadraticExtension { + #[inline] + fn sub_assign(&mut self, rhs: Self) { + *self = *self - rhs; + } +} + +// Note: disable the default `Mul` implementation to avoid overlapping with the +// specialisation for the GoldilocksField quadratic extension. +// impl> Mul for QuadraticExtension { +// type Output = Self; + +// #[inline] +// default fn mul(self, rhs: Self) -> Self { +// let Self([a0, a1]) = self; +// let Self([b0, b1]) = rhs; + +// let c0 = a0 * b0 + >::W * a1 * b1; +// let c1 = a0 * b1 + a1 * b0; + +// Self([c0, c1]) +// } +// } + +impl> MulAssign for QuadraticExtension +where + Self: Mul, +{ + #[inline] + fn mul_assign(&mut self, rhs: Self) { + *self = *self * rhs; + } +} + +impl> Square for QuadraticExtension +where + Self: Mul, +{ + #[inline(always)] + fn square(&self) -> Self { + // Specialising mul reduces the computation of c1 from 2 muls + // and one add to one mul and a shift + + let Self([a0, a1]) = *self; + + let c0 = a0.square() + >::W * a1.square(); + let c1 = a0 * a1.double(); + + Self([c0, c1]) + } +} + +impl> Product for QuadraticExtension +where + Self: Mul, +{ + fn product>(iter: I) -> Self { + iter.fold(Self::ONE, |acc, x| acc * x) + } +} + +impl> Div for QuadraticExtension +where + Self: Mul, +{ + type Output = Self; + + #[allow(clippy::suspicious_arithmetic_impl)] + fn div(self, rhs: Self) -> Self::Output { + self * rhs.inverse() + } +} + +impl> DivAssign for QuadraticExtension +where + Self: Mul, +{ + fn div_assign(&mut self, rhs: Self) { + *self = *self / rhs; + } +} + +#[cfg(test)] +mod tests { + use super::{Extendable, QuadraticExtension}; + use crate::field::field_testing::Sample; + + impl + Sample> Sample for QuadraticExtension { + #[inline] + fn sample(rng: &mut R) -> Self + where + R: rand::RngCore + ?Sized, + { + Self([F::sample(rng), F::sample(rng)]) + } + } + + mod goldilocks { + use crate::{test_field_arithmetic, test_field_extension}; + + test_field_extension!(crate::field::goldilocks_field::GoldilocksField, 2); + test_field_arithmetic!( + crate::field::extension::quadratic::QuadraticExtension< + crate::field::goldilocks_field::GoldilocksField, + > + ); + } +} diff --git a/gprimitives/client/src/field/field_testing.rs b/gprimitives/client/src/field/field_testing.rs new file mode 100644 index 00000000000..2ffd011d540 --- /dev/null +++ b/gprimitives/client/src/field/field_testing.rs @@ -0,0 +1,270 @@ +// This file is part of Gear. + +// Copyright (C) 2021-2024 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Cloned from the [`plonky2`](https://github.com/0xPolygonZero/plonky2.git). + +use crate::field::{ + extension::{Extendable, Frobenius}, + ops::Square, + types::Field, +}; +use rand::rngs::OsRng; + +/// Sampling +#[allow(dead_code)] +pub trait Sample: Sized { + /// Samples a single value using `rng`. + fn sample(rng: &mut R) -> Self + where + R: rand::RngCore + ?Sized; + + /// Samples a single value using the [`OsRng`]. + #[inline] + fn rand() -> Self { + Self::sample(&mut OsRng) + } + + /// Samples a [`Vec`] of values of length `n` using [`OsRng`]. + #[inline] + fn rand_vec(n: usize) -> Vec { + (0..n).map(|_| Self::rand()).collect() + } + + /// Samples an array of values of length `N` using [`OsRng`]. + #[inline] + fn rand_array() -> [Self; N] { + Self::rand_vec(N) + .try_into() + .ok() + .expect("This conversion can never fail.") + } +} + +#[macro_export] +macro_rules! test_field_arithmetic { + ($field:ty) => { + mod field_arithmetic { + use alloc::vec::Vec; + + use num::bigint::BigUint; + use rand::{rngs::OsRng, Rng, RngCore}; + use $crate::field::{field_testing::Sample, types::Field}; + + #[test] + fn modular_reduction() { + let mut rng = OsRng; + for _ in 0..10 { + let x_lo = rng.next_u64(); + let x_hi = rng.next_u32(); + let x = (x_lo as u128) + ((x_hi as u128) << 64); + let a = <$field>::from_noncanonical_u128(x); + let b = <$field>::from_noncanonical_u96((x_lo, x_hi)); + assert_eq!(a, b); + } + } + + #[test] + fn batch_inversion() { + for n in 0..20 { + let xs = (1..=n as u64) + .map(|i| <$field>::from_canonical_u64(i)) + .collect::>(); + let invs = <$field>::batch_multiplicative_inverse(&xs); + assert_eq!(invs.len(), n); + for (x, inv) in xs.into_iter().zip(invs) { + assert_eq!(x * inv, <$field>::ONE); + } + } + } + + #[test] + fn primitive_root_order() { + let max_power = 8.min(<$field>::TWO_ADICITY); + for n_power in 0..max_power { + let root = <$field>::primitive_root_of_unity(n_power); + let order = <$field>::generator_order(root); + assert_eq!(order, 1 << n_power, "2^{}'th primitive root", n_power); + } + } + + #[test] + fn negation() { + type F = $field; + + for x in [F::ZERO, F::ONE, F::TWO, F::NEG_ONE] { + assert_eq!(x + -x, F::ZERO); + } + } + + #[test] + fn exponentiation() { + type F = $field; + + assert_eq!(F::ZERO.exp_u64(0), ::ONE); + assert_eq!(F::ONE.exp_u64(0), ::ONE); + assert_eq!(F::TWO.exp_u64(0), ::ONE); + + assert_eq!(F::ZERO.exp_u64(1), ::ZERO); + assert_eq!(F::ONE.exp_u64(1), ::ONE); + assert_eq!(F::TWO.exp_u64(1), ::TWO); + + assert_eq!(F::ZERO.kth_root_u64(1), ::ZERO); + assert_eq!(F::ONE.kth_root_u64(1), ::ONE); + assert_eq!(F::TWO.kth_root_u64(1), ::TWO); + + for power in 1..10 { + if F::is_monomial_permutation_u64(power) { + let x = F::rand(); + assert_eq!(x.exp_u64(power).kth_root_u64(power), x); + } + } + } + + #[test] + fn exponentiation_large() { + type F = $field; + + let mut rng = OsRng; + + let base = F::rand(); + let pow = BigUint::from(rng.gen::()); + let cycles = rng.gen::(); + let mul_group_order = F::order() - 1u32; + let big_pow = &pow + &mul_group_order * cycles; + let big_pow_wrong = &pow + &mul_group_order * cycles + 1u32; + + assert_eq!(base.exp_biguint(&pow), base.exp_biguint(&big_pow)); + assert_ne!(base.exp_biguint(&pow), base.exp_biguint(&big_pow_wrong)); + } + + #[test] + fn inverses() { + type F = $field; + + let x = F::rand(); + let x1 = x.inverse(); + let x2 = x1.inverse(); + let x3 = x2.inverse(); + + assert_eq!(x, x2); + assert_eq!(x1, x3); + } + } + }; +} + +#[allow(clippy::eq_op)] +pub(crate) fn test_add_neg_sub_mul, const D: usize>() +where + BF::Extension: Sample, +{ + let x = BF::Extension::rand(); + let y = BF::Extension::rand(); + let z = BF::Extension::rand(); + assert_eq!(x + (-x), BF::Extension::ZERO); + assert_eq!(-x, BF::Extension::ZERO - x); + assert_eq!(x + x, x * BF::Extension::TWO); + assert_eq!(x * (-x), -x.square()); + assert_eq!(x + y, y + x); + assert_eq!(x * y, y * x); + assert_eq!(x * (y * z), (x * y) * z); + assert_eq!(x - (y + z), (x - y) - z); + assert_eq!((x + y) - z, x + (y - z)); + assert_eq!(x * (y + z), x * y + x * z); +} + +pub(crate) fn test_inv_div, const D: usize>() +where + BF::Extension: Sample, +{ + let x = BF::Extension::rand(); + let y = BF::Extension::rand(); + let z = BF::Extension::rand(); + assert_eq!(x * x.inverse(), BF::Extension::ONE); + assert_eq!(x.inverse() * x, BF::Extension::ONE); + assert_eq!(x.square().inverse(), x.inverse().square()); + assert_eq!((x / y) * y, x); + assert_eq!(x / (y * z), (x / y) / z); + assert_eq!((x * y) / z, x * (y / z)); +} + +pub(crate) fn test_frobenius, const D: usize>() +where + BF::Extension: Sample, +{ + let x = BF::Extension::rand(); + assert_eq!(x.exp_biguint(&BF::order()), x.frobenius()); + for count in 2..D { + assert_eq!( + x.repeated_frobenius(count), + (0..count).fold(x, |acc, _| acc.frobenius()) + ); + } +} + +pub(crate) fn test_field_order, const D: usize>() +where + BF::Extension: Sample, +{ + let x = BF::Extension::rand(); + assert_eq!( + x.exp_biguint(&(BF::Extension::order() - 1u8)), + BF::Extension::ONE + ); +} + +pub(crate) fn test_power_of_two_gen, const D: usize>() { + assert_eq!( + BF::Extension::MULTIPLICATIVE_GROUP_GENERATOR + .exp_biguint(&(BF::Extension::order() >> BF::Extension::TWO_ADICITY)), + BF::Extension::POWER_OF_TWO_GENERATOR, + ); + assert_eq!( + BF::Extension::POWER_OF_TWO_GENERATOR + .exp_u64(1 << (BF::Extension::TWO_ADICITY - BF::TWO_ADICITY)), + BF::POWER_OF_TWO_GENERATOR.into() + ); +} + +#[macro_export] +macro_rules! test_field_extension { + ($field:ty, $d:expr) => { + mod field_extension { + #[test] + fn test_add_neg_sub_mul() { + $crate::field::field_testing::test_add_neg_sub_mul::<$field, $d>(); + } + #[test] + fn test_inv_div() { + $crate::field::field_testing::test_inv_div::<$field, $d>(); + } + #[test] + fn test_frobenius() { + $crate::field::field_testing::test_frobenius::<$field, $d>(); + } + #[test] + fn test_field_order() { + $crate::field::field_testing::test_field_order::<$field, $d>(); + } + #[test] + fn test_power_of_two_gen() { + $crate::field::field_testing::test_power_of_two_gen::<$field, $d>(); + } + } + }; +} diff --git a/gprimitives/client/src/field/goldilocks_extensions.rs b/gprimitives/client/src/field/goldilocks_extensions.rs new file mode 100644 index 00000000000..84aa7c12e36 --- /dev/null +++ b/gprimitives/client/src/field/goldilocks_extensions.rs @@ -0,0 +1,132 @@ +// This file is part of Gear. + +// Copyright (C) 2021-2024 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Modified based on the [`plonky2`](https://github.com/0xPolygonZero/plonky2.git). +//! Goldilocks field extensions. + +#![allow(dead_code)] + +use core::ops::Mul; + +use crate::field::{ + extension::{quadratic::QuadraticExtension, Extendable, Frobenius}, + goldilocks_field::{reduce160, GoldilocksField}, +}; + +impl Frobenius<1> for GoldilocksField {} + +impl Extendable<2> for GoldilocksField { + type Extension = QuadraticExtension; + + // Verifiable in Sage with + // `R. = GF(p)[]; assert (x^2 - 7).is_irreducible()`. + const W: Self = Self(7); + + // DTH_ROOT = W^((ORDER - 1)/2) + const DTH_ROOT: Self = Self(18446744069414584320); + + const EXT_MULTIPLICATIVE_GROUP_GENERATOR: [Self; 2] = [Self(0), Self(11713931119993638672)]; + + const EXT_POWER_OF_TWO_GENERATOR: [Self; 2] = [Self(0), Self(7226896044987257365)]; +} + +impl Mul for QuadraticExtension { + type Output = Self; + + #[inline] + fn mul(self, rhs: Self) -> Self { + let Self([a0, a1]) = self; + let Self([b0, b1]) = rhs; + let c = ext2_mul([a0.0, a1.0], [b0.0, b1.0]); + Self(c) + } +} + +/* + * The functions extD_add_prods[0-4] are helper functions for + * computing products for extensions of degree D over the Goldilocks + * field. They are faster than the generic method because all + * reductions are delayed until the end which means only one per + * result coefficient is necessary. + */ + +/// Return `a`, `b` such that `a + b*2^128 = 3*(x + y*2^128)` with `a < 2^128` and `b < 2^32`. +#[inline(always)] +const fn u160_times_3(x: u128, y: u32) -> (u128, u32) { + let (s, cy) = x.overflowing_add(x << 1); + (s, 3 * y + (x >> 127) as u32 + cy as u32) +} + +/// Return `a`, `b` such that `a + b*2^128 = 7*(x + y*2^128)` with `a < 2^128` and `b < 2^32`. +#[inline(always)] +const fn u160_times_7(x: u128, y: u32) -> (u128, u32) { + let (d, br) = (x << 3).overflowing_sub(x); + // NB: subtracting the borrow can't underflow + (d, 7 * y + (x >> (128 - 3)) as u32 - br as u32) +} + +/* + * Quadratic multiplication and squaring + */ + +#[inline(always)] +fn ext2_add_prods0(a: &[u64; 2], b: &[u64; 2]) -> GoldilocksField { + // Computes a0 * b0 + W * a1 * b1; + let [a0, a1] = *a; + let [b0, b1] = *b; + + let cy; + + // W * a1 * b1 + let (mut cumul_lo, mut cumul_hi) = u160_times_7((a1 as u128) * (b1 as u128), 0u32); + + // a0 * b0 + (cumul_lo, cy) = cumul_lo.overflowing_add((a0 as u128) * (b0 as u128)); + cumul_hi += cy as u32; + + unsafe { reduce160(cumul_lo, cumul_hi) } +} + +#[inline(always)] +fn ext2_add_prods1(a: &[u64; 2], b: &[u64; 2]) -> GoldilocksField { + // Computes a0 * b1 + a1 * b0; + let [a0, a1] = *a; + let [b0, b1] = *b; + + let cy; + + // a0 * b1 + let mut cumul_lo = (a0 as u128) * (b1 as u128); + + // a1 * b0 + (cumul_lo, cy) = cumul_lo.overflowing_add((a1 as u128) * (b0 as u128)); + let cumul_hi = cy as u32; + + unsafe { reduce160(cumul_lo, cumul_hi) } +} + +/// Multiply a and b considered as elements of GF(p^2). +#[inline(always)] +pub(crate) fn ext2_mul(a: [u64; 2], b: [u64; 2]) -> [GoldilocksField; 2] { + // The code in ext2_add_prods[01] assumes the quadratic extension generator is 7. + const _: () = assert!(>::W.0 == 7u64); + + let c0 = ext2_add_prods0(&a, &b); + let c1 = ext2_add_prods1(&a, &b); + [c0, c1] +} diff --git a/gprimitives/client/src/field/goldilocks_field.rs b/gprimitives/client/src/field/goldilocks_field.rs new file mode 100644 index 00000000000..8d4bb747365 --- /dev/null +++ b/gprimitives/client/src/field/goldilocks_field.rs @@ -0,0 +1,500 @@ +// This file is part of Gear. + +// Copyright (C) 2021-2024 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Modified based on the [`plonky2`](https://github.com/0xPolygonZero/plonky2.git). +//! Goldilocks field implementation. + +use crate::{ + field::{ + ops::Square, + types::{Field, Field64, PrimeField, PrimeField64}, + }, + util::{assume, branch_hint}, +}; +use core::{ + fmt::{self, Debug, Display, Formatter}, + hash::{Hash, Hasher}, + iter::{Product, Sum}, + ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}, +}; +use num::{BigUint, Integer, ToPrimitive}; + +const EPSILON: u64 = (1 << 32) - 1; + +/// A field selected to have fast reduction. +/// +/// Its order is 2^64 - 2^32 + 1. +/// ```ignore +/// P = 2**64 - EPSILON +/// = 2**64 - 2**32 + 1 +/// = 2**32 * (2**32 - 1) + 1 +/// ``` +#[derive(Copy, Clone)] +#[repr(transparent)] +pub struct GoldilocksField(pub u64); + +impl Default for GoldilocksField { + fn default() -> Self { + Self::ZERO + } +} + +impl PartialEq for GoldilocksField { + fn eq(&self, other: &Self) -> bool { + self.to_canonical_u64() == other.to_canonical_u64() + } +} + +impl Eq for GoldilocksField {} + +impl Hash for GoldilocksField { + fn hash(&self, state: &mut H) { + state.write_u64(self.to_canonical_u64()) + } +} + +impl Display for GoldilocksField { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + Display::fmt(&self.to_canonical_u64(), f) + } +} + +impl Debug for GoldilocksField { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + Debug::fmt(&self.to_canonical_u64(), f) + } +} + +impl Field for GoldilocksField { + const ZERO: Self = Self(0); + const ONE: Self = Self(1); + const TWO: Self = Self(2); + const NEG_ONE: Self = Self(Self::ORDER - 1); + + const TWO_ADICITY: usize = 32; + const CHARACTERISTIC_TWO_ADICITY: usize = Self::TWO_ADICITY; + + // Sage: `g = GF(p).multiplicative_generator()` + const MULTIPLICATIVE_GROUP_GENERATOR: Self = Self(14293326489335486720); + + // Sage: + // ``` + // g_2 = g^((p - 1) / 2^32) + // g_2.multiplicative_order().factor() + // ``` + const POWER_OF_TWO_GENERATOR: Self = Self(7277203076849721926); + + const BITS: usize = 64; + + fn order() -> BigUint { + Self::ORDER.into() + } + fn characteristic() -> BigUint { + Self::order() + } + + /// Returns the inverse of the field element, using Fermat's little theorem. + /// The inverse of `a` is computed as `a^(p-2)`, where `p` is the prime order of the field. + /// + /// Mathematically, this is equivalent to: + /// $a^(p-1) = 1 (mod p)$ + /// $a^(p-2) * a = 1 (mod p)$ + /// Therefore $a^(p-2) = a^-1 (mod p)$ + /// + /// The following code has been adapted from winterfell/math/src/field/f64/mod.rs + /// located at . + fn try_inverse(&self) -> Option { + if self.is_zero() { + return None; + } + + // compute base^(P - 2) using 72 multiplications + // The exponent P - 2 is represented in binary as: + // 0b1111111111111111111111111111111011111111111111111111111111111111 + + // compute base^11 + let t2 = self.square() * *self; + + // compute base^111 + let t3 = t2.square() * *self; + + // compute base^111111 (6 ones) + // repeatedly square t3 3 times and multiply by t3 + let t6 = exp_acc::<3>(t3, t3); + + // compute base^111111111111 (12 ones) + // repeatedly square t6 6 times and multiply by t6 + let t12 = exp_acc::<6>(t6, t6); + + // compute base^111111111111111111111111 (24 ones) + // repeatedly square t12 12 times and multiply by t12 + let t24 = exp_acc::<12>(t12, t12); + + // compute base^1111111111111111111111111111111 (31 ones) + // repeatedly square t24 6 times and multiply by t6 first. then square t30 and + // multiply by base + let t30 = exp_acc::<6>(t24, t6); + let t31 = t30.square() * *self; + + // compute base^111111111111111111111111111111101111111111111111111111111111111 + // repeatedly square t31 32 times and multiply by t31 + let t63 = exp_acc::<32>(t31, t31); + + // compute base^1111111111111111111111111111111011111111111111111111111111111111 + Some(t63.square() * *self) + } + + fn from_noncanonical_biguint(n: BigUint) -> Self { + Self(n.mod_floor(&Self::order()).to_u64().unwrap()) + } + + #[inline(always)] + fn from_canonical_u64(n: u64) -> Self { + debug_assert!(n < Self::ORDER); + Self(n) + } + + fn from_noncanonical_u96((n_lo, n_hi): (u64, u32)) -> Self { + reduce96((n_lo, n_hi)) + } + + fn from_noncanonical_u128(n: u128) -> Self { + reduce128(n) + } + + #[inline] + fn from_noncanonical_u64(n: u64) -> Self { + Self(n) + } + + #[inline] + fn from_noncanonical_i64(n: i64) -> Self { + Self::from_canonical_u64(if n < 0 { + // If n < 0, then this is guaranteed to overflow since + // both arguments have their high bit set, so the result + // is in the canonical range. + Self::ORDER.wrapping_add(n as u64) + } else { + n as u64 + }) + } + + #[inline] + fn multiply_accumulate(&self, x: Self, y: Self) -> Self { + // u64 + u64 * u64 cannot overflow. + reduce128((self.0 as u128) + (x.0 as u128) * (y.0 as u128)) + } +} + +impl PrimeField for GoldilocksField { + fn to_canonical_biguint(&self) -> BigUint { + self.to_canonical_u64().into() + } +} + +impl Field64 for GoldilocksField { + const ORDER: u64 = 0xFFFFFFFF00000001; + + #[inline] + unsafe fn add_canonical_u64(&self, rhs: u64) -> Self { + let (res_wrapped, carry) = self.0.overflowing_add(rhs); + // Add EPSILON * carry cannot overflow unless rhs is not in canonical form. + Self(res_wrapped + EPSILON * (carry as u64)) + } + + #[inline] + unsafe fn sub_canonical_u64(&self, rhs: u64) -> Self { + let (res_wrapped, borrow) = self.0.overflowing_sub(rhs); + // Sub EPSILON * carry cannot underflow unless rhs is not in canonical form. + Self(res_wrapped - EPSILON * (borrow as u64)) + } +} + +impl PrimeField64 for GoldilocksField { + #[inline] + fn to_canonical_u64(&self) -> u64 { + let mut c = self.0; + // We only need one condition subtraction, since 2 * ORDER would not fit in a u64. + if c >= Self::ORDER { + c -= Self::ORDER; + } + c + } + + #[inline(always)] + fn to_noncanonical_u64(&self) -> u64 { + self.0 + } +} + +impl Neg for GoldilocksField { + type Output = Self; + + #[inline] + fn neg(self) -> Self { + if self.is_zero() { + Self::ZERO + } else { + Self(Self::ORDER - self.to_canonical_u64()) + } + } +} + +impl Add for GoldilocksField { + type Output = Self; + + #[inline] + #[allow(clippy::suspicious_arithmetic_impl)] + fn add(self, rhs: Self) -> Self { + let (sum, over) = self.0.overflowing_add(rhs.0); + let (mut sum, over) = sum.overflowing_add((over as u64) * EPSILON); + if over { + // NB: self.0 > Self::ORDER && rhs.0 > Self::ORDER is necessary but not sufficient for + // double-overflow. + // This assume does two things: + // 1. If compiler knows that either self.0 or rhs.0 <= ORDER, then it can skip this + // check. + // 2. Hints to the compiler how rare this double-overflow is (thus handled better with + // a branch). + assume(self.0 > Self::ORDER && rhs.0 > Self::ORDER); + branch_hint(); + sum += EPSILON; // Cannot overflow. + } + Self(sum) + } +} + +impl AddAssign for GoldilocksField { + #[inline] + fn add_assign(&mut self, rhs: Self) { + *self = *self + rhs; + } +} + +impl Sum for GoldilocksField { + fn sum>(iter: I) -> Self { + iter.fold(Self::ZERO, |acc, x| acc + x) + } +} + +impl Sub for GoldilocksField { + type Output = Self; + + #[inline] + #[allow(clippy::suspicious_arithmetic_impl)] + fn sub(self, rhs: Self) -> Self { + let (diff, under) = self.0.overflowing_sub(rhs.0); + let (mut diff, under) = diff.overflowing_sub((under as u64) * EPSILON); + if under { + // NB: self.0 < EPSILON - 1 && rhs.0 > Self::ORDER is necessary but not sufficient for + // double-underflow. + // This assume does two things: + // 1. If compiler knows that either self.0 >= EPSILON - 1 or rhs.0 <= ORDER, then it + // can skip this check. + // 2. Hints to the compiler how rare this double-underflow is (thus handled better + // with a branch). + assume(self.0 < EPSILON - 1 && rhs.0 > Self::ORDER); + branch_hint(); + diff -= EPSILON; // Cannot underflow. + } + Self(diff) + } +} + +impl SubAssign for GoldilocksField { + #[inline] + fn sub_assign(&mut self, rhs: Self) { + *self = *self - rhs; + } +} + +impl Mul for GoldilocksField { + type Output = Self; + + #[inline] + fn mul(self, rhs: Self) -> Self { + reduce128((self.0 as u128) * (rhs.0 as u128)) + } +} + +impl MulAssign for GoldilocksField { + #[inline] + fn mul_assign(&mut self, rhs: Self) { + *self = *self * rhs; + } +} + +impl Product for GoldilocksField { + fn product>(iter: I) -> Self { + iter.fold(Self::ONE, |acc, x| acc * x) + } +} + +impl Div for GoldilocksField { + type Output = Self; + + #[allow(clippy::suspicious_arithmetic_impl)] + fn div(self, rhs: Self) -> Self::Output { + self * rhs.inverse() + } +} + +impl DivAssign for GoldilocksField { + fn div_assign(&mut self, rhs: Self) { + *self = *self / rhs; + } +} + +impl Square for GoldilocksField { + fn square(&self) -> Self { + *self * *self + } +} + +/// Fast addition modulo ORDER for x86-64. +/// This function is marked unsafe for the following reasons: +/// - It is only correct if x + y < 2**64 + ORDER = 0x1ffffffff00000001. +/// - It is only faster in some circumstances. In particular, on x86 it overwrites both inputs in +/// the registers, so its use is not recommended when either input will be used again. +#[inline(always)] +#[cfg(target_arch = "x86_64")] +unsafe fn add_no_canonicalize_trashing_input(x: u64, y: u64) -> u64 { + let res_wrapped: u64; + let adjustment: u64; + core::arch::asm!( + "add {0}, {1}", + // Trick. The carry flag is set iff the addition overflowed. + // sbb x, y does x := x - y - CF. In our case, x and y are both {1:e}, so it simply does + // {1:e} := 0xffffffff on overflow and {1:e} := 0 otherwise. {1:e} is the low 32 bits of + // {1}; the high 32-bits are zeroed on write. In the end, we end up with 0xffffffff in {1} + // on overflow; this happens be EPSILON. + // Note that the CPU does not realize that the result of sbb x, x does not actually depend + // on x. We must write the result to a register that we know to be ready. We have a + // dependency on {1} anyway, so let's use it. + "sbb {1:e}, {1:e}", + inlateout(reg) x => res_wrapped, + inlateout(reg) y => adjustment, + options(pure, nomem, nostack), + ); + assume(x != 0 || (res_wrapped == y && adjustment == 0)); + assume(y != 0 || (res_wrapped == x && adjustment == 0)); + // Add EPSILON == subtract ORDER. + // Cannot overflow unless the assumption if x + y < 2**64 + ORDER is incorrect. + res_wrapped + adjustment +} + +#[inline(always)] +#[cfg(not(target_arch = "x86_64"))] +const unsafe fn add_no_canonicalize_trashing_input(x: u64, y: u64) -> u64 { + let (res_wrapped, carry) = x.overflowing_add(y); + // Below cannot overflow unless the assumption if x + y < 2**64 + ORDER is incorrect. + res_wrapped + EPSILON * (carry as u64) +} + +/// Reduces to a 64-bit value. The result might not be in canonical form; it could be in between the +/// field order and `2^64`. +#[inline] +fn reduce96((x_lo, x_hi): (u64, u32)) -> GoldilocksField { + let t1 = x_hi as u64 * EPSILON; + let t2 = unsafe { add_no_canonicalize_trashing_input(x_lo, t1) }; + GoldilocksField(t2) +} + +/// Reduces to a 64-bit value. The result might not be in canonical form; it could be in between the +/// field order and `2^64`. +#[inline] +fn reduce128(x: u128) -> GoldilocksField { + let (x_lo, x_hi) = split(x); // This is a no-op + let x_hi_hi = x_hi >> 32; + let x_hi_lo = x_hi & EPSILON; + + let (mut t0, borrow) = x_lo.overflowing_sub(x_hi_hi); + if borrow { + branch_hint(); // A borrow is exceedingly rare. It is faster to branch. + t0 -= EPSILON; // Cannot underflow. + } + let t1 = x_hi_lo * EPSILON; + let t2 = unsafe { add_no_canonicalize_trashing_input(t0, t1) }; + GoldilocksField(t2) +} + +#[inline] +const fn split(x: u128) -> (u64, u64) { + (x as u64, (x >> 64) as u64) +} + +/// Reduce the value x_lo + x_hi * 2^128 to an element in the +/// Goldilocks field. +/// +/// This function is marked 'unsafe' because correctness relies on the +/// unchecked assumption that x < 2^160 - 2^128 + 2^96. Further, +/// performance may degrade as x_hi increases beyond 2**40 or so. +#[inline(always)] +pub(crate) unsafe fn reduce160(x_lo: u128, x_hi: u32) -> GoldilocksField { + let x_hi = (x_lo >> 96) as u64 + ((x_hi as u64) << 32); // shld to form x_hi + let x_mid = (x_lo >> 64) as u32; // shr to form x_mid + let x_lo = x_lo as u64; + + // sub + jc (should fuse) + let (mut t0, borrow) = x_lo.overflowing_sub(x_hi); + if borrow { + // The maximum possible value of x is (2^64 - 1)^2 * 4 * 7 < 2^133, + // so x_hi < 2^37. A borrow will happen roughly one in 134 million + // times, so it's best to branch. + branch_hint(); + // NB: this assumes that x < 2^160 - 2^128 + 2^96. + t0 -= EPSILON; // Cannot underflow if x_hi is canonical. + } + // imul + let t1 = (x_mid as u64) * EPSILON; + // add, sbb, add + let t2 = add_no_canonicalize_trashing_input(t0, t1); + GoldilocksField(t2) +} + +/// Squares the base N number of times and multiplies the result by the tail value. +#[inline(always)] +fn exp_acc(base: GoldilocksField, tail: GoldilocksField) -> GoldilocksField { + base.exp_power_of_2(N) * tail +} + +#[cfg(test)] +mod tests { + use super::GoldilocksField; + use crate::{ + field::{ + field_testing::Sample, + types::{Field, Field64}, + }, + test_field_arithmetic, test_prime_field_arithmetic, + }; + + impl Sample for GoldilocksField { + #[inline] + fn sample(rng: &mut R) -> Self + where + R: rand::RngCore + ?Sized, + { + use rand::Rng; + Self::from_canonical_u64(rng.gen_range(0..Self::ORDER)) + } + } + + test_prime_field_arithmetic!(crate::field::goldilocks_field::GoldilocksField); + test_field_arithmetic!(crate::field::goldilocks_field::GoldilocksField); +} diff --git a/gprimitives/client/src/field/mod.rs b/gprimitives/client/src/field/mod.rs new file mode 100644 index 00000000000..89b084fb137 --- /dev/null +++ b/gprimitives/client/src/field/mod.rs @@ -0,0 +1,33 @@ +// This file is part of Gear. + +// Copyright (C) 2021-2024 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +pub(crate) mod arch; + +pub mod extension; +pub mod goldilocks_extensions; +pub mod goldilocks_field; +pub mod ops; +pub mod packable; +pub mod packed; +pub mod types; + +#[cfg(test)] +mod field_testing; + +#[cfg(test)] +mod prime_field_testing; diff --git a/gprimitives/client/src/field/ops.rs b/gprimitives/client/src/field/ops.rs new file mode 100644 index 00000000000..43c155dbe9e --- /dev/null +++ b/gprimitives/client/src/field/ops.rs @@ -0,0 +1,23 @@ +// This file is part of Gear. + +// Copyright (C) 2021-2024 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Modified based on the [`plonky2`](https://github.com/0xPolygonZero/plonky2.git). + +pub trait Square { + fn square(&self) -> Self; +} diff --git a/gprimitives/client/src/field/packable.rs b/gprimitives/client/src/field/packable.rs new file mode 100644 index 00000000000..8074d10adef --- /dev/null +++ b/gprimitives/client/src/field/packable.rs @@ -0,0 +1,38 @@ +// This file is part of Gear. + +// Copyright (C) 2021-2024 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Modified based on the [`plonky2`](https://github.com/0xPolygonZero/plonky2.git). + +use crate::field::{packed::PackedField, types::Field}; + +/// Points us to the default packing for a particular field. There may me multiple choices of +/// PackedField for a particular Field (e.g. every Field is also a PackedField), but this is the +/// recommended one. The recommended packing varies by target_arch and target_feature. +pub trait Packable: Field { + type Packing: PackedField; +} + +#[cfg(all(target_arch = "x86_64", target_feature = "avx2"))] +impl Packable for crate::field::goldilocks_field::GoldilocksField { + type Packing = crate::field::arch::x86_64::avx2_goldilocks_field::Avx2GoldilocksField; +} + +#[cfg(not(all(target_arch = "x86_64", target_feature = "avx2")))] +impl Packable for crate::field::goldilocks_field::GoldilocksField { + type Packing = Self; +} diff --git a/gprimitives/client/src/field/packed.rs b/gprimitives/client/src/field/packed.rs new file mode 100644 index 00000000000..ed6f2707d3d --- /dev/null +++ b/gprimitives/client/src/field/packed.rs @@ -0,0 +1,148 @@ +// This file is part of Gear. + +// Copyright (C) 2021-2024 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Modified based on the [`plonky2`](https://github.com/0xPolygonZero/plonky2.git). + +use core::{ + fmt::Debug, + iter::{Product, Sum}, + ops::{Add, AddAssign, Div, Mul, MulAssign, Neg, Sub, SubAssign}, + slice, +}; + +use crate::field::{ops::Square, types::Field}; + +/// # Safety +/// - WIDTH is assumed to be a power of 2. +/// - If P implements PackedField then P must be castable to/from [P::Scalar; P::WIDTH] without UB. +pub unsafe trait PackedField: + 'static + + Add + + Add + + AddAssign + + AddAssign + + Copy + + Debug + + Default + + From + // TODO: Implement packed / packed division + + Div + + Mul + + Mul + + MulAssign + + MulAssign + + Square + + Neg + + Product + + Send + + Sub + + Sub + + SubAssign + + SubAssign + + Sum + + Sync +where + Self::Scalar: Add, + Self::Scalar: Mul, + Self::Scalar: Sub, +{ + type Scalar: Field; + + const WIDTH: usize; + const ZEROS: Self; + const ONES: Self; + + fn from_slice(slice: &[Self::Scalar]) -> &Self; + fn from_slice_mut(slice: &mut [Self::Scalar]) -> &mut Self; + fn as_slice(&self) -> &[Self::Scalar]; + fn as_slice_mut(&mut self) -> &mut [Self::Scalar]; + + /// Take interpret two vectors as chunks of block_len elements. Unpack and interleave those + /// chunks. This is best seen with an example. If we have: + /// A = [x0, y0, x1, y1], + /// B = [x2, y2, x3, y3], + /// then + /// interleave(A, B, 1) = ([x0, x2, x1, x3], [y0, y2, y1, y3]). + /// Pairs that were adjacent in the input are at corresponding positions in the output. + /// r lets us set the size of chunks we're interleaving. If we set block_len = 2, then for + /// A = [x0, x1, y0, y1], + /// B = [x2, x3, y2, y3], + /// we obtain + /// interleave(A, B, block_len) = ([x0, x1, x2, x3], [y0, y1, y2, y3]). + /// We can also think about this as stacking the vectors, dividing them into 2x2 matrices, and + /// transposing those matrices. + /// When block_len = WIDTH, this operation is a no-op. block_len must divide WIDTH. Since + /// WIDTH is specified to be a power of 2, block_len must also be a power of 2. It cannot be 0 + /// and it cannot be > WIDTH. + fn interleave(&self, other: Self, block_len: usize) -> (Self, Self); + + fn pack_slice(buf: &[Self::Scalar]) -> &[Self] { + assert!( + buf.len() % Self::WIDTH == 0, + "Slice length (got {}) must be a multiple of packed field width ({}).", + buf.len(), + Self::WIDTH + ); + let buf_ptr = buf.as_ptr().cast::(); + let n = buf.len() / Self::WIDTH; + unsafe { slice::from_raw_parts(buf_ptr, n) } + } + fn pack_slice_mut(buf: &mut [Self::Scalar]) -> &mut [Self] { + assert!( + buf.len() % Self::WIDTH == 0, + "Slice length (got {}) must be a multiple of packed field width ({}).", + buf.len(), + Self::WIDTH + ); + let buf_ptr = buf.as_mut_ptr().cast::(); + let n = buf.len() / Self::WIDTH; + unsafe { slice::from_raw_parts_mut(buf_ptr, n) } + } + + fn doubles(&self) -> Self { + *self * Self::Scalar::TWO + } +} + +unsafe impl PackedField for F { + type Scalar = Self; + + const WIDTH: usize = 1; + const ZEROS: Self = F::ZERO; + const ONES: Self = F::ONE; + + fn from_slice(slice: &[Self::Scalar]) -> &Self { + &slice[0] + } + fn from_slice_mut(slice: &mut [Self::Scalar]) -> &mut Self { + &mut slice[0] + } + fn as_slice(&self) -> &[Self::Scalar] { + slice::from_ref(self) + } + fn as_slice_mut(&mut self) -> &mut [Self::Scalar] { + slice::from_mut(self) + } + + fn interleave(&self, other: Self, block_len: usize) -> (Self, Self) { + match block_len { + 1 => (*self, other), + _ => panic!("unsupported block length"), + } + } +} diff --git a/gprimitives/client/src/field/prime_field_testing.rs b/gprimitives/client/src/field/prime_field_testing.rs new file mode 100644 index 00000000000..f963299a14b --- /dev/null +++ b/gprimitives/client/src/field/prime_field_testing.rs @@ -0,0 +1,208 @@ +// This file is part of Gear. + +// Copyright (C) 2021-2024 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use alloc::vec::Vec; + +use crate::field::types::PrimeField64; + +/// Generates a series of non-negative integers less than `modulus` which cover a range of +/// interesting test values. +pub fn test_inputs(modulus: u64) -> Vec { + const CHUNK_SIZE: u64 = 10; + + (0..CHUNK_SIZE) + .chain((1 << 31) - CHUNK_SIZE..(1 << 31) + CHUNK_SIZE) + .chain((1 << 32) - CHUNK_SIZE..(1 << 32) + CHUNK_SIZE) + .chain((1 << 63) - CHUNK_SIZE..(1 << 63) + CHUNK_SIZE) + .chain(modulus - CHUNK_SIZE..modulus) + .filter(|&x| x < modulus) + .collect() +} + +/// Apply the unary functions `op` and `expected_op` +/// coordinate-wise to the inputs from `test_inputs(modulus, +/// word_bits)` and panic if the two resulting vectors differ. +pub fn run_unaryop_test_cases(op: UnaryOp, expected_op: ExpectedOp) +where + F: PrimeField64, + UnaryOp: Fn(F) -> F, + ExpectedOp: Fn(u64) -> u64, +{ + let inputs = test_inputs(F::ORDER); + let expected: Vec<_> = inputs.iter().map(|&x| expected_op(x)).collect(); + let output: Vec<_> = inputs + .iter() + .cloned() + .map(|x| op(F::from_canonical_u64(x)).to_canonical_u64()) + .collect(); + // Compare expected outputs with actual outputs + for i in 0..inputs.len() { + assert_eq!( + output[i], expected[i], + "Expected {}, got {} for input {}", + expected[i], output[i], inputs[i] + ); + } +} + +/// Apply the binary functions `op` and `expected_op` to each pair of inputs. +pub fn run_binaryop_test_cases(op: BinaryOp, expected_op: ExpectedOp) +where + F: PrimeField64, + BinaryOp: Fn(F, F) -> F, + ExpectedOp: Fn(u64, u64) -> u64, +{ + let inputs = test_inputs(F::ORDER); + + for &lhs in &inputs { + for &rhs in &inputs { + let lhs_f = F::from_canonical_u64(lhs); + let rhs_f = F::from_canonical_u64(rhs); + let actual = op(lhs_f, rhs_f).to_canonical_u64(); + let expected = expected_op(lhs, rhs); + assert_eq!( + actual, expected, + "Expected {}, got {} for inputs ({}, {})", + expected, actual, lhs, rhs + ); + } + } +} + +#[macro_export] +macro_rules! test_prime_field_arithmetic { + ($field:ty) => { + mod prime_field_arithmetic { + use core::ops::{Add, Mul, Neg, Sub}; + + use $crate::field::{ + ops::Square, + types::{Field, Field64}, + }; + + #[test] + fn arithmetic_addition() { + let modulus = <$field>::ORDER; + $crate::field::prime_field_testing::run_binaryop_test_cases( + <$field>::add, + |x, y| ((x as u128 + y as u128) % (modulus as u128)) as u64, + ) + } + + #[test] + fn arithmetic_subtraction() { + let modulus = <$field>::ORDER; + $crate::field::prime_field_testing::run_binaryop_test_cases( + <$field>::sub, + |x, y| { + if x >= y { + x - y + } else { + modulus - y + x + } + }, + ) + } + + #[test] + fn arithmetic_negation() { + let modulus = <$field>::ORDER; + $crate::field::prime_field_testing::run_unaryop_test_cases(<$field>::neg, |x| { + if x == 0 { + 0 + } else { + modulus - x + } + }) + } + + #[test] + fn arithmetic_multiplication() { + let modulus = <$field>::ORDER; + $crate::field::prime_field_testing::run_binaryop_test_cases( + <$field>::mul, + |x, y| ((x as u128) * (y as u128) % (modulus as u128)) as u64, + ) + } + + #[test] + fn arithmetic_square() { + let modulus = <$field>::ORDER; + $crate::field::prime_field_testing::run_unaryop_test_cases( + |x: $field| x.square(), + |x| ((x as u128 * x as u128) % (modulus as u128)) as u64, + ) + } + + #[test] + fn inversion() { + let zero = <$field>::ZERO; + let one = <$field>::ONE; + let modulus = <$field>::ORDER; + + assert_eq!(zero.try_inverse(), None); + + let inputs = $crate::field::prime_field_testing::test_inputs(modulus); + + for x in inputs { + if x != 0 { + let x = <$field>::from_canonical_u64(x); + let inv = x.inverse(); + assert_eq!(x * inv, one); + } + } + } + + #[test] + fn inverse_2exp() { + type F = $field; + + let v = ::TWO_ADICITY; + + for e in [0, 1, 2, 3, 4, v - 2, v - 1, v, v + 1, v + 2, 123 * v] { + let x = F::TWO.exp_u64(e as u64); + let y = F::inverse_2exp(e); + assert_eq!(x * y, F::ONE); + } + } + + #[test] + fn subtraction_double_wraparound() { + type F = $field; + + let (a, b) = (F::from_canonical_u64((F::ORDER + 1u64) / 2u64), F::TWO); + let x = a * b; + assert_eq!(x, F::ONE); + assert_eq!(F::ZERO - x, F::NEG_ONE); + } + + #[test] + fn addition_double_wraparound() { + type F = $field; + + let a = F::from_canonical_u64(u64::MAX - F::ORDER); + let b = F::NEG_ONE; + + let c = (a + a) + (b + b); + let d = (a + b) + (a + b); + + assert_eq!(c, d); + } + } + }; +} diff --git a/gprimitives/client/src/field/types.rs b/gprimitives/client/src/field/types.rs new file mode 100644 index 00000000000..9ba14a0b29e --- /dev/null +++ b/gprimitives/client/src/field/types.rs @@ -0,0 +1,622 @@ +// This file is part of Gear. + +// Copyright (C) 2021-2024 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use crate::{ + field::{extension::Frobenius, ops::Square}, + util::bits_u64, +}; +use alloc::{vec, vec::Vec}; +use core::{ + fmt::{Debug, Display}, + hash::Hash, + iter::{Product, Sum}, + ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}, +}; +use num::{bigint::BigUint, Integer, One, ToPrimitive, Zero}; + +/// A finite field. +pub trait Field: + 'static + + Copy + + Eq + + Hash + + Neg + + Add + + AddAssign + + Sum + + Sub + + SubAssign + + Mul + + MulAssign + + Square + + Product + + Div + + DivAssign + + Debug + + Default + + Display + + Send + + Sync +{ + const ZERO: Self; + const ONE: Self; + const TWO: Self; + const NEG_ONE: Self; + + /// The 2-adicity of this field's multiplicative group. + const TWO_ADICITY: usize; + + /// The field's characteristic and it's 2-adicity. + /// Set to `None` when the characteristic doesn't fit in a u64. + const CHARACTERISTIC_TWO_ADICITY: usize; + + /// Generator of the entire multiplicative group, i.e. all non-zero elements. + const MULTIPLICATIVE_GROUP_GENERATOR: Self; + /// Generator of a multiplicative subgroup of order `2^TWO_ADICITY`. + const POWER_OF_TWO_GENERATOR: Self; + + /// The bit length of the field order. + const BITS: usize; + + fn order() -> BigUint; + fn characteristic() -> BigUint; + + #[inline] + fn is_zero(&self) -> bool { + *self == Self::ZERO + } + + #[inline] + fn is_nonzero(&self) -> bool { + *self != Self::ZERO + } + + #[inline] + fn is_one(&self) -> bool { + *self == Self::ONE + } + + #[inline] + fn double(&self) -> Self { + *self + *self + } + + #[inline] + fn cube(&self) -> Self { + self.square() * *self + } + + fn triple(&self) -> Self { + *self * (Self::ONE + Self::TWO) + } + + /// Compute the multiplicative inverse of this field element. + fn try_inverse(&self) -> Option; + + fn inverse(&self) -> Self { + self.try_inverse().expect("Tried to invert zero") + } + + fn batch_multiplicative_inverse(x: &[Self]) -> Vec { + // This is Montgomery's trick. At a high level, we invert the product of the given field + // elements, then derive the individual inverses from that via multiplication. + + // The usual Montgomery trick involves calculating an array of cumulative products, + // resulting in a long dependency chain. To increase instruction-level parallelism, we + // compute WIDTH separate cumulative product arrays that only meet at the end. + + // Higher WIDTH increases instruction-level parallelism, but too high a value will cause us + // to run out of registers. + const WIDTH: usize = 4; + // JN note: WIDTH is 4. The code is specialized to this value and will need + // modification if it is changed. I tried to make it more generic, but Rust's const + // generics are not yet good enough. + + // Handle special cases. Paradoxically, below is repetitive but concise. + // The branches should be very predictable. + let n = x.len(); + if n == 0 { + return Vec::new(); + } else if n == 1 { + return vec![x[0].inverse()]; + } else if n == 2 { + let x01 = x[0] * x[1]; + let x01inv = x01.inverse(); + return vec![x01inv * x[1], x01inv * x[0]]; + } else if n == 3 { + let x01 = x[0] * x[1]; + let x012 = x01 * x[2]; + let x012inv = x012.inverse(); + let x01inv = x012inv * x[2]; + return vec![x01inv * x[1], x01inv * x[0], x012inv * x01]; + } + debug_assert!(n >= WIDTH); + + // Buf is reused for a few things to save allocations. + // Fill buf with cumulative product of x, only taking every 4th value. Concretely, buf will + // be [ + // x[0], x[1], x[2], x[3], + // x[0] * x[4], x[1] * x[5], x[2] * x[6], x[3] * x[7], + // x[0] * x[4] * x[8], x[1] * x[5] * x[9], x[2] * x[6] * x[10], x[3] * x[7] * x[11], + // ... + // ]. + // If n is not a multiple of WIDTH, the result is truncated from the end. For example, + // for n == 5, we get [x[0], x[1], x[2], x[3], x[0] * x[4]]. + let mut buf: Vec = Vec::with_capacity(n); + // cumul_prod holds the last WIDTH elements of buf. This is redundant, but it's how we + // convince LLVM to keep the values in the registers. + let mut cumul_prod: [Self; WIDTH] = x[..WIDTH].try_into().unwrap(); + buf.extend(cumul_prod); + for (i, &xi) in x[WIDTH..].iter().enumerate() { + cumul_prod[i % WIDTH] *= xi; + buf.push(cumul_prod[i % WIDTH]); + } + debug_assert_eq!(buf.len(), n); + + let mut a_inv = { + // This is where the four dependency chains meet. + // Take the last four elements of buf and invert them all. + let c01 = cumul_prod[0] * cumul_prod[1]; + let c23 = cumul_prod[2] * cumul_prod[3]; + let c0123 = c01 * c23; + let c0123inv = c0123.inverse(); + let c01inv = c0123inv * c23; + let c23inv = c0123inv * c01; + [ + c01inv * cumul_prod[1], + c01inv * cumul_prod[0], + c23inv * cumul_prod[3], + c23inv * cumul_prod[2], + ] + }; + + for i in (WIDTH..n).rev() { + // buf[i - WIDTH] has not been written to by this loop, so it equals + // x[i % WIDTH] * x[i % WIDTH + WIDTH] * ... * x[i - WIDTH]. + buf[i] = buf[i - WIDTH] * a_inv[i % WIDTH]; + // buf[i] now holds the inverse of x[i]. + a_inv[i % WIDTH] *= x[i]; + } + for i in (0..WIDTH).rev() { + buf[i] = a_inv[i]; + } + + for (&bi, &xi) in buf.iter().zip(x) { + // Sanity check only. + debug_assert_eq!(bi * xi, Self::ONE); + } + + buf + } + + /// Compute the inverse of 2^exp in this field. + #[inline] + fn inverse_2exp(exp: usize) -> Self { + // Let p = char(F). Since 2^exp is in the prime subfield, i.e. an + // element of GF_p, its inverse must be as well. Thus we may add + // multiples of p without changing the result. In particular, + // 2^-exp = 2^-exp - p 2^-exp + // = 2^-exp (1 - p) + // = p - (p - 1) / 2^exp + + // If this field's two adicity, t, is at least exp, then 2^exp divides + // p - 1, so this division can be done with a simple bit shift. If + // exp > t, we repeatedly multiply by 2^-t and reduce exp until it's in + // the right range. + + if let Some(p) = Self::characteristic().to_u64() { + // NB: The only reason this is split into two cases is to save + // the multiplication (and possible calculation of + // inverse_2_pow_adicity) in the usual case that exp <= + // TWO_ADICITY. Can remove the branch and simplify if that + // saving isn't worth it. + + if exp > Self::CHARACTERISTIC_TWO_ADICITY { + // NB: This should be a compile-time constant + let inverse_2_pow_adicity: Self = + Self::from_canonical_u64(p - ((p - 1) >> Self::CHARACTERISTIC_TWO_ADICITY)); + + let mut res = inverse_2_pow_adicity; + let mut e = exp - Self::CHARACTERISTIC_TWO_ADICITY; + + while e > Self::CHARACTERISTIC_TWO_ADICITY { + res *= inverse_2_pow_adicity; + e -= Self::CHARACTERISTIC_TWO_ADICITY; + } + res * Self::from_canonical_u64(p - ((p - 1) >> e)) + } else { + Self::from_canonical_u64(p - ((p - 1) >> exp)) + } + } else { + Self::TWO.inverse().exp_u64(exp as u64) + } + } + + fn primitive_root_of_unity(n_log: usize) -> Self { + assert!(n_log <= Self::TWO_ADICITY); + let base = Self::POWER_OF_TWO_GENERATOR; + base.exp_power_of_2(Self::TWO_ADICITY - n_log) + } + + /// Computes a multiplicative subgroup whose order is known in advance. + fn cyclic_subgroup_known_order(generator: Self, order: usize) -> Vec { + generator.powers().take(order).collect() + } + + /// Computes the subgroup generated by the root of unity of a given order generated by `Self::primitive_root_of_unity`. + fn two_adic_subgroup(n_log: usize) -> Vec { + let generator = Self::primitive_root_of_unity(n_log); + generator.powers().take(1 << n_log).collect() + } + + fn cyclic_subgroup_unknown_order(generator: Self) -> Vec { + let mut subgroup = Vec::new(); + for power in generator.powers() { + if power.is_one() && !subgroup.is_empty() { + break; + } + subgroup.push(power); + } + subgroup + } + + fn generator_order(generator: Self) -> usize { + generator.powers().skip(1).position(|y| y.is_one()).unwrap() + 1 + } + + /// Computes a coset of a multiplicative subgroup whose order is known in advance. + fn cyclic_subgroup_coset_known_order(generator: Self, shift: Self, order: usize) -> Vec { + let subgroup = Self::cyclic_subgroup_known_order(generator, order); + subgroup.into_iter().map(|x| x * shift).collect() + } + + /// Returns `n % Self::characteristic()`. + fn from_noncanonical_biguint(n: BigUint) -> Self; + + /// Returns `n`. Assumes that `n` is already in canonical form, i.e. `n < Self::order()`. + // TODO: Should probably be unsafe. + fn from_canonical_u64(n: u64) -> Self; + + /// Returns `n`. Assumes that `n` is already in canonical form, i.e. `n < Self::order()`. + // TODO: Should probably be unsafe. + fn from_canonical_u32(n: u32) -> Self { + Self::from_canonical_u64(n as u64) + } + + /// Returns `n`. Assumes that `n` is already in canonical form, i.e. `n < Self::order()`. + // TODO: Should probably be unsafe. + fn from_canonical_u16(n: u16) -> Self { + Self::from_canonical_u64(n as u64) + } + + /// Returns `n`. Assumes that `n` is already in canonical form, i.e. `n < Self::order()`. + // TODO: Should probably be unsafe. + fn from_canonical_u8(n: u8) -> Self { + Self::from_canonical_u64(n as u64) + } + + /// Returns `n`. Assumes that `n` is already in canonical form, i.e. `n < Self::order()`. + // TODO: Should probably be unsafe. + fn from_canonical_usize(n: usize) -> Self { + Self::from_canonical_u64(n as u64) + } + + fn from_bool(b: bool) -> Self { + Self::from_canonical_u64(b as u64) + } + + /// Returns `n % Self::characteristic()`. + fn from_noncanonical_u128(n: u128) -> Self; + + /// Returns `x % Self::CHARACTERISTIC`. + fn from_noncanonical_u64(n: u64) -> Self; + + /// Returns `n` as an element of this field. + fn from_noncanonical_i64(n: i64) -> Self; + + /// Returns `n % Self::characteristic()`. May be cheaper than from_noncanonical_u128 when we know + /// that `n < 2 ** 96`. + #[inline] + fn from_noncanonical_u96((n_lo, n_hi): (u64, u32)) -> Self { + // Default implementation. + let n: u128 = ((n_hi as u128) << 64) + (n_lo as u128); + Self::from_noncanonical_u128(n) + } + + fn exp_power_of_2(&self, power_log: usize) -> Self { + let mut res = *self; + for _ in 0..power_log { + res = res.square(); + } + res + } + + fn exp_u64(&self, power: u64) -> Self { + let mut current = *self; + let mut product = Self::ONE; + + for j in 0..bits_u64(power) { + if (power >> j & 1) != 0 { + product *= current; + } + current = current.square(); + } + product + } + + fn exp_biguint(&self, power: &BigUint) -> Self { + let mut result = Self::ONE; + for &digit in power.to_u64_digits().iter().rev() { + result = result.exp_power_of_2(64); + result *= self.exp_u64(digit); + } + result + } + + /// Returns whether `x^power` is a permutation of this field. + fn is_monomial_permutation_u64(power: u64) -> bool { + match power { + 0 => false, + 1 => true, + _ => (Self::order() - 1u32).gcd(&BigUint::from(power)).is_one(), + } + } + + fn kth_root_u64(&self, k: u64) -> Self { + let p = Self::order(); + let p_minus_1 = &p - 1u32; + debug_assert!( + Self::is_monomial_permutation_u64(k), + "Not a permutation of this field" + ); + + // By Fermat's little theorem, x^p = x and x^(p - 1) = 1, so x^(p + n(p - 1)) = x for any n. + // Our assumption that the k'th root operation is a permutation implies gcd(p - 1, k) = 1, + // so there exists some n such that p + n(p - 1) is a multiple of k. Once we find such an n, + // we can rewrite the above as + // x^((p + n(p - 1))/k)^k = x, + // implying that x^((p + n(p - 1))/k) is a k'th root of x. + for n in 0..k { + let numerator = &p + &p_minus_1 * n; + if (&numerator % k).is_zero() { + let power = (numerator / k) % p_minus_1; + return self.exp_biguint(&power); + } + } + panic!( + "x^{} and x^(1/{}) are not permutations of this field, or we have a bug!", + k, k + ); + } + + fn cube_root(&self) -> Self { + self.kth_root_u64(3) + } + + fn powers(&self) -> Powers { + self.shifted_powers(Self::ONE) + } + + fn shifted_powers(&self, start: Self) -> Powers { + Powers { + base: *self, + current: start, + } + } + + /// Representative `g` of the coset used in FRI, so that LDEs in FRI are done over `gH`. + fn coset_shift() -> Self { + Self::MULTIPLICATIVE_GROUP_GENERATOR + } + + /// Equivalent to *self + x * y, but may be cheaper. + #[inline] + fn multiply_accumulate(&self, x: Self, y: Self) -> Self { + // Default implementation. + *self + x * y + } +} + +pub trait PrimeField: Field { + fn to_canonical_biguint(&self) -> BigUint; + + fn is_quadratic_residue(&self) -> bool { + if self.is_zero() { + return true; + } + // This is based on Euler's criterion. + let power = Self::NEG_ONE.to_canonical_biguint() / 2u8; + let exp = self.exp_biguint(&power); + if exp == Self::ONE { + return true; + } + if exp == Self::NEG_ONE { + return false; + } + panic!("Unreachable") + } + + fn sqrt(&self) -> Option { + if self.is_zero() { + Some(*self) + } else if self.is_quadratic_residue() { + let t = (Self::order() - BigUint::from(1u32)) + / (BigUint::from(2u32).pow(Self::TWO_ADICITY as u32)); + let mut z = Self::POWER_OF_TWO_GENERATOR; + let mut w = self.exp_biguint(&((t - BigUint::from(1u32)) / BigUint::from(2u32))); + let mut x = w * *self; + let mut b = x * w; + + let mut v = Self::TWO_ADICITY; + + while !b.is_one() { + let mut k = 0usize; + let mut b2k = b; + while !b2k.is_one() { + b2k = b2k * b2k; + k += 1; + } + let j = v - k - 1; + w = z; + for _ in 0..j { + w = w * w; + } + + z = w * w; + b *= z; + x *= w; + v = k; + } + Some(x) + } else { + None + } + } +} + +/// A finite field of the order less than 2^64. +pub trait Field64: Field { + const ORDER: u64; + + /// Returns `n` as an element of this field. Assumes that `0 <= n < Self::ORDER`. + // TODO: Move to `Field`. + // TODO: Should probably be unsafe. + #[inline] + fn from_canonical_i64(n: i64) -> Self { + Self::from_canonical_u64(n as u64) + } + + #[inline] + // TODO: Move to `Field`. + fn add_one(&self) -> Self { + unsafe { self.add_canonical_u64(1) } + } + + #[inline] + // TODO: Move to `Field`. + fn sub_one(&self) -> Self { + unsafe { self.sub_canonical_u64(1) } + } + + /// # Safety + /// Equivalent to *self + Self::from_canonical_u64(rhs), but may be cheaper. The caller must + /// ensure that 0 <= rhs < Self::ORDER. The function may return incorrect results if this + /// precondition is not met. It is marked unsafe for this reason. + // TODO: Move to `Field`. + #[inline] + unsafe fn add_canonical_u64(&self, rhs: u64) -> Self { + // Default implementation. + *self + Self::from_canonical_u64(rhs) + } + + /// # Safety + /// Equivalent to *self - Self::from_canonical_u64(rhs), but may be cheaper. The caller must + /// ensure that 0 <= rhs < Self::ORDER. The function may return incorrect results if this + /// precondition is not met. It is marked unsafe for this reason. + // TODO: Move to `Field`. + #[inline] + unsafe fn sub_canonical_u64(&self, rhs: u64) -> Self { + // Default implementation. + *self - Self::from_canonical_u64(rhs) + } +} + +/// A finite field of prime order less than 2^64. +pub trait PrimeField64: PrimeField + Field64 { + fn to_canonical_u64(&self) -> u64; + + fn to_noncanonical_u64(&self) -> u64; + + #[inline(always)] + fn to_canonical(&self) -> Self { + Self::from_canonical_u64(self.to_canonical_u64()) + } +} + +/// An iterator over the powers of a certain base element `b`: `b^0, b^1, b^2, ...`. +#[must_use = "iterators are lazy and do nothing unless consumed"] +#[derive(Clone, Debug)] +pub struct Powers { + base: F, + current: F, +} + +impl Iterator for Powers { + type Item = F; + + fn next(&mut self) -> Option { + let result = self.current; + self.current *= self.base; + Some(result) + } + + fn size_hint(&self) -> (usize, Option) { + (usize::MAX, None) + } + + fn nth(&mut self, n: usize) -> Option { + let result = self.current * self.base.exp_u64(n.try_into().unwrap()); + self.current = result * self.base; + Some(result) + } + + fn last(self) -> Option { + panic!("called `Iterator::last()` on an infinite sequence") + } + + fn count(self) -> usize { + panic!("called `Iterator::count()` on an infinite sequence") + } +} + +impl Powers { + /// Apply the Frobenius automorphism `k` times. + pub fn repeated_frobenius(self, k: usize) -> Self + where + F: Frobenius, + { + let Self { base, current } = self; + Self { + base: base.repeated_frobenius(k), + current: current.repeated_frobenius(k), + } + } +} + +#[cfg(test)] +mod tests { + use super::Field; + use crate::field::goldilocks_field::GoldilocksField; + + #[test] + fn test_powers_nth() { + type F = GoldilocksField; + + const N: usize = 10; + let powers_of_two: Vec = F::TWO.powers().take(N).collect(); + + for (n, &expect) in powers_of_two.iter().enumerate() { + let mut iter = F::TWO.powers(); + assert_eq!(iter.nth(n), Some(expect)); + + for &expect_next in &powers_of_two[n + 1..] { + assert_eq!(iter.next(), Some(expect_next)); + } + } + } +} diff --git a/gprimitives/client/src/hash/arch/aarch64/mod.rs b/gprimitives/client/src/hash/arch/aarch64/mod.rs new file mode 100644 index 00000000000..591ac987b82 --- /dev/null +++ b/gprimitives/client/src/hash/arch/aarch64/mod.rs @@ -0,0 +1,20 @@ +// This file is part of Gear. + +// Copyright (C) 2021-2024 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#[cfg(target_feature = "neon")] +pub(crate) mod poseidon_goldilocks_neon; diff --git a/gprimitives/client/src/hash/arch/aarch64/poseidon_goldilocks_neon.rs b/gprimitives/client/src/hash/arch/aarch64/poseidon_goldilocks_neon.rs new file mode 100644 index 00000000000..f16c6928b49 --- /dev/null +++ b/gprimitives/client/src/hash/arch/aarch64/poseidon_goldilocks_neon.rs @@ -0,0 +1,973 @@ +// This file is part of Gear. + +// Copyright (C) 2021-2024 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#![allow(clippy::assertions_on_constants)] + +use core::{ + arch::{aarch64::*, asm}, + mem::transmute, +}; + +use unroll::unroll_for_loops; + +use crate::{ + field::goldilocks_field::GoldilocksField, hash::poseidon::Poseidon, util::branch_hint, +}; + +// ========================================== CONSTANTS =========================================== + +const WIDTH: usize = 12; + +const EPSILON: u64 = 0xffffffff; + +// The round constants to be applied by the second set of full rounds. These are just the usual +// round constants, shifted by one round, with zeros shifted in. +/* +const fn make_final_round_constants() -> [u64; WIDTH * HALF_N_FULL_ROUNDS] { + let mut res = [0; WIDTH * HALF_N_FULL_ROUNDS]; + let mut i: usize = 0; + while i < WIDTH * (HALF_N_FULL_ROUNDS - 1) { + res[i] = ALL_ROUND_CONSTANTS[i + WIDTH * (HALF_N_FULL_ROUNDS + N_PARTIAL_ROUNDS + 1)]; + i += 1; + } + res +} +const FINAL_ROUND_CONSTANTS: [u64; WIDTH * HALF_N_FULL_ROUNDS] = make_final_round_constants(); +*/ + +// ===================================== COMPILE-TIME CHECKS ====================================== + +/// The MDS matrix multiplication ASM is specific to the MDS matrix below. We want this file to +/// fail to compile if it has been changed. +#[allow(dead_code)] +const fn check_mds_matrix() -> bool { + // Can't == two arrays in a const_assert! (: + let mut i = 0; + let wanted_matrix_circ = [17, 15, 41, 16, 2, 28, 13, 13, 39, 18, 34, 20]; + let wanted_matrix_diag = [8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + while i < WIDTH { + if ::MDS_MATRIX_CIRC[i] != wanted_matrix_circ[i] + || ::MDS_MATRIX_DIAG[i] != wanted_matrix_diag[i] + { + return false; + } + i += 1; + } + true +} + +const _: () = assert!(check_mds_matrix()); + +/// Ensure that the first WIDTH round constants are in canonical* form. This is required because +/// the first constant layer does not handle double overflow. +/// *: round_const == GoldilocksField::ORDER is safe. +/* +#[allow(dead_code)] +const fn check_round_const_bounds_init() -> bool { + let mut i = 0; + while i < WIDTH { + if ALL_ROUND_CONSTANTS[i] > GoldilocksField::ORDER { + return false; + } + i += 1; + } + true +} +const_assert!(check_round_const_bounds_init()); +*/ +// ====================================== SCALAR ARITHMETIC ======================================= + +/// Addition modulo ORDER accounting for wraparound. Correct only when a + b < 2**64 + ORDER. +#[inline(always)] +unsafe fn add_with_wraparound(a: u64, b: u64) -> u64 { + let res: u64; + let adj: u64; + asm!( + "adds {res}, {a}, {b}", + // Set adj to 0xffffffff if addition overflowed and 0 otherwise. + // 'cs' for 'carry set'. + "csetm {adj:w}, cs", + a = in(reg) a, + b = in(reg) b, + res = lateout(reg) res, + adj = lateout(reg) adj, + options(pure, nomem, nostack), + ); + res + adj // adj is EPSILON if wraparound occurred and 0 otherwise +} + +/// Subtraction of a and (b >> 32) modulo ORDER accounting for wraparound. +#[inline(always)] +unsafe fn sub_with_wraparound_lsr32(a: u64, b: u64) -> u64 { + let mut b_hi = b >> 32; + // Make sure that LLVM emits two separate instructions for the shift and the subtraction. This + // reduces pressure on the execution units with access to the flags, as they are no longer + // responsible for the shift. The hack is to insert a fake computation between the two + // instructions with an `asm` block to make LLVM think that they can't be merged. + asm!( + "/* {0} */", // Make Rust think we're using the register. + inlateout(reg) b_hi, + options(nomem, nostack, preserves_flags, pure), + ); + // This could be done with a.overflowing_add(b_hi), but `checked_sub` signals to the compiler + // that overflow is unlikely (note: this is a standard library implementation detail, not part + // of the spec). + match a.checked_sub(b_hi) { + Some(res) => res, + None => { + // Super rare. Better off branching. + branch_hint(); + let res_wrapped = a.wrapping_sub(b_hi); + res_wrapped - EPSILON + } + } +} + +/// Multiplication of the low word (i.e., x as u32) by EPSILON. +#[inline(always)] +unsafe fn mul_epsilon(x: u64) -> u64 { + let res; + asm!( + // Use UMULL to save one instruction. The compiler emits two: extract the low word and then + // multiply. + "umull {res}, {x:w}, {epsilon:w}", + x = in(reg) x, + epsilon = in(reg) EPSILON, + res = lateout(reg) res, + options(pure, nomem, nostack, preserves_flags), + ); + res +} + +#[inline(always)] +unsafe fn multiply(x: u64, y: u64) -> u64 { + let xy = (x as u128) * (y as u128); + let xy_lo = xy as u64; + let xy_hi = (xy >> 64) as u64; + + let res0 = sub_with_wraparound_lsr32(xy_lo, xy_hi); + + let xy_hi_lo_mul_epsilon = mul_epsilon(xy_hi); + + // add_with_wraparound is safe, as xy_hi_lo_mul_epsilon <= 0xfffffffe00000001 <= ORDER. + add_with_wraparound(res0, xy_hi_lo_mul_epsilon) +} + +// ==================================== STANDALONE CONST LAYER ===================================== + +/// Standalone const layer. Run only once, at the start of round 1. Remaining const layers are fused +/// with the preceding MDS matrix multiplication. +/* +#[inline(always)] +#[unroll_for_loops] +unsafe fn const_layer_full( + mut state: [u64; WIDTH], + round_constants: &[u64; WIDTH], +) -> [u64; WIDTH] { + assert!(WIDTH == 12); + for i in 0..12 { + let rc = round_constants[i]; + // add_with_wraparound is safe, because rc is in canonical form. + state[i] = add_with_wraparound(state[i], rc); + } + state +} +*/ +// ========================================== FULL ROUNDS ========================================== + +/// Full S-box. +#[inline(always)] +#[unroll_for_loops] +unsafe fn sbox_layer_full(state: [u64; WIDTH]) -> [u64; WIDTH] { + // This is done in scalar. S-boxes in vector are only slightly slower throughput-wise but have + // an insane latency (~100 cycles) on the M1. + + let mut state2 = [0u64; WIDTH]; + assert!(WIDTH == 12); + for i in 0..12 { + state2[i] = multiply(state[i], state[i]); + } + + let mut state3 = [0u64; WIDTH]; + let mut state4 = [0u64; WIDTH]; + assert!(WIDTH == 12); + for i in 0..12 { + state3[i] = multiply(state[i], state2[i]); + state4[i] = multiply(state2[i], state2[i]); + } + + let mut state7 = [0u64; WIDTH]; + assert!(WIDTH == 12); + for i in 0..12 { + state7[i] = multiply(state3[i], state4[i]); + } + + state7 +} + +#[inline(always)] +unsafe fn mds_reduce( + // `cumul_a` and `cumul_b` represent two separate field elements. We take advantage of + // vectorization by reducing them simultaneously. + [cumul_a, cumul_b]: [uint32x4_t; 2], +) -> uint64x2_t { + // Form: + // `lo = [cumul_a[0] + cumul_a[2] * 2**32, cumul_b[0] + cumul_b[2] * 2**32]` + // `hi = [cumul_a[1] + cumul_a[3] * 2**32, cumul_b[1] + cumul_b[3] * 2**32]` + // Observe that the result `== lo + hi * 2**16 (mod Goldilocks)`. + let mut lo = vreinterpretq_u64_u32(vuzp1q_u32(cumul_a, cumul_b)); + let mut hi = vreinterpretq_u64_u32(vuzp2q_u32(cumul_a, cumul_b)); + // Add the high 48 bits of `lo` to `hi`. This cannot overflow. + hi = vsraq_n_u64::<16>(hi, lo); + // Now, result `== lo.bits[0..16] + hi * 2**16 (mod Goldilocks)`. + // Set the high 48 bits of `lo` to the low 48 bits of `hi`. + lo = vsliq_n_u64::<16>(lo, hi); + // At this point, result `== lo + hi.bits[48..64] * 2**64 (mod Goldilocks)`. + // It remains to fold `hi.bits[48..64]` into `lo`. + let top = { + // Extract the top 16 bits of `hi` as a `u32`. + // Interpret `hi` as a vector of bytes, so we can use a table lookup instruction. + let hi_u8 = vreinterpretq_u8_u64(hi); + // Indices defining the permutation. `0xff` is out of bounds, producing `0`. + let top_idx = + transmute::<[u8; 8], uint8x8_t>([0x06, 0x07, 0xff, 0xff, 0x0e, 0x0f, 0xff, 0xff]); + let top_u8 = vqtbl1_u8(hi_u8, top_idx); + vreinterpret_u32_u8(top_u8) + }; + // result `== lo + top * 2**64 (mod Goldilocks)`. + let adj_lo = vmlal_n_u32(lo, top, EPSILON as u32); + let wraparound_mask = vcgtq_u64(lo, adj_lo); + vsraq_n_u64::<32>(adj_lo, wraparound_mask) // Add epsilon on overflow. +} + +#[inline(always)] +unsafe fn mds_layer_full(state: [u64; WIDTH]) -> [u64; WIDTH] { + // This function performs an MDS multiplication in complex FFT space. + // However, instead of performing a width-12 FFT, we perform three width-4 FFTs, which is + // cheaper. The 12x12 matrix-vector multiplication (a convolution) becomes two 3x3 real + // matrix-vector multiplications and one 3x3 complex matrix-vector multiplication. + + // We split each 64-bit into four chunks of 16 bits. To prevent overflow, each chunk is 32 bits + // long. Each NEON vector below represents one field element and consists of four 32-bit chunks: + // `elem == vector[0] + vector[1] * 2**16 + vector[2] * 2**32 + vector[3] * 2**48`. + + // Constants that we multiply by. + let mut consts: uint32x4_t = transmute::<[u32; 4], _>([2, 4, 8, 16]); + + // Prevent LLVM from turning fused multiply (by power of 2)-add (1 instruction) into shift and + // add (two instructions). This fake `asm` block means that LLVM no longer knows the contents of + // `consts`. + asm!("/* {0:v} */", // Make Rust think the register is being used. + inout(vreg) consts, + options(pure, nomem, nostack, preserves_flags), + ); + + // Four length-3 complex FFTs. + let mut state_fft = [vdupq_n_u32(0); 12]; + for i in 0..3 { + // Interpret each field element as a 4-vector of `u16`s. + let x0 = vcreate_u16(state[i]); + let x1 = vcreate_u16(state[i + 3]); + let x2 = vcreate_u16(state[i + 6]); + let x3 = vcreate_u16(state[i + 9]); + + // `vaddl_u16` and `vsubl_u16` yield 4-vectors of `u32`s. + let y0 = vaddl_u16(x0, x2); + let y1 = vaddl_u16(x1, x3); + let y2 = vsubl_u16(x0, x2); + let y3 = vsubl_u16(x1, x3); + + let z0 = vaddq_u32(y0, y1); + let z1 = vsubq_u32(y0, y1); + let z2 = y2; + let z3 = y3; + + // The FFT is `[z0, z2 + z3 i, z1, z2 - z3 i]`. + + state_fft[i] = z0; + state_fft[i + 3] = z1; + state_fft[i + 6] = z2; + state_fft[i + 9] = z3; + } + + // 3x3 real matrix-vector mul for component 0 of the FFTs. + // Multiply the vector `[x0, x1, x2]` by the matrix + // `[[ 64, 64, 128],` + // ` [128, 64, 64],` + // ` [ 64, 128, 64]]` + // The results are divided by 4 (this ends up cancelling out some later computations). + { + let x0 = state_fft[0]; + let x1 = state_fft[1]; + let x2 = state_fft[2]; + + let t = vshlq_n_u32::<4>(x0); + let u = vaddq_u32(x1, x2); + + let y0 = vshlq_n_u32::<4>(u); + let y1 = vmlaq_laneq_u32::<3>(t, x2, consts); + let y2 = vmlaq_laneq_u32::<3>(t, x1, consts); + + state_fft[0] = vaddq_u32(y0, y1); + state_fft[1] = vaddq_u32(y1, y2); + state_fft[2] = vaddq_u32(y0, y2); + } + + // 3x3 real matrix-vector mul for component 2 of the FFTs. + // Multiply the vector `[x0, x1, x2]` by the matrix + // `[[ -4, -8, 32],` + // ` [-32, -4, -8],` + // ` [ 8, -32, -4]]` + // The results are divided by 4 (this ends up cancelling out some later computations). + { + let x0 = state_fft[3]; + let x1 = state_fft[4]; + let x2 = state_fft[5]; + state_fft[3] = vmlsq_laneq_u32::<2>(vmlaq_laneq_u32::<0>(x0, x1, consts), x2, consts); + state_fft[4] = vmlaq_laneq_u32::<0>(vmlaq_laneq_u32::<2>(x1, x0, consts), x2, consts); + state_fft[5] = vmlsq_laneq_u32::<0>(x2, vmlsq_laneq_u32::<1>(x0, x1, consts), consts); + } + + // 3x3 complex matrix-vector mul for components 1 and 3 of the FFTs. + // Multiply the vector `[x0r + x0i i, x1r + x1i i, x2r + x2i i]` by the matrix + // `[[ 4 + 2i, 2 + 32i, 2 - 8i],` + // ` [-8 - 2i, 4 + 2i, 2 + 32i],` + // ` [32 - 2i, -8 - 2i, 4 + 2i]]` + // The results are divided by 2 (this ends up cancelling out some later computations). + { + let x0r = state_fft[6]; + let x1r = state_fft[7]; + let x2r = state_fft[8]; + + let x0i = state_fft[9]; + let x1i = state_fft[10]; + let x2i = state_fft[11]; + + // real part of result <- real part of input + let r0rr = vaddq_u32(vmlaq_laneq_u32::<0>(x1r, x0r, consts), x2r); + let r1rr = vmlaq_laneq_u32::<0>(x2r, vmlsq_laneq_u32::<0>(x1r, x0r, consts), consts); + let r2rr = vmlsq_laneq_u32::<0>(x2r, vmlsq_laneq_u32::<1>(x1r, x0r, consts), consts); + + // real part of result <- imaginary part of input + let r0ri = vmlsq_laneq_u32::<1>(vmlaq_laneq_u32::<3>(x0i, x1i, consts), x2i, consts); + let r1ri = vmlsq_laneq_u32::<3>(vsubq_u32(x0i, x1i), x2i, consts); + let r2ri = vsubq_u32(vaddq_u32(x0i, x1i), x2i); + + // real part of result (total) + let r0r = vsubq_u32(r0rr, r0ri); + let r1r = vaddq_u32(r1rr, r1ri); + let r2r = vmlaq_laneq_u32::<0>(r2ri, r2rr, consts); + + // imaginary part of result <- real part of input + let r0ir = vmlsq_laneq_u32::<1>(vmlaq_laneq_u32::<3>(x0r, x1r, consts), x2r, consts); + let r1ir = vmlaq_laneq_u32::<3>(vsubq_u32(x1r, x0r), x2r, consts); + let r2ir = vsubq_u32(x2r, vaddq_u32(x0r, x1r)); + + // imaginary part of result <- imaginary part of input + let r0ii = vaddq_u32(vmlaq_laneq_u32::<0>(x1i, x0i, consts), x2i); + let r1ii = vmlaq_laneq_u32::<0>(x2i, vmlsq_laneq_u32::<0>(x1i, x0i, consts), consts); + let r2ii = vmlsq_laneq_u32::<0>(x2i, vmlsq_laneq_u32::<1>(x1i, x0i, consts), consts); + + // imaginary part of result (total) + let r0i = vaddq_u32(r0ir, r0ii); + let r1i = vaddq_u32(r1ir, r1ii); + let r2i = vmlaq_laneq_u32::<0>(r2ir, r2ii, consts); + + state_fft[6] = r0r; + state_fft[7] = r1r; + state_fft[8] = r2r; + + state_fft[9] = r0i; + state_fft[10] = r1i; + state_fft[11] = r2i; + } + + // Three length-4 inverse FFTs. + // Normally, such IFFT would divide by 4, but we've already taken care of that. + for i in 0..3 { + let z0 = state_fft[i]; + let z1 = state_fft[i + 3]; + let z2 = state_fft[i + 6]; + let z3 = state_fft[i + 9]; + + let y0 = vsubq_u32(z0, z1); + let y1 = vaddq_u32(z0, z1); + let y2 = z2; + let y3 = z3; + + let x0 = vaddq_u32(y0, y2); + let x1 = vaddq_u32(y1, y3); + let x2 = vsubq_u32(y0, y2); + let x3 = vsubq_u32(y1, y3); + + state_fft[i] = x0; + state_fft[i + 3] = x1; + state_fft[i + 6] = x2; + state_fft[i + 9] = x3; + } + + // Perform `res[0] += state[0] * 8` for the diagonal component of the MDS matrix. + state_fft[0] = vmlal_laneq_u16::<4>( + state_fft[0], + vcreate_u16(state[0]), // Each 16-bit chunk gets zero-extended. + vreinterpretq_u16_u32(consts), // Hack: these constants fit in `u16s`, so we can bit-cast. + ); + + let mut res_arr = [0; 12]; + for i in 0..6 { + let res = mds_reduce([state_fft[2 * i], state_fft[2 * i + 1]]); + res_arr[2 * i] = vgetq_lane_u64::<0>(res); + res_arr[2 * i + 1] = vgetq_lane_u64::<1>(res); + } + + res_arr +} + +// ======================================== PARTIAL ROUNDS ========================================= + +/* +#[rustfmt::skip] +macro_rules! mds_reduce_asm { + ($c0:literal, $c1:literal, $out:literal, $consts:literal) => { + concat!( + // Swizzle + "zip1.2d ", $out, ",", $c0, ",", $c1, "\n", // lo + "zip2.2d ", $c0, ",", $c0, ",", $c1, "\n", // hi + + // Reduction from u96 + "usra.2d ", $c0, ",", $out, ", #32\n", "sli.2d ", $out, ",", $c0, ", #32\n", + // Extract high 32-bits. + "uzp2.4s ", $c0, ",", $c0, ",", $c0, "\n", + // Multiply by EPSILON and accumulate. + "mov.16b ", $c1, ",", $out, "\n", + "umlal.2d ", $out, ",", $c0, ", ", $consts, "[0]\n", + "cmhi.2d ", $c1, ",", $c1, ",", $out, "\n", + "usra.2d ", $out, ",", $c1, ", #32", + ) + }; +} + +#[inline(always)] +unsafe fn partial_round( + (state_scalar, state_vector): ([u64; WIDTH], [uint64x2_t; 5]), + round_constants: &[u64; WIDTH], +) -> ([u64; WIDTH], [uint64x2_t; 5]) { + // see readme-asm.md + + // mds_consts0 == [0xffffffff, 1 << 1, 1 << 3, 1 << 5] + // mds_consts1 == [1 << 8, 1 << 10, 1 << 12, 1 << 16] + let mds_consts0: uint32x4_t = vld1q_u32((&MDS_CONSTS[0..4]).as_ptr().cast::()); + let mds_consts1: uint32x4_t = vld1q_u32((&MDS_CONSTS[4..8]).as_ptr().cast::()); + + let res0: u64; + let res1: u64; + let res23: uint64x2_t; + let res45: uint64x2_t; + let res67: uint64x2_t; + let res89: uint64x2_t; + let res1011: uint64x2_t; + + let res2_scalar: u64; + let res3_scalar: u64; + let res4_scalar: u64; + let res5_scalar: u64; + let res6_scalar: u64; + let res7_scalar: u64; + let res8_scalar: u64; + let res9_scalar: u64; + let res10_scalar: u64; + let res11_scalar: u64; + + asm!( + "ldp d0, d1, [{rc_ptr}, #16]", + "fmov d21, {s1}", + "ldp {lo0}, {lo1}, [{rc_ptr}]", + "umulh {t0}, {s0}, {s0}", + "mul {t1}, {s0}, {s0}", + "subs {t1}, {t1}, {t0}, lsr #32", + "csetm {t2:w}, cc", + "lsl {t3}, {t0}, #32", + "sub {t1}, {t1}, {t2}", + "mov {t0:w}, {t0:w}", + "sub {t0}, {t3}, {t0}", + "adds {t0}, {t1}, {t0}", + "csetm {t1:w}, cs", + "add {t0}, {t0}, {t1}", + "umulh {t1}, {s0}, {t0}", + "umulh {t2}, {t0}, {t0}", + "mul {s0}, {s0}, {t0}", + "mul {t0}, {t0}, {t0}", + "subs {s0}, {s0}, {t1}, lsr #32", + "csetm {t3:w}, cc", + "subs {t0}, {t0}, {t2}, lsr #32", + "csetm {t4:w}, cc", + "lsl {t5}, {t1}, #32", + "lsl {t6}, {t2}, #32", + "sub {s0}, {s0}, {t3}", + "sub {t0}, {t0}, {t4}", + "mov {t1:w}, {t1:w}", + "mov {t2:w}, {t2:w}", + "sub {t1}, {t5}, {t1}", + "ushll.2d v10, v21, #10", + "sub {t2}, {t6}, {t2}", + "ushll.2d v11, v21, #16", + "adds {t1}, {s0}, {t1}", + "uaddw.2d v0, v0, v22", + "csetm {s0:w}, cs", + "umlal.2d v1, v22, v31[1]", + "adds {t2}, {t0}, {t2}", + "uaddw2.2d v10, v10, v22", + "csetm {t0:w}, cs", + "uaddw2.2d v11, v11, v22", + "add {t1}, {t1}, {s0}", + "ldp d2, d3, [{rc_ptr}, #32]", + "add {t2}, {t2}, {t0}", + "ushll.2d v12, v21, #3", + "umulh {s0}, {t1}, {t2}", + "ushll.2d v13, v21, #12", + "mul {t0}, {t1}, {t2}", + "umlal.2d v0, v23, v30[1]", + "add {lo1}, {lo1}, {s1:w}, uxtw", + "uaddw2.2d v10, v10, v23", + "add {lo0}, {lo0}, {s1:w}, uxtw", + "uaddw.2d v11, v11, v23", + "lsr {hi0}, {s1}, #32", + "umlal2.2d v1, v23, v30[1]", + "lsr {t3}, {s2}, #32", + "umlal.2d v2, v22, v31[3]", + "lsr {t4}, {s3}, #32", + "umlal2.2d v12, v22, v31[1]", + "add {hi1}, {hi0}, {t3}", + "umlal.2d v3, v22, v30[2]", + "add {hi0}, {hi0}, {t3}, lsl #1", + "umlal2.2d v13, v22, v31[3]", + "add {lo1}, {lo1}, {s2:w}, uxtw", + "ldp d4, d5, [{rc_ptr}, #48]", + "add {lo0}, {lo0}, {s2:w}, uxtw #1", + "ushll.2d v14, v21, #8", + "lsr {t3}, {s4}, #32", + "ushll.2d v15, v21, #1", + "lsr {t5}, {s5}, #32", + "umlal.2d v0, v24, v30[2]", + "subs {t0}, {t0}, {s0}, lsr #32", + "umlal2.2d v10, v24, v30[3]", + "add {hi1}, {hi1}, {t4}, lsl #1", + "umlal2.2d v11, v24, v30[2]", + "add {t6}, {t3}, {t5}, lsl #3", + "uaddw.2d v1, v1, v24", + "add {t5}, {t3}, {t5}, lsl #2", + "uaddw.2d v2, v2, v23", + "lsr {t3}, {s6}, #32", + "umlal.2d v3, v23, v31[1]", + "lsr {s1}, {s7}, #32", + "uaddw2.2d v12, v12, v23", + "mov {s2:w}, {s4:w}", + "uaddw2.2d v13, v13, v23", + "add {hi0}, {hi0}, {t4}", + "umlal.2d v4, v22, v31[2]", + "add {lo1}, {lo1}, {s3:w}, uxtw #1", + "umlal2.2d v14, v22, v30[2]", + "add {lo0}, {lo0}, {s3:w}, uxtw", + "umlal.2d v5, v22, v31[0]", + "add {t4}, {s2}, {s5:w}, uxtw #3", + "umlal2.2d v15, v22, v31[2]", + "add {s2}, {s2}, {s5:w}, uxtw #2", + "ldp d6, d7, [{rc_ptr}, #64]", + "add {s3}, {s1}, {t3}, lsl #4", + "ushll.2d v16, v21, #5", + "csetm {t1:w}, cc", + "ushll.2d v17, v21, #3", + "add {hi1}, {hi1}, {t6}", + "umlal.2d v0, v25, v30[1]", + "add {hi0}, {hi0}, {t5}, lsl #3", + "umlal2.2d v10, v25, v31[0]", + "mov {t5:w}, {s6:w}", + "umlal.2d v1, v25, v30[3]", + "mov {t6:w}, {s7:w}", + "umlal2.2d v11, v25, v30[1]", + "add {s4}, {t6}, {t5}, lsl #4", + "umlal.2d v2, v24, v30[1]", + "add {t3}, {t3}, {s1}, lsl #7", + "uaddw2.2d v12, v12, v24", + "lsr {s1}, {s8}, #32", + "uaddw.2d v13, v13, v24", + "lsr {s5}, {s9}, #32", + "umlal2.2d v3, v24, v30[1]", + "lsl {t2}, {s0}, #32", + "umlal.2d v4, v23, v31[3]", + "sub {t0}, {t0}, {t1}", + "umlal2.2d v14, v23, v31[1]", + "add {lo1}, {lo1}, {t4}", + "umlal.2d v5, v23, v30[2]", + "add {lo0}, {lo0}, {s2}, lsl #3", + "umlal2.2d v15, v23, v31[3]", + "add {t4}, {t5}, {t6}, lsl #7", + "umlal.2d v6, v22, v30[1]", + "add {hi1}, {hi1}, {s3}, lsl #1", + "umlal2.2d v16, v22, v31[0]", + "add {t5}, {s1}, {s5}, lsl #4", + "umlal.2d v7, v22, v30[3]", + "mov {s0:w}, {s0:w}", + "umlal2.2d v17, v22, v30[1]", + "sub {s0}, {t2}, {s0}", + "ldp d8, d9, [{rc_ptr}, #80]", + "add {lo1}, {lo1}, {s4}, lsl #1", + "ushll.2d v18, v21, #0", + "add {hi0}, {hi0}, {t3}, lsl #1", + "ushll.2d v19, v21, #1", + "mov {t3:w}, {s9:w}", + "umlal.2d v0, v26, v31[2]", + "mov {t6:w}, {s8:w}", + "umlal2.2d v10, v26, v30[2]", + "add {s2}, {t6}, {t3}, lsl #4", + "umlal.2d v1, v26, v31[0]", + "add {s1}, {s5}, {s1}, lsl #9", + "umlal2.2d v11, v26, v31[2]", + "lsr {s3}, {s10}, #32", + "umlal.2d v2, v25, v30[2]", + "lsr {s4}, {s11}, #32", + "umlal2.2d v12, v25, v30[3]", + "adds {s0}, {t0}, {s0}", + "umlal2.2d v13, v25, v30[2]", + "add {lo0}, {lo0}, {t4}, lsl #1", + "uaddw.2d v3, v3, v25", + "add {t3}, {t3}, {t6}, lsl #9", + "uaddw.2d v4, v4, v24", + "add {hi1}, {hi1}, {t5}, lsl #8", + "umlal.2d v5, v24, v31[1]", + "add {t4}, {s3}, {s4}, lsl #13", + "uaddw2.2d v14, v14, v24", + "csetm {t0:w}, cs", + "uaddw2.2d v15, v15, v24", + "add {lo1}, {lo1}, {s2}, lsl #8", + "umlal.2d v6, v23, v31[2]", + "add {hi0}, {hi0}, {s1}, lsl #3", + "umlal2.2d v16, v23, v30[2]", + "mov {t5:w}, {s10:w}", + "umlal.2d v7, v23, v31[0]", + "mov {t6:w}, {s11:w}", + "umlal2.2d v17, v23, v31[2]", + "add {s1}, {t5}, {t6}, lsl #13", + "umlal.2d v8, v22, v30[2]", + "add {s2}, {s4}, {s3}, lsl #6", + "umlal2.2d v18, v22, v30[3]", + "add {s0}, {s0}, {t0}", + "uaddw.2d v9, v9, v22", + "add {lo0}, {lo0}, {t3}, lsl #3", + "umlal2.2d v19, v22, v30[2]", + "add {t3}, {t6}, {t5}, lsl #6", + "add.2d v0, v0, v10", + "add {hi1}, {hi1}, {t4}, lsl #3", + "add.2d v1, v1, v11", + "fmov d20, {s0}", + "umlal.2d v0, v20, v31[3]", + "add {lo1}, {lo1}, {s1}, lsl #3", + "umlal.2d v1, v20, v30[2]", + "add {hi0}, {hi0}, {s2}, lsl #10", + "zip1.2d v22, v0, v1", + "lsr {t4}, {s0}, #32", + "zip2.2d v0, v0, v1", + "add {lo0}, {lo0}, {t3}, lsl #10", + "usra.2d v0, v22, #32", + "add {hi1}, {hi1}, {t4}, lsl #10", + "sli.2d v22, v0, #32", + "mov {t3:w}, {s0:w}", + "uzp2.4s v0, v0, v0", + "add {lo1}, {lo1}, {t3}, lsl #10", + "mov.16b v1, v22", + "add {hi0}, {hi0}, {t4}", + "umlal.2d v22, v0, v30[0]", + "add {lo0}, {lo0}, {t3}", + "cmhi.2d v1, v1, v22", + "lsl {t0}, {hi0}, #32", + "usra.2d v22, v1, #32", + "lsl {t1}, {hi1}, #32", + "fmov {s2}, d22", + "adds {lo0}, {lo0}, {t0}", + "fmov.d {s3}, v22[1]", + "csetm {t0:w}, cs", + "umlal.2d v2, v26, v30[1]", + "adds {lo1}, {lo1}, {t1}", + "umlal2.2d v12, v26, v31[0]", + "csetm {t1:w}, cs", + "umlal.2d v3, v26, v30[3]", + "and {t2}, {hi0}, #0xffffffff00000000", + "umlal2.2d v13, v26, v30[1]", + "and {t3}, {hi1}, #0xffffffff00000000", + "umlal.2d v4, v25, v30[1]", + "lsr {hi0}, {hi0}, #32", + "uaddw2.2d v14, v14, v25", + "lsr {hi1}, {hi1}, #32", + "uaddw.2d v15, v15, v25", + "sub {hi0}, {t2}, {hi0}", + "umlal2.2d v5, v25, v30[1]", + "sub {hi1}, {t3}, {hi1}", + "umlal.2d v6, v24, v31[3]", + "add {lo0}, {lo0}, {t0}", + "umlal2.2d v16, v24, v31[1]", + "add {lo1}, {lo1}, {t1}", + "umlal.2d v7, v24, v30[2]", + "adds {lo0}, {lo0}, {hi0}", + "umlal2.2d v17, v24, v31[3]", + "csetm {t0:w}, cs", + "umlal.2d v8, v23, v30[1]", + "adds {lo1}, {lo1}, {hi1}", + "umlal2.2d v18, v23, v31[0]", + "csetm {t1:w}, cs", + "umlal.2d v9, v23, v30[3]", + "add {s0}, {lo0}, {t0}", + "umlal2.2d v19, v23, v30[1]", + "add {s1}, {lo1}, {t1}", + "add.2d v2, v2, v12", + "add.2d v3, v3, v13", + "umlal.2d v2, v20, v31[2]", + "umlal.2d v3, v20, v31[0]", + mds_reduce_asm!("v2", "v3", "v23", "v30"), + "fmov {s4}, d23", + "fmov.d {s5}, v23[1]", + "umlal.2d v4, v26, v30[2]", + "umlal2.2d v14, v26, v30[3]", + "umlal2.2d v15, v26, v30[2]", + "uaddw.2d v5, v5, v26", + "uaddw.2d v6, v6, v25", + "uaddw2.2d v16, v16, v25", + "uaddw2.2d v17, v17, v25", + "umlal.2d v7, v25, v31[1]", + "umlal.2d v8, v24, v31[2]", + "umlal2.2d v18, v24, v30[2]", + "umlal.2d v9, v24, v31[0]", + "umlal2.2d v19, v24, v31[2]", + "add.2d v4, v4, v14", + "add.2d v5, v5, v15", + "umlal.2d v4, v20, v30[1]", + "umlal.2d v5, v20, v30[3]", + mds_reduce_asm!("v4", "v5", "v24", "v30"), + "fmov {s6}, d24", + "fmov.d {s7}, v24[1]", + "umlal.2d v6, v26, v30[1]", + "uaddw2.2d v16, v16, v26", + "umlal2.2d v17, v26, v30[1]", + "uaddw.2d v7, v7, v26", + "umlal.2d v8, v25, v31[3]", + "umlal2.2d v18, v25, v31[1]", + "umlal.2d v9, v25, v30[2]", + "umlal2.2d v19, v25, v31[3]", + "add.2d v6, v6, v16", + "add.2d v7, v7, v17", + "umlal.2d v6, v20, v30[2]", + "uaddw.2d v7, v7, v20", + mds_reduce_asm!("v6", "v7", "v25", "v30"), + "fmov {s8}, d25", + "fmov.d {s9}, v25[1]", + "uaddw.2d v8, v8, v26", + "uaddw2.2d v18, v18, v26", + "umlal.2d v9, v26, v31[1]", + "uaddw2.2d v19, v19, v26", + "add.2d v8, v8, v18", + "add.2d v9, v9, v19", + "umlal.2d v8, v20, v30[1]", + "uaddw.2d v9, v9, v20", + mds_reduce_asm!("v8", "v9", "v26", "v30"), + "fmov {s10}, d26", + "fmov.d {s11}, v26[1]", + + // Scalar inputs/outputs + // s0 is transformed by the S-box + s0 = inout(reg) state_scalar[0] => res0, + // s1-s6 double as scratch in the MDS matrix multiplication + s1 = inout(reg) state_scalar[1] => res1, + // s2-s11 are copied from the vector inputs/outputs + s2 = inout(reg) state_scalar[2] => res2_scalar, + s3 = inout(reg) state_scalar[3] => res3_scalar, + s4 = inout(reg) state_scalar[4] => res4_scalar, + s5 = inout(reg) state_scalar[5] => res5_scalar, + s6 = inout(reg) state_scalar[6] => res6_scalar, + s7 = inout(reg) state_scalar[7] => res7_scalar, + s8 = inout(reg) state_scalar[8] => res8_scalar, + s9 = inout(reg) state_scalar[9] => res9_scalar, + s10 = inout(reg) state_scalar[10] => res10_scalar, + s11 = inout(reg) state_scalar[11] => res11_scalar, + + // Pointer to the round constants + rc_ptr = in(reg) round_constants.as_ptr(), + + // Scalar MDS multiplication accumulators + lo1 = out(reg) _, + hi1 = out(reg) _, + lo0 = out(reg) _, + hi0 = out(reg) _, + + // Scalar scratch registers + // All are used in the scalar S-box + t0 = out(reg) _, + t1 = out(reg) _, + t2 = out(reg) _, + // t3-t6 are used in the scalar MDS matrix multiplication + t3 = out(reg) _, + t4 = out(reg) _, + t5 = out(reg) _, + t6 = out(reg) _, + + // Vector MDS multiplication accumulators + // v{n} and v1{n} are accumulators for res[n + 2] (we need two to mask latency) + // The low and high 64-bits are accumulators for the low and high results, respectively + out("v0") _, + out("v1") _, + out("v2") _, + out("v3") _, + out("v4") _, + out("v5") _, + out("v6") _, + out("v7") _, + out("v8") _, + out("v9") _, + out("v10") _, + out("v11") _, + out("v12") _, + out("v13") _, + out("v14") _, + out("v15") _, + out("v16") _, + out("v17") _, + out("v18") _, + out("v19") _, + + // Inputs into vector MDS matrix multiplication + // v20 and v21 are sbox(state0) and state1, respectively. They are copied from the scalar + // registers. + out("v20") _, + out("v21") _, + // v22, ..., v26 hold state[2,3], ..., state[10,11] + inout("v22") state_vector[0] => res23, + inout("v23") state_vector[1] => res45, + inout("v24") state_vector[2] => res67, + inout("v25") state_vector[3] => res89, + inout("v26") state_vector[4] => res1011, + + // Useful constants + in("v30") mds_consts0, + in("v31") mds_consts1, + + options(nostack, pure, readonly), + ); + ( + [ + res0, + res1, + res2_scalar, + res3_scalar, + res4_scalar, + res5_scalar, + res6_scalar, + res7_scalar, + res8_scalar, + res9_scalar, + res10_scalar, + res11_scalar, + ], + [res23, res45, res67, res89, res1011], + ) +} +*/ + +// ========================================== GLUE CODE =========================================== + +/* +#[inline(always)] +unsafe fn full_round(state: [u64; 12], round_constants: &[u64; WIDTH]) -> [u64; 12] { + let state = sbox_layer_full(state); + mds_layer_full(state, round_constants) +} + +#[inline] +unsafe fn full_rounds( + mut state: [u64; 12], + round_constants: &[u64; WIDTH * HALF_N_FULL_ROUNDS], +) -> [u64; 12] { + for round_constants_chunk in round_constants.chunks_exact(WIDTH) { + state = full_round(state, round_constants_chunk.try_into().unwrap()); + } + state +} + +#[inline(always)] +unsafe fn partial_rounds( + state: [u64; 12], + round_constants: &[u64; WIDTH * N_PARTIAL_ROUNDS], +) -> [u64; 12] { + let mut state = ( + state, + [ + vcombine_u64(vcreate_u64(state[2]), vcreate_u64(state[3])), + vcombine_u64(vcreate_u64(state[4]), vcreate_u64(state[5])), + vcombine_u64(vcreate_u64(state[6]), vcreate_u64(state[7])), + vcombine_u64(vcreate_u64(state[8]), vcreate_u64(state[9])), + vcombine_u64(vcreate_u64(state[10]), vcreate_u64(state[11])), + ], + ); + for round_constants_chunk in round_constants.chunks_exact(WIDTH) { + state = partial_round(state, round_constants_chunk.try_into().unwrap()); + } + state.0 +} +*/ + +#[inline(always)] +fn unwrap_state(state: [GoldilocksField; 12]) -> [u64; 12] { + state.map(|s| s.0) +} + +#[inline(always)] +fn wrap_state(state: [u64; 12]) -> [GoldilocksField; 12] { + state.map(GoldilocksField) +} + +/* +#[inline(always)] +pub unsafe fn poseidon(state: [GoldilocksField; 12]) -> [GoldilocksField; 12] { + let state = unwrap_state(state); + let state = const_layer_full(state, ALL_ROUND_CONSTANTS[0..WIDTH].try_into().unwrap()); + let state = full_rounds( + state, + ALL_ROUND_CONSTANTS[WIDTH..WIDTH * (HALF_N_FULL_ROUNDS + 1)] + .try_into() + .unwrap(), + ); + let state = partial_rounds( + state, + ALL_ROUND_CONSTANTS + [WIDTH * (HALF_N_FULL_ROUNDS + 1)..WIDTH * (HALF_N_FULL_ROUNDS + N_PARTIAL_ROUNDS + 1)] + .try_into() + .unwrap(), + ); + let state = full_rounds(state, &FINAL_ROUND_CONSTANTS); + wrap_state(state) +} +*/ + +#[inline(always)] +pub unsafe fn sbox_layer(state: &mut [GoldilocksField; WIDTH]) { + *state = wrap_state(sbox_layer_full(unwrap_state(*state))); +} + +#[inline(always)] +pub unsafe fn mds_layer(state: &[GoldilocksField; WIDTH]) -> [GoldilocksField; WIDTH] { + let state = unwrap_state(*state); + let state = mds_layer_full(state); + wrap_state(state) +} diff --git a/gprimitives/client/src/hash/arch/aarch64/readme-asm.md b/gprimitives/client/src/hash/arch/aarch64/readme-asm.md new file mode 100644 index 00000000000..c83c5868cc6 --- /dev/null +++ b/gprimitives/client/src/hash/arch/aarch64/readme-asm.md @@ -0,0 +1,495 @@ +Partial rounds ASM +================== + +The partial rounds are written in hand-rolled ASM. This was necessary to ensure proper pipelining. Indeed, the ASM shaves 40% off the execution time of the original vector intrinsics-based partial round. + +The partial layer performs two operations: + 1. Apply the S-box to state[0] + 2. Apply an affine transform (MDS matrix + constant layer) to the entire state vector. + +The S-box must be performed in scalar to minimize latency. The MDS matrix is done mostly in vector to maximize throughput. To take advantage of the otherwise idle scalar execution units, MDS matrix multiplication for result[0..2] is done in scalar. Clearly, this necessitates some data movement, as the input state must be available to both scalar and vector execution units. + +This task has plentiful opportunities for pipelining and parallelism. Most immediately, the S-box—with its long latency chain—can be performed simultaneously with most of the MDS matrix multiplication, with the permuted input only available right before the reduction. In addition, the MDS matrix multiplication can be scheduled in a way that interleaves different kinds of operations, masking the latency of the reduction step. + +There are three chains of ASM: + 1. the S-box, + 2. the scalar part of MDS multiplication (for result[0..2]), + 3. the vector part of MDS multiplication (for result[2..12]). +Those chains are explained individually below. They interact sporadically to exchange results. In the compiled file, they have been interleaved. + + +S-box +----- + +The ASM for the S-box is as follows: +```assembly + umulh {t0}, {s0}, {s0} + mul {t1}, {s0}, {s0} + subs {t1}, {t1}, {t0}, lsr #32 + csetm {t2:w}, cc + lsl {t3}, {t0}, #32 + sub {t1}, {t1}, {t2} + mov {t0:w}, {t0:w} + sub {t0}, {t3}, {t0} + adds {t0}, {t1}, {t0} + csetm {t1:w}, cs + add {t0}, {t0}, {t1} + + // t0 now contains state ** 2 + umulh {t1}, {s0}, {t0} + umulh {t2}, {t0}, {t0} + mul {s0}, {s0}, {t0} + mul {t0}, {t0}, {t0} + subs {s0}, {s0}, {t1}, lsr #32 + csetm {t3:w}, cc + subs {t0}, {t0}, {t2}, lsr #32 + csetm {t4:w}, cc + lsl {t5}, {t1}, #32 + lsl {t6}, {t2}, #32 + sub {s0}, {s0}, {t3} + sub {t0}, {t0}, {t4} + mov {t1:w}, {t1:w} + mov {t2:w}, {t2:w} + sub {t1}, {t5}, {t1} + sub {t2}, {t6}, {t2} + adds {t1}, {s0}, {t1} + csetm {s0:w}, cs + adds {t2}, {t0}, {t2} + csetm {t0:w}, cs + add {t1}, {t1}, {s0} + add {t2}, {t2}, {t0} + + // t1 now contains state ** 3 + // t2 now contains state ** 4 + umulh {s0}, {t1}, {t2} + mul {t0}, {t1}, {t2} + subs {t0}, {t0}, {s0}, lsr #32 + csetm {t1:w}, cc + lsl {t2}, {s0}, #32 + sub {t0}, {t0}, {t1} + mov {s0:w}, {s0:w} + sub {s0}, {t2}, {s0} + adds {s0}, {t0}, {s0} + csetm {t0:w}, cs + add {s0}, {s0}, {t0} + + // s0 now contains state **7 + fmov d20, {s0} +``` + +It is merely four repetitions of a block of 11 instructions (the middle two repetitions are interleaved). The input and output are in `s0`. `t0` through `t6` are scratch registers. The `fmov` copies the result to the bottom 64 bits of the vector register v20. + +Trick: `csetm` sets its destination to all 1s if the condition is met. In our case the destination is 32-bits and the condition is overflow/underflow of the previous instruction, so we get EPSILON on over/underflow and 0 otherwise. + +Note: the last multiplication does not use `t3` through `t6`, making them available to scalar MDS multiplication. + + +Scalar MDS multiplication +------------------------- + +The ASM for the scalar MDS multiplication is +```assembly + ldp {lo0}, {lo1}, [{rc_ptr}] + add {lo1}, {lo1}, {s1:w}, uxtw + add {lo0}, {lo0}, {s1:w}, uxtw + lsr {hi0}, {s1}, #32 + lsr {t3}, {s2}, #32 + lsr {t4}, {s3}, #32 + add {hi1}, {hi0}, {t3} + add {hi0}, {hi0}, {t3}, lsl #1 + add {lo1}, {lo1}, {s2:w}, uxtw + add {lo0}, {lo0}, {s2:w}, uxtw #1 + lsr {t3}, {s4}, #32 + lsr {t5}, {s5}, #32 + add {hi1}, {hi1}, {t4}, lsl #1 + add {t6}, {t3}, {t5}, lsl #3 + add {t5}, {t3}, {t5}, lsl #2 + lsr {t3}, {s6}, #32 + lsr {s1}, {s7}, #32 + mov {s2:w}, {s4:w} + add {hi0}, {hi0}, {t4} + add {lo1}, {lo1}, {s3:w}, uxtw #1 + add {lo0}, {lo0}, {s3:w}, uxtw + add {t4}, {s2}, {s5:w}, uxtw #3 + add {s2}, {s2}, {s5:w}, uxtw #2 + add {s3}, {s1}, {t3}, lsl #4 + add {hi1}, {hi1}, {t6} + add {hi0}, {hi0}, {t5}, lsl #3 + mov {t5:w}, {s6:w} + mov {t6:w}, {s7:w} + add {s4}, {t6}, {t5}, lsl #4 + add {t3}, {t3}, {s1}, lsl #7 + lsr {s1}, {s8}, #32 + lsr {s5}, {s9}, #32 + add {lo1}, {lo1}, {t4} + add {lo0}, {lo0}, {s2}, lsl #3 + add {t4}, {t5}, {t6}, lsl #7 + add {hi1}, {hi1}, {s3}, lsl #1 + add {t5}, {s1}, {s5}, lsl #4 + add {lo1}, {lo1}, {s4}, lsl #1 + add {hi0}, {hi0}, {t3}, lsl #1 + mov {t3:w}, {s9:w} + mov {t6:w}, {s8:w} + add {s2}, {t6}, {t3}, lsl #4 + add {s1}, {s5}, {s1}, lsl #9 + lsr {s3}, {s10}, #32 + lsr {s4}, {s11}, #32 + add {lo0}, {lo0}, {t4}, lsl #1 + add {t3}, {t3}, {t6}, lsl #9 + add {hi1}, {hi1}, {t5}, lsl #8 + add {t4}, {s3}, {s4}, lsl #13 + add {lo1}, {lo1}, {s2}, lsl #8 + add {hi0}, {hi0}, {s1}, lsl #3 + mov {t5:w}, {s10:w} + mov {t6:w}, {s11:w} + add {s1}, {t5}, {t6}, lsl #13 + add {s2}, {s4}, {s3}, lsl #6 + add {lo0}, {lo0}, {t3}, lsl #3 + add {t3}, {t6}, {t5}, lsl #6 + add {hi1}, {hi1}, {t4}, lsl #3 + add {lo1}, {lo1}, {s1}, lsl #3 + add {hi0}, {hi0}, {s2}, lsl #10 + lsr {t4}, {s0}, #32 + add {lo0}, {lo0}, {t3}, lsl #10 + add {hi1}, {hi1}, {t4}, lsl #10 + mov {t3:w}, {s0:w} + add {lo1}, {lo1}, {t3}, lsl #10 + add {hi0}, {hi0}, {t4} + add {lo0}, {lo0}, {t3} + + // Reduction + lsl {t0}, {hi0}, #32 + lsl {t1}, {hi1}, #32 + adds {lo0}, {lo0}, {t0} + csetm {t0:w}, cs + adds {lo1}, {lo1}, {t1} + csetm {t1:w}, cs + and {t2}, {hi0}, #0xffffffff00000000 + and {t3}, {hi1}, #0xffffffff00000000 + lsr {hi0}, {hi0}, #32 + lsr {hi1}, {hi1}, #32 + sub {hi0}, {t2}, {hi0} + sub {hi1}, {t3}, {hi1} + add {lo0}, {lo0}, {t0} + add {lo1}, {lo1}, {t1} + adds {lo0}, {lo0}, {hi0} + csetm {t0:w}, cs + adds {lo1}, {lo1}, {hi1} + csetm {t1:w}, cs + add {s0}, {lo0}, {t0} + add {s1}, {lo1}, {t1} +``` + +The MDS multiplication is done separately on the low 32 bits and the high 32 bits of the input, and combined by linearity. Each input is split into the low part and the high part. There are separate accumulators for the low and high parts of the result `lo0`/`lo1`, for result[0] and result[1] respectively, and `hi0`/`hi1`. + +The pointer to the round constants is given in `rc_ptr`. Registers `s0`-`s11` contain the state vector at the start, and are later used as scratch. `t3`-`t6` are temporaries. + +`s1` is assumed to be available first, as it is computed in scalar. `s2`-`s11` are used next. `s0` is assumed to be available last, as it must be transformed by the S-box. + +The reduction is +```assembly + lsl {t0}, {hi0}, #32 + adds {lo0}, {lo0}, {t0} + csetm {t0:w}, cs + and {t2}, {hi0}, #0xffffffff00000000 + lsr {hi0}, {hi0}, #32 + sub {hi0}, {t2}, {hi0} + add {lo0}, {lo0}, {t0} + adds {lo0}, {lo0}, {hi0} + csetm {t0:w}, cs + add {s0}, {lo0}, {t0} +``` +repeated and interleaved. `cset` sets its destination to EPSILON if the previous instruction overflowed. + + +Vector MDS multiplication +------------------------- + +The ASM for the vector MDS multiplication is +```assembly + fmov d21, {s1} + + // res2,3 <- consts,state1 + ldp d0, d1, [{rc_ptr}, #16] + ushll.2d v10, v21, #10 // MDS[11] == 10 + ushll.2d v11, v21, #16 // MDS[10] == 16 + + // res2,3 <- state2,3 + uaddw.2d v0, v0, v22 // MDS[0] == 0 + umlal.2d v1, v22, v31[1] // MDS[11] == 10 + uaddw2.2d v10, v10, v22 // MDS[1] == 0 + uaddw2.2d v11, v11, v22 // MDS[0] == 0 + + // res4,5 <- consts,state1 + ldp d2, d3, [{rc_ptr}, #32] + ushll.2d v12, v21, #3 // MDS[9] == 3 + ushll.2d v13, v21, #12 // MDS[8] == 12 + + // res2,3 <- state4,5 + umlal.2d v0, v23, v30[1] // MDS[2] == 1 + uaddw2.2d v10, v10, v23 // MDS[3] == 0 + uaddw.2d v11, v11, v23 // MDS[1] == 0 + umlal2.2d v1, v23, v30[1] // MDS[2] == 1 + + // res4,5 <- state2,3 + umlal.2d v2, v22, v31[3] // MDS[10] == 16 + umlal2.2d v12, v22, v31[1] // MDS[11] == 10 + umlal.2d v3, v22, v30[2] // MDS[9] == 3 + umlal2.2d v13, v22, v31[3] // MDS[10] == 16 + + // res6,7 <- consts,state1 + ldp d4, d5, [{rc_ptr}, #48] + ushll.2d v14, v21, #8 // MDS[7] == 8 + ushll.2d v15, v21, #1 // MDS[6] == 1 + + // res2,3 <- state6,7 + umlal.2d v0, v24, v30[2] // MDS[4] == 3 + umlal2.2d v10, v24, v30[3] // MDS[5] == 5 + umlal2.2d v11, v24, v30[2] // MDS[4] == 3 + uaddw.2d v1, v1, v24 // MDS[3] == 0 + + // res4,5 <- state4,5 + uaddw.2d v2, v2, v23 // MDS[0] == 0 + umlal.2d v3, v23, v31[1] // MDS[11] == 10 + uaddw2.2d v12, v12, v23 // MDS[1] == 0 + uaddw2.2d v13, v13, v23 // MDS[0] == 0 + + // res6,7 <- state2,3 + umlal.2d v4, v22, v31[2] // MDS[8] == 12 + umlal2.2d v14, v22, v30[2] // MDS[9] == 3 + umlal.2d v5, v22, v31[0] // MDS[7] == 8 + umlal2.2d v15, v22, v31[2] // MDS[8] == 12 + + // res8,9 <- consts,state1 + ldp d6, d7, [{rc_ptr}, #64] + ushll.2d v16, v21, #5 // MDS[5] == 5 + ushll.2d v17, v21, #3 // MDS[4] == 3 + + // res2,3 <- state8,9 + umlal.2d v0, v25, v30[1] // MDS[6] == 1 + umlal2.2d v10, v25, v31[0] // MDS[7] == 8 + umlal.2d v1, v25, v30[3] // MDS[5] == 5 + umlal2.2d v11, v25, v30[1] // MDS[6] == 1 + + // res4,5 <- state6,7 + umlal.2d v2, v24, v30[1] // MDS[2] == 1 + uaddw2.2d v12, v12, v24 // MDS[3] == 0 + uaddw.2d v13, v13, v24 // MDS[1] == 0 + umlal2.2d v3, v24, v30[1] // MDS[2] == 1 + + // res6,7 <- state4,5 + umlal.2d v4, v23, v31[3] // MDS[10] == 16 + umlal2.2d v14, v23, v31[1] // MDS[11] == 10 + umlal.2d v5, v23, v30[2] // MDS[9] == 3 + umlal2.2d v15, v23, v31[3] // MDS[10] == 16 + + // res8,9 <- state2,3 + umlal.2d v6, v22, v30[1] // MDS[6] == 1 + umlal2.2d v16, v22, v31[0] // MDS[7] == 8 + umlal.2d v7, v22, v30[3] // MDS[5] == 5 + umlal2.2d v17, v22, v30[1] // MDS[6] == 1 + + // res10,11 <- consts,state1 + ldp d8, d9, [{rc_ptr}, #80] + ushll.2d v18, v21, #0 // MDS[3] == 0 + ushll.2d v19, v21, #1 // MDS[2] == 1 + + // res2,3 <- state10,11 + umlal.2d v0, v26, v31[2] // MDS[8] == 12 + umlal2.2d v10, v26, v30[2] // MDS[9] == 3 + umlal.2d v1, v26, v31[0] // MDS[7] == 8 + umlal2.2d v11, v26, v31[2] // MDS[8] == 12 + + // res4,5 <- state8,9 + umlal.2d v2, v25, v30[2] // MDS[4] == 3 + umlal2.2d v12, v25, v30[3] // MDS[5] == 5 + umlal2.2d v13, v25, v30[2] // MDS[4] == 3 + uaddw.2d v3, v3, v25 // MDS[3] == 0 + + // res6,7 <- state6,7 + uaddw.2d v4, v4, v24 // MDS[0] == 0 + umlal.2d v5, v24, v31[1] // MDS[11] == 10 + uaddw2.2d v14, v14, v24 // MDS[1] == 0 + uaddw2.2d v15, v15, v24 // MDS[0] == 0 + + // res8,9 <- state4,5 + umlal.2d v6, v23, v31[2] // MDS[8] == 12 + umlal2.2d v16, v23, v30[2] // MDS[9] == 3 + umlal.2d v7, v23, v31[0] // MDS[7] == 8 + umlal2.2d v17, v23, v31[2] // MDS[8] == 12 + + // res10,11 <- state2,3 + umlal.2d v8, v22, v30[2] // MDS[4] == 3 + umlal2.2d v18, v22, v30[3] // MDS[5] == 5 + uaddw.2d v9, v9, v22 // MDS[3] == 0 + umlal2.2d v19, v22, v30[2] // MDS[4] == 3 + + // merge accumulators, res2,3 <- state0, and reduce + add.2d v0, v0, v10 + add.2d v1, v1, v11 + + umlal.2d v0, v20, v31[3] // MDS[10] == 16 + umlal.2d v1, v20, v30[2] // MDS[9] == 3 + mds_reduce_asm(v0, v1, v22) + fmov {s2}, d22 + fmov.d {s3}, v22[1] + + // res4,5 <- state10,11 + umlal.2d v2, v26, v30[1] // MDS[6] == 1 + umlal2.2d v12, v26, v31[0] // MDS[7] == 8 + umlal.2d v3, v26, v30[3] // MDS[5] == 5 + umlal2.2d v13, v26, v30[1] // MDS[6] == 1 + + // res6,7 <- state8,9 + umlal.2d v4, v25, v30[1] // MDS[2] == 1 + uaddw2.2d v14, v14, v25 // MDS[3] == 0 + uaddw.2d v15, v15, v25 // MDS[1] == 0 + umlal2.2d v5, v25, v30[1] // MDS[2] == 1 + + // res8,9 <- state6,7 + umlal.2d v6, v24, v31[3] // MDS[10] == 16 + umlal2.2d v16, v24, v31[1] // MDS[11] == 10 + umlal.2d v7, v24, v30[2] // MDS[9] == 3 + umlal2.2d v17, v24, v31[3] // MDS[10] == 16 + + // res10,11 <- state4,5 + umlal.2d v8, v23, v30[1] // MDS[6] == 1 + umlal2.2d v18, v23, v31[0] // MDS[7] == 8 + umlal.2d v9, v23, v30[3] // MDS[5] == 5 + umlal2.2d v19, v23, v30[1] // MDS[6] == 1 + + // merge accumulators, res4,5 <- state0, and reduce + add.2d v2, v2, v12 + add.2d v3, v3, v13 + + umlal.2d v2, v20, v31[2] // MDS[8] == 12 + umlal.2d v3, v20, v31[0] // MDS[7] == 8 + mds_reduce_asm(v2, v3, v23) + fmov {s4}, d23 + fmov.d {s5}, v23[1] + + // res6,7 <- state10,11 + umlal.2d v4, v26, v30[2] // MDS[4] == 3 + umlal2.2d v14, v26, v30[3] // MDS[5] == 5 + umlal2.2d v15, v26, v30[2] // MDS[4] == 3 + uaddw.2d v5, v5, v26 // MDS[3] == 0 + + // res8,9 <- state8,9 + uaddw.2d v6, v6, v25 // MDS[0] == 0 + uaddw2.2d v16, v16, v25 // MDS[1] == 0 + uaddw2.2d v17, v17, v25 // MDS[0] == 0 + umlal.2d v7, v25, v31[1] // MDS[11] == 10 + + // res10,11 <- state6,7 + umlal.2d v8, v24, v31[2] // MDS[8] == 12 + umlal2.2d v18, v24, v30[2] // MDS[9] == 3 + umlal.2d v9, v24, v31[0] // MDS[7] == 8 + umlal2.2d v19, v24, v31[2] // MDS[8] == 12 + + // merge accumulators, res6,7 <- state0, and reduce + add.2d v4, v4, v14 + add.2d v5, v5, v15 + + umlal.2d v4, v20, v30[1] // MDS[6] == 1 + umlal.2d v5, v20, v30[3] // MDS[5] == 5 + mds_reduce_asm(v4, v5, v24) + fmov {s6}, d24 + fmov.d {s7}, v24[1] + + // res8,9 <- state10,11 + umlal.2d v6, v26, v30[1] // MDS[2] == 1 + uaddw2.2d v16, v16, v26 // MDS[3] == 0 + umlal2.2d v17, v26, v30[1] // MDS[2] == 1 + uaddw.2d v7, v7, v26 // MDS[1] == 0 + + // res10,11 <- state8,9 + umlal.2d v8, v25, v31[3] // MDS[10] == 16 + umlal2.2d v18, v25, v31[1] // MDS[11] == 10 + umlal.2d v9, v25, v30[2] // MDS[9] == 3 + umlal2.2d v19, v25, v31[3] // MDS[10] == 16 + + // merge accumulators, res8,9 <- state0, and reduce + add.2d v6, v6, v16 + add.2d v7, v7, v17 + + umlal.2d v6, v20, v30[2] // MDS[4] == 3 + uaddw.2d v7, v7, v20 // MDS[3] == 0 + mds_reduce_asm(v6, v7, v25) + fmov {s8}, d25 + fmov.d {s9}, v25[1] + + // res10,11 <- state10,11 + uaddw.2d v8, v8, v26 // MDS[0] == 0 + uaddw2.2d v18, v18, v26 // MDS[1] == 0 + umlal.2d v9, v26, v31[1] // MDS[11] == 10 + uaddw2.2d v19, v19, v26 // MDS[0] == 0 + + // merge accumulators, res10,11 <- state0, and reduce + add.2d v8, v8, v18 + add.2d v9, v9, v19 + + umlal.2d v8, v20, v30[1] // MDS[2] == 1 + uaddw.2d v9, v9, v20 // MDS[1] == 0 + mds_reduce_asm(v8, v9, v26) + fmov {s10}, d26 + fmov.d {s11}, v26[1] +``` +where the macro `mds_reduce_asm` is defined as +```assembly + ($c0, $c1, $out) => { + // Swizzle + zip1.2d $out, $c0, $c1 // lo + zip2.2d $c0, $c0, $c1 // hi + + // Reduction from u96 + usra.2d $c0, $out, #32 + sli.2d $out, $c0, #32 + // Extract high 32-bits. + uzp2.4s $c0, $c0, $c0 + // Multiply by EPSILON and accumulate. + mov.16b $c1, $out + umlal.2d $out, $c0, v30[0] + cmhi.2d $c1, $c1, $out + usra.2d $out, $c1, #32 + } +``` + +The order in which inputs are assumed to be available is: +- state[1] +- state[2] and state[3] +- state[4] and state[5] +- state[6] and state[7] +- state[8] and state[9] +- state[10] and state[11] +- state[0] + +The order in which the results are produced is: +- state[2] and state[3] +- state[4] and state[5] +- state[6] and state[7] +- state[8] and state[9] +- state[10] and state[11] + +The order of the instructions in the assembly should be thought of as a setting the relative priority of each instruction; because of CPU reordering, it does not correspond exactly to execution order in time. Ideally, we'd like the MDS matrix multiplication to happen in the following order: + s[1] s[2..4] s[4..6] s[6..8] s[8..10] s[10..12] s[0] + res[2..4] 1 2 4 7 11 16 21 + res[4..6] 3 5 8 12 17 22 26 +output res[6..8] 6 9 13 18 23 27 30 + res[8..10] 10 14 19 24 28 31 33 + res[10..12] 15 20 25 29 32 34 35 + +This is the order in which the operations are ordered in the ASM. It permits the start of one iteration to be interleaved with the end of the previous iteration (CPU reordering means we don't have to do it manually). Reductions, which have high latency, are executed as soon as the unreduced product is available; the pipelining permits them to be executed simultaneously with multiplication/accumulation, masking the latency. + +The registers `v0`-`v19` are used for scratch. `v0` and `v10` are accumulators for res[2], `v1` and `v11` are accumulators for res[3], and so on. The accumulators hold the low result in the low 64 bits and the high result in the high 64 bits (this is convenient as both low and high are always multiplied by the same constant). They must be added before reduction. + +The inputs for state[0] and state[1] are in the low 64 bits of `v20` and `v21`, respectively. The inputs and outputs for state[2..4], ..., state[10..12] are in `v22`, ..., `v26`, respectively. + +`v30` and `v31` contains the constants [EPSILON, 1 << 1, 1 << 3, 1 << 5], [1 << 8, 1 << 10, 1 << 12, 1 << 16]. EPSILON is used in the reduction. The remaining constants are MDS matrix elements (except 1, which is omitted) and are used to form the dot products. + +The instruction `umlal.2d v4, v20, v30[1]` can be read as: +1. take the low 64 bits (`umlal2` for high 64 bits) of `v20` (state[0]), +2. multiply the low and high 32 bits thereof by `v30[1]` (1), +3. add the low and high product to the low and high 64-bits of `v4` respectively, +4. save to `v4`. + +We do not use `umlal` when the MDS coefficient is 1; instead, we use `uaddw` ("widening add") to reduce latency. diff --git a/gprimitives/client/src/hash/arch/mod.rs b/gprimitives/client/src/hash/arch/mod.rs new file mode 100644 index 00000000000..123b75bc35f --- /dev/null +++ b/gprimitives/client/src/hash/arch/mod.rs @@ -0,0 +1,23 @@ +// This file is part of Gear. + +// Copyright (C) 2021-2024 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#[cfg(target_arch = "x86_64")] +pub(crate) mod x86_64; + +#[cfg(target_arch = "aarch64")] +pub(crate) mod aarch64; diff --git a/gprimitives/client/src/hash/arch/x86_64/mod.rs b/gprimitives/client/src/hash/arch/x86_64/mod.rs new file mode 100644 index 00000000000..752f31d3456 --- /dev/null +++ b/gprimitives/client/src/hash/arch/x86_64/mod.rs @@ -0,0 +1,23 @@ +// This file is part of Gear. + +// Copyright (C) 2021-2024 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +// // Requires: +// // - AVX2 +// // - BMI2 (for MULX and SHRX) +// #[cfg(all(target_feature = "avx2", target_feature = "bmi2"))] +// pub(crate) mod poseidon_goldilocks_avx2_bmi2; diff --git a/gprimitives/client/src/hash/arch/x86_64/poseidon_goldilocks_avx2_bmi2.rs b/gprimitives/client/src/hash/arch/x86_64/poseidon_goldilocks_avx2_bmi2.rs new file mode 100644 index 00000000000..4a8ce887b6c --- /dev/null +++ b/gprimitives/client/src/hash/arch/x86_64/poseidon_goldilocks_avx2_bmi2.rs @@ -0,0 +1,999 @@ +// This file is part of Gear. + +// Copyright (C) 2021-2024 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use core::arch::asm; +use core::arch::x86_64::*; +use core::mem::size_of; + +use static_assertions::const_assert; + +use crate::field::goldilocks_field::GoldilocksField; +use crate::field::types::Field; +use crate::hash::poseidon::{ + Poseidon, ALL_ROUND_CONSTANTS, HALF_N_FULL_ROUNDS, N_PARTIAL_ROUNDS, N_ROUNDS, +}; +use crate::util::branch_hint; + +// WARNING: This code contains tricks that work for the current MDS matrix and round constants, but +// are not guaranteed to work if those are changed. + +// * Constant definitions * + +const WIDTH: usize = 12; + +// These transformed round constants are used where the constant layer is fused with the preceding +// MDS layer. The FUSED_ROUND_CONSTANTS for round i are the ALL_ROUND_CONSTANTS for round i + 1. +// The FUSED_ROUND_CONSTANTS for the very last round are 0, as it is not followed by a constant +// layer. On top of that, all FUSED_ROUND_CONSTANTS are shifted by 2 ** 63 to save a few XORs per +// round. +const fn make_fused_round_constants() -> [u64; WIDTH * N_ROUNDS] { + let mut res = [0x8000000000000000u64; WIDTH * N_ROUNDS]; + let mut i: usize = WIDTH; + while i < WIDTH * N_ROUNDS { + res[i - WIDTH] ^= ALL_ROUND_CONSTANTS[i]; + i += 1; + } + res +} +const FUSED_ROUND_CONSTANTS: [u64; WIDTH * N_ROUNDS] = make_fused_round_constants(); + +// This is the top row of the MDS matrix. Concretely, it's the MDS exps vector at the following +// indices: [0, 11, ..., 1]. +static TOP_ROW_EXPS: [usize; 12] = [0, 10, 16, 3, 12, 8, 1, 5, 3, 0, 1, 0]; + +// * Compile-time checks * + +/// The MDS matrix multiplication ASM is specific to the MDS matrix below. We want this file to +/// fail to compile if it has been changed. +#[allow(dead_code)] +const fn check_mds_matrix() -> bool { + // Can't == two arrays in a const_assert! (: + let mut i = 0; + let wanted_matrix_exps = [0, 0, 1, 0, 3, 5, 1, 8, 12, 3, 16, 10]; + while i < WIDTH { + if ::MDS_MATRIX_EXPS[i] != wanted_matrix_exps[i] { + return false; + } + i += 1; + } + true +} +const_assert!(check_mds_matrix()); + +/// The maximum amount by which the MDS matrix will multiply the input. +/// i.e. max(MDS(state)) <= mds_matrix_inf_norm() * max(state). +const fn mds_matrix_inf_norm() -> u64 { + let mut cumul = 0; + let mut i = 0; + while i < WIDTH { + cumul += 1 << ::MDS_MATRIX_EXPS[i]; + i += 1; + } + cumul +} + +/// Ensure that adding round constants to the low result of the MDS multiplication can never +/// overflow. +#[allow(dead_code)] +const fn check_round_const_bounds_mds() -> bool { + let max_mds_res = mds_matrix_inf_norm() * (u32::MAX as u64); + let mut i = WIDTH; // First const layer is handled specially. + while i < WIDTH * N_ROUNDS { + if ALL_ROUND_CONSTANTS[i].overflowing_add(max_mds_res).1 { + return false; + } + i += 1; + } + true +} +const_assert!(check_round_const_bounds_mds()); + +/// Ensure that the first WIDTH round constants are in canonical form for the vpcmpgtd trick. +#[allow(dead_code)] +const fn check_round_const_bounds_init() -> bool { + let max_permitted_round_const = 0xffffffff00000000; + let mut i = 0; // First const layer is handled specially. + while i < WIDTH { + if ALL_ROUND_CONSTANTS[i] > max_permitted_round_const { + return false; + } + i += 1; + } + true +} +const_assert!(check_round_const_bounds_init()); + +// Preliminary notes: +// 1. AVX does not support addition with carry but 128-bit (2-word) addition can be easily +// emulated. The method recognizes that for a + b overflowed iff (a + b) < a: +// i. res_lo = a_lo + b_lo +// ii. carry_mask = res_lo < a_lo +// iii. res_hi = a_hi + b_hi - carry_mask +// Notice that carry_mask is subtracted, not added. This is because AVX comparison instructions +// return -1 (all bits 1) for true and 0 for false. +// +// 2. AVX does not have unsigned 64-bit comparisons. Those can be emulated with signed comparisons +// by recognizing that a , $v:ident) => { + ($f::<$l>($v.0), $f::<$l>($v.1), $f::<$l>($v.2)) + }; + ($f:ident::<$l:literal>, $v1:ident, $v2:ident) => { + ( + $f::<$l>($v1.0, $v2.0), + $f::<$l>($v1.1, $v2.1), + $f::<$l>($v1.2, $v2.2), + ) + }; + ($f:ident, $v:ident) => { + ($f($v.0), $f($v.1), $f($v.2)) + }; + ($f:ident, $v0:ident, $v1:ident) => { + ($f($v0.0, $v1.0), $f($v0.1, $v1.1), $f($v0.2, $v1.2)) + }; + ($f:ident, $v0:ident, rep $v1:ident) => { + ($f($v0.0, $v1), $f($v0.1, $v1), $f($v0.2, $v1)) + }; +} + +#[inline(always)] +unsafe fn const_layer( + state: (__m256i, __m256i, __m256i), + round_const_arr: &[u64; 12], +) -> (__m256i, __m256i, __m256i) { + let sign_bit = _mm256_set1_epi64x(i64::MIN); + let round_const = ( + _mm256_loadu_si256((&round_const_arr[0..4]).as_ptr().cast::<__m256i>()), + _mm256_loadu_si256((&round_const_arr[4..8]).as_ptr().cast::<__m256i>()), + _mm256_loadu_si256((&round_const_arr[8..12]).as_ptr().cast::<__m256i>()), + ); + let state_s = map3!(_mm256_xor_si256, state, rep sign_bit); // Shift by 2**63. + let res_maybe_wrapped_s = map3!(_mm256_add_epi64, state_s, round_const); + // 32-bit compare is much faster than 64-bit compare on Intel. We can use 32-bit compare here + // as long as we can guarantee that state > res_maybe_wrapped iff state >> 32 > + // res_maybe_wrapped >> 32. Clearly, if state >> 32 > res_maybe_wrapped >> 32, then state > + // res_maybe_wrapped, and similarly for <. + // It remains to show that we can't have state >> 32 == res_maybe_wrapped >> 32 with state > + // res_maybe_wrapped. If state >> 32 == res_maybe_wrapped >> 32, then round_const >> 32 = + // 0xffffffff and the addition of the low doubleword generated a carry bit. This can never + // occur if all round constants are < 0xffffffff00000001 = ORDER: if the high bits are + // 0xffffffff, then the low bits are 0, so the carry bit cannot occur. So this trick is valid + // as long as all the round constants are in canonical form. + // The mask contains 0xffffffff in the high doubleword if wraparound occurred and 0 otherwise. + // We will ignore the low doubleword. + let wraparound_mask = map3!(_mm256_cmpgt_epi32, state_s, res_maybe_wrapped_s); + // wraparound_adjustment contains 0xffffffff = EPSILON if wraparound occurred and 0 otherwise. + let wraparound_adjustment = map3!(_mm256_srli_epi64::<32>, wraparound_mask); + // XOR commutes with the addition below. Placing it here helps mask latency. + let res_maybe_wrapped = map3!(_mm256_xor_si256, res_maybe_wrapped_s, rep sign_bit); + // Add EPSILON = subtract ORDER. + let res = map3!(_mm256_add_epi64, res_maybe_wrapped, wraparound_adjustment); + res +} + +#[inline(always)] +unsafe fn square3( + x: (__m256i, __m256i, __m256i), +) -> ((__m256i, __m256i, __m256i), (__m256i, __m256i, __m256i)) { + let x_hi = { + // Move high bits to low position. The high bits of x_hi are ignored. Swizzle is faster than + // bitshift. This instruction only has a floating-point flavor, so we cast to/from float. + // This is safe and free. + let x_ps = map3!(_mm256_castsi256_ps, x); + let x_hi_ps = map3!(_mm256_movehdup_ps, x_ps); + map3!(_mm256_castps_si256, x_hi_ps) + }; + + // All pairwise multiplications. + let mul_ll = map3!(_mm256_mul_epu32, x, x); + let mul_lh = map3!(_mm256_mul_epu32, x, x_hi); + let mul_hh = map3!(_mm256_mul_epu32, x_hi, x_hi); + + // Bignum addition, but mul_lh is shifted by 33 bits (not 32). + let mul_ll_hi = map3!(_mm256_srli_epi64::<33>, mul_ll); + let t0 = map3!(_mm256_add_epi64, mul_lh, mul_ll_hi); + let t0_hi = map3!(_mm256_srli_epi64::<31>, t0); + let res_hi = map3!(_mm256_add_epi64, mul_hh, t0_hi); + + // Form low result by adding the mul_ll and the low 31 bits of mul_lh (shifted to the high + // position). + let mul_lh_lo = map3!(_mm256_slli_epi64::<33>, mul_lh); + let res_lo = map3!(_mm256_add_epi64, mul_ll, mul_lh_lo); + + (res_lo, res_hi) +} + +#[inline(always)] +unsafe fn mul3( + x: (__m256i, __m256i, __m256i), + y: (__m256i, __m256i, __m256i), +) -> ((__m256i, __m256i, __m256i), (__m256i, __m256i, __m256i)) { + let epsilon = _mm256_set1_epi64x(0xffffffff); + let x_hi = { + // Move high bits to low position. The high bits of x_hi are ignored. Swizzle is faster than + // bitshift. This instruction only has a floating-point flavor, so we cast to/from float. + // This is safe and free. + let x_ps = map3!(_mm256_castsi256_ps, x); + let x_hi_ps = map3!(_mm256_movehdup_ps, x_ps); + map3!(_mm256_castps_si256, x_hi_ps) + }; + let y_hi = { + let y_ps = map3!(_mm256_castsi256_ps, y); + let y_hi_ps = map3!(_mm256_movehdup_ps, y_ps); + map3!(_mm256_castps_si256, y_hi_ps) + }; + + // All four pairwise multiplications + let mul_ll = map3!(_mm256_mul_epu32, x, y); + let mul_lh = map3!(_mm256_mul_epu32, x, y_hi); + let mul_hl = map3!(_mm256_mul_epu32, x_hi, y); + let mul_hh = map3!(_mm256_mul_epu32, x_hi, y_hi); + + // Bignum addition + // Extract high 32 bits of mul_ll and add to mul_hl. This cannot overflow. + let mul_ll_hi = map3!(_mm256_srli_epi64::<32>, mul_ll); + let t0 = map3!(_mm256_add_epi64, mul_hl, mul_ll_hi); + // Extract low 32 bits of t0 and add to mul_lh. Again, this cannot overflow. + // Also, extract high 32 bits of t0 and add to mul_hh. + let t0_lo = map3!(_mm256_and_si256, t0, rep epsilon); + let t0_hi = map3!(_mm256_srli_epi64::<32>, t0); + let t1 = map3!(_mm256_add_epi64, mul_lh, t0_lo); + let t2 = map3!(_mm256_add_epi64, mul_hh, t0_hi); + // Lastly, extract the high 32 bits of t1 and add to t2. + let t1_hi = map3!(_mm256_srli_epi64::<32>, t1); + let res_hi = map3!(_mm256_add_epi64, t2, t1_hi); + + // Form res_lo by combining the low half of mul_ll with the low half of t1 (shifted into high + // position). + let t1_lo = { + let t1_ps = map3!(_mm256_castsi256_ps, t1); + let t1_lo_ps = map3!(_mm256_moveldup_ps, t1_ps); + map3!(_mm256_castps_si256, t1_lo_ps) + }; + let res_lo = map3!(_mm256_blend_epi32::<0xaa>, mul_ll, t1_lo); + + (res_lo, res_hi) +} + +/// Addition, where the second operand is `0 <= y < 0xffffffff00000001`. +#[inline(always)] +unsafe fn add_small( + x_s: (__m256i, __m256i, __m256i), + y: (__m256i, __m256i, __m256i), +) -> (__m256i, __m256i, __m256i) { + let res_wrapped_s = map3!(_mm256_add_epi64, x_s, y); + let mask = map3!(_mm256_cmpgt_epi32, x_s, res_wrapped_s); + let wrapback_amt = map3!(_mm256_srli_epi64::<32>, mask); // EPSILON if overflowed else 0. + let res_s = map3!(_mm256_add_epi64, res_wrapped_s, wrapback_amt); + res_s +} + +#[inline(always)] +unsafe fn maybe_adj_sub(res_wrapped_s: __m256i, mask: __m256i) -> __m256i { + // The subtraction is very unlikely to overflow so we're best off branching. + // The even u32s in `mask` are meaningless, so we want to ignore them. `_mm256_testz_pd` + // branches depending on the sign bit of double-precision (64-bit) floats. Bit cast `mask` to + // floating-point (this is free). + let mask_pd = _mm256_castsi256_pd(mask); + // `_mm256_testz_pd(mask_pd, mask_pd) == 1` iff all sign bits are 0, meaning that underflow + // did not occur for any of the vector elements. + if _mm256_testz_pd(mask_pd, mask_pd) == 1 { + res_wrapped_s + } else { + branch_hint(); + // Highly unlikely: underflow did occur. Find adjustment per element and apply it. + let adj_amount = _mm256_srli_epi64::<32>(mask); // EPSILON if underflow. + _mm256_sub_epi64(res_wrapped_s, adj_amount) + } +} + +/// Addition, where the second operand is much smaller than `0xffffffff00000001`. +#[inline(always)] +unsafe fn sub_tiny( + x_s: (__m256i, __m256i, __m256i), + y: (__m256i, __m256i, __m256i), +) -> (__m256i, __m256i, __m256i) { + let res_wrapped_s = map3!(_mm256_sub_epi64, x_s, y); + let mask = map3!(_mm256_cmpgt_epi32, res_wrapped_s, x_s); + let res_s = map3!(maybe_adj_sub, res_wrapped_s, mask); + res_s +} + +#[inline(always)] +unsafe fn reduce3( + (lo0, hi0): ((__m256i, __m256i, __m256i), (__m256i, __m256i, __m256i)), +) -> (__m256i, __m256i, __m256i) { + let sign_bit = _mm256_set1_epi64x(i64::MIN); + let epsilon = _mm256_set1_epi64x(0xffffffff); + let lo0_s = map3!(_mm256_xor_si256, lo0, rep sign_bit); + let hi_hi0 = map3!(_mm256_srli_epi64::<32>, hi0); + let lo1_s = sub_tiny(lo0_s, hi_hi0); + let t1 = map3!(_mm256_mul_epu32, hi0, rep epsilon); + let lo2_s = add_small(lo1_s, t1); + let lo2 = map3!(_mm256_xor_si256, lo2_s, rep sign_bit); + lo2 +} + +#[inline(always)] +unsafe fn sbox_layer_full(state: (__m256i, __m256i, __m256i)) -> (__m256i, __m256i, __m256i) { + let state2_unreduced = square3(state); + let state2 = reduce3(state2_unreduced); + let state4_unreduced = square3(state2); + let state3_unreduced = mul3(state2, state); + let state4 = reduce3(state4_unreduced); + let state3 = reduce3(state3_unreduced); + let state7_unreduced = mul3(state3, state4); + let state7 = reduce3(state7_unreduced); + state7 +} + +#[inline(always)] +unsafe fn mds_layer_reduce( + lo_s: (__m256i, __m256i, __m256i), + hi: (__m256i, __m256i, __m256i), +) -> (__m256i, __m256i, __m256i) { + // This is done in assembly because, frankly, it's cleaner than intrinsics. We also don't have + // to worry about whether the compiler is doing weird things. This entire routine needs proper + // pipelining so there's no point rewriting this, only to have to rewrite it again. + let res0: __m256i; + let res1: __m256i; + let res2: __m256i; + let epsilon = _mm256_set1_epi64x(0xffffffff); + let sign_bit = _mm256_set1_epi64x(i64::MIN); + asm!( + // The high results are in ymm3, ymm4, ymm5. + // The low results (shifted by 2**63) are in ymm0, ymm1, ymm2 + + // We want to do: ymm0 := ymm0 + (ymm3 * 2**32) in modulo P. + // This can be computed by ymm0 + (ymm3 << 32) + (ymm3 >> 32) * EPSILON, + // where the additions must correct for over/underflow. + + // First, do ymm0 + (ymm3 << 32) (first chain) + "vpsllq ymm6, ymm3, 32", + "vpsllq ymm7, ymm4, 32", + "vpsllq ymm8, ymm5, 32", + "vpaddq ymm6, ymm6, ymm0", + "vpaddq ymm7, ymm7, ymm1", + "vpaddq ymm8, ymm8, ymm2", + "vpcmpgtd ymm0, ymm0, ymm6", + "vpcmpgtd ymm1, ymm1, ymm7", + "vpcmpgtd ymm2, ymm2, ymm8", + + // Now we interleave the chains so this gets a bit uglier. + // Form ymm3 := (ymm3 >> 32) * EPSILON (second chain) + "vpsrlq ymm9, ymm3, 32", + "vpsrlq ymm10, ymm4, 32", + "vpsrlq ymm11, ymm5, 32", + // (first chain again) + "vpsrlq ymm0, ymm0, 32", + "vpsrlq ymm1, ymm1, 32", + "vpsrlq ymm2, ymm2, 32", + // (second chain again) + "vpandn ymm3, ymm14, ymm3", + "vpandn ymm4, ymm14, ymm4", + "vpandn ymm5, ymm14, ymm5", + "vpsubq ymm3, ymm3, ymm9", + "vpsubq ymm4, ymm4, ymm10", + "vpsubq ymm5, ymm5, ymm11", + // (first chain again) + "vpaddq ymm0, ymm6, ymm0", + "vpaddq ymm1, ymm7, ymm1", + "vpaddq ymm2, ymm8, ymm2", + + // Merge two chains (second addition) + "vpaddq ymm3, ymm0, ymm3", + "vpaddq ymm4, ymm1, ymm4", + "vpaddq ymm5, ymm2, ymm5", + "vpcmpgtd ymm0, ymm0, ymm3", + "vpcmpgtd ymm1, ymm1, ymm4", + "vpcmpgtd ymm2, ymm2, ymm5", + "vpsrlq ymm6, ymm0, 32", + "vpsrlq ymm7, ymm1, 32", + "vpsrlq ymm8, ymm2, 32", + "vpxor ymm3, ymm15, ymm3", + "vpxor ymm4, ymm15, ymm4", + "vpxor ymm5, ymm15, ymm5", + "vpaddq ymm0, ymm6, ymm3", + "vpaddq ymm1, ymm7, ymm4", + "vpaddq ymm2, ymm8, ymm5", + inout("ymm0") lo_s.0 => res0, + inout("ymm1") lo_s.1 => res1, + inout("ymm2") lo_s.2 => res2, + inout("ymm3") hi.0 => _, + inout("ymm4") hi.1 => _, + inout("ymm5") hi.2 => _, + out("ymm6") _, out("ymm7") _, out("ymm8") _, out("ymm9") _, out("ymm10") _, out("ymm11") _, + in("ymm14") epsilon, in("ymm15") sign_bit, + options(pure, nomem, preserves_flags, nostack), + ); + (res0, res1, res2) +} + +#[inline(always)] +unsafe fn mds_multiply_and_add_round_const_s( + state: (__m256i, __m256i, __m256i), + (base, index): (*const u64, usize), +) -> ((__m256i, __m256i, __m256i), (__m256i, __m256i, __m256i)) { + // TODO: Would it be faster to save the input to memory and do unaligned + // loads instead of swizzling? It would reduce pressure on port 5 but it + // would also have high latency (no store forwarding). + // TODO: Would it be faster to store the lo and hi inputs and outputs on one + // vector? I.e., we currently operate on [lo(s[0]), lo(s[1]), lo(s[2]), + // lo(s[3])] and [hi(s[0]), hi(s[1]), hi(s[2]), hi(s[3])] separately. Using + // [lo(s[0]), lo(s[1]), hi(s[0]), hi(s[1])] and [lo(s[2]), lo(s[3]), + // hi(s[2]), hi(s[3])] would save us a few swizzles but would also need more + // registers. + // TODO: Plain-vanilla matrix-vector multiplication might also work. We take + // one element of the input (a scalar), multiply a column by it, and + // accumulate. It would require shifts by amounts loaded from memory, but + // would eliminate all swizzles. The downside is that we can no longer + // special-case MDS == 0 and MDS == 1, so we end up with more shifts. + // TODO: Building on the above: FMA? It has high latency (4 cycles) but we + // have enough operands to mask it. The main annoyance will be conversion + // to/from floating-point. + // TODO: Try taking the complex Fourier transform and doing the convolution + // with elementwise Fourier multiplication. Alternatively, try a Fourier + // transform modulo Q, such that the prime field fits the result without + // wraparound (i.e. Q > 0x1_1536_fffe_eac9) and has fast multiplication/- + // reduction. + + // At the end of the matrix-vector multiplication r = Ms, + // - ymm3 holds r[0:4] + // - ymm4 holds r[4:8] + // - ymm5 holds r[8:12] + // - ymm6 holds r[2:6] + // - ymm7 holds r[6:10] + // - ymm8 holds concat(r[10:12], r[0:2]) + // Note that there are duplicates. E.g. r[0] is represented by ymm3[0] and + // ymm8[2]. To obtain the final result, we must sum the duplicate entries: + // ymm3[0:2] += ymm8[2:4] + // ymm3[2:4] += ymm6[0:2] + // ymm4[0:2] += ymm6[2:4] + // ymm4[2:4] += ymm7[0:2] + // ymm5[0:2] += ymm7[2:4] + // ymm5[2:4] += ymm8[0:2] + // Thus, the final result resides in ymm3, ymm4, ymm5. + + // WARNING: This code assumes that sum(1 << exp for exp in MDS_EXPS) * 0xffffffff fits in a + // u64. If this guarantee ceases to hold, then it will no longer be correct. + let (unreduced_lo0_s, unreduced_lo1_s, unreduced_lo2_s): (__m256i, __m256i, __m256i); + let (unreduced_hi0, unreduced_hi1, unreduced_hi2): (__m256i, __m256i, __m256i); + let epsilon = _mm256_set1_epi64x(0xffffffff); + asm!( + // Extract low 32 bits of the word + "vpand ymm9, ymm14, ymm0", + "vpand ymm10, ymm14, ymm1", + "vpand ymm11, ymm14, ymm2", + + "mov eax, 1", + + // Fall through for MDS matrix multiplication on low 32 bits + + // This is a GCC _local label_. For details, see + // https://doc.rust-lang.org/rust-by-example/unsafe/asm.html#labels + // In short, the assembler makes sure to assign a unique name to replace `2:` with a unique + // name, so the label does not clash with any compiler-generated label. `2:` can appear + // multiple times; to disambiguate, we must refer to it as `2b` or `2f`, specifying the + // direction as _backward_ or _forward_. + "2:", + // NB: This block is run twice: once on the low 32 bits and once for the + // high 32 bits. The 32-bit -> 64-bit matrix multiplication is responsible + // for the majority of the instructions in this routine. By reusing them, + // we decrease the burden on instruction caches by over one third. + + // 32-bit -> 64-bit MDS matrix multiplication + // The scalar loop goes: + // for r in 0..WIDTH { + // let mut res = 0u128; + // for i in 0..WIDTH { + // res += (state[(i + r) % WIDTH] as u128) << MDS_MATRIX_EXPS[i]; + // } + // result[r] = reduce(res); + // } + // + // Here, we swap the loops. Equivalent to: + // let mut res = [0u128; WIDTH]; + // for i in 0..WIDTH { + // let mds_matrix_exp = MDS_MATRIX_EXPS[i]; + // for r in 0..WIDTH { + // res[r] += (state[(i + r) % WIDTH] as u128) << mds_matrix_exp; + // } + // } + // for r in 0..WIDTH { + // result[r] = reduce(res[r]); + // } + // + // Notice that in the lower version, all iterations of the inner loop + // shift by the same amount. In vector, we perform multiple iterations of + // the loop at once, and vector shifts are cheaper when all elements are + // shifted by the same amount. + // + // We use a trick to avoid rotating the state vector many times. We + // have as input the state vector and the state vector rotated by one. We + // also have two accumulators: an unrotated one and one that's rotated by + // two. Rotations by three are achieved by matching an input rotated by + // one with an accumulator rotated by two. Rotations by four are free: + // they are done by using a different register. + + // mds[0 - 0] = 0 not done; would be a move from in0 to ymm3 + // ymm3 not set + // mds[0 - 4] = 12 + "vpsllq ymm4, ymm9, 12", + // mds[0 - 8] = 3 + "vpsllq ymm5, ymm9, 3", + // mds[0 - 2] = 16 + "vpsllq ymm6, ymm9, 16", + // mds[0 - 6] = mds[0 - 10] = 1 + "vpaddq ymm7, ymm9, ymm9", + // ymm8 not written + // ymm3 and ymm8 have not been written to, because those would be unnecessary + // copies. Implicitly, ymm3 := in0 and ymm8 := ymm7. + + // ymm12 := [ymm9[1], ymm9[2], ymm9[3], ymm10[0]] + "vperm2i128 ymm13, ymm9, ymm10, 0x21", + "vshufpd ymm12, ymm9, ymm13, 0x5", + + // ymm3 and ymm8 are not read because they have not been written to + // earlier. Instead, the "current value" of ymm3 is read from ymm9 and the + // "current value" of ymm8 is read from ymm7. + // mds[4 - 0] = 3 + "vpsllq ymm13, ymm10, 3", + "vpaddq ymm3, ymm9, ymm13", + // mds[4 - 4] = 0 + "vpaddq ymm4, ymm4, ymm10", + // mds[4 - 8] = 12 + "vpsllq ymm13, ymm10, 12", + "vpaddq ymm5, ymm5, ymm13", + // mds[4 - 2] = mds[4 - 10] = 1 + "vpaddq ymm13, ymm10, ymm10", + "vpaddq ymm6, ymm6, ymm13", + "vpaddq ymm8, ymm7, ymm13", + // mds[4 - 6] = 16 + "vpsllq ymm13, ymm10, 16", + "vpaddq ymm7, ymm7, ymm13", + + // mds[1 - 0] = 0 + "vpaddq ymm3, ymm3, ymm12", + // mds[1 - 4] = 3 + "vpsllq ymm13, ymm12, 3", + "vpaddq ymm4, ymm4, ymm13", + // mds[1 - 8] = 5 + "vpsllq ymm13, ymm12, 5", + "vpaddq ymm5, ymm5, ymm13", + // mds[1 - 2] = 10 + "vpsllq ymm13, ymm12, 10", + "vpaddq ymm6, ymm6, ymm13", + // mds[1 - 6] = 8 + "vpsllq ymm13, ymm12, 8", + "vpaddq ymm7, ymm7, ymm13", + // mds[1 - 10] = 0 + "vpaddq ymm8, ymm8, ymm12", + + // ymm10 := [ymm10[1], ymm10[2], ymm10[3], ymm11[0]] + "vperm2i128 ymm13, ymm10, ymm11, 0x21", + "vshufpd ymm10, ymm10, ymm13, 0x5", + + // mds[8 - 0] = 12 + "vpsllq ymm13, ymm11, 12", + "vpaddq ymm3, ymm3, ymm13", + // mds[8 - 4] = 3 + "vpsllq ymm13, ymm11, 3", + "vpaddq ymm4, ymm4, ymm13", + // mds[8 - 8] = 0 + "vpaddq ymm5, ymm5, ymm11", + // mds[8 - 2] = mds[8 - 6] = 1 + "vpaddq ymm13, ymm11, ymm11", + "vpaddq ymm6, ymm6, ymm13", + "vpaddq ymm7, ymm7, ymm13", + // mds[8 - 10] = 16 + "vpsllq ymm13, ymm11, 16", + "vpaddq ymm8, ymm8, ymm13", + + // ymm9 := [ymm11[1], ymm11[2], ymm11[3], ymm9[0]] + "vperm2i128 ymm13, ymm11, ymm9, 0x21", + "vshufpd ymm9, ymm11, ymm13, 0x5", + + // mds[5 - 0] = 5 + "vpsllq ymm13, ymm10, 5", + "vpaddq ymm3, ymm3, ymm13", + // mds[5 - 4] = 0 + "vpaddq ymm4, ymm4, ymm10", + // mds[5 - 8] = 3 + "vpsllq ymm13, ymm10, 3", + "vpaddq ymm5, ymm5, ymm13", + // mds[5 - 2] = 0 + "vpaddq ymm6, ymm6, ymm10", + // mds[5 - 6] = 10 + "vpsllq ymm13, ymm10, 10", + "vpaddq ymm7, ymm7, ymm13", + // mds[5 - 10] = 8 + "vpsllq ymm13, ymm10, 8", + "vpaddq ymm8, ymm8, ymm13", + + // mds[9 - 0] = 3 + "vpsllq ymm13, ymm9, 3", + "vpaddq ymm3, ymm3, ymm13", + // mds[9 - 4] = 5 + "vpsllq ymm13, ymm9, 5", + "vpaddq ymm4, ymm4, ymm13", + // mds[9 - 8] = 0 + "vpaddq ymm5, ymm5, ymm9", + // mds[9 - 2] = 8 + "vpsllq ymm13, ymm9, 8", + "vpaddq ymm6, ymm6, ymm13", + // mds[9 - 6] = 0 + "vpaddq ymm7, ymm7, ymm9", + // mds[9 - 10] = 10 + "vpsllq ymm13, ymm9, 10", + "vpaddq ymm8, ymm8, ymm13", + + // Rotate ymm6-ymm8 and add to the corresponding elements of ymm3-ymm5 + "vperm2i128 ymm13, ymm8, ymm6, 0x21", + "vpaddq ymm3, ymm3, ymm13", + "vperm2i128 ymm13, ymm6, ymm7, 0x21", + "vpaddq ymm4, ymm4, ymm13", + "vperm2i128 ymm13, ymm7, ymm8, 0x21", + "vpaddq ymm5, ymm5, ymm13", + + // If this is the first time we have run 2: (low 32 bits) then continue. + // If second time (high 32 bits), then jump to 3:. + "dec eax", + // Jump to the _local label_ (see above) `3:`. `f` for _forward_ specifies the direction. + "jnz 3f", + + // Extract high 32 bits + "vpsrlq ymm9, ymm0, 32", + "vpsrlq ymm10, ymm1, 32", + "vpsrlq ymm11, ymm2, 32", + + // Need to move the low result from ymm3-ymm5 to ymm0-13 so it is not + // overwritten. Save three instructions by combining the move with the constant layer, + // which would otherwise be done in 3:. The round constants include the shift by 2**63, so + // the resulting ymm0,1,2 are also shifted by 2**63. + // It is safe to add the round constants here without checking for overflow. The values in + // ymm3,4,5 are guaranteed to be <= 0x11536fffeeac9. All round constants are < 2**64 + // - 0x11536fffeeac9. + // WARNING: If this guarantee ceases to hold due to a change in the MDS matrix or round + // constants, then this code will no longer be correct. + "vpaddq ymm0, ymm3, [{base} + {index}]", + "vpaddq ymm1, ymm4, [{base} + {index} + 32]", + "vpaddq ymm2, ymm5, [{base} + {index} + 64]", + + // MDS matrix multiplication, again. This time on high 32 bits. + // Jump to the _local label_ (see above) `2:`. `b` for _backward_ specifies the direction. + "jmp 2b", + + // `3:` is a _local label_ (see above). + "3:", + // Just done the MDS matrix multiplication on high 32 bits. + // The high results are in ymm3, ymm4, ymm5. + // The low results (shifted by 2**63 and including the following constant layer) are in + // ymm0, ymm1, ymm2. + base = in(reg) base, + index = in(reg) index, + inout("ymm0") state.0 => unreduced_lo0_s, + inout("ymm1") state.1 => unreduced_lo1_s, + inout("ymm2") state.2 => unreduced_lo2_s, + out("ymm3") unreduced_hi0, + out("ymm4") unreduced_hi1, + out("ymm5") unreduced_hi2, + out("ymm6") _,out("ymm7") _, out("ymm8") _, out("ymm9") _, + out("ymm10") _, out("ymm11") _, out("ymm12") _, out("ymm13") _, + in("ymm14") epsilon, + out("rax") _, + options(pure, nomem, nostack), + ); + ( + (unreduced_lo0_s, unreduced_lo1_s, unreduced_lo2_s), + (unreduced_hi0, unreduced_hi1, unreduced_hi2), + ) +} + +#[inline(always)] +unsafe fn mds_const_layers_full( + state: (__m256i, __m256i, __m256i), + round_constants: (*const u64, usize), +) -> (__m256i, __m256i, __m256i) { + let (unreduced_lo_s, unreduced_hi) = mds_multiply_and_add_round_const_s(state, round_constants); + mds_layer_reduce(unreduced_lo_s, unreduced_hi) +} + +/// Compute x ** 7 +#[inline(always)] +unsafe fn sbox_partial(mut x: u64) -> u64 { + // This is done in assembly to fix LLVM's poor treatment of wraparound addition/subtraction + // and to ensure that multiplication by EPSILON is done with bitshifts, leaving port 1 for + // vector operations. + // TODO: Interleave with MDS multiplication. + asm!( + "mov r9, rdx", + + // rdx := rdx ^ 2 + "mulx rdx, rax, rdx", + "shrx r8, rdx, r15", + "mov r12d, edx", + "shl rdx, 32", + "sub rdx, r12", + // rax - r8, with underflow + "sub rax, r8", + "sbb r8d, r8d", // sets r8 to 2^32 - 1 if subtraction underflowed + "sub rax, r8", + // rdx + rax, with overflow + "add rdx, rax", + "sbb eax, eax", + "add rdx, rax", + + // rax := rdx * r9, rdx := rdx ** 2 + "mulx rax, r11, r9", + "mulx rdx, r12, rdx", + + "shrx r9, rax, r15", + "shrx r10, rdx, r15", + + "sub r11, r9", + "sbb r9d, r9d", + "sub r12, r10", + "sbb r10d, r10d", + "sub r11, r9", + "sub r12, r10", + + "mov r9d, eax", + "mov r10d, edx", + "shl rax, 32", + "shl rdx, 32", + "sub rax, r9", + "sub rdx, r10", + + "add rax, r11", + "sbb r11d, r11d", + "add rdx, r12", + "sbb r12d, r12d", + "add rax, r11", + "add rdx, r12", + + // rax := rax * rdx + "mulx rax, rdx, rax", + "shrx r11, rax, r15", + "mov r12d, eax", + "shl rax, 32", + "sub rax, r12", + // rdx - r11, with underflow + "sub rdx, r11", + "sbb r11d, r11d", // sets r11 to 2^32 - 1 if subtraction underflowed + "sub rdx, r11", + // rdx + rax, with overflow + "add rdx, rax", + "sbb eax, eax", + "add rdx, rax", + inout("rdx") x, + out("rax") _, + out("r8") _, + out("r9") _, + out("r10") _, + out("r11") _, + out("r12") _, + in("r15") 32, + options(pure, nomem, nostack), + ); + x +} + +#[inline(always)] +unsafe fn partial_round( + (state0, state1, state2): (__m256i, __m256i, __m256i), + round_constants: (*const u64, usize), +) -> (__m256i, __m256i, __m256i) { + // Extract the low quadword + let state0ab: __m128i = _mm256_castsi256_si128(state0); + let mut state0a = _mm_cvtsi128_si64(state0ab) as u64; + + // Zero the low quadword + let zero = _mm256_setzero_si256(); + let state0bcd = _mm256_blend_epi32::<0x3>(state0, zero); + + // Scalar exponentiation + state0a = sbox_partial(state0a); + + let epsilon = _mm256_set1_epi64x(0xffffffff); + let ( + (mut unreduced_lo0_s, mut unreduced_lo1_s, mut unreduced_lo2_s), + (mut unreduced_hi0, mut unreduced_hi1, mut unreduced_hi2), + ) = mds_multiply_and_add_round_const_s((state0bcd, state1, state2), round_constants); + asm!( + // Just done the MDS matrix multiplication on high 32 bits. + // The high results are in ymm3, ymm4, ymm5. + // The low results (shifted by 2**63) are in ymm0, ymm1, ymm2 + + // The MDS matrix multiplication was done with state[0] set to 0. + // We must: + // 1. propagate the vector product to state[0], which is stored in rdx. + // 2. offset state[1..12] by the appropriate multiple of rdx + // 3. zero the lowest quadword in the vector registers + "vmovq xmm12, {state0a}", + "vpbroadcastq ymm12, xmm12", + "vpsrlq ymm13, ymm12, 32", + "vpand ymm12, ymm14, ymm12", + + // The current matrix-vector product goes not include state[0] as an input. (Imagine Mv + // multiplication where we've set the first element to 0.) Add the remaining bits now. + // TODO: This is a bit of an afterthought, which is why these constants are loaded 22 + // times... There's likely a better way of merging those results. + "vmovdqu ymm6, [{mds_matrix}]", + "vmovdqu ymm7, [{mds_matrix} + 32]", + "vmovdqu ymm8, [{mds_matrix} + 64]", + "vpsllvq ymm9, ymm13, ymm6", + "vpsllvq ymm10, ymm13, ymm7", + "vpsllvq ymm11, ymm13, ymm8", + "vpsllvq ymm6, ymm12, ymm6", + "vpsllvq ymm7, ymm12, ymm7", + "vpsllvq ymm8, ymm12, ymm8", + "vpaddq ymm3, ymm9, ymm3", + "vpaddq ymm4, ymm10, ymm4", + "vpaddq ymm5, ymm11, ymm5", + "vpaddq ymm0, ymm6, ymm0", + "vpaddq ymm1, ymm7, ymm1", + "vpaddq ymm2, ymm8, ymm2", + // Reduction required. + + state0a = in(reg) state0a, + mds_matrix = in(reg) &TOP_ROW_EXPS, + inout("ymm0") unreduced_lo0_s, + inout("ymm1") unreduced_lo1_s, + inout("ymm2") unreduced_lo2_s, + inout("ymm3") unreduced_hi0, + inout("ymm4") unreduced_hi1, + inout("ymm5") unreduced_hi2, + out("ymm6") _,out("ymm7") _, out("ymm8") _, out("ymm9") _, + out("ymm10") _, out("ymm11") _, out("ymm12") _, out("ymm13") _, + in("ymm14") epsilon, + options(pure, nomem, preserves_flags, nostack), + ); + mds_layer_reduce( + (unreduced_lo0_s, unreduced_lo1_s, unreduced_lo2_s), + (unreduced_hi0, unreduced_hi1, unreduced_hi2), + ) +} + +#[inline(always)] +unsafe fn full_round( + state: (__m256i, __m256i, __m256i), + round_constants: (*const u64, usize), +) -> (__m256i, __m256i, __m256i) { + let state = sbox_layer_full(state); + let state = mds_const_layers_full(state, round_constants); + state +} + +#[inline] // Called twice; permit inlining but don't _require_ it +unsafe fn half_full_rounds( + mut state: (__m256i, __m256i, __m256i), + start_round: usize, +) -> (__m256i, __m256i, __m256i) { + let base = (&FUSED_ROUND_CONSTANTS + [WIDTH * start_round..WIDTH * start_round + WIDTH * HALF_N_FULL_ROUNDS]) + .as_ptr(); + + for i in 0..HALF_N_FULL_ROUNDS { + state = full_round(state, (base, i * WIDTH * size_of::())); + } + state +} + +#[inline(always)] +unsafe fn all_partial_rounds( + mut state: (__m256i, __m256i, __m256i), + start_round: usize, +) -> (__m256i, __m256i, __m256i) { + let base = (&FUSED_ROUND_CONSTANTS + [WIDTH * start_round..WIDTH * start_round + WIDTH * N_PARTIAL_ROUNDS]) + .as_ptr(); + + for i in 0..N_PARTIAL_ROUNDS { + state = partial_round(state, (base, i * WIDTH * size_of::())); + } + state +} + +#[inline(always)] +unsafe fn load_state(state: &[GoldilocksField; 12]) -> (__m256i, __m256i, __m256i) { + ( + _mm256_loadu_si256((&state[0..4]).as_ptr().cast::<__m256i>()), + _mm256_loadu_si256((&state[4..8]).as_ptr().cast::<__m256i>()), + _mm256_loadu_si256((&state[8..12]).as_ptr().cast::<__m256i>()), + ) +} + +#[inline(always)] +unsafe fn store_state(buf: &mut [GoldilocksField; 12], state: (__m256i, __m256i, __m256i)) { + _mm256_storeu_si256((&mut buf[0..4]).as_mut_ptr().cast::<__m256i>(), state.0); + _mm256_storeu_si256((&mut buf[4..8]).as_mut_ptr().cast::<__m256i>(), state.1); + _mm256_storeu_si256((&mut buf[8..12]).as_mut_ptr().cast::<__m256i>(), state.2); +} + +#[inline] +pub unsafe fn poseidon(state: &[GoldilocksField; 12]) -> [GoldilocksField; 12] { + let state = load_state(state); + + // The first constant layer must be done explicitly. The remaining constant layers are fused + // with the preceding MDS layer. + let state = const_layer(state, &ALL_ROUND_CONSTANTS[0..WIDTH].try_into().unwrap()); + + let state = half_full_rounds(state, 0); + let state = all_partial_rounds(state, HALF_N_FULL_ROUNDS); + let state = half_full_rounds(state, HALF_N_FULL_ROUNDS + N_PARTIAL_ROUNDS); + + let mut res = [GoldilocksField::ZERO; 12]; + store_state(&mut res, state); + res +} + +#[inline(always)] +pub unsafe fn constant_layer(state_arr: &mut [GoldilocksField; WIDTH], round_ctr: usize) { + let state = load_state(state_arr); + let round_consts = &ALL_ROUND_CONSTANTS[WIDTH * round_ctr..][..WIDTH] + .try_into() + .unwrap(); + let state = const_layer(state, round_consts); + store_state(state_arr, state); +} + +#[inline(always)] +pub unsafe fn sbox_layer(state_arr: &mut [GoldilocksField; WIDTH]) { + let state = load_state(state_arr); + let state = sbox_layer_full(state); + store_state(state_arr, state); +} + +#[inline(always)] +pub unsafe fn mds_layer(state: &[GoldilocksField; WIDTH]) -> [GoldilocksField; WIDTH] { + let state = load_state(state); + // We want to do an MDS layer without the constant layer. + // The FUSED_ROUND_CONSTANTS for the last round are all 0 (shifted by 2**63 as required). + let round_consts = FUSED_ROUND_CONSTANTS[WIDTH * (N_ROUNDS - 1)..].as_ptr(); + let state = mds_const_layers_full(state, (round_consts, 0)); + let mut res = [GoldilocksField::ZERO; 12]; + store_state(&mut res, state); + res +} diff --git a/gprimitives/client/src/hash/hash_types.rs b/gprimitives/client/src/hash/hash_types.rs new file mode 100644 index 00000000000..b8968806295 --- /dev/null +++ b/gprimitives/client/src/hash/hash_types.rs @@ -0,0 +1,12 @@ +#[cfg(not(feature = "std"))] +use alloc::vec::Vec; + +use crate::{ + field::{goldilocks_field::GoldilocksField, types::PrimeField64}, + hash::poseidon::Poseidon, +}; + +/// A prime order field with the features we need to use it as a base field in our argument system. +pub trait RichField: PrimeField64 + Poseidon {} + +impl RichField for GoldilocksField {} diff --git a/gprimitives/client/src/hash/mod.rs b/gprimitives/client/src/hash/mod.rs new file mode 100644 index 00000000000..0ea468ea44f --- /dev/null +++ b/gprimitives/client/src/hash/mod.rs @@ -0,0 +1,25 @@ +// This file is part of Gear. + +// Copyright (C) 2021-2024 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! plonky2 hashing logic for Merkle proof verification, +//! as well as specific hash functions implementation. + +mod arch; +pub mod hash_types; +pub mod poseidon; +pub mod poseidon_goldilocks; diff --git a/gprimitives/client/src/hash/poseidon.rs b/gprimitives/client/src/hash/poseidon.rs new file mode 100644 index 00000000000..20a7193cd20 --- /dev/null +++ b/gprimitives/client/src/hash/poseidon.rs @@ -0,0 +1,690 @@ +// This file is part of Gear. + +// Copyright (C) 2021-2024 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Implementation of the Poseidon hash function for a Goldilocks field implementation, +//! as described in +//! Derived from the implementation in the [`plonky2`](https://crates.io/crates/plonky2) crate. + +#![allow(clippy::needless_range_loop)] + +#[cfg(not(feature = "std"))] +use alloc::{vec, vec::Vec}; + +use unroll::unroll_for_loops; + +use crate::{ + field::{ + extension::{Extendable, FieldExtension}, + packed::PackedField, + types::PrimeField64, + }, + hash::hash_types::RichField, +}; + +pub const SPONGE_RATE: usize = 8; +pub const SPONGE_CAPACITY: usize = 4; +pub const SPONGE_WIDTH: usize = SPONGE_RATE + SPONGE_CAPACITY; + +// The number of full rounds and partial rounds is given by the +// calc_round_numbers.py script. They happen to be the same for both +// width 8 and width 12 with s-box x^7. +// +// NB: Changing any of these values will require regenerating all of +// the precomputed constant arrays in this file. +pub const HALF_N_FULL_ROUNDS: usize = 4; +pub(crate) const N_FULL_ROUNDS_TOTAL: usize = 2 * HALF_N_FULL_ROUNDS; +pub const N_PARTIAL_ROUNDS: usize = 22; +pub const N_ROUNDS: usize = N_FULL_ROUNDS_TOTAL + N_PARTIAL_ROUNDS; +const MAX_WIDTH: usize = 12; // we only have width 8 and 12, and 12 is bigger. :) + +#[inline(always)] +const fn add_u160_u128((x_lo, x_hi): (u128, u32), y: u128) -> (u128, u32) { + let (res_lo, over) = x_lo.overflowing_add(y); + let res_hi = x_hi + (over as u32); + (res_lo, res_hi) +} + +#[inline(always)] +fn reduce_u160((n_lo, n_hi): (u128, u32)) -> F { + let n_lo_hi = (n_lo >> 64) as u64; + let n_lo_lo = n_lo as u64; + let reduced_hi: u64 = F::from_noncanonical_u96((n_lo_hi, n_hi)).to_noncanonical_u64(); + let reduced128: u128 = ((reduced_hi as u128) << 64) + (n_lo_lo as u128); + F::from_noncanonical_u128(reduced128) +} + +/// Note that these work for the Goldilocks field, but not necessarily others. See +/// `generate_constants` about how these were generated. We include enough for a width of 12; +/// smaller widths just use a subset. +#[rustfmt::skip] +pub const ALL_ROUND_CONSTANTS: [u64; MAX_WIDTH * N_ROUNDS] = [ + // WARNING: The AVX2 Goldilocks specialization relies on all round constants being in + // 0..0xfffeeac900011537. If these constants are randomly regenerated, there is a ~.6% chance + // that this condition will no longer hold. + // + // WARNING: If these are changed in any way, then all the + // implementations of Poseidon must be regenerated. See comments + // in `poseidon_goldilocks.rs`. + 0xb585f766f2144405, 0x7746a55f43921ad7, 0xb2fb0d31cee799b4, 0x0f6760a4803427d7, + 0xe10d666650f4e012, 0x8cae14cb07d09bf1, 0xd438539c95f63e9f, 0xef781c7ce35b4c3d, + 0xcdc4a239b0c44426, 0x277fa208bf337bff, 0xe17653a29da578a1, 0xc54302f225db2c76, + 0x86287821f722c881, 0x59cd1a8a41c18e55, 0xc3b919ad495dc574, 0xa484c4c5ef6a0781, + 0x308bbd23dc5416cc, 0x6e4a40c18f30c09c, 0x9a2eedb70d8f8cfa, 0xe360c6e0ae486f38, + 0xd5c7718fbfc647fb, 0xc35eae071903ff0b, 0x849c2656969c4be7, 0xc0572c8c08cbbbad, + 0xe9fa634a21de0082, 0xf56f6d48959a600d, 0xf7d713e806391165, 0x8297132b32825daf, + 0xad6805e0e30b2c8a, 0xac51d9f5fcf8535e, 0x502ad7dc18c2ad87, 0x57a1550c110b3041, + 0x66bbd30e6ce0e583, 0x0da2abef589d644e, 0xf061274fdb150d61, 0x28b8ec3ae9c29633, + 0x92a756e67e2b9413, 0x70e741ebfee96586, 0x019d5ee2af82ec1c, 0x6f6f2ed772466352, + 0x7cf416cfe7e14ca1, 0x61df517b86a46439, 0x85dc499b11d77b75, 0x4b959b48b9c10733, + 0xe8be3e5da8043e57, 0xf5c0bc1de6da8699, 0x40b12cbf09ef74bf, 0xa637093ecb2ad631, + 0x3cc3f892184df408, 0x2e479dc157bf31bb, 0x6f49de07a6234346, 0x213ce7bede378d7b, + 0x5b0431345d4dea83, 0xa2de45780344d6a1, 0x7103aaf94a7bf308, 0x5326fc0d97279301, + 0xa9ceb74fec024747, 0x27f8ec88bb21b1a3, 0xfceb4fda1ded0893, 0xfac6ff1346a41675, + 0x7131aa45268d7d8c, 0x9351036095630f9f, 0xad535b24afc26bfb, 0x4627f5c6993e44be, + 0x645cf794b8f1cc58, 0x241c70ed0af61617, 0xacb8e076647905f1, 0x3737e9db4c4f474d, + 0xe7ea5e33e75fffb6, 0x90dee49fc9bfc23a, 0xd1b1edf76bc09c92, 0x0b65481ba645c602, + 0x99ad1aab0814283b, 0x438a7c91d416ca4d, 0xb60de3bcc5ea751c, 0xc99cab6aef6f58bc, + 0x69a5ed92a72ee4ff, 0x5e7b329c1ed4ad71, 0x5fc0ac0800144885, 0x32db829239774eca, + 0x0ade699c5830f310, 0x7cc5583b10415f21, 0x85df9ed2e166d64f, 0x6604df4fee32bcb1, + 0xeb84f608da56ef48, 0xda608834c40e603d, 0x8f97fe408061f183, 0xa93f485c96f37b89, + 0x6704e8ee8f18d563, 0xcee3e9ac1e072119, 0x510d0e65e2b470c1, 0xf6323f486b9038f0, + 0x0b508cdeffa5ceef, 0xf2417089e4fb3cbd, 0x60e75c2890d15730, 0xa6217d8bf660f29c, + 0x7159cd30c3ac118e, 0x839b4e8fafead540, 0x0d3f3e5e82920adc, 0x8f7d83bddee7bba8, + 0x780f2243ea071d06, 0xeb915845f3de1634, 0xd19e120d26b6f386, 0x016ee53a7e5fecc6, + 0xcb5fd54e7933e477, 0xacb8417879fd449f, 0x9c22190be7f74732, 0x5d693c1ba3ba3621, + 0xdcef0797c2b69ec7, 0x3d639263da827b13, 0xe273fd971bc8d0e7, 0x418f02702d227ed5, + 0x8c25fda3b503038c, 0x2cbaed4daec8c07c, 0x5f58e6afcdd6ddc2, 0x284650ac5e1b0eba, + 0x635b337ee819dab5, 0x9f9a036ed4f2d49f, 0xb93e260cae5c170e, 0xb0a7eae879ddb76d, + 0xd0762cbc8ca6570c, 0x34c6efb812b04bf5, 0x40bf0ab5fa14c112, 0xb6b570fc7c5740d3, + 0x5a27b9002de33454, 0xb1a5b165b6d2b2d2, 0x8722e0ace9d1be22, 0x788ee3b37e5680fb, + 0x14a726661551e284, 0x98b7672f9ef3b419, 0xbb93ae776bb30e3a, 0x28fd3b046380f850, + 0x30a4680593258387, 0x337dc00c61bd9ce1, 0xd5eca244c7a4ff1d, 0x7762638264d279bd, + 0xc1e434bedeefd767, 0x0299351a53b8ec22, 0xb2d456e4ad251b80, 0x3e9ed1fda49cea0b, + 0x2972a92ba450bed8, 0x20216dd77be493de, 0xadffe8cf28449ec6, 0x1c4dbb1c4c27d243, + 0x15a16a8a8322d458, 0x388a128b7fd9a609, 0x2300e5d6baedf0fb, 0x2f63aa8647e15104, + 0xf1c36ce86ecec269, 0x27181125183970c9, 0xe584029370dca96d, 0x4d9bbc3e02f1cfb2, + 0xea35bc29692af6f8, 0x18e21b4beabb4137, 0x1e3b9fc625b554f4, 0x25d64362697828fd, + 0x5a3f1bb1c53a9645, 0xdb7f023869fb8d38, 0xb462065911d4e1fc, 0x49c24ae4437d8030, + 0xd793862c112b0566, 0xaadd1106730d8feb, 0xc43b6e0e97b0d568, 0xe29024c18ee6fca2, + 0x5e50c27535b88c66, 0x10383f20a4ff9a87, 0x38e8ee9d71a45af8, 0xdd5118375bf1a9b9, + 0x775005982d74d7f7, 0x86ab99b4dde6c8b0, 0xb1204f603f51c080, 0xef61ac8470250ecf, + 0x1bbcd90f132c603f, 0x0cd1dabd964db557, 0x11a3ae5beb9d1ec9, 0xf755bfeea585d11d, + 0xa3b83250268ea4d7, 0x516306f4927c93af, 0xddb4ac49c9efa1da, 0x64bb6dec369d4418, + 0xf9cc95c22b4c1fcc, 0x08d37f755f4ae9f6, 0xeec49b613478675b, 0xf143933aed25e0b0, + 0xe4c5dd8255dfc622, 0xe7ad7756f193198e, 0x92c2318b87fff9cb, 0x739c25f8fd73596d, + 0x5636cac9f16dfed0, 0xdd8f909a938e0172, 0xc6401fe115063f5b, 0x8ad97b33f1ac1455, + 0x0c49366bb25e8513, 0x0784d3d2f1698309, 0x530fb67ea1809a81, 0x410492299bb01f49, + 0x139542347424b9ac, 0x9cb0bd5ea1a1115e, 0x02e3f615c38f49a1, 0x985d4f4a9c5291ef, + 0x775b9feafdcd26e7, 0x304265a6384f0f2d, 0x593664c39773012c, 0x4f0a2e5fb028f2ce, + 0xdd611f1000c17442, 0xd8185f9adfea4fd0, 0xef87139ca9a3ab1e, 0x3ba71336c34ee133, + 0x7d3a455d56b70238, 0x660d32e130182684, 0x297a863f48cd1f43, 0x90e0a736a751ebb7, + 0x549f80ce550c4fd3, 0x0f73b2922f38bd64, 0x16bf1f73fb7a9c3f, 0x6d1f5a59005bec17, + 0x02ff876fa5ef97c4, 0xc5cb72a2a51159b0, 0x8470f39d2d5c900e, 0x25abb3f1d39fcb76, + 0x23eb8cc9b372442f, 0xd687ba55c64f6364, 0xda8d9e90fd8ff158, 0xe3cbdc7d2fe45ea7, + 0xb9a8c9b3aee52297, 0xc0d28a5c10960bd3, 0x45d7ac9b68f71a34, 0xeeb76e397069e804, + 0x3d06c8bd1514e2d9, 0x9c9c98207cb10767, 0x65700b51aedfb5ef, 0x911f451539869408, + 0x7ae6849fbc3a0ec6, 0x3bb340eba06afe7e, 0xb46e9d8b682ea65e, 0x8dcf22f9a3b34356, + 0x77bdaeda586257a7, 0xf19e400a5104d20d, 0xc368a348e46d950f, 0x9ef1cd60e679f284, + 0xe89cd854d5d01d33, 0x5cd377dc8bb882a2, 0xa7b0fb7883eee860, 0x7684403ec392950d, + 0x5fa3f06f4fed3b52, 0x8df57ac11bc04831, 0x2db01efa1e1e1897, 0x54846de4aadb9ca2, + 0xba6745385893c784, 0x541d496344d2c75b, 0xe909678474e687fe, 0xdfe89923f6c9c2ff, + 0xece5a71e0cfedc75, 0x5ff98fd5d51fe610, 0x83e8941918964615, 0x5922040b47f150c1, + 0xf97d750e3dd94521, 0x5080d4c2b86f56d7, 0xa7de115b56c78d70, 0x6a9242ac87538194, + 0xf7856ef7f9173e44, 0x2265fc92feb0dc09, 0x17dfc8e4f7ba8a57, 0x9001a64209f21db8, + 0x90004c1371b893c5, 0xb932b7cf752e5545, 0xa0b1df81b6fe59fc, 0x8ef1dd26770af2c2, + 0x0541a4f9cfbeed35, 0x9e61106178bfc530, 0xb3767e80935d8af2, 0x0098d5782065af06, + 0x31d191cd5c1466c7, 0x410fefafa319ac9d, 0xbdf8f242e316c4ab, 0x9e8cd55b57637ed0, + 0xde122bebe9a39368, 0x4d001fd58f002526, 0xca6637000eb4a9f8, 0x2f2339d624f91f78, + 0x6d1a7918c80df518, 0xdf9a4939342308e9, 0xebc2151ee6c8398c, 0x03cc2ba8a1116515, + 0xd341d037e840cf83, 0x387cb5d25af4afcc, 0xbba2515f22909e87, 0x7248fe7705f38e47, + 0x4d61e56a525d225a, 0x262e963c8da05d3d, 0x59e89b094d220ec2, 0x055d5b52b78b9c5e, + 0x82b27eb33514ef99, 0xd30094ca96b7ce7b, 0xcf5cb381cd0a1535, 0xfeed4db6919e5a7c, + 0x41703f53753be59f, 0x5eeea940fcde8b6f, 0x4cd1f1b175100206, 0x4a20358574454ec0, + 0x1478d361dbbf9fac, 0x6f02dc07d141875c, 0x296a202ed8e556a2, 0x2afd67999bf32ee5, + 0x7acfd96efa95491d, 0x6798ba0c0abb2c6d, 0x34c6f57b26c92122, 0x5736e1bad206b5de, + 0x20057d2a0056521b, 0x3dea5bd5d0578bd7, 0x16e50d897d4634ac, 0x29bff3ecb9b7a6e3, + 0x475cd3205a3bdcde, 0x18a42105c31b7e88, 0x023e7414af663068, 0x15147108121967d7, + 0xe4a3dff1d7d6fef9, 0x01a8d1a588085737, 0x11b4c74eda62beef, 0xe587cc0d69a73346, + 0x1ff7327017aa2a6e, 0x594e29c42473d06b, 0xf6f31db1899b12d5, 0xc02ac5e47312d3ca, + 0xe70201e960cb78b8, 0x6f90ff3b6a65f108, 0x42747a7245e7fa84, 0xd1f507e43ab749b2, + 0x1c86d265f15750cd, 0x3996ce73dd832c1c, 0x8e7fba02983224bd, 0xba0dec7103255dd4, + 0x9e9cbd781628fc5b, 0xdae8645996edd6a5, 0xdebe0853b1a1d378, 0xa49229d24d014343, + 0x7be5b9ffda905e1c, 0xa3c95eaec244aa30, 0x0230bca8f4df0544, 0x4135c2bebfe148c6, + 0x166fc0cc438a3c72, 0x3762b59a8ae83efa, 0xe8928a4c89114750, 0x2a440b51a4945ee5, + 0x80cefd2b7d99ff83, 0xbb9879c6e61fd62a, 0x6e7c8f1a84265034, 0x164bb2de1bbeddc8, + 0xf3c12fe54d5c653b, 0x40b9e922ed9771e2, 0x551f5b0fbe7b1840, 0x25032aa7c4cb1811, + 0xaaed34074b164346, 0x8ffd96bbf9c9c81d, 0x70fc91eb5937085c, 0x7f795e2a5f915440, + 0x4543d9df5476d3cb, 0xf172d73e004fc90d, 0xdfd1c4febcc81238, 0xbc8dfb627fe558fc, +]; + +pub trait Poseidon: PrimeField64 { + // Total number of round constants required: width of the input + // times number of rounds. + const N_ROUND_CONSTANTS: usize = SPONGE_WIDTH * N_ROUNDS; + + // The MDS matrix we use is C + D, where C is the circulant matrix whose first + // row is given by `MDS_MATRIX_CIRC`, and D is the diagonal matrix whose + // diagonal is given by `MDS_MATRIX_DIAG`. + const MDS_MATRIX_CIRC: [u64; SPONGE_WIDTH]; + const MDS_MATRIX_DIAG: [u64; SPONGE_WIDTH]; + + // Precomputed constants for the fast Poseidon calculation. See + // the paper. + const FAST_PARTIAL_FIRST_ROUND_CONSTANT: [u64; SPONGE_WIDTH]; + const FAST_PARTIAL_ROUND_CONSTANTS: [u64; N_PARTIAL_ROUNDS]; + const FAST_PARTIAL_ROUND_VS: [[u64; SPONGE_WIDTH - 1]; N_PARTIAL_ROUNDS]; + const FAST_PARTIAL_ROUND_W_HATS: [[u64; SPONGE_WIDTH - 1]; N_PARTIAL_ROUNDS]; + const FAST_PARTIAL_ROUND_INITIAL_MATRIX: [[u64; SPONGE_WIDTH - 1]; SPONGE_WIDTH - 1]; + + #[inline(always)] + #[unroll_for_loops] + fn mds_row_shf(r: usize, v: &[u64; SPONGE_WIDTH]) -> u128 { + debug_assert!(r < SPONGE_WIDTH); + // The values of `MDS_MATRIX_CIRC` and `MDS_MATRIX_DIAG` are + // known to be small, so we can accumulate all the products for + // each row and reduce just once at the end (done by the + // caller). + + // NB: Unrolling this, calculating each term independently, and + // summing at the end, didn't improve performance for me. + let mut res = 0u128; + + // This is a hacky way of fully unrolling the loop. + for i in 0..12 { + if i < SPONGE_WIDTH { + res += (v[(i + r) % SPONGE_WIDTH] as u128) * (Self::MDS_MATRIX_CIRC[i] as u128); + } + } + res += (v[r] as u128) * (Self::MDS_MATRIX_DIAG[r] as u128); + + res + } + + /// Same as `mds_row_shf` for field extensions of `Self`. + fn mds_row_shf_field, const D: usize>( + r: usize, + v: &[F; SPONGE_WIDTH], + ) -> F { + debug_assert!(r < SPONGE_WIDTH); + let mut res = F::ZERO; + + for i in 0..SPONGE_WIDTH { + res += v[(i + r) % SPONGE_WIDTH] * F::from_canonical_u64(Self::MDS_MATRIX_CIRC[i]); + } + res += v[r] * F::from_canonical_u64(Self::MDS_MATRIX_DIAG[r]); + + res + } + + /// Same as `mds_row_shf` for `PackedField`. + fn mds_row_shf_packed_field< + F: RichField + Extendable, + const D: usize, + FE, + P, + const D2: usize, + >( + r: usize, + v: &[P; SPONGE_WIDTH], + ) -> P + where + FE: FieldExtension, + P: PackedField, + { + debug_assert!(r < SPONGE_WIDTH); + let mut res = P::ZEROS; + + for i in 0..SPONGE_WIDTH { + res += + v[(i + r) % SPONGE_WIDTH] * P::Scalar::from_canonical_u64(Self::MDS_MATRIX_CIRC[i]); + } + res += v[r] * P::Scalar::from_canonical_u64(Self::MDS_MATRIX_DIAG[r]); + + res + } + + #[inline(always)] + #[unroll_for_loops] + fn mds_layer(state_: &[Self; SPONGE_WIDTH]) -> [Self; SPONGE_WIDTH] { + let mut result = [Self::ZERO; SPONGE_WIDTH]; + + let mut state = [0u64; SPONGE_WIDTH]; + for r in 0..SPONGE_WIDTH { + state[r] = state_[r].to_noncanonical_u64(); + } + + // This is a hacky way of fully unrolling the loop. + for r in 0..12 { + if r < SPONGE_WIDTH { + let sum = Self::mds_row_shf(r, &state); + let sum_lo = sum as u64; + let sum_hi = (sum >> 64) as u32; + result[r] = Self::from_noncanonical_u96((sum_lo, sum_hi)); + } + } + + result + } + + /// Same as `mds_layer` for field extensions of `Self`. + fn mds_layer_field, const D: usize>( + state: &[F; SPONGE_WIDTH], + ) -> [F; SPONGE_WIDTH] { + let mut result = [F::ZERO; SPONGE_WIDTH]; + + for r in 0..SPONGE_WIDTH { + result[r] = Self::mds_row_shf_field(r, state); + } + + result + } + + /// Same as `mds_layer` for `PackedField`. + fn mds_layer_packed_field< + F: RichField + Extendable, + const D: usize, + FE, + P, + const D2: usize, + >( + state: &[P; SPONGE_WIDTH], + ) -> [P; SPONGE_WIDTH] + where + FE: FieldExtension, + P: PackedField, + { + let mut result = [P::ZEROS; SPONGE_WIDTH]; + + for r in 0..SPONGE_WIDTH { + result[r] = Self::mds_row_shf_packed_field(r, state); + } + + result + } + + #[inline(always)] + #[unroll_for_loops] + fn partial_first_constant_layer, const D: usize>( + state: &mut [F; SPONGE_WIDTH], + ) { + for i in 0..12 { + if i < SPONGE_WIDTH { + state[i] += F::from_canonical_u64(Self::FAST_PARTIAL_FIRST_ROUND_CONSTANT[i]); + } + } + } + + /// Same as `partial_first_constant_layer` for `PackedField`. + #[inline(always)] + #[unroll_for_loops] + fn partial_first_constant_layer_packed_field< + F: RichField + Extendable, + const D: usize, + FE, + P, + const D2: usize, + >( + state: &mut [P; SPONGE_WIDTH], + ) where + FE: FieldExtension, + P: PackedField, + { + for i in 0..12 { + if i < SPONGE_WIDTH { + state[i] += + P::Scalar::from_canonical_u64(Self::FAST_PARTIAL_FIRST_ROUND_CONSTANT[i]); + } + } + } + + #[inline(always)] + #[unroll_for_loops] + fn mds_partial_layer_init, const D: usize>( + state: &[F; SPONGE_WIDTH], + ) -> [F; SPONGE_WIDTH] { + let mut result = [F::ZERO; SPONGE_WIDTH]; + + // Initial matrix has first row/column = [1, 0, ..., 0]; + + // c = 0 + result[0] = state[0]; + + for r in 1..12 { + if r < SPONGE_WIDTH { + for c in 1..12 { + if c < SPONGE_WIDTH { + // NB: FAST_PARTIAL_ROUND_INITIAL_MATRIX is stored in + // row-major order so that this dot product is cache + // friendly. + let t = F::from_canonical_u64( + Self::FAST_PARTIAL_ROUND_INITIAL_MATRIX[r - 1][c - 1], + ); + result[c] += state[r] * t; + } + } + } + } + result + } + + /// Same as `mds_partial_layer_init` for `PackedField`. + #[inline(always)] + #[unroll_for_loops] + fn mds_partial_layer_init_packed_field< + F: RichField + Extendable, + const D: usize, + FE, + P, + const D2: usize, + >( + state: &[P; SPONGE_WIDTH], + ) -> [P; SPONGE_WIDTH] + where + FE: FieldExtension, + P: PackedField, + { + let mut result = [P::ZEROS; SPONGE_WIDTH]; + + // Initial matrix has first row/column = [1, 0, ..., 0]; + + // c = 0 + result[0] = state[0]; + + for r in 1..12 { + if r < SPONGE_WIDTH { + for c in 1..12 { + if c < SPONGE_WIDTH { + // NB: FAST_PARTIAL_ROUND_INITIAL_MATRIX is stored in + // row-major order so that this dot product is cache + // friendly. + let t = P::Scalar::from_canonical_u64( + Self::FAST_PARTIAL_ROUND_INITIAL_MATRIX[r - 1][c - 1], + ); + result[c] += state[r] * t; + } + } + } + } + result + } + + /// Computes s*A where s is the state row vector and A is the matrix + /// + /// [ M_00 | v ] + /// [ ------+--- ] + /// [ w_hat | Id ] + /// + /// M_00 is a scalar, v is 1x(t-1), w_hat is (t-1)x1 and Id is the + /// (t-1)x(t-1) identity matrix. + #[inline(always)] + #[unroll_for_loops] + fn mds_partial_layer_fast(state: &[Self; SPONGE_WIDTH], r: usize) -> [Self; SPONGE_WIDTH] { + // Set d = [M_00 | w^] dot [state] + + let mut d_sum = (0u128, 0u32); // u160 accumulator + for i in 1..12 { + if i < SPONGE_WIDTH { + let t = Self::FAST_PARTIAL_ROUND_W_HATS[r][i - 1] as u128; + let si = state[i].to_noncanonical_u64() as u128; + d_sum = add_u160_u128(d_sum, si * t); + } + } + let s0 = state[0].to_noncanonical_u64() as u128; + let mds0to0 = (Self::MDS_MATRIX_CIRC[0] + Self::MDS_MATRIX_DIAG[0]) as u128; + d_sum = add_u160_u128(d_sum, s0 * mds0to0); + let d = reduce_u160::(d_sum); + + // result = [d] concat [state[0] * v + state[shift up by 1]] + let mut result = [Self::ZERO; SPONGE_WIDTH]; + result[0] = d; + for i in 1..12 { + if i < SPONGE_WIDTH { + let t = Self::from_canonical_u64(Self::FAST_PARTIAL_ROUND_VS[r][i - 1]); + result[i] = state[i].multiply_accumulate(state[0], t); + } + } + result + } + + /// Same as `mds_partial_layer_fast` for field extensions of `Self`. + fn mds_partial_layer_fast_field, const D: usize>( + state: &[F; SPONGE_WIDTH], + r: usize, + ) -> [F; SPONGE_WIDTH] { + let s0 = state[0]; + let mds0to0 = Self::MDS_MATRIX_CIRC[0] + Self::MDS_MATRIX_DIAG[0]; + let mut d = s0 * F::from_canonical_u64(mds0to0); + for i in 1..SPONGE_WIDTH { + let t = F::from_canonical_u64(Self::FAST_PARTIAL_ROUND_W_HATS[r][i - 1]); + d += state[i] * t; + } + + // result = [d] concat [state[0] * v + state[shift up by 1]] + let mut result = [F::ZERO; SPONGE_WIDTH]; + result[0] = d; + for i in 1..SPONGE_WIDTH { + let t = F::from_canonical_u64(Self::FAST_PARTIAL_ROUND_VS[r][i - 1]); + result[i] = state[0] * t + state[i]; + } + result + } + + /// Same as `mds_partial_layer_fast` for `PackedField. + fn mds_partial_layer_fast_packed_field< + F: RichField + Extendable, + const D: usize, + FE, + P, + const D2: usize, + >( + state: &[P; SPONGE_WIDTH], + r: usize, + ) -> [P; SPONGE_WIDTH] + where + FE: FieldExtension, + P: PackedField, + { + let s0 = state[0]; + let mds0to0 = Self::MDS_MATRIX_CIRC[0] + Self::MDS_MATRIX_DIAG[0]; + let mut d = s0 * P::Scalar::from_canonical_u64(mds0to0); + for i in 1..SPONGE_WIDTH { + let t = P::Scalar::from_canonical_u64(Self::FAST_PARTIAL_ROUND_W_HATS[r][i - 1]); + d += state[i] * t; + } + + // result = [d] concat [state[0] * v + state[shift up by 1]] + let mut result = [P::ZEROS; SPONGE_WIDTH]; + result[0] = d; + for i in 1..SPONGE_WIDTH { + let t = P::Scalar::from_canonical_u64(Self::FAST_PARTIAL_ROUND_VS[r][i - 1]); + result[i] = state[0] * t + state[i]; + } + result + } + + #[inline(always)] + #[unroll_for_loops] + fn constant_layer(state: &mut [Self; SPONGE_WIDTH], round_ctr: usize) { + for i in 0..12 { + if i < SPONGE_WIDTH { + let round_constant = ALL_ROUND_CONSTANTS[i + SPONGE_WIDTH * round_ctr]; + unsafe { + state[i] = state[i].add_canonical_u64(round_constant); + } + } + } + } + + /// Same as `constant_layer` for field extensions of `Self`. + fn constant_layer_field, const D: usize>( + state: &mut [F; SPONGE_WIDTH], + round_ctr: usize, + ) { + for i in 0..SPONGE_WIDTH { + state[i] += F::from_canonical_u64(ALL_ROUND_CONSTANTS[i + SPONGE_WIDTH * round_ctr]); + } + } + + /// Same as `constant_layer` for PackedFields. + fn constant_layer_packed_field< + F: RichField + Extendable, + const D: usize, + FE, + P, + const D2: usize, + >( + state: &mut [P; SPONGE_WIDTH], + round_ctr: usize, + ) where + FE: FieldExtension, + P: PackedField, + { + for i in 0..SPONGE_WIDTH { + state[i] += + P::Scalar::from_canonical_u64(ALL_ROUND_CONSTANTS[i + SPONGE_WIDTH * round_ctr]); + } + } + + #[inline(always)] + fn sbox_monomial, const D: usize>(x: F) -> F { + // x |--> x^7 + let x2 = x.square(); + let x4 = x2.square(); + let x3 = x * x2; + x3 * x4 + } + + #[inline(always)] + #[unroll_for_loops] + fn sbox_layer(state: &mut [Self; SPONGE_WIDTH]) { + for i in 0..12 { + if i < SPONGE_WIDTH { + state[i] = Self::sbox_monomial(state[i]); + } + } + } + + /// Same as `sbox_layer` for field extensions of `Self`. + fn sbox_layer_field, const D: usize>( + state: &mut [F; SPONGE_WIDTH], + ) { + for i in 0..SPONGE_WIDTH { + state[i] = Self::sbox_monomial(state[i]); + } + } + + #[inline] + fn full_rounds(state: &mut [Self; SPONGE_WIDTH], round_ctr: &mut usize) { + for _ in 0..HALF_N_FULL_ROUNDS { + Self::constant_layer(state, *round_ctr); + Self::sbox_layer(state); + *state = Self::mds_layer(state); + *round_ctr += 1; + } + } + + #[inline] + fn partial_rounds(state: &mut [Self; SPONGE_WIDTH], round_ctr: &mut usize) { + Self::partial_first_constant_layer(state); + *state = Self::mds_partial_layer_init(state); + + for i in 0..N_PARTIAL_ROUNDS { + state[0] = Self::sbox_monomial(state[0]); + unsafe { + state[0] = state[0].add_canonical_u64(Self::FAST_PARTIAL_ROUND_CONSTANTS[i]); + } + *state = Self::mds_partial_layer_fast(state, i); + } + *round_ctr += N_PARTIAL_ROUNDS; + } + + #[inline] + fn poseidon(input: [Self; SPONGE_WIDTH]) -> [Self; SPONGE_WIDTH] { + let mut state = input; + let mut round_ctr = 0; + + Self::full_rounds(&mut state, &mut round_ctr); + Self::partial_rounds(&mut state, &mut round_ctr); + Self::full_rounds(&mut state, &mut round_ctr); + debug_assert_eq!(round_ctr, N_ROUNDS); + + state + } + + // For testing only, to ensure that various tricks are correct. + #[inline] + fn partial_rounds_naive(state: &mut [Self; SPONGE_WIDTH], round_ctr: &mut usize) { + for _ in 0..N_PARTIAL_ROUNDS { + Self::constant_layer(state, *round_ctr); + state[0] = Self::sbox_monomial(state[0]); + *state = Self::mds_layer(state); + *round_ctr += 1; + } + } + + #[inline] + fn poseidon_naive(input: [Self; SPONGE_WIDTH]) -> [Self; SPONGE_WIDTH] { + let mut state = input; + let mut round_ctr = 0; + + Self::full_rounds(&mut state, &mut round_ctr); + Self::partial_rounds_naive(&mut state, &mut round_ctr); + Self::full_rounds(&mut state, &mut round_ctr); + debug_assert_eq!(round_ctr, N_ROUNDS); + + state + } +} + +#[cfg(test)] +pub(crate) mod test_helpers { + use super::*; + + pub(crate) fn check_test_vectors( + test_vectors: Vec<([u64; SPONGE_WIDTH], [u64; SPONGE_WIDTH])>, + ) where + F: Poseidon, + { + for (input_, expected_output_) in test_vectors.into_iter() { + let mut input = [F::ZERO; SPONGE_WIDTH]; + for i in 0..SPONGE_WIDTH { + input[i] = F::from_canonical_u64(input_[i]); + } + let output = F::poseidon(input); + for i in 0..SPONGE_WIDTH { + let ex_output = F::from_canonical_u64(expected_output_[i]); + assert_eq!(output[i], ex_output); + } + } + } + + pub(crate) fn check_consistency() + where + F: Poseidon, + { + let mut input = [F::ZERO; SPONGE_WIDTH]; + for i in 0..SPONGE_WIDTH { + input[i] = F::from_canonical_u64(i as u64); + } + let output = F::poseidon(input); + let output_naive = F::poseidon_naive(input); + for i in 0..SPONGE_WIDTH { + assert_eq!(output[i], output_naive[i]); + } + } +} diff --git a/gprimitives/client/src/hash/poseidon_goldilocks.rs b/gprimitives/client/src/hash/poseidon_goldilocks.rs new file mode 100644 index 00000000000..7af00f2b094 --- /dev/null +++ b/gprimitives/client/src/hash/poseidon_goldilocks.rs @@ -0,0 +1,522 @@ +// This file is part of Gear. + +// Copyright (C) 2021-2024 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Necessary functions implementations for Poseidon over Goldilocks field of widths 8 and 12. +//! Derived from the implementation in the [`plonky2`](https://crates.io/crates/plonky2) crate +//! that follows the original paper . + +//! The constants used in this implementation *must* be generated using the +//! `poseidon_constants.sage` script in the `0xPolygonZero/hash-constants` +//! repository. + +#[cfg(not(all(target_arch = "aarch64", target_feature = "neon")))] +use crate::field::types::Field; + +use crate::{ + field::goldilocks_field::GoldilocksField, + hash::poseidon::{Poseidon, N_PARTIAL_ROUNDS}, +}; + +#[rustfmt::skip] +impl Poseidon for GoldilocksField { + // The MDS matrix we use is C + D, where C is the circulant matrix whose first row is given by + // `MDS_MATRIX_CIRC`, and D is the diagonal matrix whose diagonal is given by `MDS_MATRIX_DIAG`. + // + // WARNING: If the MDS matrix is changed, then the following + // constants need to be updated accordingly: + // - FAST_PARTIAL_ROUND_CONSTANTS + // - FAST_PARTIAL_ROUND_VS + // - FAST_PARTIAL_ROUND_W_HATS + // - FAST_PARTIAL_ROUND_INITIAL_MATRIX + const MDS_MATRIX_CIRC: [u64; 12] = [17, 15, 41, 16, 2, 28, 13, 13, 39, 18, 34, 20]; + const MDS_MATRIX_DIAG: [u64; 12] = [8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + + const FAST_PARTIAL_FIRST_ROUND_CONSTANT: [u64; 12] = [ + 0x3cc3f892184df408, 0xe993fd841e7e97f1, 0xf2831d3575f0f3af, 0xd2500e0a350994ca, + 0xc5571f35d7288633, 0x91d89c5184109a02, 0xf37f925d04e5667b, 0x2d6e448371955a69, + 0x740ef19ce01398a1, 0x694d24c0752fdf45, 0x60936af96ee2f148, 0xc33448feadc78f0c, + ]; + + const FAST_PARTIAL_ROUND_CONSTANTS: [u64; N_PARTIAL_ROUNDS] = [ + 0x74cb2e819ae421ab, 0xd2559d2370e7f663, 0x62bf78acf843d17c, 0xd5ab7b67e14d1fb4, + 0xb9fe2ae6e0969bdc, 0xe33fdf79f92a10e8, 0x0ea2bb4c2b25989b, 0xca9121fbf9d38f06, + 0xbdd9b0aa81f58fa4, 0x83079fa4ecf20d7e, 0x650b838edfcc4ad3, 0x77180c88583c76ac, + 0xaf8c20753143a180, 0xb8ccfe9989a39175, 0x954a1729f60cc9c5, 0xdeb5b550c4dca53b, + 0xf01bb0b00f77011e, 0xa1ebb404b676afd9, 0x860b6e1597a0173e, 0x308bb65a036acbce, + 0x1aca78f31c97c876, 0x0, + ]; + + const FAST_PARTIAL_ROUND_VS: [[u64; 12 - 1]; N_PARTIAL_ROUNDS] = [ + [0x94877900674181c3, 0xc6c67cc37a2a2bbd, 0xd667c2055387940f, 0x0ba63a63e94b5ff0, + 0x99460cc41b8f079f, 0x7ff02375ed524bb3, 0xea0870b47a8caf0e, 0xabcad82633b7bc9d, + 0x3b8d135261052241, 0xfb4515f5e5b0d539, 0x3ee8011c2b37f77c, ], + [0x0adef3740e71c726, 0xa37bf67c6f986559, 0xc6b16f7ed4fa1b00, 0x6a065da88d8bfc3c, + 0x4cabc0916844b46f, 0x407faac0f02e78d1, 0x07a786d9cf0852cf, 0x42433fb6949a629a, + 0x891682a147ce43b0, 0x26cfd58e7b003b55, 0x2bbf0ed7b657acb3, ], + [0x481ac7746b159c67, 0xe367de32f108e278, 0x73f260087ad28bec, 0x5cfc82216bc1bdca, + 0xcaccc870a2663a0e, 0xdb69cd7b4298c45d, 0x7bc9e0c57243e62d, 0x3cc51c5d368693ae, + 0x366b4e8cc068895b, 0x2bd18715cdabbca4, 0xa752061c4f33b8cf, ], + [0xb22d2432b72d5098, 0x9e18a487f44d2fe4, 0x4b39e14ce22abd3c, 0x9e77fde2eb315e0d, + 0xca5e0385fe67014d, 0x0c2cb99bf1b6bddb, 0x99ec1cd2a4460bfe, 0x8577a815a2ff843f, + 0x7d80a6b4fd6518a5, 0xeb6c67123eab62cb, 0x8f7851650eca21a5, ], + [0x11ba9a1b81718c2a, 0x9f7d798a3323410c, 0xa821855c8c1cf5e5, 0x535e8d6fac0031b2, + 0x404e7c751b634320, 0xa729353f6e55d354, 0x4db97d92e58bb831, 0xb53926c27897bf7d, + 0x965040d52fe115c5, 0x9565fa41ebd31fd7, 0xaae4438c877ea8f4, ], + [0x37f4e36af6073c6e, 0x4edc0918210800e9, 0xc44998e99eae4188, 0x9f4310d05d068338, + 0x9ec7fe4350680f29, 0xc5b2c1fdc0b50874, 0xa01920c5ef8b2ebe, 0x59fa6f8bd91d58ba, + 0x8bfc9eb89b515a82, 0xbe86a7a2555ae775, 0xcbb8bbaa3810babf, ], + [0x577f9a9e7ee3f9c2, 0x88c522b949ace7b1, 0x82f07007c8b72106, 0x8283d37c6675b50e, + 0x98b074d9bbac1123, 0x75c56fb7758317c1, 0xfed24e206052bc72, 0x26d7c3d1bc07dae5, + 0xf88c5e441e28dbb4, 0x4fe27f9f96615270, 0x514d4ba49c2b14fe, ], + [0xf02a3ac068ee110b, 0x0a3630dafb8ae2d7, 0xce0dc874eaf9b55c, 0x9a95f6cff5b55c7e, + 0x626d76abfed00c7b, 0xa0c1cf1251c204ad, 0xdaebd3006321052c, 0x3d4bd48b625a8065, + 0x7f1e584e071f6ed2, 0x720574f0501caed3, 0xe3260ba93d23540a, ], + [0xab1cbd41d8c1e335, 0x9322ed4c0bc2df01, 0x51c3c0983d4284e5, 0x94178e291145c231, + 0xfd0f1a973d6b2085, 0xd427ad96e2b39719, 0x8a52437fecaac06b, 0xdc20ee4b8c4c9a80, + 0xa2c98e9549da2100, 0x1603fe12613db5b6, 0x0e174929433c5505, ], + [0x3d4eab2b8ef5f796, 0xcfff421583896e22, 0x4143cb32d39ac3d9, 0x22365051b78a5b65, + 0x6f7fd010d027c9b6, 0xd9dd36fba77522ab, 0xa44cf1cb33e37165, 0x3fc83d3038c86417, + 0xc4588d418e88d270, 0xce1320f10ab80fe2, 0xdb5eadbbec18de5d, ], + [0x1183dfce7c454afd, 0x21cea4aa3d3ed949, 0x0fce6f70303f2304, 0x19557d34b55551be, + 0x4c56f689afc5bbc9, 0xa1e920844334f944, 0xbad66d423d2ec861, 0xf318c785dc9e0479, + 0x99e2032e765ddd81, 0x400ccc9906d66f45, 0xe1197454db2e0dd9, ], + [0x84d1ecc4d53d2ff1, 0xd8af8b9ceb4e11b6, 0x335856bb527b52f4, 0xc756f17fb59be595, + 0xc0654e4ea5553a78, 0x9e9a46b61f2ea942, 0x14fc8b5b3b809127, 0xd7009f0f103be413, + 0x3e0ee7b7a9fb4601, 0xa74e888922085ed7, 0xe80a7cde3d4ac526, ], + [0x238aa6daa612186d, 0x9137a5c630bad4b4, 0xc7db3817870c5eda, 0x217e4f04e5718dc9, + 0xcae814e2817bd99d, 0xe3292e7ab770a8ba, 0x7bb36ef70b6b9482, 0x3c7835fb85bca2d3, + 0xfe2cdf8ee3c25e86, 0x61b3915ad7274b20, 0xeab75ca7c918e4ef, ], + [0xd6e15ffc055e154e, 0xec67881f381a32bf, 0xfbb1196092bf409c, 0xdc9d2e07830ba226, + 0x0698ef3245ff7988, 0x194fae2974f8b576, 0x7a5d9bea6ca4910e, 0x7aebfea95ccdd1c9, + 0xf9bd38a67d5f0e86, 0xfa65539de65492d8, 0xf0dfcbe7653ff787, ], + [0x0bd87ad390420258, 0x0ad8617bca9e33c8, 0x0c00ad377a1e2666, 0x0ac6fc58b3f0518f, + 0x0c0cc8a892cc4173, 0x0c210accb117bc21, 0x0b73630dbb46ca18, 0x0c8be4920cbd4a54, + 0x0bfe877a21be1690, 0x0ae790559b0ded81, 0x0bf50db2f8d6ce31, ], + [0x000cf29427ff7c58, 0x000bd9b3cf49eec8, 0x000d1dc8aa81fb26, 0x000bc792d5c394ef, + 0x000d2ae0b2266453, 0x000d413f12c496c1, 0x000c84128cfed618, 0x000db5ebd48fc0d4, + 0x000d1b77326dcb90, 0x000beb0ccc145421, 0x000d10e5b22b11d1, ], + [0x00000e24c99adad8, 0x00000cf389ed4bc8, 0x00000e580cbf6966, 0x00000cde5fd7e04f, + 0x00000e63628041b3, 0x00000e7e81a87361, 0x00000dabe78f6d98, 0x00000efb14cac554, + 0x00000e5574743b10, 0x00000d05709f42c1, 0x00000e4690c96af1, ], + [0x0000000f7157bc98, 0x0000000e3006d948, 0x0000000fa65811e6, 0x0000000e0d127e2f, + 0x0000000fc18bfe53, 0x0000000fd002d901, 0x0000000eed6461d8, 0x0000001068562754, + 0x0000000fa0236f50, 0x0000000e3af13ee1, 0x0000000fa460f6d1, ], + [0x0000000011131738, 0x000000000f56d588, 0x0000000011050f86, 0x000000000f848f4f, + 0x00000000111527d3, 0x00000000114369a1, 0x00000000106f2f38, 0x0000000011e2ca94, + 0x00000000110a29f0, 0x000000000fa9f5c1, 0x0000000010f625d1, ], + [0x000000000011f718, 0x000000000010b6c8, 0x0000000000134a96, 0x000000000010cf7f, + 0x0000000000124d03, 0x000000000013f8a1, 0x0000000000117c58, 0x0000000000132c94, + 0x0000000000134fc0, 0x000000000010a091, 0x0000000000128961, ], + [0x0000000000001300, 0x0000000000001750, 0x000000000000114e, 0x000000000000131f, + 0x000000000000167b, 0x0000000000001371, 0x0000000000001230, 0x000000000000182c, + 0x0000000000001368, 0x0000000000000f31, 0x00000000000015c9, ], + [0x0000000000000014, 0x0000000000000022, 0x0000000000000012, 0x0000000000000027, + 0x000000000000000d, 0x000000000000000d, 0x000000000000001c, 0x0000000000000002, + 0x0000000000000010, 0x0000000000000029, 0x000000000000000f, ], + ]; + + const FAST_PARTIAL_ROUND_W_HATS: [[u64; 12 - 1]; N_PARTIAL_ROUNDS] = [ + [0x3d999c961b7c63b0, 0x814e82efcd172529, 0x2421e5d236704588, 0x887af7d4dd482328, + 0xa5e9c291f6119b27, 0xbdc52b2676a4b4aa, 0x64832009d29bcf57, 0x09c4155174a552cc, + 0x463f9ee03d290810, 0xc810936e64982542, 0x043b1c289f7bc3ac, ], + [0x673655aae8be5a8b, 0xd510fe714f39fa10, 0x2c68a099b51c9e73, 0xa667bfa9aa96999d, + 0x4d67e72f063e2108, 0xf84dde3e6acda179, 0x40f9cc8c08f80981, 0x5ead032050097142, + 0x6591b02092d671bb, 0x00e18c71963dd1b7, 0x8a21bcd24a14218a, ], + [0x202800f4addbdc87, 0xe4b5bdb1cc3504ff, 0xbe32b32a825596e7, 0x8e0f68c5dc223b9a, + 0x58022d9e1c256ce3, 0x584d29227aa073ac, 0x8b9352ad04bef9e7, 0xaead42a3f445ecbf, + 0x3c667a1d833a3cca, 0xda6f61838efa1ffe, 0xe8f749470bd7c446, ], + [0xc5b85bab9e5b3869, 0x45245258aec51cf7, 0x16e6b8e68b931830, 0xe2ae0f051418112c, + 0x0470e26a0093a65b, 0x6bef71973a8146ed, 0x119265be51812daf, 0xb0be7356254bea2e, + 0x8584defff7589bd7, 0x3c5fe4aeb1fb52ba, 0x9e7cd88acf543a5e, ], + [0x179be4bba87f0a8c, 0xacf63d95d8887355, 0x6696670196b0074f, 0xd99ddf1fe75085f9, + 0xc2597881fef0283b, 0xcf48395ee6c54f14, 0x15226a8e4cd8d3b6, 0xc053297389af5d3b, + 0x2c08893f0d1580e2, 0x0ed3cbcff6fcc5ba, 0xc82f510ecf81f6d0, ], + [0x94b06183acb715cc, 0x500392ed0d431137, 0x861cc95ad5c86323, 0x05830a443f86c4ac, + 0x3b68225874a20a7c, 0x10b3309838e236fb, 0x9b77fc8bcd559e2c, 0xbdecf5e0cb9cb213, + 0x30276f1221ace5fa, 0x7935dd342764a144, 0xeac6db520bb03708, ], + [0x7186a80551025f8f, 0x622247557e9b5371, 0xc4cbe326d1ad9742, 0x55f1523ac6a23ea2, + 0xa13dfe77a3d52f53, 0xe30750b6301c0452, 0x08bd488070a3a32b, 0xcd800caef5b72ae3, + 0x83329c90f04233ce, 0xb5b99e6664a0a3ee, 0x6b0731849e200a7f, ], + [0xec3fabc192b01799, 0x382b38cee8ee5375, 0x3bfb6c3f0e616572, 0x514abd0cf6c7bc86, + 0x47521b1361dcc546, 0x178093843f863d14, 0xad1003c5d28918e7, 0x738450e42495bc81, + 0xaf947c59af5e4047, 0x4653fb0685084ef2, 0x057fde2062ae35bf, ], + [0xe376678d843ce55e, 0x66f3860d7514e7fc, 0x7817f3dfff8b4ffa, 0x3929624a9def725b, + 0x0126ca37f215a80a, 0xfce2f5d02762a303, 0x1bc927375febbad7, 0x85b481e5243f60bf, + 0x2d3c5f42a39c91a0, 0x0811719919351ae8, 0xf669de0add993131, ], + [0x7de38bae084da92d, 0x5b848442237e8a9b, 0xf6c705da84d57310, 0x31e6a4bdb6a49017, + 0x889489706e5c5c0f, 0x0e4a205459692a1b, 0xbac3fa75ee26f299, 0x5f5894f4057d755e, + 0xb0dc3ecd724bb076, 0x5e34d8554a6452ba, 0x04f78fd8c1fdcc5f, ], + [0x4dd19c38779512ea, 0xdb79ba02704620e9, 0x92a29a3675a5d2be, 0xd5177029fe495166, + 0xd32b3298a13330c1, 0x251c4a3eb2c5f8fd, 0xe1c48b26e0d98825, 0x3301d3362a4ffccb, + 0x09bb6c88de8cd178, 0xdc05b676564f538a, 0x60192d883e473fee, ], + [0x16b9774801ac44a0, 0x3cb8411e786d3c8e, 0xa86e9cf505072491, 0x0178928152e109ae, + 0x5317b905a6e1ab7b, 0xda20b3be7f53d59f, 0xcb97dedecebee9ad, 0x4bd545218c59f58d, + 0x77dc8d856c05a44a, 0x87948589e4f243fd, 0x7e5217af969952c2, ], + [0xbc58987d06a84e4d, 0x0b5d420244c9cae3, 0xa3c4711b938c02c0, 0x3aace640a3e03990, + 0x865a0f3249aacd8a, 0x8d00b2a7dbed06c7, 0x6eacb905beb7e2f8, 0x045322b216ec3ec7, + 0xeb9de00d594828e6, 0x088c5f20df9e5c26, 0xf555f4112b19781f, ], + [0xa8cedbff1813d3a7, 0x50dcaee0fd27d164, 0xf1cb02417e23bd82, 0xfaf322786e2abe8b, + 0x937a4315beb5d9b6, 0x1b18992921a11d85, 0x7d66c4368b3c497b, 0x0e7946317a6b4e99, + 0xbe4430134182978b, 0x3771e82493ab262d, 0xa671690d8095ce82, ], + [0xb035585f6e929d9d, 0xba1579c7e219b954, 0xcb201cf846db4ba3, 0x287bf9177372cf45, + 0xa350e4f61147d0a6, 0xd5d0ecfb50bcff99, 0x2e166aa6c776ed21, 0xe1e66c991990e282, + 0x662b329b01e7bb38, 0x8aa674b36144d9a9, 0xcbabf78f97f95e65, ], + [0xeec24b15a06b53fe, 0xc8a7aa07c5633533, 0xefe9c6fa4311ad51, 0xb9173f13977109a1, + 0x69ce43c9cc94aedc, 0xecf623c9cd118815, 0x28625def198c33c7, 0xccfc5f7de5c3636a, + 0xf5e6c40f1621c299, 0xcec0e58c34cb64b1, 0xa868ea113387939f, ], + [0xd8dddbdc5ce4ef45, 0xacfc51de8131458c, 0x146bb3c0fe499ac0, 0x9e65309f15943903, + 0x80d0ad980773aa70, 0xf97817d4ddbf0607, 0xe4626620a75ba276, 0x0dfdc7fd6fc74f66, + 0xf464864ad6f2bb93, 0x02d55e52a5d44414, 0xdd8de62487c40925, ], + [0xc15acf44759545a3, 0xcbfdcf39869719d4, 0x33f62042e2f80225, 0x2599c5ead81d8fa3, + 0x0b306cb6c1d7c8d0, 0x658c80d3df3729b1, 0xe8d1b2b21b41429c, 0xa1b67f09d4b3ccb8, + 0x0e1adf8b84437180, 0x0d593a5e584af47b, 0xa023d94c56e151c7, ], + [0x49026cc3a4afc5a6, 0xe06dff00ab25b91b, 0x0ab38c561e8850ff, 0x92c3c8275e105eeb, + 0xb65256e546889bd0, 0x3c0468236ea142f6, 0xee61766b889e18f2, 0xa206f41b12c30415, + 0x02fe9d756c9f12d1, 0xe9633210630cbf12, 0x1ffea9fe85a0b0b1, ], + [0x81d1ae8cc50240f3, 0xf4c77a079a4607d7, 0xed446b2315e3efc1, 0x0b0a6b70915178c3, + 0xb11ff3e089f15d9a, 0x1d4dba0b7ae9cc18, 0x65d74e2f43b48d05, 0xa2df8c6b8ae0804a, + 0xa4e6f0a8c33348a6, 0xc0a26efc7be5669b, 0xa6b6582c547d0d60, ], + [0x84afc741f1c13213, 0x2f8f43734fc906f3, 0xde682d72da0a02d9, 0x0bb005236adb9ef2, + 0x5bdf35c10a8b5624, 0x0739a8a343950010, 0x52f515f44785cfbc, 0xcbaf4e5d82856c60, + 0xac9ea09074e3e150, 0x8f0fa011a2035fb0, 0x1a37905d8450904a, ], + [0x3abeb80def61cc85, 0x9d19c9dd4eac4133, 0x075a652d9641a985, 0x9daf69ae1b67e667, + 0x364f71da77920a18, 0x50bd769f745c95b1, 0xf223d1180dbbf3fc, 0x2f885e584e04aa99, + 0xb69a0fa70aea684a, 0x09584acaa6e062a0, 0x0bc051640145b19b, ], + ]; + + // NB: This is in ROW-major order to support cache-friendly pre-multiplication. + const FAST_PARTIAL_ROUND_INITIAL_MATRIX: [[u64; 12 - 1]; 12 - 1] = [ + [0x80772dc2645b280b, 0xdc927721da922cf8, 0xc1978156516879ad, 0x90e80c591f48b603, + 0x3a2432625475e3ae, 0x00a2d4321cca94fe, 0x77736f524010c932, 0x904d3f2804a36c54, + 0xbf9b39e28a16f354, 0x3a1ded54a6cd058b, 0x42392870da5737cf, ], + [0xe796d293a47a64cb, 0xb124c33152a2421a, 0x0ee5dc0ce131268a, 0xa9032a52f930fae6, + 0x7e33ca8c814280de, 0xad11180f69a8c29e, 0xc75ac6d5b5a10ff3, 0xf0674a8dc5a387ec, + 0xb36d43120eaa5e2b, 0x6f232aab4b533a25, 0x3a1ded54a6cd058b, ], + [0xdcedab70f40718ba, 0x14a4a64da0b2668f, 0x4715b8e5ab34653b, 0x1e8916a99c93a88e, + 0xbba4b5d86b9a3b2c, 0xe76649f9bd5d5c2e, 0xaf8e2518a1ece54d, 0xdcda1344cdca873f, + 0xcd080204256088e5, 0xb36d43120eaa5e2b, 0xbf9b39e28a16f354, ], + [0xf4a437f2888ae909, 0xc537d44dc2875403, 0x7f68007619fd8ba9, 0xa4911db6a32612da, + 0x2f7e9aade3fdaec1, 0xe7ffd578da4ea43d, 0x43a608e7afa6b5c2, 0xca46546aa99e1575, + 0xdcda1344cdca873f, 0xf0674a8dc5a387ec, 0x904d3f2804a36c54, ], + [0xf97abba0dffb6c50, 0x5e40f0c9bb82aab5, 0x5996a80497e24a6b, 0x07084430a7307c9a, + 0xad2f570a5b8545aa, 0xab7f81fef4274770, 0xcb81f535cf98c9e9, 0x43a608e7afa6b5c2, + 0xaf8e2518a1ece54d, 0xc75ac6d5b5a10ff3, 0x77736f524010c932, ], + [0x7f8e41e0b0a6cdff, 0x4b1ba8d40afca97d, 0x623708f28fca70e8, 0xbf150dc4914d380f, + 0xc26a083554767106, 0x753b8b1126665c22, 0xab7f81fef4274770, 0xe7ffd578da4ea43d, + 0xe76649f9bd5d5c2e, 0xad11180f69a8c29e, 0x00a2d4321cca94fe, ], + [0x726af914971c1374, 0x1d7f8a2cce1a9d00, 0x18737784700c75cd, 0x7fb45d605dd82838, + 0x862361aeab0f9b6e, 0xc26a083554767106, 0xad2f570a5b8545aa, 0x2f7e9aade3fdaec1, + 0xbba4b5d86b9a3b2c, 0x7e33ca8c814280de, 0x3a2432625475e3ae, ], + [0x64dd936da878404d, 0x4db9a2ead2bd7262, 0xbe2e19f6d07f1a83, 0x02290fe23c20351a, + 0x7fb45d605dd82838, 0xbf150dc4914d380f, 0x07084430a7307c9a, 0xa4911db6a32612da, + 0x1e8916a99c93a88e, 0xa9032a52f930fae6, 0x90e80c591f48b603, ], + [0x85418a9fef8a9890, 0xd8a2eb7ef5e707ad, 0xbfe85ababed2d882, 0xbe2e19f6d07f1a83, + 0x18737784700c75cd, 0x623708f28fca70e8, 0x5996a80497e24a6b, 0x7f68007619fd8ba9, + 0x4715b8e5ab34653b, 0x0ee5dc0ce131268a, 0xc1978156516879ad, ], + [0x156048ee7a738154, 0x91f7562377e81df5, 0xd8a2eb7ef5e707ad, 0x4db9a2ead2bd7262, + 0x1d7f8a2cce1a9d00, 0x4b1ba8d40afca97d, 0x5e40f0c9bb82aab5, 0xc537d44dc2875403, + 0x14a4a64da0b2668f, 0xb124c33152a2421a, 0xdc927721da922cf8, ], + [0xd841e8ef9dde8ba0, 0x156048ee7a738154, 0x85418a9fef8a9890, 0x64dd936da878404d, + 0x726af914971c1374, 0x7f8e41e0b0a6cdff, 0xf97abba0dffb6c50, 0xf4a437f2888ae909, + 0xdcedab70f40718ba, 0xe796d293a47a64cb, 0x80772dc2645b280b, ], + ]; + + #[cfg(not(all(target_arch = "aarch64", target_feature = "neon")))] + #[inline(always)] + #[unroll::unroll_for_loops] + fn mds_layer(state: &[Self; 12]) -> [Self; 12] { + let mut result = [GoldilocksField::ZERO; 12]; + + // Using the linearity of the operations we can split the state into a low||high decomposition + // and operate on each with no overflow and then combine/reduce the result to a field element. + let mut state_l = [0u64; 12]; + let mut state_h = [0u64; 12]; + + for r in 0..12 { + let s = state[r].0; + state_h[r] = s >> 32; + state_l[r] = (s as u32) as u64; + } + + let state_h = poseidon12_mds::mds_multiply_freq(state_h); + let state_l = poseidon12_mds::mds_multiply_freq(state_l); + + for r in 0..12 { + let s = state_l[r] as u128 + ((state_h[r] as u128) << 32); + + result[r] = GoldilocksField::from_noncanonical_u96((s as u64, (s >> 64) as u32)); + } + + // Add first element with the only non-zero diagonal matrix coefficient. + let s = Self::MDS_MATRIX_DIAG[0] as u128 * (state[0].0 as u128); + result[0] += GoldilocksField::from_noncanonical_u96((s as u64, (s >> 64) as u32)); + + result + } + + // #[cfg(all(target_arch="x86_64", target_feature="avx2", target_feature="bmi2"))] + // #[inline] + // fn poseidon(input: [Self; 12]) -> [Self; 12] { + // unsafe { + // crate::hash::arch::x86_64::poseidon_goldilocks_avx2_bmi2::poseidon(&input) + // } + // } + + // #[cfg(all(target_arch="x86_64", target_feature="avx2", target_feature="bmi2"))] + // #[inline(always)] + // fn constant_layer(state: &mut [Self; 12], round_ctr: usize) { + // unsafe { + // crate::hash::arch::x86_64::poseidon_goldilocks_avx2_bmi2::constant_layer(state, round_ctr); + // } + // } + + // #[cfg(all(target_arch="x86_64", target_feature="avx2", target_feature="bmi2"))] + // #[inline(always)] + // fn sbox_layer(state: &mut [Self; 12]) { + // unsafe { + // crate::hash::arch::x86_64::poseidon_goldilocks_avx2_bmi2::sbox_layer(state); + // } + // } + + // #[cfg(all(target_arch="x86_64", target_feature="avx2", target_feature="bmi2"))] + // #[inline(always)] + // fn mds_layer(state: &[Self; 12]) -> [Self; 12] { + // unsafe { + // crate::hash::arch::x86_64::poseidon_goldilocks_avx2_bmi2::mds_layer(state) + // } + // } + + // #[cfg(all(target_arch="aarch64", target_feature="neon"))] + // #[inline] + // fn poseidon(input: [Self; 12]) -> [Self; 12] { + // unsafe { + // crate::hash::arch::aarch64::poseidon_goldilocks_neon::poseidon(input) + // } + // } + + #[cfg(all(target_arch="aarch64", target_feature="neon"))] + #[inline(always)] + fn sbox_layer(state: &mut [Self; 12]) { + unsafe { + crate::hash::arch::aarch64::poseidon_goldilocks_neon::sbox_layer(state); + } + } + + #[cfg(all(target_arch="aarch64", target_feature="neon"))] + #[inline(always)] + fn mds_layer(state: &[Self; 12]) -> [Self; 12] { + unsafe { + crate::hash::arch::aarch64::poseidon_goldilocks_neon::mds_layer(state) + } + } +} + +// MDS layer helper methods +// The following code has been adapted from winterfell/crypto/src/hash/mds/mds_f64_12x12.rs +// located at https://github.com/facebook/winterfell. +#[cfg(not(all(target_arch = "aarch64", target_feature = "neon")))] +mod poseidon12_mds { + const MDS_FREQ_BLOCK_ONE: [i64; 3] = [16, 32, 16]; + const MDS_FREQ_BLOCK_TWO: [(i64, i64); 3] = [(2, -1), (-4, 1), (16, 1)]; + const MDS_FREQ_BLOCK_THREE: [i64; 3] = [-1, -8, 2]; + + /// Split 3 x 4 FFT-based MDS vector-multiplication with the Poseidon circulant MDS matrix. + #[inline(always)] + pub(crate) const fn mds_multiply_freq(state: [u64; 12]) -> [u64; 12] { + let [s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, s10, s11] = state; + + let (u0, u1, u2) = fft4_real([s0, s3, s6, s9]); + let (u4, u5, u6) = fft4_real([s1, s4, s7, s10]); + let (u8, u9, u10) = fft4_real([s2, s5, s8, s11]); + + // This where the multiplication in frequency domain is done. More precisely, and with + // the appropriate permutations in between, the sequence of + // 3-point FFTs --> multiplication by twiddle factors --> Hadamard multiplication --> + // 3 point iFFTs --> multiplication by (inverse) twiddle factors + // is "squashed" into one step composed of the functions "block1", "block2" and "block3". + // The expressions in the aforementioned functions are the result of explicit computations + // combined with the Karatsuba trick for the multiplication of complex numbers. + + let [v0, v4, v8] = block1([u0, u4, u8], MDS_FREQ_BLOCK_ONE); + let [v1, v5, v9] = block2([u1, u5, u9], MDS_FREQ_BLOCK_TWO); + let [v2, v6, v10] = block3([u2, u6, u10], MDS_FREQ_BLOCK_THREE); + // The 4th block is not computed as it is similar to the 2nd one, up to complex conjugation. + + let [s0, s3, s6, s9] = ifft4_real_unreduced((v0, v1, v2)); + let [s1, s4, s7, s10] = ifft4_real_unreduced((v4, v5, v6)); + let [s2, s5, s8, s11] = ifft4_real_unreduced((v8, v9, v10)); + + [s0, s1, s2, s3, s4, s5, s6, s7, s8, s9, s10, s11] + } + + #[inline(always)] + const fn block1(x: [i64; 3], y: [i64; 3]) -> [i64; 3] { + let [x0, x1, x2] = x; + let [y0, y1, y2] = y; + let z0 = x0 * y0 + x1 * y2 + x2 * y1; + let z1 = x0 * y1 + x1 * y0 + x2 * y2; + let z2 = x0 * y2 + x1 * y1 + x2 * y0; + + [z0, z1, z2] + } + + #[inline(always)] + const fn block2(x: [(i64, i64); 3], y: [(i64, i64); 3]) -> [(i64, i64); 3] { + let [(x0r, x0i), (x1r, x1i), (x2r, x2i)] = x; + let [(y0r, y0i), (y1r, y1i), (y2r, y2i)] = y; + let x0s = x0r + x0i; + let x1s = x1r + x1i; + let x2s = x2r + x2i; + let y0s = y0r + y0i; + let y1s = y1r + y1i; + let y2s = y2r + y2i; + + // Compute x0​y0 ​− ix1​y2​ − ix2​y1​ using Karatsuba for complex numbers multiplication + let m0 = (x0r * y0r, x0i * y0i); + let m1 = (x1r * y2r, x1i * y2i); + let m2 = (x2r * y1r, x2i * y1i); + let z0r = (m0.0 - m0.1) + (x1s * y2s - m1.0 - m1.1) + (x2s * y1s - m2.0 - m2.1); + let z0i = (x0s * y0s - m0.0 - m0.1) + (-m1.0 + m1.1) + (-m2.0 + m2.1); + let z0 = (z0r, z0i); + + // Compute x0​y1​ + x1​y0​ − ix2​y2 using Karatsuba for complex numbers multiplication + let m0 = (x0r * y1r, x0i * y1i); + let m1 = (x1r * y0r, x1i * y0i); + let m2 = (x2r * y2r, x2i * y2i); + let z1r = (m0.0 - m0.1) + (m1.0 - m1.1) + (x2s * y2s - m2.0 - m2.1); + let z1i = (x0s * y1s - m0.0 - m0.1) + (x1s * y0s - m1.0 - m1.1) + (-m2.0 + m2.1); + let z1 = (z1r, z1i); + + // Compute x0​y2​ + x1​y1 ​+ x2​y0​ using Karatsuba for complex numbers multiplication + let m0 = (x0r * y2r, x0i * y2i); + let m1 = (x1r * y1r, x1i * y1i); + let m2 = (x2r * y0r, x2i * y0i); + let z2r = (m0.0 - m0.1) + (m1.0 - m1.1) + (m2.0 - m2.1); + let z2i = (x0s * y2s - m0.0 - m0.1) + (x1s * y1s - m1.0 - m1.1) + (x2s * y0s - m2.0 - m2.1); + let z2 = (z2r, z2i); + + [z0, z1, z2] + } + + #[inline(always)] + const fn block3(x: [i64; 3], y: [i64; 3]) -> [i64; 3] { + let [x0, x1, x2] = x; + let [y0, y1, y2] = y; + let z0 = x0 * y0 - x1 * y2 - x2 * y1; + let z1 = x0 * y1 + x1 * y0 - x2 * y2; + let z2 = x0 * y2 + x1 * y1 + x2 * y0; + + [z0, z1, z2] + } + + /// Real 2-FFT over u64 integers. + #[inline(always)] + pub(crate) const fn fft2_real(x: [u64; 2]) -> [i64; 2] { + [(x[0] as i64 + x[1] as i64), (x[0] as i64 - x[1] as i64)] + } + + /// Real 2-iFFT over u64 integers. + /// Division by two to complete the inverse FFT is not performed here. + #[inline(always)] + pub(crate) const fn ifft2_real_unreduced(y: [i64; 2]) -> [u64; 2] { + [(y[0] + y[1]) as u64, (y[0] - y[1]) as u64] + } + + /// Real 4-FFT over u64 integers. + #[inline(always)] + pub(crate) const fn fft4_real(x: [u64; 4]) -> (i64, (i64, i64), i64) { + let [z0, z2] = fft2_real([x[0], x[2]]); + let [z1, z3] = fft2_real([x[1], x[3]]); + let y0 = z0 + z1; + let y1 = (z2, -z3); + let y2 = z0 - z1; + (y0, y1, y2) + } + + /// Real 4-iFFT over u64 integers. + /// Division by four to complete the inverse FFT is not performed here. + #[inline(always)] + pub(crate) const fn ifft4_real_unreduced(y: (i64, (i64, i64), i64)) -> [u64; 4] { + let z0 = y.0 + y.2; + let z1 = y.0 - y.2; + let z2 = y.1 .0; + let z3 = -y.1 .1; + + let [x0, x2] = ifft2_real_unreduced([z0, z2]); + let [x1, x3] = ifft2_real_unreduced([z1, z3]); + + [x0, x1, x2, x3] + } +} + +#[cfg(test)] +mod tests { + #[cfg(not(feature = "std"))] + use alloc::{vec, vec::Vec}; + + use crate::{ + field::{ + goldilocks_field::GoldilocksField as F, + types::{Field, PrimeField64}, + }, + hash::poseidon::test_helpers::{check_consistency, check_test_vectors}, + }; + + #[test] + fn test_vectors() { + // Test inputs are: + // 1. all zeros + // 2. range 0..WIDTH + // 3. all -1's + // 4. random elements of GoldilocksField. + // expected output calculated with (modified) hadeshash reference implementation. + + let neg_one: u64 = F::NEG_ONE.to_canonical_u64(); + + #[rustfmt::skip] + let test_vectors12: Vec<([u64; 12], [u64; 12])> = vec![ + ([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, ], + [0x3c18a9786cb0b359, 0xc4055e3364a246c3, 0x7953db0ab48808f4, 0xc71603f33a1144ca, + 0xd7709673896996dc, 0x46a84e87642f44ed, 0xd032648251ee0b3c, 0x1c687363b207df62, + 0xdf8565563e8045fe, 0x40f5b37ff4254dae, 0xd070f637b431067c, 0x1792b1c4342109d7, ]), + ([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, ], + [0xd64e1e3efc5b8e9e, 0x53666633020aaa47, 0xd40285597c6a8825, 0x613a4f81e81231d2, + 0x414754bfebd051f0, 0xcb1f8980294a023f, 0x6eb2a9e4d54a9d0f, 0x1902bc3af467e056, + 0xf045d5eafdc6021f, 0xe4150f77caaa3be5, 0xc9bfd01d39b50cce, 0x5c0a27fcb0e1459b, ]), + ([neg_one, neg_one, neg_one, neg_one, + neg_one, neg_one, neg_one, neg_one, + neg_one, neg_one, neg_one, neg_one, ], + [0xbe0085cfc57a8357, 0xd95af71847d05c09, 0xcf55a13d33c1c953, 0x95803a74f4530e82, + 0xfcd99eb30a135df1, 0xe095905e913a3029, 0xde0392461b42919b, 0x7d3260e24e81d031, + 0x10d3d0465d9deaa0, 0xa87571083dfc2a47, 0xe18263681e9958f8, 0xe28e96f1ae5e60d3, ]), + ([0x8ccbbbea4fe5d2b7, 0xc2af59ee9ec49970, 0x90f7e1a9e658446a, 0xdcc0630a3ab8b1b8, + 0x7ff8256bca20588c, 0x5d99a7ca0c44ecfb, 0x48452b17a70fbee3, 0xeb09d654690b6c88, + 0x4a55d3a39c676a88, 0xc0407a38d2285139, 0xa234bac9356386d1, 0xe1633f2bad98a52f, ], + [0xa89280105650c4ec, 0xab542d53860d12ed, 0x5704148e9ccab94f, 0xd3a826d4b62da9f5, + 0x8a7a6ca87892574f, 0xc7017e1cad1a674e, 0x1f06668922318e34, 0xa3b203bc8102676f, + 0xfcc781b0ce382bf2, 0x934c69ff3ed14ba5, 0x504688a5996e8f13, 0x401f3f2ed524a2ba, ]), + ]; + + check_test_vectors::(test_vectors12); + } + + #[test] + fn consistency() { + check_consistency::(); + } +} diff --git a/gprimitives/client/src/lib.rs b/gprimitives/client/src/lib.rs new file mode 100644 index 00000000000..c3789481e2e --- /dev/null +++ b/gprimitives/client/src/lib.rs @@ -0,0 +1,29 @@ +// This file is part of Gear. + +// Copyright (C) 2021-2024 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +//! Some necessary self-contained primitives that need to be compiled as a part of the node. +//! Includes some Goldilocks finite field arithmetic used in Poseidon hashing logic, +//! as well as some Poseidon hashing funcitons over the Goldilocks field. + +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +pub mod field; +pub mod hash; +pub mod util; diff --git a/gprimitives/client/src/util.rs b/gprimitives/client/src/util.rs new file mode 100644 index 00000000000..2429f3a156d --- /dev/null +++ b/gprimitives/client/src/util.rs @@ -0,0 +1,409 @@ +// This file is part of Gear. + +// Copyright (C) 2021-2024 Gear Technologies Inc. +// SPDX-License-Identifier: GPL-3.0-or-later WITH Classpath-exception-2.0 + +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. + +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. + +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +#![allow(clippy::needless_range_loop)] + +use alloc::vec::Vec; +use core::hint::unreachable_unchecked; + +// use crate::transpose_util::transpose_in_place_square; + +// mod transpose_util; + +pub const fn bits_u64(n: u64) -> usize { + (64 - n.leading_zeros()) as usize +} + +/// Computes `ceil(log_2(n))`. +#[must_use] +pub const fn log2_ceil(n: usize) -> usize { + (usize::BITS - n.saturating_sub(1).leading_zeros()) as usize +} + +/// Computes `log_2(n)`, panicking if `n` is not a power of two. +pub fn log2_strict(n: usize) -> usize { + let res = n.trailing_zeros(); + assert!(n.wrapping_shr(res) == 1, "Not a power of two: {n}"); + // Tell the optimizer about the semantics of `log2_strict`. i.e. it can replace `n` with + // `1 << res` and vice versa. + assume(n == 1 << res); + res as usize +} + +/// Returns the largest integer `i` such that `base**i <= n`. +pub const fn log_floor(n: u64, base: u64) -> usize { + assert!(n > 0); + assert!(base > 1); + let mut i = 0; + let mut cur: u64 = 1; + loop { + let (mul, overflow) = cur.overflowing_mul(base); + if overflow || mul > n { + return i; + } else { + i += 1; + cur = mul; + } + } +} + +/// Permutes `arr` such that each index is mapped to its reverse in binary. +pub fn reverse_index_bits(arr: &[T]) -> Vec { + let n = arr.len(); + let n_power = log2_strict(n); + + if n_power <= 6 { + reverse_index_bits_small(arr, n_power) + } else { + reverse_index_bits_large(arr, n_power) + } +} + +/* Both functions below are semantically equivalent to: + for i in 0..n { + result.push(arr[reverse_bits(i, n_power)]); + } + where reverse_bits(i, n_power) computes the n_power-bit reverse. The complications are there + to guide the compiler to generate optimal assembly. +*/ + +fn reverse_index_bits_small(arr: &[T], n_power: usize) -> Vec { + let n = arr.len(); + let mut result = Vec::with_capacity(n); + // BIT_REVERSE_6BIT holds 6-bit reverses. This shift makes them n_power-bit reverses. + let dst_shr_amt = 6 - n_power; + for i in 0..n { + let src = (BIT_REVERSE_6BIT[i] as usize) >> dst_shr_amt; + result.push(arr[src]); + } + result +} + +fn reverse_index_bits_large(arr: &[T], n_power: usize) -> Vec { + let n = arr.len(); + // LLVM does not know that it does not need to reverse src at each iteration (which is expensive + // on x86). We take advantage of the fact that the low bits of dst change rarely and the high + // bits of dst are dependent only on the low bits of src. + let src_lo_shr_amt = 64 - (n_power - 6); + let src_hi_shl_amt = n_power - 6; + let mut result = Vec::with_capacity(n); + for i_chunk in 0..(n >> 6) { + let src_lo = i_chunk.reverse_bits() >> src_lo_shr_amt; + for i_lo in 0..(1 << 6) { + let src_hi = (BIT_REVERSE_6BIT[i_lo] as usize) << src_hi_shl_amt; + let src = src_hi + src_lo; + result.push(arr[src]); + } + } + result +} + +// /// Bit-reverse the order of elements in `arr`. +// /// SAFETY: ensure that `arr.len() == 1 << lb_n`. +// #[cfg(not(target_arch = "aarch64"))] +// unsafe fn reverse_index_bits_in_place_small(arr: &mut [T], lb_n: usize) { +// if lb_n <= 6 { +// // BIT_REVERSE_6BIT holds 6-bit reverses. This shift makes them lb_n-bit reverses. +// let dst_shr_amt = 6 - lb_n as u32; +// for src in 0..arr.len() { +// // `wrapping_shr` handles the case when `arr.len() == 1`. In that case `src == 0`, so +// // `src.reverse_bits() == 0`. `usize::wrapping_shr` by 64 is a no-op, but it gives the +// // correct result. +// let dst = (BIT_REVERSE_6BIT[src] as usize).wrapping_shr(dst_shr_amt); +// if src < dst { +// swap(arr.get_unchecked_mut(src), arr.get_unchecked_mut(dst)); +// } +// } +// } else { +// // LLVM does not know that it does not need to reverse src at each iteration (which is +// // expensive on x86). We take advantage of the fact that the low bits of dst change rarely and the high +// // bits of dst are dependent only on the low bits of src. +// let dst_lo_shr_amt = usize::BITS - (lb_n - 6) as u32; +// let dst_hi_shl_amt = lb_n - 6; +// for src_chunk in 0..(arr.len() >> 6) { +// let src_hi = src_chunk << 6; +// // `wrapping_shr` handles the case when `arr.len() == 1`. In that case `src == 0`, so +// // `src.reverse_bits() == 0`. `usize::wrapping_shr` by 64 is a no-op, but it gives the +// // correct result. +// let dst_lo = src_chunk.reverse_bits().wrapping_shr(dst_lo_shr_amt); +// for src_lo in 0..(1 << 6) { +// let dst_hi = (BIT_REVERSE_6BIT[src_lo] as usize) << dst_hi_shl_amt; +// let src = src_hi + src_lo; +// let dst = dst_hi + dst_lo; +// if src < dst { +// swap(arr.get_unchecked_mut(src), arr.get_unchecked_mut(dst)); +// } +// } +// } +// } +// } + +// /// Bit-reverse the order of elements in `arr`. +// /// SAFETY: ensure that `arr.len() == 1 << lb_n`. +// #[cfg(target_arch = "aarch64")] +// unsafe fn reverse_index_bits_in_place_small(arr: &mut [T], lb_n: usize) { +// // Aarch64 can reverse bits in one instruction, so the trivial version works best. +// for src in 0..arr.len() { +// // `wrapping_shr` handles the case when `arr.len() == 1`. In that case `src == 0`, so +// // `src.reverse_bits() == 0`. `usize::wrapping_shr` by 64 is a no-op, but it gives the +// // correct result. +// let dst = src.reverse_bits().wrapping_shr(usize::BITS - lb_n as u32); +// if src < dst { +// swap(arr.get_unchecked_mut(src), arr.get_unchecked_mut(dst)); +// } +// } +// } + +// /// Split `arr` chunks and bit-reverse the order of the chunks. There are `1 << lb_num_chunks` +// /// chunks, each of length `1 << lb_chunk_size`. +// /// SAFETY: ensure that `arr.len() == 1 << lb_num_chunks + lb_chunk_size`. +// unsafe fn reverse_index_bits_in_place_chunks( +// arr: &mut [T], +// lb_num_chunks: usize, +// lb_chunk_size: usize, +// ) { +// for i in 0..1usize << lb_num_chunks { +// // `wrapping_shr` handles the silly case when `lb_num_chunks == 0`. +// let j = i +// .reverse_bits() +// .wrapping_shr(usize::BITS - lb_num_chunks as u32); +// if i < j { +// swap_nonoverlapping( +// arr.get_unchecked_mut(i << lb_chunk_size), +// arr.get_unchecked_mut(j << lb_chunk_size), +// 1 << lb_chunk_size, +// ); +// } +// } +// } + +// // Ensure that SMALL_ARR_SIZE >= 4 * BIG_T_SIZE. +// const BIG_T_SIZE: usize = 1 << 14; +// const SMALL_ARR_SIZE: usize = 1 << 16; +// pub fn reverse_index_bits_in_place(arr: &mut [T]) { +// let n = arr.len(); +// let lb_n = log2_strict(n); +// // If the whole array fits in fast cache, then the trivial algorithm is cache friendly. Also, if +// // `T` is really big, then the trivial algorithm is cache-friendly, no matter the size of the +// // array. +// if size_of::() << lb_n <= SMALL_ARR_SIZE || size_of::() >= BIG_T_SIZE { +// unsafe { +// reverse_index_bits_in_place_small(arr, lb_n); +// } +// } else { +// debug_assert!(n >= 4); // By our choice of `BIG_T_SIZE` and `SMALL_ARR_SIZE`. + +// // Algorithm: +// // +// // Treat `arr` as a `sqrt(n)` by `sqrt(n)` row-major matrix. (Assume for now that `lb_n` is +// // even, i.e., `n` is a square number.) To perform bit-order reversal we: +// // 1. Bit-reverse the order of the rows. (They are contiguous in memory, so this is +// // basically a series of large `memcpy`s.) +// // 2. Transpose the matrix. +// // 3. Bit-reverse the order of the rows. +// // This is equivalent to, for every index `0 <= i < n`: +// // 1. bit-reversing `i[lb_n / 2..lb_n]`, +// // 2. swapping `i[0..lb_n / 2]` and `i[lb_n / 2..lb_n]`, +// // 3. bit-reversing `i[lb_n / 2..lb_n]`. +// // +// // If `lb_n` is odd, i.e., `n` is not a square number, then the above procedure requires +// // slight modification. At steps 1 and 3 we bit-reverse bits `ceil(lb_n / 2)..lb_n`, of the +// // index (shuffling `floor(lb_n / 2)` chunks of length `ceil(lb_n / 2)`). At step 2, we +// // perform _two_ transposes. We treat `arr` as two matrices, one where the middle bit of the +// // index is `0` and another, where the middle bit is `1`; we transpose each individually. + +// let lb_num_chunks = lb_n >> 1; +// let lb_chunk_size = lb_n - lb_num_chunks; +// unsafe { +// reverse_index_bits_in_place_chunks(arr, lb_num_chunks, lb_chunk_size); +// transpose_in_place_square(arr, lb_chunk_size, lb_num_chunks, 0); +// if lb_num_chunks != lb_chunk_size { +// // `arr` cannot be interpreted as a square matrix. We instead interpret it as a +// // `1 << lb_num_chunks` by `2` by `1 << lb_num_chunks` tensor, in row-major order. +// // The above transpose acted on `tensor[..., 0, ...]` (all indices with middle bit +// // `0`). We still need to transpose `tensor[..., 1, ...]`. To do so, we advance +// // arr by `1 << lb_num_chunks` effectively, adding that to every index. +// let arr_with_offset = &mut arr[1 << lb_num_chunks..]; +// transpose_in_place_square(arr_with_offset, lb_chunk_size, lb_num_chunks, 0); +// } +// reverse_index_bits_in_place_chunks(arr, lb_num_chunks, lb_chunk_size); +// } +// } +// } + +// Lookup table of 6-bit reverses. +// NB: 2^6=64 bytes is a cacheline. A smaller table wastes cache space. +#[rustfmt::skip] +const BIT_REVERSE_6BIT: &[u8] = &[ + 0o00, 0o40, 0o20, 0o60, 0o10, 0o50, 0o30, 0o70, + 0o04, 0o44, 0o24, 0o64, 0o14, 0o54, 0o34, 0o74, + 0o02, 0o42, 0o22, 0o62, 0o12, 0o52, 0o32, 0o72, + 0o06, 0o46, 0o26, 0o66, 0o16, 0o56, 0o36, 0o76, + 0o01, 0o41, 0o21, 0o61, 0o11, 0o51, 0o31, 0o71, + 0o05, 0o45, 0o25, 0o65, 0o15, 0o55, 0o35, 0o75, + 0o03, 0o43, 0o23, 0o63, 0o13, 0o53, 0o33, 0o73, + 0o07, 0o47, 0o27, 0o67, 0o17, 0o57, 0o37, 0o77, +]; + +#[inline(always)] +pub fn assume(p: bool) { + debug_assert!(p); + if !p { + unsafe { + unreachable_unchecked(); + } + } +} + +/// Try to force Rust to emit a branch. Example: +/// if x > 2 { +/// y = foo(); +/// branch_hint(); +/// } else { +/// y = bar(); +/// } +/// This function has no semantics. It is a hint only. +#[inline(always)] +pub fn branch_hint() { + // NOTE: These are the currently supported assembly architectures. See the + // [nightly reference](https://doc.rust-lang.org/nightly/reference/inline-assembly.html) for + // the most up-to-date list. + #[cfg(any( + target_arch = "aarch64", + target_arch = "arm", + target_arch = "riscv32", + target_arch = "riscv64", + target_arch = "x86", + target_arch = "x86_64", + ))] + unsafe { + core::arch::asm!("", options(nomem, nostack, preserves_flags)); + } +} + +#[cfg(test)] +mod tests { + use alloc::{vec, vec::Vec}; + + use rand::{rngs::OsRng, Rng}; + + use super::{log2_ceil, log2_strict}; + + #[test] + fn test_reverse_index_bits() { + let lengths = [32, 128, 1 << 16]; + let mut rng = OsRng; + for _ in 0..32 { + for length in lengths { + let mut rand_list: Vec = Vec::with_capacity(length); + rand_list.resize_with(length, || rng.gen()); + + let out = super::reverse_index_bits(&rand_list); + let expect = reverse_index_bits_naive(&rand_list); + + for (out, expect) in out.iter().zip(&expect) { + assert_eq!(out, expect); + } + } + } + } + + // #[test] + // fn test_reverse_index_bits_in_place() { + // let lengths = [32, 128, 1 << 16]; + // let mut rng = OsRng; + // for _ in 0..32 { + // for length in lengths { + // let mut rand_list: Vec = Vec::with_capacity(length); + // rand_list.resize_with(length, || rng.gen()); + + // let expect = reverse_index_bits_naive(&rand_list); + + // super::reverse_index_bits_in_place(&mut rand_list); + + // for (got, expect) in rand_list.iter().zip(&expect) { + // assert_eq!(got, expect); + // } + // } + // } + // } + + #[test] + fn test_log2_strict() { + assert_eq!(log2_strict(1), 0); + assert_eq!(log2_strict(2), 1); + assert_eq!(log2_strict(1 << 18), 18); + assert_eq!(log2_strict(1 << 31), 31); + assert_eq!( + log2_strict(1 << (usize::BITS - 1)), + usize::BITS as usize - 1 + ); + } + + #[test] + #[should_panic] + fn test_log2_strict_zero() { + log2_strict(0); + } + + #[test] + #[should_panic] + fn test_log2_strict_nonpower_2() { + log2_strict(0x78c341c65ae6d262); + } + + #[test] + #[should_panic] + fn test_log2_strict_usize_max() { + log2_strict(usize::MAX); + } + + #[test] + fn test_log2_ceil() { + // Powers of 2 + assert_eq!(log2_ceil(0), 0); + assert_eq!(log2_ceil(1), 0); + assert_eq!(log2_ceil(2), 1); + assert_eq!(log2_ceil(1 << 18), 18); + assert_eq!(log2_ceil(1 << 31), 31); + assert_eq!(log2_ceil(1 << (usize::BITS - 1)), usize::BITS as usize - 1); + + // Nonpowers; want to round up + assert_eq!(log2_ceil(3), 2); + assert_eq!(log2_ceil(0x14fe901b), 29); + assert_eq!( + log2_ceil((1 << (usize::BITS - 1)) + 1), + usize::BITS as usize + ); + assert_eq!(log2_ceil(usize::MAX - 1), usize::BITS as usize); + assert_eq!(log2_ceil(usize::MAX), usize::BITS as usize); + } + + fn reverse_index_bits_naive(arr: &[T]) -> Vec { + let n = arr.len(); + let n_power = log2_strict(n); + + let mut out = vec![None; n]; + for (i, v) in arr.iter().enumerate() { + let dst = i.reverse_bits() >> (64 - n_power); + out[dst] = Some(*v); + } + + out.into_iter().map(|x| x.unwrap()).collect() + } +} diff --git a/gsdk/src/metadata/generated.rs b/gsdk/src/metadata/generated.rs index 738654544e0..eafa3823304 100644 --- a/gsdk/src/metadata/generated.rs +++ b/gsdk/src/metadata/generated.rs @@ -3287,6 +3287,7 @@ pub mod runtime_types { runtime_types::sp_weights::weight_v2::Weight, pub gr_create_program_wgas_salt_per_byte: runtime_types::sp_weights::weight_v2::Weight, + pub gr_permute: runtime_types::sp_weights::weight_v2::Weight, } #[derive(Debug, crate::gp::Decode, crate::gp::DecodeAsType, crate::gp::Encode)] pub struct TaskWeights { diff --git a/gsys/src/lib.rs b/gsys/src/lib.rs index 01fd3398ed0..68d09d428f1 100644 --- a/gsys/src/lib.rs +++ b/gsys/src/lib.rs @@ -64,6 +64,21 @@ pub type SignalCode = u32; /// Represents value type. pub type Value = u128; +/// Poseidon permutation input/output +// #[repr(C, packed)] +// #[derive(Default, Debug)] +// pub struct PoseidonInOut([u64; 12]); +// impl PoseidonInOut { +// fn as_ptr(&self) -> *const [u64; 12] { +// self.as_ref().as_ptr() as *const _ +// } + +// fn as_mut_ptr(&mut self) -> *mut [u64; 12] { +// self.as_mut().as_mut_ptr() as *mut _ +// } +// } +pub type PoseidonInOut = [u64; 12]; + /// Represents type defining concatenated block number with hash. 36 bytes. #[repr(C, packed)] #[derive(Default, Debug)] @@ -962,4 +977,11 @@ syscalls! { /// - `delay`: `u32` amount of blocks to delay. /// - `err_mid`: `mut ptr` for error code. pub fn gr_wake(message_id: *const Hash, delay: BlockNumber, err: *mut ErrorCode); + + /// Infallible `gr_permute` calculate syscall. + /// + /// Arguments type: + /// - `data`: `const ptr` to the permutation data. + /// - `hash_out`: `mut ptr` to the Poseidon hash output. + pub fn gr_permute(data: *const PoseidonInOut, hash_out: *mut PoseidonInOut); } diff --git a/node/service/src/client.rs b/node/service/src/client.rs index 959e4530521..c20de806158 100644 --- a/node/service/src/client.rs +++ b/node/service/src/client.rs @@ -53,6 +53,7 @@ pub type ExtendHostFunctions = ( gear_ri::sandbox::HostFunctions, sp_crypto_ec_utils::bls12_381::host_calls::HostFunctions, gear_ri::gear_bls_12_381::HostFunctions, + gear_ri::poseidon_hash::HostFunctions, ); /// Otherwise we only use the default Substrate host functions. #[cfg(not(feature = "runtime-benchmarks"))] @@ -61,6 +62,7 @@ pub type ExtendHostFunctions = ( gear_ri::sandbox::HostFunctions, sp_crypto_ec_utils::bls12_381::host_calls::HostFunctions, gear_ri::gear_bls_12_381::HostFunctions, + gear_ri::poseidon_hash::HostFunctions, ); /// A set of APIs that polkadot-like runtimes must implement. diff --git a/node/testing/src/client.rs b/node/testing/src/client.rs index 7f1fcae99d8..80bb601f870 100644 --- a/node/testing/src/client.rs +++ b/node/testing/src/client.rs @@ -29,6 +29,7 @@ pub type ExtendHostFunctions = ( gear_runtime_interface::sandbox::HostFunctions, sp_crypto_ec_utils::bls12_381::host_calls::HostFunctions, gear_runtime_interface::gear_bls_12_381::HostFunctions, + gear_runtime_interface::poseidon_hash::HostFunctions, ); /// Test client backend. diff --git a/pallets/gear-builtin/Cargo.toml b/pallets/gear-builtin/Cargo.toml index 707230fbdf0..270964c1230 100644 --- a/pallets/gear-builtin/Cargo.toml +++ b/pallets/gear-builtin/Cargo.toml @@ -72,6 +72,7 @@ ark-ec.workspace = true ark-ff.workspace = true ark-std.workspace = true sha2 = { workspace = true, features = ["std"] } +hex.workspace = true [features] default = ["std"] diff --git a/pallets/gear-builtin/src/benchmarking.rs b/pallets/gear-builtin/src/benchmarking.rs index 698fbee9074..0a778cf5d1e 100644 --- a/pallets/gear-builtin/src/benchmarking.rs +++ b/pallets/gear-builtin/src/benchmarking.rs @@ -19,7 +19,6 @@ //! Benchmarks for the `pallet-gear-builtin` #[allow(unused)] -use crate::Pallet as BuiltinActorPallet; use crate::*; use ark_bls12_381::{Bls12_381, G1Affine, G1Projective as G1, G2Affine, G2Projective as G2}; use ark_ec::{pairing::Pairing, short_weierstrass::SWCurveConfig, Group, ScalarMul}; @@ -29,7 +28,8 @@ use ark_std::{ops::Mul, UniformRand}; use common::Origin; use frame_benchmarking::{benchmarks, impl_benchmark_test_suite}; use gear_core::message::MAX_PAYLOAD_SIZE; -use parity_scale_codec::{Compact, Encode, Input}; +use pallet_gear::{BuiltinDispatcherFactory, Config as GearConfig}; +use parity_scale_codec::{Compact, Decode, Encode, Input}; type ArkScale = ark_scale::ArkScale; type ScalarField = ::ScalarField; @@ -49,7 +49,7 @@ fn naive_var_base_msm(bases: &[G::MulBase], scalars: &[G::ScalarFi benchmarks! { where_clause { where - T: pallet_gear::Config, + T: GearConfig, T::AccountId: Origin, } @@ -63,7 +63,7 @@ benchmarks! { create_dispatcher { }: { - let _ = ::BuiltinDispatcherFactory::create(); + let _ = ::BuiltinDispatcherFactory::create(); } verify { // No changes in runtime are expected since the actual dispatch doesn't take place. } @@ -290,8 +290,4 @@ benchmarks! { } } -impl_benchmark_test_suite!( - BuiltinActorPallet, - crate::mock::new_test_ext(), - crate::mock::Test, -); +impl_benchmark_test_suite!(Pallet, crate::mock::new_test_ext(), crate::mock::Test); diff --git a/pallets/gear-builtin/src/lib.rs b/pallets/gear-builtin/src/lib.rs index b0d2922ce12..4e55d1598b9 100644 --- a/pallets/gear-builtin/src/lib.rs +++ b/pallets/gear-builtin/src/lib.rs @@ -100,6 +100,12 @@ pub enum BuiltinActorError { GasAllowanceExceeded, } +impl From<&'static str> for BuiltinActorError { + fn from(e: &'static str) -> Self { + BuiltinActorError::Custom(LimitedStr::from_small_str(e)) + } +} + impl From for ActorExecutionErrorReplyReason { /// Convert [`BuiltinActorError`] to [`core_processor::common::ActorExecutionErrorReplyReason`] fn from(err: BuiltinActorError) -> Self { diff --git a/pallets/gear/Cargo.toml b/pallets/gear/Cargo.toml index f7a15effebf..b7882f5cedc 100644 --- a/pallets/gear/Cargo.toml +++ b/pallets/gear/Cargo.toml @@ -123,6 +123,7 @@ demo-async-critical = { workspace = true, features = ["debug"] } demo-async-reply-hook = { workspace = true, features = ["debug"] } demo-create-program-reentrance = { workspace = true, features = ["debug"] } demo-value-sender.workspace = true +demo-plonky2-verifier.workspace = true test-syscalls = { workspace = true, features = ["debug"] } page_size.workspace = true frame-support-test = { workspace = true, features = ["std"] } diff --git a/pallets/gear/src/benchmarking/mod.rs b/pallets/gear/src/benchmarking/mod.rs index f565967b93b..29d19c92412 100644 --- a/pallets/gear/src/benchmarking/mod.rs +++ b/pallets/gear/src/benchmarking/mod.rs @@ -1489,6 +1489,17 @@ benchmarks! { verify_process(res.unwrap()); } + gr_permute { + let r in 0 .. API_BENCHMARK_BATCHES; + let mut res = None; + let exec = Benches::::gr_permute(r)?; + }: { + res.replace(run_process(exec)); + } + verify { + verify_process(res.unwrap()); + } + lazy_pages_signal_read { let p in 0 .. DEFAULT_MEM_SIZE as u32; let mut res = None; diff --git a/pallets/gear/src/benchmarking/syscalls.rs b/pallets/gear/src/benchmarking/syscalls.rs index d62ca565bbd..8ea2daeaf6c 100644 --- a/pallets/gear/src/benchmarking/syscalls.rs +++ b/pallets/gear/src/benchmarking/syscalls.rs @@ -43,7 +43,7 @@ use gear_core::{ }; use gear_core_errors::*; use gear_wasm_instrument::{parity_wasm::elements::Instruction, syscalls::SyscallName}; -use rand::{seq::SliceRandom, SeedableRng}; +use rand::{seq::SliceRandom, Rng, SeedableRng}; use rand_pcg::Pcg64; use sp_core::Get; use sp_runtime::{codec::Encode, traits::UniqueSaturatedInto}; @@ -64,6 +64,8 @@ const PID_SIZE: u32 = size_of::() as u32; const MID_SIZE: u32 = size_of::() as u32; /// Random subject size const RANDOM_SUBJECT_SIZE: u32 = 32; +/// Poseidon permutation input size (size_of:: * 12) +const PERMUTE_DATA_SIZE: u32 = 96_u32; /// Size of struct with fields: error len and handle const ERR_HANDLE_SIZE: u32 = ERR_LEN_SIZE + HANDLE_SIZE; @@ -88,6 +90,9 @@ const COMMON_PAYLOAD_LEN: u32 = 100; const MAX_REPETITIONS: u32 = API_BENCHMARK_BATCHES * API_BENCHMARK_BATCH_SIZE; +// Goldilocks base field order +const GF_ORDER: u64 = 0xFFFFFFFF00000001; + fn kb_to_bytes(size_in_kb: u32) -> u32 { size_in_kb.checked_mul(1024).unwrap() } @@ -1513,6 +1518,48 @@ where Self::prepare_handle(module, 0) } + pub fn gr_permute(r: u32) -> Result, &'static str> { + let seed = 1000; + let sample_goldilocks_field = |n: usize| -> Vec { + let mut rng = Pcg64::seed_from_u64(seed); + (0..n) + .flat_map(|_| { + (0..12) + .map(|_| rng.gen_range(0..GF_ORDER)) + .flat_map(|value| value.to_le_bytes().to_vec()) + .collect::>() + }) + .collect() + }; + + let inputs = sample_goldilocks_field(MAX_REPETITIONS as usize); + + let repetitions = r * API_BENCHMARK_BATCH_SIZE; + let input_offset = COMMON_OFFSET; + let res_offset = input_offset + PERMUTE_DATA_SIZE * MAX_REPETITIONS; + + let module = ModuleDefinition { + memory: Some(ImportedMemory::new(SMALL_MEM_SIZE * 3)), + imported_functions: vec![SyscallName::Permute], + data_segments: vec![DataSegment { + offset: input_offset, + value: inputs, + }], + handle_body: Some(body::syscall( + repetitions, + &[ + // data offset + InstrI32Const(input_offset), + // bn random offset + InstrI32Const(res_offset), + ], + )), + ..Default::default() + }; + + Self::prepare_handle(module, 0) + } + pub fn lazy_pages_signal_read(end_page: WasmPage) -> Result, &'static str> { let instrs = body::read_access_all_pages_instrs(end_page, vec![]); let module = ModuleDefinition { diff --git a/pallets/gear/src/benchmarking/tests/syscalls_integrity.rs b/pallets/gear/src/benchmarking/tests/syscalls_integrity.rs index d249fc1d6bf..365d2f103f6 100644 --- a/pallets/gear/src/benchmarking/tests/syscalls_integrity.rs +++ b/pallets/gear/src/benchmarking/tests/syscalls_integrity.rs @@ -214,7 +214,8 @@ where T: Config, T::AccountId: Origin, { - SyscallName::all().for_each(|syscall| { + vec![SyscallName::Permute].into_iter().for_each(|syscall| { + // SyscallName::all().for_each(|syscall| { log::info!("run test for {syscall:?}"); match syscall { SyscallName::Send => check_send::(None), @@ -272,6 +273,7 @@ where SyscallName::ReservationReply => check_gr_reservation_reply::(), SyscallName::ReservationReplyCommit => check_gr_reservation_reply_commit::(), SyscallName::SystemReserveGas => check_gr_system_reserve_gas::(), + SyscallName::Permute => check_gr_permute::(), } }); } @@ -1041,6 +1043,34 @@ where }) } +fn check_gr_permute() +where + T: Config, + T::AccountId: Origin, +{ + run_tester::(|_, _| { + let input = [1_u64; 12]; + let expected_hash: [u64; 12] = [ + 16428316519797902711, + 13351830238340666928, + 682362844289978626, + 12150588177266359240, + 17253754121560429078, + 451271978634734260, + 18275013734444918923, + 2683513634502220619, + 11021424422480713329, + 9919697188140387146, + 12631792409692871611, + 12948832098596279325, + ]; + + let mp = vec![Kind::Permute(input, expected_hash)].encode().into(); + + (TestCall::send_message(mp), None::) + }) +} + fn run_tester(get_test_call_params: S) where T: Config + frame_system::Config, diff --git a/pallets/gear/src/mock.rs b/pallets/gear/src/mock.rs index 603ff539ee6..5f0d301fe67 100644 --- a/pallets/gear/src/mock.rs +++ b/pallets/gear/src/mock.rs @@ -135,7 +135,11 @@ impl DynamicSchedule { SCHEDULE.with(|schedule| { schedule .borrow_mut() - .get_or_insert_with(Default::default) + .get_or_insert_with(|| { + let mut schedule = Schedule::::default(); + schedule.limits.code_len *= 2; + schedule + }) .clone() }) } diff --git a/pallets/gear/src/schedule.rs b/pallets/gear/src/schedule.rs index 5c9fbf0bfc0..f8cbe59debd 100644 --- a/pallets/gear/src/schedule.rs +++ b/pallets/gear/src/schedule.rs @@ -572,6 +572,9 @@ pub struct SyscallWeights { /// Weight per salt byte by `create_program_wgas`. pub gr_create_program_wgas_salt_per_byte: Weight, + /// Weight of calling `gr_permute`. + pub gr_permute: Weight, + /// The type parameter is used in the default implementation. #[codec(skip)] #[cfg_attr(feature = "std", serde(skip))] @@ -1143,6 +1146,7 @@ impl Default for SyscallWeights { 0, 1, ), + gr_permute: cost_batched(W::::gr_permute), _phantom: PhantomData, } } @@ -1230,6 +1234,7 @@ impl From> for SyscallCosts { .gr_create_program_wgas_salt_per_byte .ref_time() .into(), + gr_permute: val.gr_permute.ref_time().into(), } } } diff --git a/pallets/gear/src/test_data/factorial.hex b/pallets/gear/src/test_data/factorial.hex new file mode 100644 index 00000000000..2d623e25163 --- /dev/null +++ b/pallets/gear/src/test_data/factorial.hex @@ -0,0 +1 @@ +040000000000000025eb73c27ba8236ac381b83848d1e228b2ddb43e7591d11c7ab7a01e730eb828c090a456fde33686a957bf34ce5514ae5fe7a9d296b35beed736c2344cdff76ae8ee077b78f6d83d82edf7d0225b2ed7dca95cf84e8820631d56767e2f430e78821c2b8bc13ca7ae7320603bb245d3b135d6c644df5276e7250ecc69707fa12c1af38e67e35cb926bcb9e84287bd5fc883b0fc401ca3f4eaf3f05ca7a125c0c450fc92ce66e905440191a162be1114f2c62eff5389732771d573c035e657279431b65c41c9f8f2824792fbd49d1f8b572f7abfea6058642b164d5808a8d4fa9fd27c585776b00656e2bcfc27bd47bc84aaac6f843fd5a627bce4a081fe5d4ac36afa29c4fb4928cd3933c01d40e9e2a3377122bdbd734ffb4a30a35c4d286306ce06423b3ef411249807e39389b51f6a9e727915ecd03003b74739127574b2a381e4d9d6849ffdf21970c844cc4a0fe82e214c971ac01f90e234e98d8cda68f3dbff7c547273229988157e5da5cde541abe8e208ecd45a50814a1c89bf62d5eaefb344d8601c6458cfd2866737b37daa5802957b29ad4431d71a450a59b59bd2444ac2ce388efdda0ef7a6281e92cee633ab9a2a384fc0577d5205d51be486208a54b931a4be22dbc2d3e1957bbc278d81e731cfd0284f0788e923d6db9f8563437254db2fd77707ae12f89b6617ada56467e21e148ea1fd3e31d8dcd2c71e90976634a0d7251a400a99108134cebe9e8f6773d813ddb1d638bfaebaca5c6d1b8700000000000000500000000000000002000000000000006400000000000000020000000000000008000000000000000100030000000000000004000000000000001c00000000000000100000000104000000000000000500000000000000030000000000000004000000000000001c00000000000000100000000104000000000000000500000000000000010000000000000004000000000000000600000000000000000500000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000200000000000000000000000000000004000000000000000400000000000000050000000000000008000000000000007b00000000000000040000000000000002000000000000005000000000000000010000000000000000d98577b6185cc64dbcd2910fee39419addbd89852326319dcfd9d95a8b88fc21c6b08f959b83dbd8447a10887e02c16d74a1b73b330098201a2498a6841556abfa99e44d25cc2d1fab04ab1a85975d9fd9e29ceac06eda30dd5bbdc01ea003e72d7c5d9991a7dd39267c2ddee5d7e0eae8c0f558b9ca74499c6e486536b7a39ec456a4125d21e201f3b4494befb24da613507a9f80fcd70d2137684d117e1879a5af810f2865635a302815d7881f34e4191d3a2ed9423ca02c20e61065c2b28d1d3e4370692d9453d0c881bf78435a586222967d4eef46ba8841981e0eb8b4cbf031b2977e32c2c543543cab7dc1f74f799b20830acdbea4eed88a16d1ab1619ce17302c48da3e8e469d4ed2d917440c4444106fd973ab68c1c62ba7f61599c35c35ba00d23b473acafc704a006a1ac44839c7b5e8d29ca02d9a3841beaeb3ceb165f4efee076531c6fcd080b946205ce7c5fa1115e2889b5dd010f92341529e008ac16b34ebfdcce9c3c2fb0a6ede30699629e261622c539e6c5dbe8521ee802897033f55cf414cf74bc14ed67752c2479c01c33787abb43df09cd9e5a5aaf883d7c1a7c5dcc78cd8d5fce0e087fecc8a24177a049c7b2c6c08c583a91ca30da7fe83bfccd92b63b5f32cd4593633b70384b2bf2b1bea928525cbf5c34e6e637b4732aca6393bc54319d660121d90362b0fd1603297734ac67282d1fc173c54f4f68ab239910b50761598242ba07a88e4e33fa38b52f20a659d7c5f983068f58befcf257ad590a91d71d31c3f917439781fd2714e314874400c1c0e0cf762969e7b8c52f4c22586e0a3f2762168df3e93f12555598d2920e19c222a14a4204e14783b3912e798db7752abeee51090535da20a55c4373509000000000000000000000000000000000000000000000000000000000000000500000000000000090000000300000002000000000000000c0000000000000014000000000000000b000000722d979dd5e54c8576eb797d3a4ce1a4dc36b67072b2e7a56c87edf5e082f504d73b1af146fbf88dcf3fab07eb7faa1aa21b3c6b0d4451a7e9bc18b983e9bc6f8912023409ab6aa0755ee156a27f2ffd368be42ae22f9e5428d72de3619082f52a630c980801a49e0be6616bb6c0a5de84e30ae1cbfa1d8c77ffebc365798575211ab09e34de392fcf92439325cc47e43dfffe86d7c961ea372025ed4b59c30e84934d68284bb8c9d745775579f1e424f108452c146283bc0754650c8a9f202586a8a6655948fc144944440df5d3c582cfb90d8f1d21f5fa7e1a73720fb1ae639458d2c0502f771dd906f5d29f1280b0239d09f76550bad390c13e92a5925e9f05b647ec80f8b4b90aa1b6a71a690c8828d6a5e4664b0104c6462ed99425a57e2c82bca275b949fdf16cf08d115c1d61b30bf79b18bca1abc96a3ce4979e81370f8f7205016dd0090c208a34884529719c31b00cd7bc6aeb2298b0668d660adffa95f3c1056d26f5d2b127262ab08d3e6838a5ea1f1556721623d1431149ea439043704933e8a3b0c4f832a3997da7dfdba71e345aeef40c111da3c170b4d573fe229e02470412a7695d4eb5daf48bbbc36ee0be1aab1794aa8ada11bf77979e79072cd1ff9d27212989f4e3f89d3ffcef7d1135f4cb8c7fca6dd7708e0b1d3bd8268acfda3be680dece2ce1cfdc68b65053db2b78036f0aaaac5138e3ca3bd438ec2f54dfc14fda667ea2d2b57b0a26b3263c269c88510efe4a75ec2459ed755120eb5edbf91a8b8d6d730a7f43d1101bb3d0fad63ffafef113a45d4249617b3941b8b2f299b34bcad5ec28be2948484ea366236a008951d46ad6467b1e8c64522af4aee319587695d48471017fbf979ade9d1589b373489c727dfe1d91fafa39895de7e1d28a573c67730abe67252cf3b7d220479a6761464437fa30c85feb514b61dabf23cb9381db507a5b9994e75015bcaf0fa9898715081369fc34a5b46e923be5ae6164b2d4c512c280fbd952ca05c99d3d0ca44f1ad5faf029ef94413ebb92bcfc6ece8edb8166ba082b22f9e9955b0028d4727222d0e811bad13077cfe04f78f5b5eddbffac8cfa2e3e7cb45f61c5f27156c2189dd3398b616d872121d41453d62a9a3137e8333b70a1a02df48284aab39d2e23367a5dfd2112ed2073b3ae4e941264b2bb75bb04001c27e638c13c4a74cfcf8f79602fd5d0a527953161edd86eed4be5a29a2dbc55f9971aa1e84838d8c33a0c72ece76da43333bcb88c9378cd788030a8a4327755b87786e47055745a1c9afd4a3ed2dce73933e332e65c3f60c98b2197b141f8a5337d55f8b397023aba5f2765dc3ca18ec933afcd0d5d1d1acbf5483f1338fb6acdbf6fa091875579e18b0b7e154cba9e5013d781daeaa2e9182986513d6cb159bca0926cbb35ae566c64292d0c4cf8e906ea29e1c71b4ae695622d1056a6d3d58019ecd728137c38acef70052aa30959ed45a6d96950caf466b65680c496cfc324e4e29a655cb37879e6856bfd7fe8c0053c1a424b2a496479696032e93c0957c9a00d2831dd8892798a774980806be0a6ad1bb5076c9f7ae53df13ceb052d4f26f524a565c582e23f859297e73086f2c14604b2f8cbfd9f5818b6aa16c67aa0f6f41d9447ad7b314006e195bf9b9b90caef2381617e7451c7d28a3d2f99449ac79839b45603b51bc413090e0fe3cab4aa81b664bf505ca5375068acc7a9e1cd45dcc8f542c5f87c460ad79d6b1fe28eb07334ea6a4f5433ea44bd8c48755fc37c45d415c08c4469ac5c42fca156ab9c40963d451978cd4f0c76c61d0879901593771b1916293ba820fdbbd54d2991edcac5da55a7ae21970893b79347724ac9ba6dd01ea5f9d5a2c21b05b754f6b33daab670ebe7d6dd987280f468f71b268c874340f2b54ab05056ae4121af008e88aae4c56335156bb6650506e3ffb743e355f31499c8bdd9ccda89b1e89e45faf10617f9362149490e0861bef50852b8c5392658767a430aeed49d9733ca003ab53eaddbc11f073a9f76a47128136127d4fcda9b54f247e589109f4aa34f4b6f635876aec1240eca8452ae9e4d5994b7dd36eaa1cfb20a5cdc1cc260fae19839d3beec566da712b25ee25208e0ff29c5bfc8989b3f83d3d8c57ad7d8c32dcdb092a16bf02a281d3e04ca506b77a10d62ad12bec469f2d2044e1a98e6ed5839532cef3112f6c582d01b872847c184b0f13ef220243c51bd5a5573b71525c458c590361f6470b992e1c74b0ee25474b93170dd809845c74475dad45b90e7143b6a474f734dcf0c024c41dc631f5e6ceb1a3147e6677872e4ef59c92e70836d8334cf8bbc2cfa223911c1887d784086a61d41492c01d18c1197d121e1c4df702813ad9a168d96514f70b378a5ff2d55ed77029ce5ca6b7a3f889fcf76b0c87067977002de735e88ef3c5b47275ac88aa6668e224875aec81daa5dc6c3a822569ce7532b4d93b2b36f01650f65e04599f60702abc2fc344a28dac2b1aa0d0ca8d26e9a7d4ea0806e14681ffc339c03b832369f98120f6306e420c139dfb79c78782a533ed9bffa955cbdd4876adb97495b5c168a6d4ec28f154169f04f87509e0e6cb29bb39352784dfa5112f52dc5c23a8537632a471e2b7e6cb47700550e7808b46fa3b46b8fa4ab77b02f5978c8bb28c86c14b70ed163beda2028316b06eaf9dd110d7dd53051d1a902350edf59ee55667c0c43da9f053f37cf24474ace26c53436efaa330d749c3d0e7b27916f6d09c76badff43e035d310d4fd17c8038940a7ebdd696daddb3fe45e4828387bcf96c7f65a57a8f39d6e7ccc10fe0fff0cb51f52d1135e8cb22fc869625db9f4d32e19610ae7792603d53cced03c6230b352d60d1a39547b9dc7624f5744eaabdedb2bec240940079724bda04fb6f738e621003ac197e695b32dbe9f008e1e7f6db586e052e18df7deaf3845351a304392fe34ffff92b020cab90f06d8c2dbfe7c46585b79455d0665aa721a6839bdae583039ebe6c53d8b3110783ddf44c5d072a97e6db83f9387506b6a9bf6a1465ed650bd320597af013436f854403560b41b7bd15283b0a0a52662921359524fff3c35532b414efac2f5134721ce33aaf2a81d25b69e7ede3dcc6a745b551e6a4e39ba28e38cb5194e003dbc824bebcb1720a84c13d97658c841a55a5bdeea6dd048e13ed4d8af536923e30acee50a5b3fbb1ee7b141e5363c7a2bac96122c733a3354fe928b545bcb579d6c7a47ca515413a5116b0524e33a647aa2aaa68a25aa0101d4ef6ceb201898dddf5d80818e140f80841ee65ff100b5a0c691895ee094207521f329cbe2bf0065ac779800831f77db910ce37c02575ca647049c459f83068cac4d6cf839d703eab2ede8856ec1b2dd96dc679c56de00c0c802bcc20174b09b01b8c8c1c85fe277cf3c8b5bfb02b62d775aef558c37f24e8e59a1afa6dd386f72d74202a0de87d75c0b11304bd496e197a07114f3f794c24b44250612258529c6a6e4ef2eb8ceebe0f8dde14a10f97dd0149be02c6ef6217d9400a7568e9d37cdad940860170d7d40c4b3a274f619a95bebaead9027e6c89cc13c1dd40b0717982683fddcdd5397640572cbdb8d7d4a7abadb2fc1af1a1ef431d1b9835cba87b859c363cb02fde962e5f43f69edef15aa0314ce2f54f928ea63e359381359889898f887afc2c1c546822e4e80fa202d97fec4fd4510e3d65a4dccf3c734c4e1a28754cfda0566b3a2082e7b794216d208f45f1c96243abf1b55ca2e5b3d6a39549874cfbf0e830791f4b990d99c5968e8594b42115c762297641b0f0be13d8f99c7c5b4231fe27d2338748b1f028690a42be378097d816c0e9712515969f24e7f4a389935bfc924cf1a2c482f61423761d2f1757519a92182b637489de422f7a154713b37947b77772df2679820f84164bc96083fc82937833122a427908d869612f809840b9e7a3fe4bde024587761e753fd6fdf17e02af2ebdcfdf5e8592274b9f07dbafb8be36dd266502dbf842277e5ab18781ba96efe23f31a9f1a591858618a2716730609003d1b07e1853ebfa120d2ca536c7c4bb4023a1c943e6d478483bbc8e55885f9080431234bda60fb3153dbfede2d0e4728eceae703873cf24f2f4d68c5f04fe16cd7a33c8c9337e491efe004e897fe6fd15ba40989dd5bdb34d9d0230e7d97847ad5160ab870754fc83712dc3192c942fc6b967e6dd4fc14baced08a8042499a1f40ab3c3d55c58269d6b198ce63589dd4fbab5974759cfff2c0f5b60f48757bad8f1e54817e2f3147af5e10ae3967190fb86314de743304c7c3e152b16eb6579b922e91d5d6af24d1fcb26f8c96768844569bd1316c40ec4afc10389bfee8e31ab93c29ca2985ccb024824b8f12484a0f357bbe70138a2521235bac2dbce436f8a060f38777a86d1c3fb958d5ae1065f22664cb98fb9c43fa99e4888a92757ae78bda265841d041a696c2526077ad08d88e9863aba9d669790a3d4f4d7f48554d0adce241f9e94ed1ad21a43ef3dadf4c79783d71a365ca18a44cd8dfc3eb8b6df13b9f876d4ed08827bfc12a39280aae9a0a0150de67c3e3f9b87efa496ba2115d00bc8543c60541e1fc5b0da74e2ea0c365078da042d086d5213c5e00dd97e21a5281ac32494ba20745ee105feb6be1b36b39804e6c785a85524c2ca8f0307d4680a90576425ffe4df4b03b081bbab30c9ccb4f84e73830f403d10229cbbbbcf693bb236bcd8b1960fdc1ec0b08d7e66a98b6c0f0256f5eb743c8801058663a91e17135511c8d463bd1a35ce2681560ac5e1f247452ec75327692b1b3964ee1a668f44fe3a288329322b85f49df9ab7e12ead8f4f9f6a2bdca2f1e79aacc73ca2bb1cb6cbaa786ff7acede02213712aa3d313a44b93edaa9ca1197da6bd444dc0591d23ab2b2400aa90a2077f949c998a56b4f0305433bf3dd90b8073389c627b394003b034645763fa57ce2205efb5db7ce2da552eb0909a8c3a7de5ab3fc066f8ed3b8e1e8ee45299bf40bdf830e7ddcf1f650f1b48b3404bda8e6777c104deffa7852c02eb0fa5909385ce40b7cda21638ace57b80e0968907ae9a24b68909208e420be598594a0aecfa7bdc8eeaaccc461d3e98f341ab158f8a5c9000b8bcf8167be1592dcdf3e5aefb4aff3edb6d3ba3a28ebf9f20604e5325a2bb5567a83d9706d8f7be2eb03b069378c62a8faa4f633aa422492d05e904fc64dc222d4891dbc02d85e526c66a8a3bae8e990c5814b36eb9d4816588f41e2f443f9db9fd18b0d8e23046824fd08e4bd4782f2fbd1e84cffce625f3b68d2fd0621738ac841e3ed6b6094a787285140b078950e74df607a979158c5994ffe96b445e54896679968de1ec1b6c275af9de099065bace6dc45cdf5f2fff74f2ecb5a4ccbe5bd8a569f9630db3ce4200ec2443f5d745bbdaca1c58885b534a0c50aa246d5141056c0ccd556343d925df2b7073d2a3c1be57ce9b37b09aa0a41013cb460a329b5ecf98b484e28bf238218a4b765a7559223bbadabe7d4626deede95bb825806baee0cc2f9e2f56ea1408d4a9049442ed2d49a3b32790a28d5d1f987872e7de4fb90a18b20f32cacedd04fd7e7638c6239e63e1b8a7e7218d1eb8f249a8a1499b026c4bb7788b640b18044413d04fe37fa4eff349f19028fffd7fa44b40b6999066f25fcc639f69d53124071b12c9b0fffe717297d8d8060049124110ac3e50f20e346a36c6a37a9182ef37d22b0f8e6a552a82a7ad81f66f32a7b04f805a326158a0b6174815c87f30f92f87cac1224aa8d26a397dd39b74bdc520f1dca13bb12d22b73e169897066fae394505ebc953a5b6576d76691e369ac353fc9fdc61cf705fb6150aa7208cdfb2d4ee192f99e8c74aae5a7007007dba255eb9cc4d958b7da1ed24468b0269688cf080d9371d7fa5c12f41f2e602470acc9b2d6ecbd4daaa33ab340c8fdd73f76058e93af372e2461509deaac4a52d900eb8022bf103090655f59bdf24683b2b40f0838f1fb36d04f95eaa109af63aaef17a7f057affbc741c1278e8bf0b6f163611d20d9cad4b60ea5aae562e84ad56215f47695fac9f78b869b32c6ed995df3303534f8e94c270ff396a41acb98235bc00c6971b59f5620a6e4dc9e9e06e5b6fde65061153d786e7f11573f0d16abdcaf2c9d3e5b32ba009e812f4805ddd1b1de810ed4519aa8a507ade0219036491094d968a0db2443d3e44c7bf30f9264f448913e0b1a966ae1718ba3389ec1af9ed25f25feb2d319db0638f1bfa4da93bff9c87e8d53e61c6c91694d3f0a2a677fb4422ef606ccaf2392a4dec44934249cfb05a4b586fb4c0b91fedc41ed1ee0aebb7ea27f1efe24e6a2b822ee3089b34cacc947f61d83b2fe1bb0214cbfcad927039daf7000a8850846b6ad65ae57e724a160c27067e13838d365580c65ed5c2ccee35243a7001cfa2b988c9fe2a540e4b523cd519bd9733c133a46adcb482fd4bf8c069740cf4aec178a6c01e4b8d7cfc8b16cb772915c96a8c3bd376668006f53769489778e92d833a7aef8a58c1cd01049de393ff81bc3deca0695ff9c60e134506e06957d69fe2e2ea8f8e16ed176c374fd4f5ff0998ea6be056df97c36cd8c419cb0d4b33972344ac1bbf8cb93174ebc61adecef5788550b743b5d061152efefdd8a2409e1228dc3b191ca325824e589c0f81dc8e1f2d80867aaaae7825245782ab32aec40684a24d40ba96b1b867bd9c7bba462a709bfbb80b99d675ab46408f0cc56602c2010e0b6a3bec2ec9dfb9f258409a3ce529e09eac8818f093d3ebddf115c71beb3abd98fc1fd02925b625434874f160832adc000c6fa3f64bb5c107cf57a0d58d750c7b8c73d3064456060a9b710080cb408edd6cc86b8ecf4ddcec879012c8391834d3f914cdcee008f854188d6b5d550094de7a184f64adb85b0d50500b8d7ad6f0352c3c0b81861e8324c3831692eb5af7efb2c7bd1389d34945bcbfc7f08a1690a1ffa055da42b0b2fa9c26e6910641b97dc732df919b596b6811fcad7ae4f253fb8f8ded4e4eb2fd031e355ae2a93b322c90303613e5b997b7c1d1400fdd6d3b9574165e3e15a9074973a827b883116f62c25c0b9bb3ade4b177c355f72bb4d1e31b2e7f1e46275fd3967a9ae462362ad0259f5d9826e0c2b763c6cf01b4b09d87cef4ebc2316277bb9191dbe145a840c9ff974c2e0c13a067b62be314583c41de846d7f7095146c768e5f09ec3f53171958b0e46c059eb13a3f5d69d5599669fd3f05d6bd0fb3d75ebd6f3defc2f8b0cf8322b29d1703034d8c207477a2e1b503786dd2fde37ce1ed8e78bf2fa9128e5d8cbb28c84e0ce8c2d095b2fcf456a99dd9e1410baff167ad2a8052e0da421bd9e8d21fba11aa4774fb0d3dfcdba8abe589d295ba8dc6e1073a7982b949d7f898d683ec753112b7c5a4cddc28c5f184dbc94960ff681ff24b9f89bc94ea14d27c5259cc9773bf7f68f96eee669831750d599e441df9af7fe16e3492bdd89fa764e55423f41b0e723de2afdea6e526f600bc826fdaf67157f1f5ecf9a9930359ecde1d14988e485b5646f2c31efad6665a8b20d676b3c47f84a1ce66c1bca7e251986790accaed74dd791c267f1a1d504dcbf7b93aff79fa2792fb37188700297150fae8567dc5969ba7d3fbc755c26950cab1c655d54e284f2a1223cccf6868260c999aa66556f0fe8c68bbf6c65391b52667dcdaeab6a2e00edca72196483e1a16605d32160c3d5e1908a104f17fb53547448a8dba55908ab6b642700c297ff942d72d605a17934efd25f48948227023e6750a904e90dd590accb5a33d28889ad7ccb643298aec4179afd9d12fab8a26bc6f3f176282930f6c59016e0f10ddbc1b849b6d07dc15f0105473f34edf53e046db64ad61a6aaf71fb25948c317ab6761a969700278b98667801a36355395d645504988aca758cb8d73ce144aee6b347d6f1798219ba7814d41cb55b334c8df46a678fc32dae380e6802730c588cf434b1a90fbc19016aa2e214e42259a7bb6b18d791d7de543f2c3422bed8dd3537b88cbb696559c36731c0b83a7c727a7ea664abb056c74992b6874d01b76fcd0bd311b89f927f14df44ae4be51deb6fb5c0947bb1df52bfe2f4e43b1c5dfd56a8451dfcf49ef5369cd5d3d50804d4cfd6a64b9c50f5176c73d9116570823f9ed4b1bc7ab5343a2c5f8eda5c56ef9f223fa3da2d797ac1dae3b1164c91a8d7e936e11ca9cfd70b63485f2ee4706cd666e8d8a2760b4e6aecc3a16a15c60ded0e93265a64bbafb99554ca012dbc337a3dec508531f6154397ed7630fc2580cef42c4900d4f77502bf7c5ad62a4440452e20b1c91388149fd203c41b7e01d5984e1b0695ecbd1d444e6ca23d7c0f590c6081f5a003b0888f05616424590e3ba5032b9a045f286def0846ab2ad07c45ece865d74acc4104c94996b9272acdc3e02884b849d62bded5406ad2c044f9900fb8edb03ea9533d744426db7d168a581172bea690ead30a1db88d248499c34d2125328d2dc6f3ec217657349779556ffebbadbff2a20cdb92f722bf0ed77c93192b366dd9bea0f9c306e7def99d56917909b15c3d8365d4873946d0ab8a1b6b218ca6911230e76d54eaa32c4b7242f9d6e4aee64d63437eebb379f640970e3a1b6520d22b2537f6eed22e0e75707fd2a9364e802fee648922d3263a2eda174696d94f5aef57bcf491441965ed2a2e152f087031107038c1dd6e4442d3dc9455ccee25bbca037f9175bbe63c4fdaa7d7bf091419c07842ee4635d2a394279e48d67666487c5d5c5a1b009ad3d4f877f519c2b58c4394f76d2abf6456bee3c4f44574cce7a1f0abfa7b4f57359d808099b7818a90eea6edec82b45b28097cd673c2893284021b0ad32a454f64e07bfa22ce1bc5c8a09a6042b7692e52e49c6bcd63a6fc6fc9718de87183f81713f32326ce5c2efdbe53b850e601416bade33e81164882d66e2cb214e514c63fec558620c85eb5860dc6f870b796926c8c588a06e9dab6ecb655bcaf65278cd43b21664320c2694287852d1bead050b311660cfa01be97fb7153f5071e558c1e59058be0fd17a286719526358ef551c9e55dcc98ed19fb4f5e014b0889bdc74a7a4c883ae8a66b50e4115cf9e0572acaa1ede006739bb0c8827b3588766381c8407a6f81acc784cf6e2d119df8f70a5cf6c291e7658bbc5b8cf8c9a58f0d538b941e5ff9e30c9a6a5f7e32feeac382e8b2632c41b2a54b0253cef3af15dfb2e94669a6bd9ab6133b4b4afb763d20f69314a6bb69ff54be93272d739488280b1c1e69a7532d6c3e20a47c23a64606cfde66f2b69868441dc8b8179190b08ffb4873fca6d4cacbd0e4b1a7d9250139638d17d73211449fe30bc1585e6a7aed5ed28bed61ba368434846c8dc2e0dee8c85d066006d155fccce0aa0795c0954ec6938ea0322fb67ddf48ebc268af34648cd24368ee0910162b5162d2104e635086847852386da95706a6984546ad47b03762ca66066b0d518d951b4e2639aa0dd9bc7fd131c06606255da4d79157156ef997e0dc98efbe5e5a344371a7ad86e227a2c2987b2ce5eebe23582b48b9c0414b06d411485ce8d65231571127b8c7e1d4a05f17a629511465aeebaccb511f651c1108df86c1a39e69888d5fd383748a5a3b1913f9c419b3206474d74f1d4b70178285d4d815644ae99c6e84e808fa81a81e63c07830bab9578906264799a9909a33ce586bbad0d9588883646bed1130aee1790f130b7598344f8696001bc69e6a4c2a5d318de38d5558695c8d95f1ab8f50da61acc744c57babc2e7bc646824992a4c5bff8a4dbbfcc96f69eb357826de35bff21b93ee6978a9cf5676ef0de3cc44c926cb9980863d2d6daab9ae04a3b862ddfd8db4de312d8537a489c09e6f2978fa52942312c4efec387449e7a27b1a4213052bc1ede81ad68fd6eac3780226977a14396678a4ca343ba68966cd0dda61dfb8ccace2d16c539409639f4c4de1d3051461bbcd7dc979e1773c8f4cd422e428ff382a0f810bae2069efd5953677c2e78bc665466a5cbf8e682d4757adb99f9f219a0fdeabb4d05959fa881fe7d94480edbfd554f66e8369d8ed994d462a6632cfc6979e21875536ed0549497d278b7e1844b8db894480f23a5a31d4ea6057d169273dbb032919eae47b8cd68d6e0a7b11b44047f6a29e736d79d3145278c5d4f558dce237bee2be5a98e538ba66939d43eeae8844d12f8ff9a7673699b7c825905dae39cfed4fe5d5f327b3d34742b561a1dd63e2e3de6eb1739e2b0e828d4da1ce78048e19dc7e190be621fbb6a23a3749f87cd985bd858d5b9363343048bea11383f615efe0635d427d33c873692ba0586d4dd2bf6ef8c4b97f138a291667316e108f2fdf54e871260b13d70d21405d1fec98c67947228b700cbdb9b081c6fcc596184b2d34ae7b129c748d2aa9c21ade63383b75f2189b03e25dd298a032f24b3d43270788ab7254e7825df12610ef5bfcab386235beae0cb420c57bbb77ec4ebfc7512abe1c86d2b369a54b25cac60c24937cc76f9791d927610ef0d6bc4d21459bf66ca5c7886f53ee78269de92f274a33f4855e882663727b6142fc14358f6e3bbdb2d91b9126df313d30bb1ca3828d9e00ec0eb7b64faac0d3311aa79e254ad84db606c2c1462861c9cac1fa0924550da49ae80c9270d089b978ee3b1b4d0180e87233e15e0e9470d134cf0e84301ead2f2d80f83eb25f15558a7dee2766ff46e7cdadfdfc132e20c539749317b148413cba405abbf98ab8bd956275ea2287900f6cd38e64ac573203b7afc8a24270b9d592ecdd1b61d7995f542af76762b748f0bc9dcba2e7b19d8f8f5312b3205d1fdf8b2f2deb69e2686deb271de0142506a242054e7c6abd3e593bed4f7c4ab547b214d9d088847938220bf260351a121a5b69497cbc69502ab370db027c41e6a363cee2d505aa47f6215642ff69de7f1c9931e1514ccd73fe0ae2a3fe89bdb9fc0fd41eb2e526004c24d4baad3b0d9ada4ec470809cd0d282e201c2ab8826bd765a806983bbe7793082a48577d0d2c0866f13adff671d68fa1a22ebecb9c04d401c3da4d908ad259344b55c63d6fbf50c59010f43581e39c00fe4f43225d95153730d10c6716c0d6fc67b7fd98be82a6604e7923e8df6ef73b960d0be7b668913e62309d4b3623957f26d1f52fa5db22e042adce3957d94e842cdbba0eea21b7fd8e505d6238cb6d97e84f3a17592b0f50843242fd35014ab2e46f5a6865ac664cf6f6adf5425c6107fdcb6156363724dabec0d9ddbaa5c8252c61316558042e3fc3dd74ca1341d1ab664ec03d89ff1b7b7084064e76453994089a391f4a87c14f17bf0b05fe4a4349f287255b2fa92de6b169d574af29a805d6adfc0d48f56149b03759a90bdea1b4ad4b82e6b1776f2f0729422bad7150b7e18f0d3086221c848d0b0e01ed953e3e6d6b24eede6d17636e2a08f49b3a7227ab7ec89042fa15cc7febd0e23e7b09436b28ad380705d4d57e9ec378b5e8fb8d6a28b6075ee306875f5dc20853eb5d2b06a3bd6c4597ab8a129ad65b41c3608fe31797b823ccf4353e1e6cd6fcf44fc0710d0b1340c42b72725868013cd43fbc436913d9234cf851c89754a2778263357be4bf8a71214d2933fd1d6148e8c7cdd07578b850ea46ff3a220a8b1bf5108e7b5b0546cae7bd8a39adfadedb06066852df6719f03a13c5fcdc73da1e165a9c286aca2271a2bf9fcf1513a32e9f6815042272b204f76c34bb0375b5e9d22a76f6e644d3fc68791582be4c3ea129f9288cf93ee39dab09280c13b4ab05ab668ef34abf5e2f38c7af6c4be2dcf48bc564106b4ad5a29b51b40b3615c58358d4528afdb48efce4ea14f2237dd4e631188878da5c0d7cb295898f9a54d8b8bda11a528c074e0f2ec4f5a6f21a54089adcd74e8ff84362037d63590ec7d193e22aabcfb6e6583db2cdb1aa990a7b8af2e5b0b009d4d9409a5294ca6b782ee487e98b0740fa7e57613b220315c0e845b3e98175a2b5249550389a8565f56d011ddebead1e1b0736ef65b4dfd891b222464a1b6630c843a93a8eceb7efa411ddaa49474f55fe3f766a3cf05a2bb58f01a040097a409c9c37116215eba665230c202e15f9e1cbbe836a2f43fa3f673644061db7476293ea7a104457af415bc60b94d5f0e9681f0bf0742604b86aa9b5baa1b4c86b68dcbc1826c2cebb768224a5050f09e2e649f9beb58557caa478938909526adbc6bb98fb40696df20fd45c3fef3c18121dd83bf3765bd85da177fd4216fb86531420bb7ec67c76b49a4021a8abf572d588956a9b34a6a22292c1a02c36f5d784eaf3b7f8a0de896d86c1415f6bce99bece0a980f776ac7264ea1f6dadac26ec4617e6c614c9e3d8d1c16a9a6dc6e4e63c1c9afdfd82dcc0927f6a09c71658d1a6084f3122e6ea3e4fc0110d132c0ddf8710ba4df7dcfc6534c130a27ba4976945f423d957408943b161385970d32d4c6311547302e1cb43a4fa2f12ab1d50bccfa49208a9a42625d18a798091e465aa70219e6ae7a15a019c849cc3ddbef64e62e750e5069abaaa4d9dce82131537864b2a3c8a12279de24e3af5becc339aa5178d0a0c4e8cda4873888e905de5c737cdf0e34e6c0c75a773112d71411bc95ce87fd10bcd22056100b7e6f62697a75b18a9621f3452e388bd959e001e7aee275d4bff9d9ebf878b91ab1592995b84a42505ffe8b6125b28d8137c21e1b62ad76ae9f37d49fce38653333f07a1e34616ea6f6d5050d4c73acd3a2e8c48790df5f185e9bfcefd710d94b3b6d1d3d011b79adfbd97eb658433b1ae571382a40fd6d22eb076ec542d425147085c1c23d3d7cf27039fd4e2bd93fa6d593d8202d9412db80d1294048afd6e6fb39d6b8fa0e8c805a9f1b9c1a79c84ddb8107032159eeba8b36657913331f2c4e13b475fc216ee560fdd3ac1e0390f922135dc610cb2c59beba6ffb42ac2588659d100a89ffb582613929436d6bc0c534fca7ade7d8ad4327ff2159d45d23c6173de9948fcd376b4e50dfbbec1a57479dd7a8e61b1aa6ffa0257f2fe82166dd9e6f4d95c235a2dc4fa4dceffbce6d0c4ac93a99800b0db88e127686ef4896f167bed7d8bc10c571392bc5b5971835e8a9d9a2303dc16ce7cca850d54e4c74ddfac9d646a94798338897368159403dd7c7c06472c0022972752168028ea83c76cc78142edff30cef99e8162a6acfad6909a47ab0fa1f3e2949bc26aca77fe620f3850ef49109eda694722fd4635443207b46340e1d705e1d9bdb8c7fff7584d94bafa09e65b48cadac18734e74c2d4684879546cff79112bf222edf535297b27597510edce96b0ea52ef1ba36b20af9fff172f7c6fabedca733013768dc737f58aea50befe5befd0d91013b7adbd4752f8c29c6d6a8292ffc2ac496b4007d5dde0e2a6f8a431488b1d79d6468a025927effed90f1de65d940f6587d697b0ba319a76f5081f5dc0783d4f3738d70f59876efd7b3aea2991ecb6c8726b1f38df640f210d318cc148931c395654b3579fac4435df9508a973af194dc433de5938243ed31d93969a744fbcf50c6ec829db3ed1bf76b0855cf110ead1d0fb1f4ab93ceb5c4721ceadbea1acbd4827bd89619cacfd5a66c6cd63ee0c2c75901855e4aac2a8f75321428a7463dc1b4b200a73198bb9e49bd12eabfe324ce7fb18e175f9ece681a8a09cd75b41b9face9da68f904a44e59439fba0dbaa3bf9549dc2d9f45f8b20f86d1e5d2b53e436252d12bfb26791f0ff7ade24e1856d59cd12f52d52c300293ba005207cc78cee28cfc13f92c3cb7b111fc95c02c44016ec5140d492932ababeb6d51c09bd8d96be2596cc1586ce334f0696187cdabb2e458bd109b1eda655d7616f183d69c6484ac16bccafba279003ecb2ce624ce63d696f219024c7b4af261d63bb5822b2fdcfba246b036d0e45a4949a6d278a17be94c590cf59eebea7e17e1beb58bfb52199dad42627320f1da3fa8f725e4da4114cf3ad614c8adca4327c41288c5c7dc3c20945dd5a049959e46cb32cc267b2e643081814d74f544cd6fa9b981d6c4acdb77e4fe7dcd08c34e8ecdaf9cab2b533b7568927557cdc145332f8a91bdc635a8067650caca124f72b5b7c32f794e2dcc2a1487d4ca95a7866446f80c2e337d0481a0f037a92788b94c398218fb4d5f6d37d0ec05835bb88117d96ee9b67c1976b2f7b74cdd428e486bd8550b9cd174b217aa208057eb9cfe97ea815535fff522c01c4dd6f13a8f7f1f3ccd6135728396fe24a894e088b9865157a2a8f14d3a458b65dae05b00ed09101e8d2f0ba94d0f6ec2aaa3dde489a73a8d3f99990fc981beb1b963ade730c4ece91b8d16e1876f48550ef7f36669b7906652f7261699235628003438874103cb1460c36065d7d47efd667ac3836d652b908fb9fa64054fe76ea25c56fed7b43ca29b27e99179fb2aecf125b934097d13a174fb83b8b0607f3d5f97f6c2adf03b37d11bc94bfbedc9c530794faf7e16e0a79046ad7e3bec0cfd655696547e8f4ab37dbf71abbba7ce728adfa155ae0cf260497bcf65a2d44ac55db42012631765637a14fab5a7483c379fc1449eb20002485d8cfc703ec7df8b61b5352ab44f5e149e6752369a3dd357077ea9aa2537c648ccea463837634ef08f08dfbf338e47cf66bbb1a557b2a6bfeacbd614cae02d2905f499c4a7bfcb947327cce4ca231c959ff4419a7bf069da930dfce29830d85b0124571ceef12e6a9668aa57bff3a05893b87c3757887ac49749cbf1860f9079c2b4df50bbeaa885346672fc5a2046a95575dfada5b6bc0a42d9031339b72503dd774001ae930ccf2a1b627c73d772e27088ab785931721dc84b43cbd299629bc40176dd748b11d36a606022329fe7f565d7d1b99a7b0f5258d1006972a701776afc01ccacc0c9dda866f2393e90c8d34757a9ebd4c6b029ff2cc968c0a46f899cca2a0cce240a8321c0cd0c140807a10878585644a8e82b5d3ecd7689ac396caba2569fca5bebf16826f5ba51eedd4cd159a2b7352652df564afe93583234f6c38dfebf91774d73235959928fc72f2ed7dac50b50b264b2c941bca1e05aa69751906e11b368d693234829d4a227657ae947119ae25669bdba44142663db1a3bc9de567e848b5d26da98e2b216c41f7a4617653ea72688e8bcafafb9da0699cf3cb4e58a1a22d5764570bdfe2d26573d569b8a1dac30739b4fa3dd55856a43724ac174ea6d8357a364498e10c0026016a673fd80b01d7be6f8b9e813e4aed49c934d7540869e7dfc8df67c63139f4742f30e795bff46af69be5ef5b7f7ea14068052a84f86e8b0fcde11440cf8c0694db3e60523a4156dfcfb0407dca04853eb0768c01fe41b3d2a28fb81db2fb37068b024373bf6bc1e557d5d7894f148270331db8a418057fd123a2e17342d079e00f13f8ca4b2f3da5e61420e6fce532044062a21095cb678ddde2bdd30eb5e942e6e4baf92e1ef19309a9b15fa955b22825a3df8586815ee202b4086dbd08012c18bd00b0ba8e7a4c760573c1c4323cd58d1c71548036be76c545253c78e2c282825556a8d79a98590b3ff2856ec5a078629f5e6db3ff675510a56d7f4522bc09a09af19d5e892b3831828fdcc810a97741e42cb065cc87a22521a25eae7d243a46e025b16c83e16f18019c049fc02d5ff638a623032b0699b85ad99ba21338511a7777729031c269ca8c2d9a8970b6b7d45b2b3a8716d52e12d90d1e8c8259dfd982d09f92a4820808b27a4cbf2f8a8a146aea3fbaf6231524ece08dd96caa731701c1fda19e16bc1a669b98ab0543e8a68db34abb3d58affc44592a83575e163cc2494344708604433d7da10bd5b20e6d328719dff533e36f7255aa924f76be046fd7596dccd1ce25f5c5e47b28810e5b8c6b3b5bf167204b941588abb4c6dd275c2f208beacdd85c092853fb2dcb064e36b1cb815ad674f0fb5d231e6e826d98e25c0c3d3c098106e9050a231ec7158bafcb182cebc87eb5e461ec5ae513445dd14bbb2933336dc929593c052ba8abe42ccc6b5067a1b5755cb0957d267854d63867530758415827af55c7712cec9f4319a7168d504b0050440778ea921e03821dd76b1e522dcc083d31f07fcd1d7d67beea98f7d502ff15d6ee308bbe422b558035859db861f2a86f505fcabea4c6007e0a29f64701338891b5a1d606adce014cc334b65504dbd862e1cd8cf705abccab8bc20ceca68c090f6ba388cfd4afcd818b89db687933c57245898f4a29bad45be4a54616f6f60fa7383981b7ee4db788f09cff0387c82366c3eb5d5175dd94aca99fd56984054b38889e50d1c7cff41e831bd6125c77689cc2398b2c6ffed5628b5242e8e2c596345aa6e47f1356c8d0cf9cf572a75fe87e8305dfa23c9e6cccd77ca141b4fe080f44a1c22975f660909379460c748d3e3295381ef2f1c04fa28410ae999d4af942aa6f2fc581030327d0437803ef445844d2fb459a6758180734d77c1aa52f1085146ed9b4ca59f863d3bef3d94568f96bdca754816e4de82ea46b873931b473f7b4688066e3147ce26850b6dc86f9a0ca944c53b99acd6f061c9d409f66f4643889ca9d6fe0bb5541fe270689cded8d02950f26e7b656bead3b9b41d88548054e014b773fc993ffb8e08e418ae336b79e5d68c81ffabf8573d1724764f1cf42b6980cc9be6e0d63fa41be908652dbd7dc1cbeb57ccca861b2967245c42514b2e352d443f702536cb7548a7f9516420e600c003a68f60050536753a88ed29218e896f0e96369985c43c1c782330e3345d8cc0ab45e32ab08e2291a6e8901653a355b1c18a6cde50216948b969919ec1272bc6a09a9333bd3707c62cd75730c1aeb3d03078050274315ee76de1fd60c5e730b1db5ccba97a438cbe0cabc118d4816a1dc8d877ceac2963a27e5d8714cfe6b9eb41f360fa578833cb6e37296b4c937d18d905664d2744daff3381956a473e3ef23017392276129a4257cc625b202679357b38e0e1b627962a3c12b32af72a6b96fab09bf39874da4d209a30a9b148151be87d4fbfedee203ed4a2b6ee3dbb63e4ac4724af92d3d53a870a039a5ec11971d9b85c8819ae61a21850ba5a5dc9d85dccb4dd41f671d2145ca0ea05f0fd6fb91cc9222c9aef6438ff03da2e1518de9f4d9de1b619359576ce94f65e58be31bc165ee1fadde620535ec1d6f28b074b3833a886f41f7550fdf20881010f32da70ee26eae78eec3ec1f7a987467e223126a19a92dc89616928bbc29636e2252b3c44d956b4aa0328f69a6da538086e1643418e831af06b9abe452b173e051ba392998ab7a9d0c66be3393cc826964259057a0360c2c4caaa42deac4ec20339a2ca69e0fe1097f239c2a63809e033d10c5c2bd77643531a6d3e3630060dbd1127bb8ccbe71dfc80ca8db48ee230cb2879e40104bd690cfe689bbbf21d4ed78a8adb9ebca6e3112d618f3f634a63a7f2292f4d22972a18e42ba23641d11e3d251dfff5f04cdc9c2aabea0040a7bec5825f8248ef0c79ebf8bff08d267967cb2d97b321a6831ec7beb7f7064bb83b4dd2fb8ae0c9ba5c543017e70f4359e067803adae06fd175024b4f0c2875c2154b85590d01b8ae70330a38a817668e51738c3d6d8dd2ba9d812981375e4f539c40dff8d70a6f88611b5dba599ccfb0551b20c77dcac140d32f61cccf181e42d5796549fadad588f21a2153066f7ba52b09cd03acfb578cce3a50e3c0b7d46633c5f53f26a7c17966097d6f75a921a4009a9b60003fd16535bb012c763a710d6d136dedc56cd92429cdbc2ba2e91ba2257e4a86d33ebb38692b256f98a0a04de906601bd688ae45c1fbc3c77b85d600fff2207cfe5d530500edc73be7fc97c9f48501edaba5967f15cd3e08cdfb0e77b4a33e8296c27862f9350dafb1c99b91c19402de056d813faf1a4ef7ed707865005d2a25eef8d0d3f7999e5562f52c330ed241bb1244805145a868fa5d2519a842121c02ff09df18ea34ae909ff452452909abda7ad4adff96ff292d3985ab188f43764986c0e105638a7e01daeb0d456ba05b95300a885e564a65210621941a8dc0ffb65c9914526cbafad54cedd1678e78a225e34f7be6a48d8836826e2bb5c8f82e9f0d48064ecb027bedc5b7eb6acf952bc48faff0658cc2266f78c28261935996fdd6de0dbb8b835c61677310977ac52036285fb4b391e8863d062d5ff3cc0546db2455f4e01ae0e44f33141acb20f564fd07f17aa4307acebfc3f8eaff85d133897412e7a9b5d5e650afcad07d1903df0920a5d2811137904fb611dc4eea4f2eb34ed011ca8efab0deff1c9be1ad5734a222192f935ebebcce2eafa99ab82f50d83ea36b0a841a29bc21ac247d54e847855c2af5e7452f530da8f64fb512899bd609bdc567d041790107d48097112c9987ca1992a5e8baae7f4697e937a3160002e3c557af9d1f04b7213c3cae47bbeb93fea0b2fa7d511d4a8b5d66edf15396052be876ba8e4e101a396a8c30dd9b0eb8fe75fbe66417f073a98de1e8f95846e41c283cf8a03096fd7a661531c353ca034171d3c012dc7d9c63fe0bee7619ca1f4ab08104548da67fc99ce33062077759a77e3595b9ef5bd58aa05937acf18b087b499218d7126e956f4299760d6a2a0792fac251dd9c8e36069943006064b4b3987e3d0cae5a95f8a96a16a932dfcbbcb5175343e96044d209f90d5d7f3a0365f58affeda291e1fb497e8019b95718b4dd896c821c3a49f7c04124937ae2b1df1cd486f7107661c42d463608c85b18ee09cc35c048e607f42c0de2f708381c89d01b0da6dd0279f38480f8c6d7d28a7ae9fab898fd23980726b6e1c662a5b26d3f1d5052e7d2cf1f29e97aac83fc0ea777ee72f51384d07cb3ae93d87bc6889867ebf77afc24338f0d5ecce4eadae36f8d0d417413bfb3bb15a8073d940095e5b84f5d5adb62390ee58d1e6dc66dc16de1307eef4f65debdbb67a2f1ea312842e2b6392319d763e6f300dbb70b3777724cdf4d7a263cb4768af95eca66b09736059ec9cd2d28f7e6cdbf874d5cecde46f3364245cb90935a353dc20ad16e9eab0067b8d3a894a44ebd9f3109a98461feb5adf927ac8b6a7f4554a8595d27918be30d8d88de7ae81278d50bfc0e3a2a86732d61e7b3bb7d6497f9320c2d237fe870f9c6d0652d8d0aefc91cfcb1b20f4d30d85d9fae06884bb8e9fc1920df50b5f697007136b0ff1e53b79564680e71cf9911bfe6513b36bbfa0a7c45b8a17f97b7218dcbe16087bab9ea8988377463a04a0575734bf7c274b7a9807c26c12c042fc57afa819e03921ba67943f2686c4dfade22adee595f5fbaf3fbe995cc5a80c1f80cf4aad855262c6900c0047b31ee8beb1be1d97691cf1165fe147375b25ea8e2c2ef66d786e3c0278e71c2db1f76f0da7b60038d6c4094d1020d775b5489df2cecb5dceac5c07e37942295208e3ec183e6f54609403785d622d0d503d091a0b68bbc379e55d43781dba3176185bd3fc062a5e768209bf614ee720db73f574b900a427533dea6ba61cae03b6a6b072a44d2daa0831c374013a8dd7ab5510f25b8be041cfdff52f1b64c5145718d6a1134e88e3a115b09ac2cb8ee36408511667cb2766f8bbeea5ad02152b6442d27040bb42d580279889092a4b9394a54cf979d340972f3d09d354c67a02a28f2316a839c6d0d845e84019e68b6eea646ef1a1a3db6cea1b0e6d54afdf0507163cd0be2be5a749d9d9eb783db59b064cebd9376c04580bea84a039e5ef0ec54bb707acdcdcde15791744683f65c1e0bdf1116770d3e938782a0efc5ef5e71a71f95de25f25fb3c05183f2cb2ffe2c3d7b951434139cdb4ff839d0f49e79bcf2049edd2a890470ae8119f75b1ba02bb0eef74e5ff04d4c02624513be53259b3ad59e030aaced8ad22e47fd93ba5eedd1224c163956415cbbaafee12b3235ae57be870b83817127178349d776e471152327f8a4c65ff1c98fc85675167727fa8d5c69fd5f316e8f93275f73c98c7fcd0a1476d72e1bcfe6972e26ed98a4c549735ca82bb956e5192e5d459ad70e4ecf48e6c85459e594d5316ec47c007be4102f0273b68a8bb7f8635d5e3a4f2b1d9165fda3eaa6222385485dc138bedbe2412554d9b07a9ce10837badfebe91b42d012925a058472c72112bfd8b01b33de3050b2a181ceca576d8c7386a7edc69bfb6ca8bacc3215e6da402f0a2beb5b1ee7d00c665a0f6437a76bc5d39cc1810c5e983cb714b45038be78637a694dca762e88c5a226af3412bc788401a2dceb962158224cc9d111f7491c79dddc4ce6dd0df903dbd0047ce03c88d57401e0aec370b7a6c0fb2491c40083d7817ea9983bc50babde2c58b665846ef1e59146d5b9239c9420ac62b7fb140ad4eb6b3d58762b844bb827fa3012c3e4af2982a4908fbc1a34072080d7d06be068b9d643bd1d5c3f74bc0c7976028d2221035c684d3bd3e567d165c802e8e75bdeb62dc72462ae97dbd22290579e456e62d848d0248bd4b55df70e0cfdf44ec549232e0f4863e2b71ac8288d69b4f4665ac87ecee347e5931003ffce93c2565194ae02bf6ff1833054fd66ca386f33e9286264e0eba4eef54adb19a30001d3d2e7c5c28ed46bf668a89e187afbf6f055f67533eb84fb2b033ad3ee219acb01baccf13dd4290912b5beab6f300dded63bca364f9d51ea2b8b02353f72a56d2f6904d5d2c0ede1405789e5dc70d7085f2c3779e957834503095c4400f1e81a1c480586c05ac4cd787b1a94eba5917edf65e5e7baf4902b932cbb58a57ba025530a4ac154b11c982d399b1be0fd34f2d2405fa08edcce095e4da5831576f33dcbb80e86d39758dc9c81f734c45c584d7ea50ccebc2443cce3742d849783c518223e34a4d9d4a707870f5e803e95da0c99ba18e3171b1b490a06e2d181f45e3c9555655061d8f37d2cdfa743b5ccd4844a37987fa539bfa20975c609b878c008fa30b52bb74eb1190db384b1489ac891f5d0e9ee3282eaaec074765aa7161d9519ebb11df3ce6b0cee9c6d3f0d2a234d1995951246c4b44d54c279371d3cc0ecfc9647bbb27b2837212b8d2779d1f20fce7a64879bd73fe326698aba53e103c3e3cece846a8cb96041e8d37dd59aeb84e29255a54bffebfde98133d8071f4219644021d9266fc008e53fa01a088c72ad994f4f875979b5d84fce279289b488a84a7a09599d2fe5740d1100f9ab65a8f65fe2629a926636d1157cda4aa42b758777deb1b180cbd00eb1394fc4fad1b86eb14e3a82d285b1143c29d9eaa50073912d49c2f2685e61f491696019a836aa66519c917d10cb7cc0a4eea5ac05ec0d918b9f15cf746bed7b6b2b8bc2982430063155bebda2c1a67e08f3142ee3f87c5cb66c2c96330301343894f016b59aa0c50af58b2c479d8ac0d84ebc02f7fa302abb6bfdce9e74c60213cadd5ccac5fa3caf76622420bf7561db060aee7f30078ffd61a1a86925bb9cda4afacab8de496c29416643c3496a1106305f2d412a04934134cb52924f020747e5b5b141ada1e65b566703f8f9988a8b4125848d18c3b8072be1b9eda75d62f963ec4a699dd0bd3caa55ee63be685a69a1564f11fc56518986c8666cf51cd16dd4e61b95ebbf04a7de88e42932e0b47f2c6bb3a9d1b2a23b37f5ac4c78ab44c11de9fcfde2f6bc7cc37713fb93a52954e56f7fa7aa701d607dc10429e19cad178982f55beb1c4f2f5148d9e2b11ee152a83428d6e2840abbed2857f3d923148f070197f073a1c6824ca4365eae5424e0a729cd89410f795e2bd15f351ff43a8a2026171add0562eb02334105574025832c786dc4e024bc26dc56e89a20151a966e250a38ceb7c24936fbf861b40549a814f69960612b20fefee75c20de9bf8fd882a1dbe11e1c59795edd3a8c7c5bd7aff924747067054a12790ed079ddb54d53a7795eb65368cc9cc4ce2dbc3b6f87d1e03e789bf617051bde77b5d7bdf0900866d28a80027e0f83f4278589be22f5ba7a3185cd5ea3bddb9bdf98002d503088fe6e7d43a343781b410c1af97b5a8e4a870ff831868a7e1c07954c51f5f9c3a18057c79edcee92a191646ed4f5ed4e2e3b75ec728903216e853fcc29a56777e80fdef7567d2f9889ac3dffcfd6d412da6d5bfbb53ca770c75fb44ba27889c8640998051d08b25c91853c9e52407690d7fbab32e105cc2d5013441782413321362a8dde48f43b83a9607e45377d59f04170511a4e5585ff65d40c547b268d33eb0d1d042c3e8c9caa0fb849fa664c931e43f6afa2de87592195a7dc53ebebe9b2d02391a110b9c25ad43e1b82f92e07b903a93d09b7a49a12919810f5a5b4d380f560b9983b762e98ae1a92706d532b313dbc4dd222a83e5a664bbb35bd9109869d77bff4dfe880de6097c2fedd7388275d0771313040e6ba96e40b445e3ab3025e07248524aeaf785e4e8f8a4952740eac6c4f1ecaeac502ace4f6ec22593ed9796ea9d9284f378011a8bd477eabb3ac73c7daf79488db40b1792e7442eb08348b658681c477d256f50f51d683268b0abc64f245b7a443f2a004b12a0dc7cb29e63bd3bb83c78b50d7235bcb46121654a7dddd3148ae94e46fff1be0a5d5a4fb8a35e6c82889004785e32c81ae67a7a85da9b5b3f1d6974eee80bec5ee4bf7ff2be8daf533431b9fd23c355a4dc5e0794556d69ed41001f9dd18738d6581cb6df31371ac402b8dbae3ebc797eaad470d40c3018a7807d4322146f404ddc862dba480bf0196c8862a97bfefd51216b3ddfd5eb6d7db2450a4bc3a2a7ea2fa8d449ee4ac731a056340df9032b9561524d11d4753192c1784b95ea1269ce2de8d2c3c03cc95097669c2ed41cb660fc20ea6dfbf4f0191f195e4473fd110052af2c7ecb17189e0fc1f630c4e8e682e82e137c29ca69f1423168d2f3844ad2a8bfbf37a72009c51883e7dbe2be1235d1ad4cdbe4ba9f1ace81122e082c786afe2d3a6c4f2c213c6cf8a535f92c0fc69c41516aa3811a9716af7657ecf82284835c482c2ac3910bfe6457f78285cd6e949e33271b1e2e3eda2cecb12b6d3b520cd37d98dc1e77594f2eb8200a45d9b9ce1e5d95cfc05acd74f39f596787228e7fa8f200a8b9b4eede321ae53bee4bca653d681da881cda584f041ce49a89e70f8571be23e27fce74f5e4a513a811876e2a760a79943b36d218baddaeca7a9b4a2bb76f13465198ad72a490b82ecd7cfb0ab158160af8408dcecbae1ea401c36823c0e84cadd361952cfcd9cda42bd5bc706c9d89332c7d280845bfed08d69a17d6e3e8b9f3d11820d95907c4a0870adba706ef8cd5067bac7acd190c83303984c2811e6eceee056375587bfa9f2b97fff043005214fe81c8b3fc5f6dc8075c96e07816af58a4f53da7d26d5f3023a9d599826e39b5e174c13bf7b9ca6d512256b76030bd6ed3b07cf12b1967489517aa34f4b31462190cb0299a1f94fd0de03e76abcbb0bc05b5b2a3e2b8383cbb9bff61b7f7238f40fb8a64c7676d779c2d9251adb2b55520aec0699894fecc59764d775181c5d3bf1b15cba3d694a17bc12b09466ff7c9d808358fd4b3eba46108fca37ea4e72366b7908c4a7209bd8b1bb0bb1239ea156507e65a6796666c4662578923ae4914223d8a1110ba86665736278d901eb5fa20cb0cc196c3fcdb8c85da0edcfa8e78a59c5ab870af10ffcfd007305ce35cdc995e337b858a468ce63d3a304e4bb43caecbf88789e989b197e099a65b1cac466f7c39ee9b7422285c9322567a776dd5774863b2829163550af0235154e007010047790cc298349032478cc1698f0c3050e32741b2ea0f0cdc659888493cd1f8671674f1c808549b4247e26019f4a951829fda67916bf8498802d1cf9701e8636c26b950a25d214a0a978f3d526ede23c3a4345c9906f1f6610595e587cb082cefd490d2c22ead4960ea81c26772b993151f9b7a0d90a5475b9a106135496570a3856987f215ded6ac10e9534b0eedf022a082b40b43a06fa3c93a14e2c3d9461a750963d399e4675c941cfb410d28fc0a63ce8273b9c8886e2335b392638edc94bd2be0bb9acfa76bd121efe378561bb944acd7b70b3b554c470ab9772bd7ea30dda0b7c1ee82831bd6d5ebf8ddf99fd8867e75b4feb815399a550e62f1e75b33cde0278bc281e155a709779782cb7daaed8f41d4cafe30d98fde0ce86fadca317e62a90dd8506cc88c7659f30ac52fcb842f0e4ca920b30d0bb88aa9028c71a0654bc72a0962f5a057ca6473ef8fe740990e4e59284f145d421da7161760c9096db88688551f451b9a5150db440d94f4f33db29c85cb756aad2f525068fbf08e7e9e16b92a8225f83c131f64692e921069119727642d5f00ca05d36a7d3fda773e7bd77d52365c41e9560280ba2d58c5f21531af2c2c21116cc83cbc558ce86522eba5ef31bed9a9e2f8ee7fb8b89a71a3055e008613853e161ff727b54adc921d16c38e50a827d15fa2464c267d7b3b245dafd8b503ac1f2648fa659d371a14ce259a08783e4b073302ed29bc1701a538765e9bdd1d55081f3482716be9915a2f20fc23518bf8a9f3384c2378ee8e5ec2087247ddde83fa1250ca8f7e9a4834d66edfcb3c6242312bf02343a06f546aa9426f84c09aea0c6985aba520c369c87d79dd0a1ce824f6f2d804d836928920736dc07ce68ba7a757e9ab1c419c969b6dcb8356ae3355c0ca2e8fdf342abce3349cb3d079c9774153bbacc70ec2f5d5ad9e6b11111f8f43e8412ac157c899bc91da416808d3381ae2e50581dfd2f80ee3578b4bd39425e4f3aa10d542145a0389ceb833e857cf334f457fb49241a7f5a2e40b041b7976f1ea5f5d363caec7285ead35a02812fd0df0763f565c22f08b6cb136ffd0484c694864126ee66cd12f97669cc9454f53b7752e71b23d82d78730f78e2654f063e3646fde8e3f72e665780bc3ba4bf7241a638f52bdec47e1d6a09f77b0b2c3602ed7807c37002b68f2cff988760e32233addc93388f781b70af1976e6550350051bf84e150d778b2c8b08f6d25b9f1e3dfeb3fe0d0190f54953da31aa91c4ac00861cee3235d2c4cc29140795cee7014fffc6455db4db460935f9edbe898b964c9abc0b92d28e6db1a04c2ba4245510323274f36642a7df6ea99b0918b16ab8834c8f2a2a520dd7b02d706583933099bba973a62bad8eb2ed2bd71db319a08351cb272daa721ed19528c2d74cf0747bdf368d3a6067c0fecc2562d3609037a2338df27e97685742833bbf616b8689e16849417bd0d80dd77ecebcd0fb909659980bab950aa3962f416ba224d6e1b4707e394c69b87e123094154a664cddb2a157ccdeacaaf37454599920aef776932baea1f818101126de3a476557bbf7c4ef967ce91c738f233769fa0f2a36896de3d68a5782104495e1b4866a47adcb154986226553b8235a29d40a85368b10bd34e3130982f5eb0e7c8b58c9bceac2b387731bbee96cdd65d20b12868ae08cf2d32eb055c3d0a8b42394b1eb87d1a0ce5c0b05c774db9be5cc76c224803774cf4156b9299792743b5c243bf3061a98cce079b7d1910e82dacf1e5472b13fcc06dd372e1ab83046987b89b7ca8fadde50ac4ef125f8109ac288863cb6352e2dbe787c7794d666756d86d4c78112d814bca28d589acd21772267489174134693072ed1ebd8bb9866aa65d752befb08de89105ad4317d86d91611f93f1287763287312f7be33f4158ae9c26d274f486c7ca6a08332e6ef3d467c00484b08a2b5cde46205526677f257999dadb19aaf6d703b2be8eff8f0b1fc070b949381a9b47bff7f12996fb36abf9ed2dad6d8cd103d38edcefa98dae895dbcb21f1db3864998913feb40dc9f1c2f2e77a5f81d8bec3cfa111cdc4d9eca15ff1f500c2e0d060c84898f6932cf346fd3f69f8dc65ca185991409077d8080ef40f8f5217071676c84bae5da1557c8aaf1d7a91e7bba0e21170a18da05fdfcfc7acf7b0bc9ab6fd941213646630526d027fc15e7f708f35e10926836d30883b97cb77c477b02060c0c1cc5218cc81bced2a567ee5c7b8c9ba4aa13c48cf51fd13b9b99edd5d516205c1dfdcde1ca1c182925301f567ef486628969f96b81ecb4a24d156b6c879a87c02a4095d81c14cefbb5b9a6824f0023330795a3fdcb7a4a779155369d07478ec9b1748d8cdbfd3c8060d74eb24f7f2a8f9e1bc1d564a669bcdd9636fab52ca3bc8473d4bc5ed02a7fd875528931b5a8c3668c60749507ccb1cce8f1bdd70fc770d1fa378906ffcb7b0580bbcf520c947cbb62bf41f48a124f089b93b03d5d2606c181a6b6187241b0edb39823b2a8d794bd66c1816ce8f7dc4afc59ed5a800e1b8ab972faefc05ba23461c157745c2921ef2472235cadaa0f4960a969de2f770aa62e4f76b2318ba060a594402ca8518f83ff8ab39fa1e011534a6cdfc29dda8b07e984ddc2b7c4a6ccbb76c567f30b7632129c2a8248550a8cbb4bce6b4e46f647d500e124220a5afcd11b5c3ee322ce3e22cdf4083d37c1f176286c0d1211cefc87619453bf3949839018a1bb897325f8827510a71c0e1289d5cd4436c6e36be6ca14c316edd0fa4e20d766e456b0b3204e544928f5673b0aae84d2ac7ac623e11bd75bc47f9e9f73915100b05a62d7594dd970c8f5e425ff00e31c5b590507ca7101c8c57250e39199d1784480b108bbc8b6f49386d698d0b95bab43b35d6fe2f79100a87e7731c937a18886de35f990ee31e2c543f6e9dc8ae89c8b81ac7d6e76e5c7b314fb97e6253f966b0ef7567ebd715f30eab5114a394844e53b93a4624b4afa75ff8a9dbe3dea3ddae518f03b0abc129b3941305a0f6858ccd133f18a18cce7e9b280713a42dfa6ba8f811aa77448f470dabaa8df47e8c0997ae04d45a609cc0ddc58d102c398ff392346611e9187287fdbf53d0312fb7d400b40e08df2bf80e207100f6c845e505b9cc377e5823f63800c822ae61912976b675b416f0018beebb388cd6f9bce9fe085dab73c731a271e72b8089042d50384883a532b654bb4e6e24e1a164a12f5af5eac81f1101f6045cad0d6430f1b5d5f2804a39e28660d259bc799a7de9fd53d860be7a324ca0a1a701aa817de1cfaae7e757f2e16bb15fb578c3a64b2d63bf0db3b721ed33e6554e8318f08434a9ef8091e3d3d64933416d5ee615cfdc04eeaca3a55b4f4c4df83fe5661ce183cceadbf21be4aad40968dc1de12c84f74d29e38136886aa3c35a5fbf0198addea6a27ffad51b70609ed911f54cd1b78b593bb7c597f92f13e5cfb50e14caebb90e67d9358a7cc94c3a2ebf92f03b073847ae3db613a1385000b60b9707f373bccf06a2572efbf772c187f75885f34a67e11ef51def4368da7c2470bcd38167f6ba4a937fb7cf671d123b147260b3c88a88912b1e12285c7937855e53a37507102d83e2e8d0c2f321950fc8dcde3fc48781d8be7b64218f0baaef411dc6a2c532a26e598ff113433f498ba763e368c6a65d463a974802a1568926edd5ba52f3e7d7221e94c74ecf8537d4e639ef82789c9e94e6f6546f6cb39e494435ec8a56164e9e65c0f5dbcb132198a552183df3cbf17811dd864a3578890b94380c05eaa180196de67bb2ee34b09aaf2aea92c43dd54fd423c4e3ebb43b4bfa640233f2d3317b4960b94e6838c7312c3e33b39ca61c17b8a9af9ebee4a6a31e63a5b6c49422574b63216588082bb755b5e03967867c48991469a7980ab84845335e24434655ff4ee46081d324ea9ad6314403171d780a55907907cb50fcead67c83f28a39b5307fb38728439efd393f5f77d643726c89ecc84d0ddc393c3479fe122c18e165ed81b59e89ceaedd709455710844a811aa27c000f10a4929f4ace8940371dffacb98d5fc3c7cd34c9adb8b8bf069e2760168a146d1bfab9b3c4844d637200d270ecd3b994d0e23e621b82cc42c8d454dd43b33d86708fc7e223d6664f77b18410b968786f3e34cc4293b42d7c6ac72144cf94ad9bec76bfd140fe36bcd9be6675df4844a9cf2d0e11a7d9b98a52086c21df4df519691e9c5bd8e546814330132c328422e50472dadd785cbc81c6ea4da673516b93591b19de864210aa8688fb9bc0f760584bd44187762bf05e7404337c92620b038f5b1a1b010d3fa6a79af3aec5ed515dcfe7b1172e9cc1690a19d4900941e00d98fdac61da7b0f8b55fbd44828d4036581adfa4bb82dc1d8541fe597a81ae30ceb2461a7255d35f87afef9c8abe7bbff8fa135e93a51533a8c959b0a6e5d0228e170b88e2b7f890bee2a68479a989bde71563c8be90bc3953b51120336f6d37f7e639678d76de7acbd431a9ad978204fd596cdd113e063159e30bbb7a599d06d428efe184879f8192806985f4e05546ccaf1a4df80809e873976c6cc599f70b867c43b757afd2da348dfac8f20a9183aa92490ed17ba34561b3f6c3d982b9dc941ccd8913859d058393f1e490fad11f09500f92e64174d72604ad689b6454cac77f397ee4c4b912e1804abb8e404bd4597ea6a4193ae1e2d9ad6ddae76ab312c08f8bd38f9872e1a2290c0adc82ac6505e67ef8ae4ab5e30e5eb56dcb489a0b68336f70ac7049f59695f85ae48c1c3084c022675b182bf8b0d371d7a129a500e0e08dbf00186719152b8c1a97ca8cb143d92829a0a5c4444d1e4be7c50cd21f31d021c71162a627e8901d86016eff768a8605834ba48b86b2b1f7c37c9f1db350f3ce29bc17f4e8735d1047c7ddd7fd3e8c680f25bb20b73c8076dc01e9c60c5c83043024cb460aa0e9bd72027c0cd82d2ca57490776652f70049eba54f56542e363f7100b884c2f948d755d952223efb73c1cd41f92afeb438a5f5394cc73ebb621009803a130b02a764d8adcd0ad49c2b80d8dd06b9ef06762fe2fc5349da8b1137d294083f65a30227ecb28a605a345c976a1256bdc5a4bf0bdb62d9e4270037493f8b03beffef8c74161d31c5aef30502fddbdc76894a7d02582e1b270a757ae65012d92176e30b27576395d33ab918da26c676a41fd1932eaf9a5b4ad6c7ee4e34bb7d5228bb690cf664b4e02baefca0cf26866e63ce5a055d0691ba378a0c5e00f8a2c714872c2fe9e1d74d1e77a9c922f65f3adfaeea85d14f3627a7d2dccda468fe958206347569d46b2a5fc091bf5c93da6d9882309de5d7b5c860986b0799f2bab18584fb23c49919fc4ef93a183573762cabb169e18a0b97d44a205226cb862e7484dad7ade49d253e78b19909ce4cd98e59bd8a0cbd3806589d10b7158a9f57e2633199b5876e6da7f25075db1c3246590cc1958a26c6cac9125b095bc9c48e2149216eee1f894c4e92ba58b8659840e095e23e1ec7fc58bd31127aa38dd07921f9d09959422b9928587a9c22541e7cfe967d355705ffe83d3654ba151473818aefe07722589edb6ed399c80426d26c44197627ab4c3d6949c93d72813a32138530a8a143ade9a1a5a87213b3e7808d75e73e864961f2e27c2e18dde63cecc970caf9e55406522007a81d500095ba533385bb30121aab432cd37790b2564be95590c2c661dafa933774cc9a01fce65c7e2ac6a406ca9cb1f52071962303d3c660ddf4b6b77fc7d41f34a4beaa43fe00c9546220c72e426916fa10feff58403830faf676708e16d087bc1cdad8c3ff9e1e28af3137305b0df82f1e347025a31813e00312dcd6786c3d1d4da5bf1048b4e02b3497bb53bbdb1f47b00dad84c331cf1e79df0d93e71380faa733645874b8eccb9036394c406634dc30e9bae1a85bf05203f99710509838f68dbc893a4d3556b474ea3a59edf8fcb301d896e35b5ec8f73f7adf8d13a535e021099a997241023ea3481ec9fb5f893d9ba42f568bd762f83ed6283dda04d1c6658068f65183115d06a3028303edad953b3c19f344f4f90fad46727abc5bd3239d0e65d6cc2cfa79d765cfd9eb49323a97694ca95a36321810b467324cd70a746a7f3e37685e8cb7812e09c6ee9c4ffde352e16ed90582098c34fa2d184852950b7f8208b5723e127b41fe32b4a291ebf77af942765af9228fc1740f00da74232f4ecc1453522daa7a5113e60ba0d12edbd63af3d7b5ce70c2b1aa59a21d030a2e9dac0e78dae1e83d35321fe8dd233b9fd7cb4590ffbef20418a1c9301459b109226df5a74bbe63d1172bb82d99e01d2f1133f7b2c52584096d7234208d54f5dcebfd7c7c531c37c5c4c0f0778e930bf88ac8728e30b427e0c036e07ba54d026f346781cd587e85ac25f8c084f29ae82b95104782009dbe697c2001c1879248e35089c629d04c4525926af24dbee9b39e2bf676d42d4be286b532652037b8c7eda75e69036a12c2a6672fbc7f000b6f0fe9015b9d781c3166875910b21b91357975b4d05776fe2236faaed5f77a120bbebb0dd9a7a23d74e1023d9215893d81945b286fbbf2a8c0feb47599e9e9cf8376b71f6a7075e48fcc379097a00d4ff9c6d4a355529e5d29945ddb9252a1d69695ced729c8c2db681aa3e0093ace720db48c73573af6fdcde75d34bddc6669389408b16aac3974b28ab793da3f29a105358a4e05cbf71143e7b26505c69c33a93f68180a616ed398905acf85ac56c197b7367e66c33b330f69203950660ddaea2a7800848037765a92ff033f90ea7209821ddf6a9c0eb705dd3b11172367d3576238752b25d92644e2c8217384fc23b37ef9729287cf48ac1d70bf1b94a634ebb959ce2304cbbec58750a02985195d0c937623a0d5d605a9a6d05d2fee368b5c4ab10dbd20b475db1f0957d7eea22d118d6f747a60fd6dc781028c5aa21abcad9e3eb6b05bf187ea12a6206f0bf4732ca43ce8330fbd78c6c0aa2821099a70e7e47b0e3e04281f8e4497c3e064104740167fd052569892d1faebca40e2c50f0fe66106f0324fca4efb6d180a4b21df03a2352fa0bc7282176f98467d34122f05dfcdcb23d4bfd79080b2d0bf9093a9d1d1da4d89ca06fe5a17f21a6a72f30e5fbd23a5f75b0394f33f04c2973bbe94f80725f529d310850b65ed6797a3852703b8c6ba854e0813d0016d61f0e1a531cd8326f747762e2b294c6a176057de334037b942ed4f6e2078154c2277b93a1b71ac50df0785e79435e440d8d2181733fdebcad89e907984396cc75b510df3eccf1bfdb63bf022a10dab92eafad4fd76ba6fdba761de937372846cc8a3127e83b7cfb8cd3b2f177d48d0eb7ed71134cea102c8dd41b23ac1ca78535c2bc0bd0d97c3a39f1d8841f09420febd96aa4416ecf6238659e57022be719aa4e6e8f7314b00b738683a46ffd6201c16ba249f7b016db64541a2de9462c9cd459b6d2000e55a166350b72dd5b3d9d6d1ce9a57e7c1e4713f9da012e05df10a682715ee57723f9ef6fed11736c8b98bef570e8b9872144359b14f0cfd19caa0c07615731da607049d78a8dbc80340214b6f90baedd9a7ca464a61020505878b8c0d1236b096daf5bdc799345f00259dccf044251a98735487e93b565c599385fece738a70eea2ca761120bf2d9b1a383b1db4825455dddf59f7a5170c681381834b81b64b8f0ae788be71a5e8ed8c641fdb48b0c4cf78893ad87de7b961d50411249b1a938d196c9eadfbc762bcea5c6f61895bff5b3ded1045c82d2241f464eda18fdeb0facb9df202b0c05c1668602ca57b883f18473bc699da925e8c1e81067b8562fd0b1ce3c65bf8375a72d386794037177bef7439f5178796acd73d4c4cfce55f41b0947d63e31c44831d010d41b20d3d8f4e218be694cf7ce1d55d370e29eee51da615dc318ffbaa378bc8908455fc404360daab63c61d22e30e7989905710eecd0ee3a2326997f4ba97af7c5e993cd90855d7569dbd74c61055ed3ab233bd28de416722927795196a457b091a8f9ee9bc1374de48660cfee4a0aa84c2ac79defc9af3cf1b9e0c204c067c2ce60ced6eaa91983061b32fef568c1e96df531f327ee6055b184b99d95591cef4b310f3c0230f17734a3845f5924dffbeb62e0a3419052f6bbaeaa92a3195637eabfc27185cdaec32908081efbf15947302c3883fc6f4628f5ba427281952b790b04d78b37b27e6e56f6225abee4b0583683d9a425f96f4aacd152830b49d39c75566e3356af2f56709aa2a7031ae6241737941aa82ec74b1a1a7230ab32b10442b85cd126056910dc2057d26c6729e0c6ce9c01457a6fc0c3bdad74fa2ae63ca975883ed36a41a4e774c48c3f1b6c61b41fd69694185fd1a31f192f84ba7695497d69761b45f94b815adb83757ba3cbc44451de2c6cb93939e8e9a70e2050fb07597c8ffcf623f2092366d0ca76d6d6e2fa0054cec0817f3437aea99d9dbace9a979d82ab1bda6a1ad921d23cc67ed6fb876a14dbfa667025a495da8a763292a81b8de175b6f3fd3d2523c94b5287084a321c7fe33e9dfc649c697a46554fc85ab0173a2e45f1c3e908523a245e4baeb7d242b86d241ab663391dad80568eb52701db212e02387eaeba2563fa51a5a23e37f8111241563035d05bb2f19bb2216e106eebd2054d1d157e5ac9a28e6008540c7ae6fff4fc1a480b9df7436a8137760ea07629ba2420e4bd14db99caf01d9f95711870025d3ab4e7963e636d90b5bc2048c2ed43c5146c83a468396869743b7923138ba22b1740f770357b45f11500b78f70943a39f61e25b14d25261851141f8e20a8fa3a4b5b8ad1a2d5b1c5da7794cc531d0b22bc98441d4acdbc8362df2d14af938b2e42bf44fd6220e7ddc5e03ec304eab3d4a34201b1f44de2700ab848ba905486d6e4b046453207c7f754953b13c5316c38234783b458da714d83d4569dd153b934fa28aec3133a3e4f2a2b13f462f0c84a23909a3192ec64362b675715ff20e1286c2b7d5d1d0b058014f3ea2d7f15e3e2a1edf48afe8ee010f5b0652a178edf83e3d72cf3a5b98dd3520b9141272ad6a4b27144d9f351bd7d75e5b4db651869d3c270a4bfac4fe1384209ec9d32708194f07eb14fdef88405ab0bb1051df66f4ee20a6986f1be4af20be3056c41d87703a72cafb988ced5c69f6608d9dfd11fefb1c45f750fcafa151f09f67675d35f7a5c3d88a33b2634c042898ce6825227058e2c011940cf7282a04a3b3e338070cc2780d159022c28bc53eba7efc7fff81012c11b083c1314d21498761c4f3bab0571b1cc7fb6caae91b59beac4b61051191e9df4211ffbb94033d367df2f0ec2ca78753a94deca58ac6853e8127a5ff6d13f64c58086312ab0845e6bc1b1cb4ed177c6ea2015ca3d6fb49719ebeb2c2826b8dec5268d6e9d6424fc77bf07467cf5729ffaa24d56aa5fdcee02e27ebc04de51ccb70775246b56f4235feba0193812e832f7c0a41d1baa828a46ed52e3f690781d703c6d59e059d85e2eafb9491597bb95249e10594ad4052eaaf988d261f33870da14b2c64def5e49ca36b0bd52608149d23ac60e29b46b50cb043c3c2c5434de4bb762ebad75ee2978622ded7c2e41ec1dbc680d3f066e336b4ede0e61a3cdb0759d5d18366e58ce8d0a866c3992ccda93f1f2c6f513610079c998d8cb484ce4b17dc8c4b6d3e1e7741e8380fb685d01f7994c5d45ef5cc29430cddd8422e9b8a7f3698903faecd07c4d8df9d5175d31e2b583ea3f4b984dbde5815161e8ff84345fcc92bea28351f87bf1d52c0719c2aeec4a217e10f1267d48cbfabe4744365b577a729f64af5cf388ea7065f32d8f26bf006546855aa8e08a5e94d5b60a75a643c1168a906e0fc4a0fc4d567d3a2dcc998f74f5b70f2400462d5cc01ae41a3af2d80dd0e8da2e3968614d1863d54afed53de03a082966fd65c0aeea3e5eba8584a4ad29d08d0c84dc981fd6e64a81b2f543e520ca00dbd2364c95ab38afbd6ce6f9f58c10176a201d17b9607bf836555714961cb45794456c841454e5231347095a1a3e97d8b42dd893347e0072a212962d3b07684d5c273dfc2306a3c70ffb8219167a6f98d0acf3d4978e283159815ce8a36e671cac30d6662f2a77fe203e6b255f724b0c77b8d34562c5ef38ea702c8bba0ac7130c0e775e6a9fdf603309fd6559bc0f1e5f05ef37987204b44b8451b65c05b96a79905c6dba790139d5506cfdf1746ed5d229acc3f11378f458f474e0367ea974c0a2a26a21bd6c53a633a64447244427792d12744020cf2639613b9fe79ce25e06a81bf5f16d938701befd5964dee38f23bd90f61b75bb62efea65e0f7af32f4c47bee206d550f5dff7139308e434f112d520577c9bc43273a1da058f99fd03c33f3d50bf486bbcfefb1093c3e770d5bf0f60d0ad32cb412f9a8f71bdc0f3541dd55b4770ceb3d8aae773e6b3b6d9685e142dffa707c056b4ac5e526d810e6d07633812bb0debaf5bd8964cbdc4e6314bdfe65cd0605ab47b2ad554e65223ad13a63f19b84a4c729e03f2f9e5d3258b36ff347c985d04538aa4f69a34952d5cbced0c84df3b43f7161573d5745c7bc7e62ff852f5d69f09de390f61b16e7718fc4273dca093666bcc95d4eb0e5bcbf7db6794b0b5f7d85a396d3e00ba963b9a24f876cee176f7f8e38a0249e039933fa491c911e4310d601f983cae7795a566ad6ba639600e7a9cf4cb0fcd28f453fdeed0a8a84f6abe3d380c7c6df2a8a17ffeaf1c4be25d66a8ca5df60b0c420654c30d4a07e3657884ec550b6cf6b2e31d40426c3bf232811e87a9d6741c706d6e79b22be735f8a483d62caefe15ff49c593121cb1ab6c683520ae433a4b03ecf6a45412b431a31afc2fa36c7c5f06e5e8a26280f53a1cc405b5aeee8f645a3f678d48ec180ee8cc93b2228067c4034950ac8cfeba3f79ab28a603603b89561df01b2ea444970729927d982da5efe78f623ebf65e59493873fe56fabfcf85f9730815de073a1e7951f8fd0d37a101ef15b3008accbea5cc4aed666ceb0cbcf1dfe333082bea353efee648543a428d3392db6ef73f3726f01dceb58bfb52199dad42627320f1da3fa8f725e4da4114cf3ad614c8adca4327c41eba70b78ff7f3cf78b9361a363abd071d11cedfa3bb13cccb4e0627cd62c1537882805905d035907e9bcc798988f14c3a498241d03631d5f3574ea0806f8dc571186909e0f5afbe458c35a311ae0b83f588ee6bb55e5ee846913fa8b6e52978985c079db0674a88c2e995f1a5269e9510b7eb7c8a2804c568d3d98eba748947ae0cd75f18f194bc4625301c3c1600e8b5a6356f10fc1971291f8d8c1072717771dd373bb19ab92129b5074a76f7ab8a029b3f6c739826283bcac9aa8ab165e462b88ad2d3805d2472d13359a9331567b6c66f6388f7ca1908d8879baa311c4bf423a9ebfd8151b62fde37f46a38812ac153720ee9b65b77841c74a12c02c4110b9245a1b9499eaf5d3edcaaee3240a0c73a678a83b8410295b74fbce2fc256b9469597c9bfcf90af4b05125eef527b63a0154a3550ad0a2ae9b74f5f144c0d209e346af8fe532c1f2ed870067f81a87343336fb1ab436b3d0c8e8b79fd2decc680ba3f6a881c66b955483f70843bbfcc446719d428c89efa96e53179736db467bafe7a3096324ead38b85d0b06761a737a68cf54e25161595eee84c3de3569e6df160618c8178590485e9d62ca0a53ea962eddab94b64554f8d6daeb9c1f53a171c079efce7c8aab7a19396ed380d02658db79350117b04a175f481981ad358d9dbf693b033d6c088b231999eab7ec93fabb1ee383c099b865b80fbbd46ea3020b11d6223329e0b75a61c22b05a61cc34b6e50d32d6778d2bc4815a11a2b781b51e148d96b8a2d3b97a778299f21adb5b85507477bbeb38bcc697e520f873535573068c3ace8deb8600c6015a99037be7792fde47e08d19579b4f55faac3250756c7c15f54a98b519dad03eb9acb7245bb1c1677ce22af245e035dea180b9f9ad5a873bc38ba2c0b2c88073055f334f044b28f9ec6e6dc3c3dea47f711d84a6e9a79e405f3086065cbac22bed8b894eb767ab27d093ee43018cb73587008583b812319eab04c25f31052791130c24daec34f7164d8977686548ab71c5286a68ea9be41999e061754e7c0a89bd3b98e054e1eac9a9533fdd5bc259a47d204d52d839ae82972e7ae608e75f3afdd458dda378ceedb52e519dcbb8c3b1facca2203b491ff5b8de22362ca2585a065ab697d1b08c0a9542411d7c593cc3a589688fc056a926cc3c4c4afb14a39219d31b11b0ba296def52eca6e05e9f9151250bab872e502efe3bd184e5c8107d14907838edc29a6d180df952cb565cc7e99e91684f8a86f2cb79c93d3561e3d4a58246b35a9b01185c47102937858a0a44c424165876cdaf929165cc8a456168ce2d3fee6462ee3f4b112fce8935d4c4c9ddb92db00adf7bb681cf7bdbc9fb326d91813141701a11177d96a39689fa1c86b7ae4a9cc608561e107d26c6134d9063226d107f463ea6ddd05b692d21abec7222c94aa9b8b9bc7ade7f42a73951bace5788783a50582813e65938fe757b078dacf3de5fe1d10faf9a067a5545fe78238b2aa9069292d1965cddd7b05b0facae0b6790bcd629439c2e9c6722832a6f73627ecacf9ccd0f5c78d179f44861bf183d04ed97693b9cbfb300563db915066539696b6c80a49543b266f910be7a5b44fcb84d2614fba54394680b6a46aba9d0b61b8fd472666b071562bd70c9a0e229457e2269c3a24a2c3b63f442a929b9ce61ed9bf8db5dbec70cff315642d9a8970b6b7d45b2b3a8716d52e12d90d1e8c8259dfd982d09f92a4820808b2ec96f440f5e8b2f35983bbe7390daede3c2f57aca90e30ac9de5ea1f8624ae066b5dca46c5ee68daaf045191df6ebfddc56134377c43e533742d775852b7aad1653fa8ae621a7c347d0328b7aeb17639bbebf979ef6780382f17ca3ed0346794b2d469bbadc199b55249fc40aabcd5c2cca22c2a3f18fc0aeb8adb60890a5a223e59eaf1d563f1e5b358068fc4b9c4b22268453ca69b44ec5549be79091bd9e305a575a593735645e1a0e29d3471f8607f54e40c0ab32830dddf830cf5153ea4a9a60691a00cb864e76d962e0b5979e6a4b2d3c0f0bd95857fdb7f557992e57f50258448babc3a322d22c42ea7b568d2fd3108c23ee48a00ba8ac3ab2151e85fbd634af6a253aef1a2a62286f9c9b557fbc866a265720d008073ab9ae4fece2e8a5fcabea4c6007e0a29f64701338891b5a1d606adce014cc334b65504dbd862e13bda52570e5639532ed514704e64bb86f2e10929700f3598189c8072664dc096f6eff2bda8d6eb2eefe387592cf47a9216834e56fabaac7689fba63668a60ba782ddff0bbdb7b29628cad020c16e024e07d2d1f67c1d411cd553897f09033589c3572035287b827c68e9642643a1e6fb7ede80a8c3c5e066407c5927abb4b7d805e622f812b205d137e87f914a7b63f2173fe6b3c41a95d36c524ef3e9181fa342cce60ab38a8a8a12628ec6e98cd9b83c60a2762cc84a68e3b944d3987653fb1cd4c89c08b9fd2417c0a9831681cdad34ffaf7c7347a8369dc8af9d566cf07f511936018600312e1a9dc8a83205cfe1ab6cec7ebef0c32ad38a0f8e6ce197a48953b99acd6f061c9d409f66f4643889ca9d6fe0bb5541fe270689cded8d02950f26e7b656bead3b9b41d88548054e014b773fc993ffb8e08e418ae336b79e5d68c81ffabf8573d1724764f1cf42b6980cc9be6e0d63fa41be908652dbd7dc1cbeb57ccca861b2967245c42514b2e352d443f702536cb7548a7f9516420e600c003a68f60050536753a88ed29218e896f0e96369985c43c1c782330e3345d8cc0ab45e32ab08e2291a6e8901653a355b1c18a6cde50216948b969919ec1272bc6a09a9333bd3707c62cd75730c1aeb3d03078050274315ee76de1fd60c5e730b1db5ccba97a438cbe0cabc118d4816a1dc8d877ceac2963a27e5d8714cfe6b9eb41f360fa578833cb6e37296b4c937d18d905664d2744daff3381956a473e3ef23017392276129a4257cc625b202679357b38e0e1b627962a3c12b32af72a6b96fab63f49b2a044b04f31d109714619d390b60cee75997cc9591ea64b4c70c12581fd72b60a668b39b5edecbc2aa981ce8031750bad000718f98232c92d13045bcfd47aec87b4e14271b31ffe50b29bf83fe4af8e02b40a0a92b4ba80c36d80a0d1cb00dfa16580e27d92fc4ded81804c0e36af64df159687f64e9099d8bbd8f7d44a3a5694c219136f570451232d7a6718ca70e8b21e8a0da85c5c920a6316b6a1c9574565b34d859db0efd3ec9684c99a6c194323d040d5ea6e3c4565759a257026ab0d72c7460f18042d564bd30d43ccc5484954898b3f4e3091c226b32892c1eb6886b119dfa465f266227f410fc36a37aac26d4ae9cd1bf4103612784e6dde474cef2c6aa5ceddb7c8ba50f9a2b9b35c71ba908ab9d0aaacc038d4f695031b2b71fbe8e9645770722917df0b678bf3513c3af644e88003d6ac2cdddb777dd5881616faaeebce09deacf810be3de5081e59796939a8e038519e613549e0f359e84a63f9de013838aa5eb66f093d8e67b00facd94d1d6f72ebe6abe2bb08080b2fdd8116bc55eed696009c4e02a46bfe10c2194e09f013395b80375bdc9edb7340a320aebb7962163b544a19d9c7e1b397780c63a6a03a94785276d3cc9afe35a6445535efd9bbebcd62b549b1441649261ac3f751b9795f7ee39c0d837534a381276f947ea6b9c4e922905268448f51fff63186356f46d6742f0b179e34da9a5e8a2601cc2caf4bbd383d5f4a0bca22f637328c3f4a55826b8877f758f27495c2ffc1427b3cf7f646e527ff38f93c0f662972721d03bf883d67d4721c9deef5e44af9b650be577070e2ca102571d587e250452f525648f1709c4a5a38c31cad03b8a86bad53e7d494d94b5fbb423484e5e4548c46c9945a60e604c3685e64cedfe5390357e631612c7bcf64103eba180044f838d5b4773e5ac8301e2fe198dce053f5540a81891b746a5b02b207c8303a49227d06d24724c325b930d4643ddd7742dc0ebcbd488f32d96915a33f5de2ef18f6a42d47f67dada411c1f52260d4a252981d5e967a9da07d7a6e5274933af497ddda5d41a3a7ad24500224a69b55337cc84070c877ccfc1ffe196da1486adf0567c25bb72f98cb28118bceb53a3349e984804353de44bad351c8a672c7ebc394df39941c6d03d31199f5312d8c853da1a2b9307f179fe8e7b655725e4057f33e6398998c04198084c74eb49892630d85aa5610cf21dde7979db4b9510a57635c203c83e3ad1f7fb3f9a68c5d215c7bc9687624a1e31979226674be7d3c031507f3c79b2282752fe0a6ccfe6dddb12ecf5d55db2c5e79cb33e066b8cb0850076cc4f0506166673231a50330ed514b583f89f8c4cd954804e621901ddd36f614d60a6b7f147ac739f6617afcd229b72edf9afd4aa6d270ee2a2fbb79540f81a4b7b8c263c02c76c1e160b26487a37f9b83b374d168eaf877239879313bd8faa597b8fb1438a6193355cc396b44455b8b4fb897f928ce29a7e413061bc8c4539abab983613bb30f16558908d09681d0d317ff31544cc52457a27d00736585d8be494b5f363ecbc0417fbe08f1e826654840a8929d08bdee500463ec1f1f6e5da49d2602dc0d4f6ff1994080669ea58c111cdbb39ce12c9596d192783ff1b2718aaef2c24caabe3ab249da7d88c426fe1ffda16edacf9a4ef6eb32d6c715170b0efbe93171b36e3d9d136fe11df87b0c753be387771ff0ac4189e07f888fd5b866ccce9df45eb31a80b64f7f2d7ec9ea08e5143d8734c73ac66236b7c3c0eaedbd38604c449c9c1da746a121ce2c89473bf67655ae0167756ea986fa7ce26abcc700c8494808d5dbdade2a858178c9feb492b9e391738b136375fab970a151abaae84795e62833b0ffc3128333ebdc7b7f68169429cbbe6260ac6c056d118131fee0b9de5c02228ebcfb0ea914b6efa4fe3e3d5d07e14f5ce5305ebb870493417d4d5dc34b691a09c0ba44ef8a7eebc91ef4f2981c095db2cb387d679f8bb3bbb0da6315d263b3ed664e7c89eadc932c537075bc19c36758cac2cf7904e566df0288c57a7eef2c7376d0b1ec610e638619b9cf153e2c11916fad359e02da67669fdeda42663bb9e87e15f7e14d6a2f39c929ec0b6222d0f25db7ff0b5fc166c00f81e782f1d4bfcc6e8598ae7f011c161ea3cbf89e05781d55cdd35d043417676b6f7e46135e7dda198f16908612507dca412b78405f6b548ed8038055f479ecd6e672b9364a729d2d2634b503e3f368d627379992e6d529a298c5d59acd18549211e5d3c2fcaa4b385b5e19091d732929d77316ceec46d3614c0f7f4f11544933d55c8d641c34068e7eae959ad2e6fd2613c7e5a5aba99c6ddca83b1ea467f85a561e15e37f437101bbf834f277b29c3ac2d1fa2da937fe1cea3100153ded77682e87fd8411e9e25732d316488f9c6862a49f452cbdc3ac6744a9015df4edb125c64cf737e0823c27e8c278fa40f446dc88a9a4d6def6b807d349c6bf7a687056b88011545235602953013639f0acaf5961ddad8eaf387517043c49b2fa5e1c97a623e772789edcf2ff09ed46c4aca365da552a56c0a7e121add277540a1263bcaeb2b9ffe8c9ad6cd1f9eb8c8355b1220e737263270182d0ce52f269825118d435ca1212c93c46b8ce3d944a35028011b17eab2d35b0b0e6c9ac6df1ede54aa59438a284919047f6e05ed1aa42f04936ec0e1a89ce860908ea46cea77cbe66ca4f9845e2a0325dfbfcee3e640af12b4076a68afe4b5aa5ece4450df0987fbde463ba4637837f22e8746fb3765bc14b7303f98da531fc6f2ffddeb8a12872338cca9f204d1493be9d4b7a038bca7282b1cbb12773139c4ddc1ae425b9bce5a0e467d8deca9c417f9326577836e57a05dd3c01da886fe0f62378bef97180f347ca8ec1259f57f06fb8ccdf85a6923129ebfe5cf524f54e3649d7efb1c15a375acf438ca21156323344bf583fa042a2338b5fed7af8cc552b1ce68f20d61301c1f0ae435d52bd7f21f2ffa96dee8b348d61017515266bd6cbbabde172c8b9eb718e74e7b4598f209ce73184d11e70463e9322c6477d6036753220dc94e720a6858debd29443e805815ec572742e3392276e7906b546a206cb4c32f997eaf1bff31491a3abe93ce8e921c1105ce1c7a99db95fd19fbd6e2f4b1b8339596a9fc3b8fca5c24924bcbea45a457a9128503ef59448dc3f1161f9c0de088b4ae528ad43812cbab6413c7cbb66ba3e96b949914dc1e25ba4a29d85b0213770b137ddbe5f776906d547fa85599ff8d36081550aa785521e6a83e57006b4923bc9089f09eed809516c34a9de02fa12222a24b3568bb0b203fcfb9adf98028323da5bb6f3961586d8552132405296d406508175c32589f704cee162f5d4ea33abdbd5aa6ad9fcd6611c0c9aeafc3d848168099d2e8b56daad33e33d0614726670c01f8c133edc41c87106d4d1cea2be8c5e8cff9801b0c9eff5a9f2f2a2b8115dcc505ff6ebcee5311ff1a8581f2c708122e16dfc0c27bff30828fedc222fb5f5b852c02b081a52b37442e30ca6e6f6b7805061f68627dbf9406ad16f44afe1107d57043c2327270761ad4f80d3ddadfe8c13faefb1c735cd28054f5d6cda804b5c795642df6b81c030e204e2c2a852439443ee2e79a010a1aa5f0f51aa8b5e6d5859530cf4e70d2509dc5647f14ca2dae8d4be01d89af273d72ee4a00065e697349fdcdacbeba88dbd59115147e11b3ceb4e90283391249f5e068f7755e014672c86b79560097f0eee93a77716c62878e16e70e570ad5a2e10ca963720da12781705ad1a0790730307eded84d55b4842e0449dd64ec2020f80c48a686b467fe9614812bbd6bd6dadc5f5badddbd32b64873fe895984d68ab9134f5870711f5a583b1dd10c3eb8f8de6f868f4b4dfb817b3360d5c61ccb0462071c364d7f4d6e7ea254fcd4d74db848c71e0125fe310ecceaa24283f0f20c19794c0d3e0e2664e5f6696e062533be2c313eaadbb5591685d013388c820950c4cfca8b67a62d326054d720732a4534b3ba31c487e617bcc45c4d8f289b58a928845217ed29d29560e1cdd376859c0c93b793685306e62717b1e358510c609b3e7bcd9b821b9e03310d82902187e94ab28201c7ace4a9159e6601c05b9b4692915c3ddb183ab6bda009c00e53b11c5f598da127069f3881007f61e1ca5ba4e95de3b80113c59916cf9a6b34d61d0d84570e7e277e653d770fafc0e995a22a0f425656e284dd11ae00896bf74e6aeeba917aa9624701fba2a3161fc1df4e1e450df26c98e28147e9f10783a02edd9e43bf9581dbfe8640bb7c3a06b718dc07f6d6c5e2d55a112f1b26422c2d2d1174c3a89faf2a35416489b67cd043d9c1be86b698f0ae1bf114160ca8feb172e3636490866796c10faee523bf1ee19bdee7676fbe00fc58ca88f2acce6b63fdd589f3936fe309c00394a6551b9e29b908ecba8071c335a0c73a84c5f9fffd1885c2caede7e4ab3c622dead6e93f0981fd5d71eb0d5da809e28a9d96bdda4219d796960cae6a3216d50d3a30da2521a62f7341c1f09fb2d947a22549f2e0db43c217f5c01df92e561c2a4c8ed4f80a88eac415875abd7596f204901d3e0314981583920877f2a58ff0881e6085321bc6ceeb160e76843462015b1c34df30f2484753bf1fbc266d4f9d75edaecb267a7a0af5ce9fe2373f29930d90c75c0d31b1aa6ff41c0002d6394bdc36dd3459e02c964378f07fe9d2c8943e62d83672781047753eb2d322608ab11f4367ac070279406d1166f889b249d47c97a16d7f5641cb73e55274bc3baa825e8fc19b5be8ec04a8bb5ea4a4e847d6c29e1164c6395152f9c9bfcbc7f0396a883d156573e98c6822ab67edf3de11c51440f355d4ca7edf9e80d469e753c35bf701eb8006e9add2a4c822d09543a82a8328049dc3faef4bdece3e6425c34a28b58625d672fa40299dca8839fdc60e3dbed00aeca15aa9c137d3bcbe3f3102e6555b470c62bbdf9f88ccebc9012e899fe8e4d74deabda230f785a8bb276806126607541fbb951a03568f1b6745bb9016d530d9ee5836a7a487235318b8ac5c0e2c9a0f589203b0e3d468ccdb54993d5dbdc993f247de8396e2d5b5d004849030935f7c080b059b19e38a660c44beea6b9d816d3a06674c2b33c02cb9a0ec78b4c7d592ddbc2410fc60e36a89b48c738eb59bc7bf7add509052fb69d3e9110a1859c499d37f55dc4fd6da5378f16476943a3f69f1d8f2524dff385cde2a33c01a0ceac4c5aeb6eff2afd7a967ae9a43dae288ee65a25bc90a13206b38d43512a57808f14458eba76fa0aafb85dd34ceaea3a98ca2e504446ffeecd0be48031078c07fee02ae2d9ca53ef6dbd8315853c465d88583300a0cc079822a57695a6ff71ac1bd49ea629a5467a6a3d17648a1518ecdd14ce3772ded438168af90c3ecfec04675ce799aff9d17fc3e956f3822bdb9972e7d42da06df1d39072ab70ff1ffe566b407bde471a0e011b6ea9f464494b14dd0647830d6237e8783d667f412540530e5b01eea62d8c280cd3547662f332d2c6139976585e33a502cf7a306b63ee553bec1fe5fac30ebcf026052d39de9bc9b580c44416aca5cea276f7ec24632f61449609d25a73e693c9864c5514e11400c763ccf4d4bf790ee8e4a8a3f334090b6dafcc2246cdc786d8d5784dec9ad350b61ddf26f7d2d9770d5e4376009d100dd67994712cb7fc968456acc17e69d6272938a959563323ded416279f3162fd8484231398a8f35306c71370a976f4b3ca34593c75ffb32fae0d777012714dc8897172d6f2b0671b4af2171513f74faa4868d5554cf62a420b39f812cb59104441741f94049450d3b876e3d593c5c8c14f3aea4228cc265fb2ab4885508c77e4edaea457ced0e07c51b31846e146fce5fcbcba4d38aa033e9300e6c6e9682ac2224a075b3349cbe395440739936abe0ceb5aa4cae004d8146733cb555f2201c2b1a39eb2efe11ad13be65975acdbdd62bde7f3dbc1f851282b964e2e010557c340671ad0980dcdfc8feb6207280afd43a0e54b63152e6f08cf9eb15fbd9e9d656283b85bd43d35fbdd972194f4e0eeab715a347e9b47af0351321ce9744443596e7095f42c77c4a3430e965f809dc47805af375ff2e9b8178021b56e97b5802c760578c84334c0d3ac3864160a46913bd3a33ca7aa6b622549cb6ceb107d66f27131fe3f43b219f5d041051b842574a455208d173e576d32c930cb67c60fc840b07ca54ee9b1753dba5c14f4c4d467882e442bf5a11f3c605da2ed524e84efcac3b66f5a805692de80cd2b856589ed49e6e29a26cc9bdea95f419ebcc74a1f0ebf39d811824766687337d3b0aaabb6b4c7c986dca29c20dd0eaddf977eb2da3bc311e6656e33937cfa688cc86cfe47321228c76e919ecb27a9bbac33706f783035d146344097f36786f3d73170f49895296f40e970529b9c9d61232c5a17dabd6a3b6000d22f02e0f264e23168b4b64b59e17cdc95d6de2f1db53f2a1a88db246ddc672cd54f79576d9e5850ed8908fd0dfd976024f565121486629340522430086c0787687ff00fdfcd6a2449b185f82461a8209e4ec4f30645270d0b5d0cc7d7a2123e7015dc55b525dbc9283813bac2716415aecd748e196ebc10126e867cb67a33288c3ff09ef817c64a3bae20b02fdb325d8d9db2c655ce7e2bb93cf0789db2ca64c0632de80e2f76ee65b37c0c61e6b784898abaff209d1b42a10d41ae070f4c34c84ff028d9e106769c0e50c5f90ed81f350e82627c85bafeda85383e9692ed9a04f46f8f04631a1882c3a87fb652154d7e7676f2a3a2597df4946b8f933d2359e60dbc92341f939acff4e10a5e6cc25b752b5690faf0ec8cf1d058a6cae313600847ff5d66cbcb764f7f5136c3fb44ef24a508ba789f8c7fe63678f83da28aea165cd624ce65e665884a56023d71f8c147a8532abc8e811134386a84a0cab9b1b64b9c8a8c9c8c7ddeb1b29386b4c2da41b96bf2875642c0d955a29e0528dc166b54f9813521dbe275c9f3561998d90282b5d087bad9c8401eb3abb23fd126a065c24ca429bbe51869ebbdee32cc1a96750cb0b208e3b5400b634768aed3546e646a232d93a6b0e6c864e08c54d5cc7af38569c709fd16a1554316c8eb11ca17847f9d0ca4794a492b55146782cacd490432aa402d6061135914708ba9690ebbc938fc920c14f9293b9f00761a71b0e759b30ec6b31d9079e7e59a8ee9a714ad12ff9e2707fb331d078db2b35a5a798f36a281328748d3a739c75e9340d2aff915cb4d62b416d8c36c8f0bc9776a7758adf0f71d39d4efca6ab8605271bd44dc3e9d10a915f5d5ada51d71b14077569aaffad118a917ed873cf98adaa855fad1ea06886b8a221c58d90e1f550a5f6b313a938019dc12cecc21143f92d69136a5aa7a144d8120a100d039158f05718e50953aaddc3a81f78dc96c7b672ca780ee1922237c6f44ba5f8a741879215ef622184853f097ca2c231929939269156250e71d34b15ebce9a56b4d2f622e59b49745be1154036077d05e871c50ebd22ae1f1677dd5d0ecf6a5591f5b7154173f50add3f75fa8b06a7ae92401da03ed000565289bb3dbc3855d527eabf6a434291b5d1ed1211140e5ed81135e0a11289954b8452b6d490b5532633532d276028564aa25f6de9e72d5a0f2eb070d66f9eecf8fd9d89287c0282a5ce463ffec688e3f6026dec71f3aa29c931adeb051957a4e0154108d04b318eb361880be05af688b13564c985132f3ddf978693bc7268484cd61e1b4dac7a7f8c22f20742f1d49b2f077a72c4484cca18bf72e2f54a0a85daf593e48bf9e1dd2e438177f9c745a41c1439a98f2b2e0779ef3390d3f6456d2e922663db452aad87a6d1ef3f36358a7c0a6e6c233f72781d46dfc79bcd30c5f5844d64fcb628e87d6981761efdf3999ea9d77d1376f69f87e023919fe43c824e1660964127d091592dd5658bca5cf1c1a1f53829ef0211c5b235b9ac5ee4e1080b78cf9d356dd3c20d5214f5cecba973704f50db6352d48ba399cd7023456b88d67b7f421b4d185113836b5614204edf0318126ef95f6d4a0adbf2b5a8b2f59484ad56e914eeb8c1b1298965814bf0df79262560bdb3129f6d8e372d40ae5178b4b4b1735d580b890f8d4670ba9a6c13e25ffff0a3c1b043568cb552c5291dbca0f155d561b8acf4832156c93d79d98575fa5ddab6841883af3b74ca6b899cfb74c73c958b2b239f2e1bd83432b60beb55036e05b340a54a54a42b716f228d871b8574237d3cbd323f65a848954241c0b457c70213c094f7bbd44fd001d3a0d8c4726a64efc1a1b01d9fed5e8a888cabe7fcd085b86ca9ba5296d8b16262fe6349de7155f741bb7013fcf1953e4475ada368b44e8569dc5733810eb2ccc08a36d065b09eea2528395f30aaaace296757034509908d3b1fbcecbdd40a2201a1847f92dc39f9f49eae92d0bdc030a19041524b1b9caae051c37a9e545b14422dfa8e06e03b657f846318b1435987e7ff74eba36c5ca11885b66c6091ee89fc094bcaa62f6f2558f45887af5cf69eacae494f4029912115486ce32910c021811ecf31cbaf076a08f9b141e40736e769612472da9f081cc6c6710fa184981436b1149bb7de96dd2925ae32060d50fbaac7554c1ef004691ccc189855956fbd6a0bad7eb061d0db824af76335c6fbf7ffa2d6c721132dc935fd66f1eaa5cae06bbc253c29ed1d350f58016cce860ea5aa97095e7c07aa5dcd3403cb3201223634db30ed63861480f42f6fd94c881623f9c309c45dffadd681e8881a5b5e290924fc300169f695adec4b3975ac94da0a792671da8291465dc1ec2eda533961f6cf3b039ed00cac4b60d617ef24589a084f6751bf5629b5d66b507adce687df8df134f3f3b7c671821f46fec98a13dec698bd5cf248872cc21e48004411ced9c99d96cd34f3323844872096561a8d1728a99ccdaa7f5bf244d6d574599d72037c5789ffe8c732b29214c5875d96e2c09551a97f72e0f942a5faf0fed976757e2e0b55308969018c66a6e13ed1c0254ee4fdaa084a9d181587f04db846aed6e1d1235a352f98832048b652f259baee85e47ae39bfc44996303b780ae61a590245496ae8aeb3d1d3cf1ffed9f9ada4f9698f2002e94a98f2d043022348e32acd6ee61c3580f48caf21379a182332bd4a8a84a6b7585eb513c30e909a58bfe53bb27ef1269a42679ed45ca682be679c2a1b97f31ae0bca92132f346c104f54e9f9d0cfcb77a638609b1b85882b94405a2e574cbafacf6986a67e93faa0b1b84c43d80587276fb6b4e61f8e5f9b69005c5f4e4dcc82cb41864dfc53aa8201e6142750a92ad7a3d99f33c54d741e805da52f46c9e2ae0c4f2137d9c719a4550c83e67bc2b2793fb3cb124191f8045f59fc6a1f7c6258a3bd57a645616ca28d3d71c46fbc81fcd0f89ed8db0270173d74795fbf40316baec741dac16295e6edbe983732fa80755e2e83983b41ee6d0782d3518f058a892dd4173d206c98384ae5afc52ba8ddba8b3d441b499f67ef101b56a65ff9d81f7e1083a08fbe1ba8d20f69b7725fed1dfee66bbf2bfbb054dbd75e4be1d0a5373ac42c7b50df8fe9b6c8d2b66f6ddd02b234d35109c12c089e3c16b97c96c7fcf7b06d8d5065f2185e73ee2f3f6d24210621a90a45cda70ef7fc77030b18187c61e71fa17176a52c13aec5c668d8f1238879bbb4dd45c810cb56f6f885e4eb93512d9d05da4f3876ef15d2befb0c5c5beae193e522d556e8977ccd86a91968d1ecf9ee4ca46e242ff4c336b38d00a10cc362f6e461f7e1356228eb9bc439d659b25e0d38c02102afd78898fa1ba9e6593a3b9d148de8954ea263059551885e53335f448fa87bd767262a3b97f63896a80e35f10d23369bb34c79fc8ab565b6e988f517f7a3ea15224fd34b701b821d1249118c6a3e2aab75f697ec9a49ba53a61ac243106a118519c8778435998059519c4b5f1aa0d34e56a06ba9c37cdef50a9344172a1a87c1fb2f29651faf7f76ae8217ff9d47793ff9afa9f4af3065bdd17c7ff81e9a756f75320b55541f9ea7c7ceb16cca20cc19defb0103621c41c28699cca7560073b119aef91ecfd79a103ce42c3ffd0ad2b3b2583688f4400c74bf2802c40c1d0c4243c13cd65c5c5d6d63df678ead6f249abb7e2c315ad34e624914aed64e51689aee1408c5554e031b2670764a3875bbcd614f4d6c3f709a372dc2633880deb16cf3dbdff31c2a670897d5ba696f6d359007795052f09134f4f36bd78dea8d5a3cfd60da53c8fbaeadf2bedf1b137da928f76b37eb952097be0a12446cefe1159eb05df0c5262affaf0469596f2c20c4ebdac0b693f5781f857da4d684960967aae2c08e5374ae1e7198757fe0241981effcc6741a470228aa77ad1d9c439350ea4bcb3c36a5e7b74c920120686572e73ac32bd79597408713a45f8bef8250b86f61fd90bce7873a08b825f85e82aee10b2f0ab1c2ade160b2480004c007dfe820c6844d70e2c05534fb32e643824c14e01cd5936dfccfe3ef9336526ab7ad2a272afb97630e05216f0a939c2212c19cca119ce54b75f6cc0753e9808d6d38bf47faf86636ab6bb6aeeac20a7ed0482bce12b6350e1a675801375918d288a902eff511e101514984a07650d3f1a13ae0b067de0af1f1aa5b5242518826fb0f6c4c0d0a2073da4a73192f9f2e133fb528e22808aa7f82fc9a1ceb31b89f424344cf105f36aaabca1c5663f39f02e7c842b94512a2f6f6ccbe418d1e6868af00d4b0393b9e896e72452243c542906ed5216c5f08a26f33e2500efbab46fabdf41e667f6aa64c9452c6e70167f9568ff09061e71301fbe80b0b00f1992529c3bddb27f1a06671f039b2ac5c80ed850ac457ca4118f86cceb9082fb0ccbd86a79640527cc0c0b8e764d9664b9ecc1dfdf9fec4a89dac28d28940bca4892c0ca0131eb5de0c5c117db1b8fe28716794fed9ff928e2a627ccfb20ae0e10bc6b3026e81e2953815a6502b4b06d3fde77ca07d22259e321edd0b92e12d3b5f777d23d005051945fd62de2793118d31fd9752d9a6e26d0c7048a1d234bbcd5f0926cc0c4c0c749d4af6a2f07418a7b2dde2e3ff4eb11e571139a8dea52d0f91f75ff516c7a8ebab2122b234fede9c9543e74a557f6ab6b42056b7e798d84c27c355eb2d98dee4265797b33d02a71a93f589cdad1d5147e969ce6abd636067f819c42bc849974018fa4184baeb9cb59ec2a5408da5943cff793ed6594547c17f9f50f9bac31d273fcc99db0f7e8634785a7537bb883761321462f6a52f93fb0387b1dc095b4b9e6c5eee4034e4d2409682c5aadabbd626da7b5665dde137ebc1cc7c5398060321b30dec057a9f0c3f65174b18513366558c80bbe97811d90c49b57c12f349e7f50808cdce6c6e5d8b93c19ff0e2f5ae6204077cafcd0029c630ab77b1977ecb945c1a1c312f837b3a665be3d137f3502edcebdbf208a60ba15cf842b541919d052c3b6cef94bab90237890514ad29506edcbcd3dd901517ed9228e10b227ebc95d50e4c652be86b90e45d1c176b5218205364b17a64f8f6b7b860b6d024f0fbc17f11af33c6cae20df9aaa4c4abaa691757d9faff251e8bdfad2fc7c271cbe14b618595c7d9ec83570e7054b0dcb187636aec62a590bb0c9246ff006de2f5ba2acb8ca9871a517b92a0fe443c8de30f5123f4621fcb157fa85ed35e3025ae8d216aeb88eaa08c9a62090ac19c6b44cebc720ec5780e78a81e20df789e7ed95aeb8adf959b1c5d1eb9793628fbecdd15d4878741630a38e9b5b38631cb594374b7e24c5a0662be68b7af0d5668000907ce4714bf1de491328aa675ecd8def15f12edb32490e46c53d73f8a905ea8415825eb95a80af4e70668f58612603d0af36e0596868b48a5bf5ca2a4446e253b1727e1f8b634f99d3f46e90f25f9c55ddaadb42bb27aa7eacb3cc45596c5405bb2e8a8a5cefa14fba01249e65711f9ab11bb1faf022fbaec827ba711f898e8b44be4960355a3f17039235d2188132614fab89d0a1b54a7d1cad88df45309e87175a3e88a0786b5b0a00e2c16534e7c8f9220b6c9e71de4316658f542142027e0e02f997877b904d9b5120c2cd0179399cbe0b1f5fae9049c5c22be51f39cec762d7af942ccac8ba41d571965ce919082be5506b373f102610a4c292fed1886d1576599d2423b19aae60bd27e01fdf9f2ded66e494166565e22a0acf7be8803b0bcd930de5a68b4d5af34c164a737621f46213cc641b7075e679097f5d6ce2991aba67480947f03bf348703eb2991b6079fc25045fc1468d1f796953accfdc194b5ea2d5470421b740365286016143704dedc21001d14286decd00111b8f18c195fc70ce31594ed1aebaa6650772ef530ee9c88962506fdfd98aaba71883b2c79bfd554706481ebda5f7f205ad01fbd894e6a6fc3bc5ab25c74b7ba5a12aa014aca6b1322cb1b025db681bde65c6fcd1b37205d0198e9697e36787ad7f83e2182719fb710016df8d1bbde498432ba6cf4d133237ac1f0a3b9fa6851aacacc779a3b65a3ac67bb8be6be2e50dccf7b8fea1979fa2e9f3024a8ae670ec2651c49533bf3f051bc9ca8dcabd16410b8513e434a46dd0dfdfa0a8f4b4b713850b7792f548e6f43dbf335e7f2bc14e97bd975c1cd8aeebb5e84c4abc4cbe6a7e92335ff9206909502c11b67c2cbf9f19a57256cbccabf92f2d1963afc6d0232ea3d366dac2b88cb6661e6d272c5f7764cad6a4a69dd0b3cc81863355f4d06035b53fdb120c16f9c8895a5c5a28ae2be6e5e97837da4070b9f606d13c1b7dc2cc0baef410e01f608a04d0dc69c809611aeb2f25633e627ab87a8557a44bc20c8eda3a78b69d434df02433ff79926d896e85c719ffac123aa6ee2dc8852509fc6af3e5cf64680810d112061bcb05f0b52a4fbb0a51c22b304ddd0a50be5cde034c94c75c4b5b0bbd6a8b8de3c11d78745f17693c5dcb2f8a52496c6b7af097531e1096e5a5c8c07f1466ff16d343602dbed939c980941c8dd11622ca8ffae8bfa1d8ce4b525900a3cd20bad21fe6af7957136fb317d25f13055cf1235bbde0218b5756826e3def998295ce776d8aae17f923a19111613187eddb978c6b778f4e89109636d8653a28e5db80b9fa6f9f1b461b8841fa99750e813d91fdfb4790eb680c0d48b13af1b821f3aeb2ecf3aa17f12c14902f2faae63a316bfd36fb3b7c4c364f5a1fcc57a227b05280111ed05bf5bf7d9a435460c8b39725902b3e05ab6f6c4ee7e37c75904c9f5448d53e4a31f26601e81f4f55297acbe198fe9237259b7c7cba8ee610cac70a43b3cb6bf26985e85d4c756f80218e549d7f2c515f52a85f195d02031de687ec116a2d9845c0fda550a8bbba9c4b5d3f3c5dafd6ec37435a6a144c6718a2a6b672812b473f0fa298827dc28fa159bf2457d5b7279ecde21a3381876cdf250cc881fd237b2913983bbd6e65cebf489051271881dd8c0afc1fe764882c797d9363c5698e35f6c9554516f174a1cfb3fa69b84778f8dd82c9f753c4a203ccd11cb5d7caa0ef5c00bde9418a8da20764cc53d72e4fa1e33e3dc33a4f07c432dce06abe0f9d650c5a5eec1a5749ec6a348441d768cf7a91a389a96615ac5c94a0ea1262e79681252a238a6fad30758139b369881edc98d2dc4caac29f877e5dde69f699faf7689708b1c0664d10f1ddacd050d5b2d7ab8402f46bf4bb8cdd7659d1160070ef4ed5ee28cfc3e86f4bb0268e831a9b43cfc22937ee47cb0a5a18856437ef9e57d16280a3e0610bbc7a769fe9862a1cdf81a51a814af516eb352fd3e618ab9e01013080f68400ea70f336251237f33015f8f4ed0656e2b4bc263e70e5daed2c4e8e8aa6d72a3bf1b7073a01d181ea5dd8c81a2bcbab01f7290a503ff12e83f62c484aa149b8eec5366df8f9850357e66af5983022b08a7b3acb0743dca472506104bfcd600e1540a7a8215554c86e1625e1309745aa4eb1d7d7a84498e2eecbc7df7af560593b2b44dc3911782676310d811c3e6dfadfc712538ed81990416cb73af814c3ad2afcd83c80d053a0aeea7f5b941ed2ebe6c1d5487cfe84360cc8df53c8615faa6dd07920768dc0caa77dee81ee5e9e76595b046584bb8dbd24d365e234bcc12edac2cd235d171ec4e6b4107ff2e070d6d3b64bf951263bd8c8fcea0f91eb6dba36de918a7d09c92b643cf3de2656b711a5a1c65b8f335f9a0e5a6ce0bf30e42e2053618dd00a14951eb80dfb087b253b0f9df37f16230ba267b8e14ab66ec0891617eed753cf0ef3ff314e83f465b911ed9af7ee3bf452c15c157016346cbd05f76eb80d12b9579147dc1323a2bd9e32cb0cb4687186205c03be91ed44feec23ece877b0874a05969c0f4136df10da84e9beb544ea05e5f90c38daadab1614fd1e1e452ebaa06d4a022fe83fc0d11facab749679d3564d485cacfbbdda95fc64cdecc8c73649d7c471ec392df76f0a4edb656039d5e2863a8165ea459f7688dc45c82f32ce9c4dd691c8dd625cdb057b0f63b095e18292d6bcbb80b671de286f2b1a7abcc889a42052d1d0fba188953bafd67a7d3d6aed68d18e47056b7eb3e7da2cef2b7d645f82b3caa2029e0cbcd71d593d0a69d91485463b4fd777b2eee49b1ef0279cebade547ffdfeb7fb73e0da747273b74a84452a6e8d21e6157ac808a37669a4fa0f47f46825c29677a9f0e58d94511e9e1265da62acd08e5e71ad7a2f4623dddd13bd3e49dc6a84fffcecdab76b0b81cbc249113409e25d7bc0b343ae08fbaeebf6cdffa41d60751b5e8298f4999e118dd717b04911e2d1e6dd771e33976dc404ca5181ab9470613a998ec58b225c35ef04e793f6d04dd5bb47824fe0483d19b4b9b97369e084dd01c2d182d96a72df78da2685e2198d4ba9546b733c5440858145f6337b8ae824bb5e34782b255ccf5cd69b982d29415e11005d3fc61ecb71b397cc0dafb0e37daa4d1a097a696e31598fcf030919675d253f155af68eb3e67a347f5c6012ad4613fdf87e8e0866dd877e2d047541e37a7e52dbaab639310605ebccbc5950c5a04337d2a46b6255dbbbeeb956e9ef4d4fc36bf20e4064ec87efe8f1555dc6285fefbc65e9d91fad135133c1a3b82131c62623af519320ee63d0cf1871117fa6bddaca4774147fdb8997aa4b64a5996127f2b4b6065a4719b21e33604f7d67e582d2871281cb8e96e880c0569df9115c51d02733058b1dbcf76ff7b5654020d180e01d28bade8cd71dd543c196eb9bb4c02ecd0833cd9d5d680efc4b9d427e4ca853f25d0eedb85cc351f38afb45563b39a2ce942f3ad0decb76c7778d30e837e17fd46bc725e4dbe1ca85ae39060b61012a9fd29af4626f81398a6243fdbb6b9f78fe15ebd16c4818dbc6396bba3179a41772eb35fec37cf495a1fd3398f1e9003247d0422f5ed464e61834470818279f43a8408aadaa99f6319c0033180460f6504683aca5e00d38fc533dd7f1840e68f2be3e012a9a81917ed8b6358ab7d933668027a48c8c1d1967c375b369bf635392f4812e6aa781c137e7481dddf5b6ba85ebc19ed9661a094b32b18db20af513feb05c5d079d8c7fa6bd711331aa38f7a455f738a62795beb9af3c020cc621e592e1192eccab06aca67c839befacd557ec0f48f7484c74b0b481905f06ffbb9acb7e047c05cd3ccc460f2efb8851dc30cc186afe728ba967c9f917cfbfcac73be168b9acc4d0d46bf18e29aea854f0405b07489aad97b9164457ef3658b0d9424f4fcc0923fbd2a6acef031826fd4dd3d9c26b1793d553efb6b869019c28cd957c06aaa18bcbf2bb50ac30c579df08f91ab88de51a1b6f78bc662f46d105d12979c516f5b208834fd1d5191a734a6921fa256cebce6adeeadfb6cdeded0746f0f8f5b7881cce52f4c5daaae51cb80dc1f62e07c9a34d097748e7e4f427c007b55dec3f4af410c1a0de9ec806725e479266754ef46bdc543c90e64f190e35ff50eedfff0f2e1578a9aad319649fe4bb279cf4f3eb7014361d49923b873f50beb55668583ba0b69d610c254dcbf8a6e831db76c82825c47de8bbc442f3c528b39aa379884da0059d8e5917761a7686354659e20f4900851cda37a36ddeba9e10dbfdc85ea4561aec9f2877e1578f8faff5b1083e13f59e765bd3ed047c14b1a1c3646d4afc18a6c5dfaa4c00b1367dffd3fcd5bf2819a5cd39dc70d3fd3512d2041b6814ff73627b7e6d4ad661fe31eba6386f975a04c647ed189982a6b0bc600ae14393c5361a7e890136a57b4991f4d3304e93df6e268442c8cdcfe294bc6d439c506485fc3acd943d5bf20ab7b17148a20b2a3fc4580deadb1cf4809e8e718617e92e1e7edecddccf3329cd79e785ee79185fef000c7d056a89b1fbe2054b69e13fd9c180c114ec75cb100c2bfff8095681abdbd1ff282a9620ba15e5eb150bf82f2467dde023e819d2cea276d690fe48982869988fc8aee88e0403e61a88397aa0b1a5a0629e01f07e272d6065d63d73f516fb45d2adced2a828d198e4ef00b3dc9a50fbdbae4401c973f5da61079f0ba6594c05ec66a382e4060b6a6c8a27cc75aeabbb86804ed3369be5baa2375be7f1e973823c2614c51e787f1932cf74ffd6174946fed71115cf04ef6947dfbe7f3c3bd90bc45ae256cbd956a99c079c834ae3ca8e8b01216eeaadb74d8012b5b0ac14af7d57801f2ed40f5db893caf95fa15c70dd58e9f40304d73e652234652a9a755e59d66ea76ac4e1a49bad5a5942e69e0bea4d7ea2f9cd3909c2b5eef5a46adcc6d2ee57568b77194114ed4ac1d3f3af5c28135088cb21b60198a83d83a79e0dde7a4dd6f0a9182794d31f56a710de024aba95b7162e49bbdccc41aeaa44cafc6711fb1f42e376fdaac8503b43ee6acaedaef9db5288b49b66161de52f2fcfa31c574a5c8f49a44277371672a02f0860601b8c0733ac47fd04ba38d07240efeb312dc9aec021fcf723bdab46eb128420207684ff317ce93fff40078d1f3e0a801782caee2f8985d314c95a4003013701f71f51ad2528632595a2c9b8f89d83661fb42ec1e1e9081e8dedc372cd5f5745215744d34693d37a16193f23bb28fc8d2cbfc56332b207ef20ca082575555c37ff6309843afdc13731d355631a75be9b68a09c4d2fd882634efd87e2b2039e68e1c9ee24c7e97dfcf9594d1bf0ec99eedf05a4ea4f9ab414d4ce16285c1e9fe799f646958b545ed54d8fac3fddca7dbe825cf1091b977716473448752f6e38d728a0a7f3ea1e2e55af227f0a679ff6e2464964c3cc3e436ccf5dbe2460d96425ee87c8399cadd24578029cca115a7c77bf79ce5cb588cc5b9fce33f9705730dd6c08be9568b1b878778d3f7f688e29358751169d2d1015d319518647249a3141dedd799636c89affa9c43762c59a49ab28b01a6211d3f53e4fa27c7601f38576654baaaff2bace0f5214b18202b283e27b62fb4837531a4b7fc620a13047556a0d7f4f9099697fd87b7865900731eed641d912a912b722261f58cb41a3b0a78fa7d4a9afc1aae52f47a76ff8ff7dd75785cefa22b9e542689ac945573eb67771a3f772a91a5bfcbab24120c0c66e7bb852ed402b09dd9347db2689d0b78229429b28f305d3a1fe2ed4eff502a618951f942b272a1fb779ffa2f74d4b7218ea898cbb2f3305a4ed406bbe5079ae7432cf5c3ff5d1c74e972a5eedb8637f75f3257188e1adcbb3f21662128439b748ded26d8378266ab094854c6793df6cab9a3c15fcecfdc861fa02cf506ffc25215abd7c60beb8776f5a5fe674e1cb354ec4dcbfb646f3a04ce40010bfd2682e2f901083b6f1e0ec8c8c2de156e6ad71663ac9995bca82bbcf2fac01803b06dac605c95fbfa520761066d9d5383e6fe46eb04cedc275e4372a5e392a2eb0b6c7bd5d2407c469ffe497b0b7d62c95dd47bbe3aadfdb76422db0aadac45ab244254b5708733c4c062823681bd36794719bfd1d68897a4ba91fe4284f4cb547a602ee2ead2951267195bedc4f73e1ce8673093966743242e967508d7a440d97736f5836624ee767601435fa5bfebe3e97f6c1e3e4c07a1dc5dc9c3a1b4baec25450895b12c6c883717ee8e201321afc7c00324d5147e6f1c0294d384e22f537a28fccbedea4c95bb14cd2cc62a7a40568154f2da3c03374edc0e5b04424d6128224d53f74c72bd23e755e2d95995937488c6be4a2e43cef7dde6137431eb66a7887c02ebb497100b8332f619a05a5ed35b8656a280e2744dee385dd59be706c458cb288c027c74e165d74eed62d2aeab95bb6e1b6be9955666b4dd4216565796f72db8d8d14f88735c3b7ba1ff906a9f67aa2f8a2377d0fd07e168a506cc54bd1dadca5eb6a55a7403ae53f881ae68ad0c7b454e18871f3ac18f73970b9834e22f1e5bbbbdedd408983ff3d57c1faa912c4f02ff52d814929d5f3c37508549a705a514317bb2d229ceb78968ccca7951559dc4be3121dc4b11789e4abca20ef4e3087557aa468499c2c2f696e69f903c5eb7ba83e6e98987c7b437bddec27b620ce1054ae0337814f688623bbaa82f592ed33569547257a16bb506890b73c0e7e3e21e9eeda04f8bbd515fc891c5740d5a572ee20523b8d3aa9e51fe455781282d6472c6643cb11ee815da90ef159b386a446c9fd614db1ce99ed09e28319fa4715005256391adc0bd48e6b0769e4f745d82cbed9aa748457bba4a2335b111adde20164d5c04c82f180a411132e8e8c2f6ef560441015e08197d4fb7d62c7ee7df49888dde1d90e7c4d2825bc578b371375a1a0a661e6193d5d0a8ace63b81f8f9dde5dad9b429e3717e22965acd046e9f6094a680af6abfa81817b93c19512e2fa1e75e943bf21f9d931b7d7df6a5f021a6fb90b47c8f673ac42c002779956e7dece6fbadd4611347858eae579c86c0ec8493417e122da8f3019c89a6bab68edf90a9e81b95ce86e13bd5ddc43ea172ea89ec99d6c3b856ede0eaf64901b5bf2bea1387bc44d3f66ae0cc5ac687019f8164b5baf0f46406324af28b58d3ce5d87969c8a19aea05599b79d4b6f9f1f90a58578e90182210827861f7ed9b48c8d5587fef775307f18cd3ac18a9aaf4ad00f702ce34978168b7a66fadffa83cca0768da27c46985a6881aa086efa329adcacc25150c26e24d85a9e9de0fcec99408c23f29e00efe37dd3c2cce9b503af273d4286fdb3473796a73efd8b5f77b53891cda712928c14bcab573e9149aea787fb042cc6d55e321d4df5373130d4de55878ad0d2d2cb12b7256371088dd119a416319557d09d21cf8e991a3133c4cf8f1b0ff6d9e26bc239665a9ba083cb1b021fd8dd2a86b8e95f3320237360a41c8addb76a7496cdf771a734f1fe6633a58fc61744007b1b356b912608ab74b741aa7f56963cb02a94e08b0f5736dfbb4720788050e4b407e2aee0b80529f7c5e289099cf1bbe6e731d98ff9e17e88f42b78c930e71f38addd4b8ca2c836cb9026ccf07437d56405fd04e25ba772a1e9d4bd96f175508a59ff759240e32f69d4a8be22ff91a2061619f65daeb1db18526bc2125ab3c977363d1739cbd5f2cf805a9c989762df881bfc332dcbccd1ede5f69386f1c55e33dda5b4445dde6527838dc4586c136057625c1c0c1e3ef27d05bdcf284d17f8524f966a1cc0225b4e49c506911c4e650c9b40a90a02841c9090bf7bf672c7310d7f7e5e71bf130657eca129b5030176e653cc7177c961b634f8fec7e29306fbefcfe916d2fdf09ab2beeaa06daca8d33c48b9bfdde46ab2d0ffde67eac15c74418ab87be7f406e141718ec522a24e4bf0a4c73798022259f0eaf6643d3869995d374d7422ec325d9edeffd1ce68f5d13e34d0702c9886d8709e9bd26f92f05427a648e242da358f69d0d90b42dd470b202cd01b76c1d78f9d90df3f1ccfdad49cca8d655520233b582e83bacbed7ebfdc8bff076df1847c69ea2f20dd9e3d753435bd74f0fb1859c72b92b0051d5b2c41bb6cf3229e7a26e23ebbdebb8b130a724dbe6261d1c8c28cce805f30ec13b1c5d4ac69d2204585ada5d46e1878ee171cb353810e033b1ac2058729371fbf8b40fc1df0a352f2b80e9f116c4b49fb08b70da9500525842c927ca8eaba985c2a7b4f38c08fe908c6dab78f7741becdb7b7bd07497b23968413b173309ce674cdf7d44f43454528fa4ecd1c33e9e50599ea7383edf463c14ec65ae861a357532e69e3aab09d6c54659ce99ec5819ba127bd9a5ee6b0133b65d2e133666b1a4214e25bfcc0164e1871e05a67ea283bf6e057dac99a46ade4590ea7cc3def196c77f1c4c163992c89b93c0539aadd8ebd385279b629fe67fa578bf16df9962c25baa0d4b8fca7d9e534c8ab9e21b8a50e0bea4e31a0691c9c0fbe2ee3fb1786f845be0cc9b80f9a8f67b8a150fb66e9a8cca926c46a911e0e318016d172366642e86f13bbdb1af276dea8be0b64442ae6bccff6a47e3d39a3be1f608e025c928d56cc5e040cd43b42c43a58f747c115732850b4721fc08bf7baff99cef1ece88070e6ca3644d735aefa73f8b2d5e9c85ea60ca0f443dc04e610c3b9ca6ccb9b45bd24c648eba2ca85576589963cdf3d18ecea347be4556979af40fec88a0126e7f993ff6fe3adbd75115cad2fef61c2c142eb78bfa80925afa78283f09732835071129c808efaf147e2e5e61e811b3c19953dc34f888573f16d021e9c44269be02559c674081ec36e8e89b52c4874711a18a02ce7a39e39f9d26372196de3d2d0544143ddd1623ff95b24fb13cf9b95dff61072bc416fa1d325cc05c3d0d8214af84a7f2b4747e45aa748c39aae55b5bb124f80a5108fec5f1e8aa367b6bc58bf39fa4b815bfa1a518fa60cb440383c946b101f2d52e8158d6080f52889a917bb707f0d12a1a2060ab0d26fbbf2d2823b0e2fe038dd7758bc1bb9e30950abefd21bd327ece92db4854008eb9e997beeeccd3f0df49d7ad275a8d1df0d1b0641f18912564e8b79462763ce2a02f065ada9216285e4e03da6953ad25f0ba6dfc86a8685dcc9af3784545cab10c6014eb81d99c4a4881c75825ca0c49ecbd7643d54052516f181e7f14b787257185b3d569e11c2b3e9265a658522ad06a1c13035515526ad078ad3adb2c8c9e7e6fdd3c62574ec6c1c0b59b236e49f639dff77c9226428d324cf2a7c39b1ea81228ca224819ff222b2593a8be6dbf9c375515e31ac501df89a4149c5c21b7ab72cd5a842817adae49174d147adc87dd41d450e7834530676a3efa1622f5edc67c713f8aa76d556a66085dcec7ec391c1f5ef8caaba265055674c8d7702f7822ddc0bbae133636f4a77e3a50ecf3f6189cc81e1f2d96e72d8b15320756dea81e5647d2a4e9389ab6cc75312b2d4e14dd6c908d4fddb8f24c8ccbbf8198be7a1ac49087012112976cb0552fed732386776683ceffa13e1e5638f8d8572d74e2718ca2d04d73555fc5fea969e53bbf1924a100b757f3752f3ba649ba05e286b146b8aba1696cb6f7cfe8dab10c7c994b8a85ed396f73dbff5c0ebe40621f769225cf25214cd7925f93f0c7508008c404ad4b8e5b29be20c5fd710d32ed33039b258de1ed4c5e759661973ccbc234690b47972f92d6c4de73dbdaf8f0a4a983f867651d075bedffd043ead42ca5fa29d9986e3019a959917cd2b774688f0132f48263b7ffb04d5170c7c9a1449ad8313d6fa00d6f05421ff4359d79f0cb1a47d4ebcb2a6f7c6e1d70492c0363ca85d52045720fd5020dd32bfe109759ac892e28cadc7655ef5fc1ec4b31fdc86751f643c0f521330f54cc3e37e146032b239e4ea96cd9d6cc709d5e54c10b7d1fabc8b44b6334a9d29245bb7fdc7ac652f81fad8215169e8312dca2a405e5a53d7bea2e31a40bdcfae6173e72e2d5ad97f8e5ca36c601de01a5d6352aefce1f49a178b3b8cd9b18ac05d81269d6df341bbc1433dfa765795e5c0d037b61a9c004b6c696b71978e8105501d7274bec0cad27a5b67cfcdd1bd6b6c8d29e68ae309f39a6354e65fd8e1ac8f78001df254a50f9c67aa07de90900faf460ebc90e1ec4389011527417bea2de06b0d85e3f8d78f5a3eb9e25cf7742f689499b2d93ca314b3c3afa34851edf1247f092dab5c1fef8efcfc0b8478c84f87b93aa5d5c06cd0817bf4cdc20fb892d3d656d35828bcb58c516f7fdff3918025a0106492f1e277ebef8b32624500ee76314d45f3cda2b99884f2ce10da869e0327a90d287780fa36230443b102f506a5c76e8e16a1ce63cab0ce228a8d0c7bd5ce5798fff12a3c89d2fe03f0e2d47a3b6c40656834584c560c8f394e32b0a9a1d683b94d844e0283454c2b116acc00d03a062f9bdc7a4b35371f6eb5afa72ed7b52364fc2bd2c3c5a266626cbe917e09af72b3639aa441a925d13f830876aed9dac788449d6adb1faa1e4acdbbafdce76c8c148c49d8408701b69016483300cdcce585f183151d8f539069f4bf1984da1ea19cbdf3b9b8058e8fd8b78f7dadd7db4075a62287a2f39f9fb4dfdfa2f48340af527dc9b7db0d85ee8e39272f9afbadf90cc251a39e2c7b2fc6b22bdd2353508ca85513655f820be8529d9ad4e10d3631fc5d8505501a7600ce3518c254e8cb1579b042af3a8c93106b47e2eb0d209f22857bd33e01fcd6d513c97c88d0a7ba48708512970417c888c84ab08f948e47cdbb617b98471b4388c6f6a11db9af36fa4edb3f21d5400061d7a8c7e9307ea0489adf69f27145eb7f5b51cb32705269e2e07efd1230616dfaafdc319526d343a7fc1d71700e51f9e94a69a2d7d2f816ce7489bac731971fa135c7681f33862514f4e23c5910f1fe7d0f68dd80de5da1415f77e4b83147a56fa20ba337467cdf9fbeb681f8ff7b12481fb79a8153367e51fe54b561f21481758113da17b9cb66b3623602adac44c3c000c3e7e9c023ddc25e8c3ce399a7ba4148d3f73beb63755199f04b53da6adcb4053f075081fd0563d7ddf7d6080f3ce16614037bed623bd8cb96787590d03194cda1d450781e35cf51cf80d5c040a5bc8b643d7be86b60942b8ba5d04cbe97b2be344331f98024a626d5f7d4ff428c00b3398ba789a8254ed7752c81f8768d2891d5506cb72d24e9a5c2055224e960c5e8ea33d6c9e309d6879a7a55b1f99985e2a09da1c3a943c78567937bce7f5cf56a509e285069eae9a5e7197fba91aef6aaef92bb885e8d16ccae31370e8e3118c8486269f2c2a0c015b5095cea9eee21a9c92b8547cb22bcd139f70ed68ff6d4df20a7c7c0b5efaf0b415ba240ee8d1eb912f1dcf511c8857309c4af8a3a8df4e4f6aaae2d7f750af0043b781117157d9ace7154af0a5281de5e4f39716f5d5afc499eb2758effa6008f75e7563f90bdefe1532874ecd1d617d3dbcdfbcaee446a3cfe0e4f06192b94db711afee31b725b81966543bc99a34b92423551df8fbd5c8ea9eaf4e9a5cfb1ebaf05f966aef4902ae4e1d3a502452001970bc0304301d2f153ab6b56771dc909b300dfee030ced31ac58ba4481d229ec746093a3c83f655b9f80e1f12b2cdb01528458ae608e69e4d8aa389ad853051a9d98c4c669e5b391986896b9bc92e30cccdd5761a46207a340d7849f5bb8dea4bd9779cf4b36136051f307d328fac5b593de61fdf9559a83ce15ea629cbf2b99dc6913f4eb699316882c7a67f20a90460fd916b8a9ae07c8fdba084601942b275e1d0333540f9b93c58ac5092675879c8cf7704501445e3147786d39e4481170e510d60180a9e56fe4cf89604ed83906111519ace148a149498766f182ccee7a45a29b55ca0368c49da57d10ad206caa1fbc0b992044c9ada8415b3c3ec1c681bef68bbf37f8ca371fcfa03dfc5b8975c1e4b738d80844879f59cd234474d7c352411765b1f40cfc7eb9876b0541bd2f7c6db3efc768414d70bee055b3c13f56f9a6dbdc41e5639dabdffb053c825964d80dfd2eefa2b1f0f692081bf201dcf45a75a53fbc4c210f64b5c73cf54a1600de9740794ea9c4781476b3f89e0c091e69b0465963f06f8c0a96bca5214f3ff93f8802eb15c99639315ce1963930adc057e29907914941d2705aae21f427e6075b52c0d8999a65d34fb1ea77dd32ffa2e2d2110a50c294cff35018a1b743beaeaf12188d467c7134c617d5f638c1644713a63baad591c0b8a49443f2a4913a146a308302b43d8bfe4c06fac4276d9b9b444716e16ebe7ad0d59d3a05a9584fa8ef376f08a5f0ff8ab927832fb223e3cd8dadd572cf0858a0a99f4f70e91483f2b0a98b1c8e01f3775608e9ed357024e02bc7ea33ba2c94d2c956bac1d4c02b9a6e71b1115d2a4e0f445ce4ecbdb66fb1c7f23f4370cf76e6b3753f6b8d515e628397dbd129eea4aac560f745a7538bee51581413d97795a0af63a3a26a08b622b2d4cf921cdac3c2f9ace0a14369e2c48122dca71b6b71a2d92a0dea081b29141caf3a2fe080a5cf7e4109a96ec7da717b76987fb3d6b78b9ef05e5f606d1477fadd3f96b4a92629ba4766e0add57e35444749503c0979f510e0b0f365f3765f6d86da5548564aa3bbfe205900128f996f97b861fd6c663f6f84d83c789d0e865f9b4a08a75415efea926b941d42c8a4a7ee047f84b37c0ad38943e7d73ce48d0cfcc00da68a7c6e72fd6d8487b654d7ba032783018f0e44ce081ab6f55d639a5199e1c9c5bf133dc18203178a098922075f2d4e4dfff8e52c32190184e1e0ec3162a5638d60f4530278f44cb4cf5f8318dbfb0c515ea8e8325c34d5d8aeb4b8cfd9650a8ad8c31c2377fa4b98b599d5637319d6793f0915ad00041f9fbea8ee7d70e84ff67dd3052653f6ea64ecd6234ea9d4e377988a71d60d5dc7e21a2c13a22aeac0c517631f510a9c42168deb5665c35212113cd95af3bcbea23211f86ce1943816747e5b6c4d7a69839a290407760ed4b95ed04541ad20c71cbe10478a6d6f2913be6e4139b22e272482996a86ed85795f52bc3f985b47571617fb105dd34a4db2a2734726a1b353a44a9e8e48e380203adc5daf28c0487b427e3dfd12181887ecfacef3552b6908131edd62026603149e433ec66e324a001cf3b9ee23c6a265f84d4cfbc7a770b00be5b836d60cc22446ca5987c654080b1f2abb6a6c8ffa5c8d3a835614b40645fb03b21a182d3a8dd53e22f1944d582392327df56877254c04c0b3f676aee86301a1bccc94f290fcf75ba2dacfd78c0fc58b445516184d7399d046d024c511a5b3136e5fdc85ba44e472b1080276731dbfaaf89c6fd1a58f7f76d96de5d67a5c00cfa19748b1ee3cd21dd27333a06298cc0beb1504c56bb583dd7ce5ade0040494aed7bf3da5f6ad34e0ede22ca28dc15b0f5743f01347b6491440fb35f86d72d44cbc788b3a24f511a5dc8fff12678dcb76245233b80a961a4df40d89ba34fb3002ec4cd3d56cf1b0f9e5d99d6b347aa68fb55c5768a7ba449ff04e487b1dd3076904e9bed76699808a039a594eabb62f683d28655d224218a716ae0604f8c1eb128353c71e71a8e19ff0ef6628972a0d4452179a47c4a89f21ffb30ae1bb6b70cece16defe1f369c538324e85ee0aaae084d444e0251b38a9fc2a006442629355ce212ee16243e66a7e1acd2a01d65c985d8dce40ff55c9a49007f13b56d7f2b2451c31ca04b3c007fb3af693b71923f640b2d7114e56932cae3ee8ae2dfe35fe943508c9424d68ca29e2af1b67aec91d811c40d95a8fea9b171af084f869804319b038e2277cf4a3efff4fa7912b8e574e394a0c214348431026e4a97cbb2ebc7ab45edc88c2dd3eaaa1927b81d6be204052fbd1fec13e93e67bf2ed39ea282b31d1982eeeb89fd9beff255d069b6a21d99832c1a7b19a6fac4a3571f0b853a5e47ee2550a9223102f605210ffc461f2070e642e3a0ac283505c4d3ce7f652045aab1840deeed47d967324095c4b75d29d4dd65176b522e216824c009a655abe5d06205578e05b9d7acd7221a20ea40cd2f993d85288fc18265d405270b3930de8f0bd828a86c3c1cd20e29d57e73ad3b8a6e773010fd7c0ac839b298c2070ad0f94c9f4e73ac84f11b5618c605e1705a65ceb56a136533f0f6156272db0b4a931735ec720b210233f9c6e9048d29d6bdca927628edd20d44e5a107e8bf5b3b216c20f3e17542ebdaffdcf30f7ce1c9865bad5dcdc7e575351efdc9925b6681e9e9ea1ab97e5ddfc7dd724715cbb1ddc600a64bf9845c81d5c9b89aa0c20659192f8e10ebc383538cec279bad5677cce3341b82041af642ef3ae4ff8b248ef460020be5a1b1a81cfa557f6a4e4b24617428fea681f92bf58bfd93eda528d63145a7573e088d69e10ec86464f62eaf2282e353c8729dc4d8884e46c4df073c2423c89791a0301f7142dd5b4e2aea265bfc517b5460ee25af559b792a687d261e6c35a689313d40794d4603cdf15bd1d033b6b26c68460af6b453f914d232ee2b627ffd65574197468732e4a45879769379fe5bcbbe3d5430d018d2ab0fc27ce55a90a2354dad9c533c53c24197adcb4d9fd8f1799d00da4f62ec83b8d4c3f919b0607232f184510e95d11634be723731eaf35a730d2695973d26e92812a4fca74c2c945beca9113dabe528162634d1d5d8021cbf6d745a30f5b228ef03338794182040902f34fefd308330a83d780f5ee51235021751bb8ef333a0dddff098d74201c0a1db0702c577397b6b534bccddf7515e5eaeefefae1149205bf6a938ae1a1f4981684d34801e8f1db993d9701df95595aa929575b6233994c9e893beddc9d524a62cfd522ca44e39d4e5e1b90c7753d062575e3dff72c080e549c385fee07f9c2c13fcb2f8daef122feb25c0677300fcf3603bd500c8cc1759fb6558ddc6c79fafbca5a1ebc1e1001dcda092de4f3a5a7a13f17c83436ad2547a90715d1d45a2665c4de2851964a85491b8f1f25f8c5e6dec80ff524ea19ba406cdb513e10bc5f81a342025001cbf34d10b57236c3cf9de4794ce65a656dfc5d86201c96a63e0e5b91ab783ea6dc12836fd97c40131f490092fab2c4977336f2f516a5aab70aeadf87378fda9b005fc24feecfaebb82a4b27a1bc25f1f4f19604f3fdc96990ce194e94d9593297c265d366ef6ee2a09a204ee8daf33fdb0b847c97f0e2cfcdb33fb067e6601219cd604bdb85d9a695d5af29acca561d7eaccc1305ad39282717b0cee56fbefcdab444314988f175a84e5bbdf08bbb9a06701ad07e46b7dace55e120d6247d9f975cc0cebbb102e169d873c1534a1053913a75ff337d8a096616608d9ed4bcf9a9b94cc197407ce74ff46902ccc1dc71f9f44894d380c56175547f1d0cc73b98e3b1be89b686d3e7f3e47478de00a76056c53e640c221e2863e54b986387ec502880c387a2e290383d08a609779d02aeba8c03ae90c16285641fe7206324893e4bfa3023f113d7ca2d60d989f654adcf648ade045e244beaeccb0a823cda0201dbc53ece1347d47b6d20ab137db628120c08b01aabb36333d1594cafc6bf1d93283104198f5db1f96f280994a97095f5a281d7560f2914b5ef26c152b0a30a14632713c9c5f720c413ff37e18e61b15fbe0fbfd09ed99f60d9ddeb7436cb538b918fba148ef23cae43e6f33ac7c973410e07591e11c6b1f1ad2c61eb3060cd82b9bc6b203aa020b605f32aab0cb06e3e06ffea40bb99dfc188b093d51020c7d480e0163b80e5dc644b43c5ce98f9769c1894d8b82fc956303c4902caa2c0d7f2f823f58def7e8a97bb485cadc3eeff35244a63279e8b97d8487600f5fd96bb435a2d5b75c56cc08443ea24822e18afeb7e0f5744c1b358732b006bc10baee2005b44e77dc4acfbbee1aa6fcfd04e455fc9c56c379f8bd078cf7990904d1082d4a6164945913de4abf5ec8e7c559778a493cae122f53b23dc1f8a93e85818a02a504969c928c1cad19f203020d45fbbf564cf857380e2eef01faaf38c7467fdbaf9b40367c5d79f872ac25c15e256581707ffd7be2ba01e18ae5d3ac43f8470e1a8a5f8a68c2bbcdd3f4c1403635aea623193c8ef61bd12283da515c12dda920a624fe6d26712fdb38a2344815b0f1633a393cb294691adb7dc7b20701c992a748cdf0d77b0d3115e84a5c5f5d2e8697cdcb74687520538bac722418a219b7e8c70dfc8f2d5676c6475147c767baa3191b2890886a1f065f5e56e2c28d206aa3e7d9a99070205b5e886907aeb97efa525fd34d67d12cdb27dd772961fa0456d222074782243b823148e3df472326957af9bff919d5fab75100a1dd9f5013b6498605795cf4c1ebcaad95030cadaa06d91fd9cd556ea58184063fd0ae66e397806b411289eaf749e0cabc06cc7d4f664442389e159cfaf12a0703699d73dfd8cf63c8b66611d9dbc27ec3b119a55848a25dfb5f712451224561511da3010de2171a8397be356342c1b44e0d6328dd47dc750b7f3a48dc9a0fb241fb2d44392caeb8cb585e1a074ef24be504a4673ba90f57c78d057da6bca935ea5a1d37cdc7ebbcd96bc794e8118078e26889a8ff1695ac19474994f4d806fc39452c752cb08031c9aab4c3dd1c2d497f113b54e4c097964ab19309b8372e3447cf1ce667f04dc43adb5a451ac5e7e4309814be14c2698b1d09eabc295c1bf1a848d6732401eb5ba5726c84834788a135497b59973b7c342f6a72b339150aa51823cd4c4173586d8f054400269e840c299e4bc182f7ee1b2bef92e48509ce8373a4ecfd88abb08f51220b1f31ccbab3eeeea647349a76f0bda8f26809b602077f5bfba8ab4f82aea72514ec49d72db91c1b26d2f145a19b89cd7c2e99b500a51828ea5194e5764596f7618ef0c135b098671f3d147c0ed6c9f475396b7639ab0547d127a28016ba975ca6392454eb2083288426569f2a89e0a64132e9826ba918ace9974b601b5a1fc26278202fcb5a49beae6619d0869854a06ebbdc3bf096b8e75cda883a71c22387c603861ff3994149776ef4da8fb1b57979a6407b356b818dc8de6205d59d21c9a55aab91b9b2a9437fdf5de73207458bf91323f4cad02680bfc870dd7cc8e02a4f2cba9e9fa005cfecde158db31a765fa903e65d0095350e965a689da39657d5c55d95e82f7eca28e9870ca76687cd79f8bf4fdb1d444d46e60c6ef4d64f44ec3a0f3f7321d3f5bb15ef0553e815b015a4912c78d9376f21da2cbe00688bddee49b25d76d38385edc702418c5b606d15ad55a4d84917c17a772dede76e5fa652322edc1f538908058fe30e5a6284442c0ab407b6293d84ac4b149edf0a45790f01ce5708a9597e7a9b4a88ff7a8d586301facf41734fbfe53f6dbb9b860450dbbc50ea7c0e2b9ec61f8ca4456684584b862ca9a497873f3ae9b889f300c6f9fc33e90f375a5a80fb2543401073a24a9ff07ba340778c35f698de3cf04b38a1c95e689458c384a05d69131d6fefcd87fb968c621c2b2b0bbb6b70ccbf7d65ab1971e93dfbc7959cde1bd8ea59f4a6d43df3cda39ff632709b5231ce7c95ae067dd3fb89a15a6416a20cd4d1ae9ba2491391dfb44285f2be330e3267ba8af3eac11eaf5239e6203ba89d83d3a166f6eb4c1f7abf26cf63b6d7413b5381e332408656ee64504587d18d124506328e5a9f68705b3d37343def345421aa99868511e6777498bb416e7c469491c5f8182236a2c3c9377a8a243f369c4322a44d07415570eb741f7bb4b449f7a54f87e88091d34131375468ffcdbb1b81dd29d81da3f3992b7f8bdbed439aa995b0db42aeeb195b9997979f32e6249b861a64fa78e83b53396599d98d6f1cf2837e003fce74455ebde937d450f5d2cd17ad739837262820151b4069f80ea8346beb9720c2a7f80b33e0196d40bc79eefd33d7e1e3e294f228a779842bf1f2ba8e03e78754898a02fd049107202877be2b98d5514cd40159ed7549460d87ee6d4e170c8e3d2f4514e16b51b7a84c688ec0baec7f5943291e1776e198479c0ee762f7c4ff79f227ff6bccd8e7709ba517122cf8d0d594e7864a732e5643645aa733ddd067f07283c97739ac57abe97c0e58dab0c2debecfe69bf81b330e671e6c7ce3800238adc79b1d4638c91b7985f81767d7cbf2b627a27ad0660f19aa00b1f712b06b920a541d18f69f9a5af22a0074211123fabbbbc65f7bbed1e14717d04190acbe2eee5df23d6a18cd3ba7f4f488e540b91126493187f819fdda7cc2fa5237bedb2721d571c0f104dc6dfb67d32ae797de07fb3ee5820f20fbd48a56d51edb4ce7c290ab8b1545757a5830e6e9346a3207b918b6220546401c8d8bd48c2c360c707b16f77a520813a12a6335a7fe99d712d4d884842eacee09c15e166e887d905b6b0c238ebc74c110e610a20097afff61f76eb11866c10c4a062ac3e2ea099ce593c1ff545a8a5e4824c8b2519f41293fe653ad88f3df192cbb8254483f49f6a679b63893d69aa5e743d30c0c0aada5c6084dbb6f20a34343cdfc349d52b4d553444714b34a273ae34213e08701608294b877d0abda60a304f124980a8ae9be149e523cc7ba4a1dd1d864be0f24f39cbf606658489d016749bf520313c76d870b44d4cafc3f147c7fd1cd3cd76ee45e1ff3068cec49a875bd1833a26763a19b82962eb706a4fd3a52baddeb7b4183737f12eff9e1f71695385301e26c33489d625f1921330867e59c7ed32e5d35b2194337b955caed589b630aaf47a2dc21b649049fe1561bd7df196759053627ce0d8663464c278794142f9e649b72b975417c51b1726c39b820cf89706a05a8becea3b062a7b11ebf5c00e6c26d816d0a69131612307b79e99bfd153590cd0c86b4e8b3a2305ba73a4824d43bbd9d801fc3a6ba8f3f9d8b12296cc7003042a38df8d164ba9d945311133d13029064367743b79cce386550be4416ef8826e195c653a1d48fd32c0e40b46022be81311bf7a1fe85a6e63af289d97d653f715507ab46cdedb3f195ca7d4e4d30bcc15480414b576316a70241e6af0cc614ae747d7dda35325d87bcadfc3c0252e4a2471f8e2b099128b5424e7fd6b8e9bcc5fe1c3d2866eda20b2f6b73751e0d6ad1ae9e06967ef346c987fa8c23bea6e5199cab192111e054350ee079d5bf71d6d51f10bc5a28ab57b8d4f1fe4da125e480e3a6532eab8dc753f3337520b29d4ae075e84f2e408056fe579563427eae14ecf1e30c42776bc9032065ad568110f3f9d819f42da6a106302f3c1fd44ab22a7568e54cefd2e48c04eb4c6cfe7bdf044fcd073baa238cea0a600f9e7d9b78514654f4afe3512df4678f3b4d85c6012460bffb556dcec7724547c516ee3ab4c4b789b2632c18ba910ccd8a629d652db3352c3effc1bdad9ec95e3019ca49b9fcc26a313691564bcaae3b03c8443182f1625e3ce974f1c925f0728b0bd35730c5edb975a091e7dc61d8226015bd5592ed4a61b8a620a224d24241fa667e48cc647d798f362dd9e7450ce87e432b690d172c9101b38546362e75bd28ad218da8f5d8d2c050f902f59c3812656ff68125f3c6327e28d81ae6972bdb5b602d0f8a371c67dfa49301b22acb977b50f2c31ea77eb7669da83d068c71d296f0578c683b2c007b6d82c447209aed379a224fdb6a692673b572383ac621ca32b588dc29eb14261a0643c883abd4fca2856711a13ce501ee17b70b05534bc43d40ae1b3607b340da17bd2f9a533f1b44d701334d5f32d1bae9f91284e48b868124e7997b76c3739db122fb1adaaf8bb568045b56028a49ef915664071ca999a05e632cc8b5bebc34744b5fc9421c02046d6c0bbe8a7bf8e9332f5619b9330a23f1b5dc198cc1a5e411ba2b704de074955bad9d314ed1bffa1cff30a25501d2f1486dfb0871f03c57e8f45e7c65c74f52dc15a67e4d45ca1f73ef02d66dc2b9acbfe308fe43e50b6a97fb9637c658b16a87a6b02b1217256847d9bd9db26e8c54a2430b4c899dd0f43af156dfdf4faa467c330ad29d7d32d912ebcb71cefa4e9b6b33d55f1d45f9f2ac4d56ea0a35a3696deed86ec3349c6e06faaeba41a4e222ce82e221ab9ddf8305863cba8a6173b7889b652265312fe730ea4339172a7c7e244859b5f86ad8fd180232d2038bffa57f5af7dc6183e960e58becd5ead4d921583da0f28f886567164518d1cded5cbb0291ea81a79b19b9edd2add44bae6c951685911395446ad81391b9828008158f5922e2b38b186e4e19bae9dfdd8391106fe003e5c37c12686b82a3991e9f6520b5c6a6944cd477e87fcb7bb6e18a7209a3aa2ed81a9eb595a609532c57b16fa7baead1088bea6405112a3ecfe0a48838b9bef10aac1d6f280fb82210b8805868e8f6d281e3ae80e2e1c022b5873adc48acf4f5287938295694d3d246cbf2fbd81b9861c11ef274aee8c0aad313e9f4b601ba0d28f9d696aa6a63f384250fe2ef140cef24398ab6ae681500e1d367eff1232f6b476a1490cfb4158fe1738d5716b6ebf141e87996244fe0c4021910b6aa25f58d3d6485effa22055b483f41cf55d16351e3de674b9ec45b290e367bac540c5f463ed5208a8c934a9176cca3a3d7a14f93cf1b3a3c455173858b99cbcaf1ac2a99ef3b801f994c2af33ea291a4a871d4604788d5e658616dd96875b0bb990f463d72b4bb86b77f29358b824a2afff548e850f1b1316d930c0328d6dfdb1471579dec2b79dc34bebaa793698e6c07b1558da08494b406a6d9fa2130cf08f6b32165de055153474d9d525702b51a5a27609b2e22c97ba708196bc97391d73de7c504fff93b2f33d4607e05e79e0d5199e73f4735f16eee1676eed86a60a0a5740a99945c378ed37e5e9cd8d8fdc8169f8203ec5e837c9bb78d1438d56d7cb3f3220b6c7f1bb36e7f3836a42d059623f7dfee61a18cdcdc4b3957bd4eea03296d5dd69212dfeb16d3480597a17cd6191d0c9f47ddfb2d2ff76e276c2f3e41e424a102b828b6bb3d7a3cb7e07460c5340d7dd81b604f3a88b0a05e967269970a638e6de32e98faf93228043081018c4c07d6a7fd662411f5a8934a01900c4a6b8a3dd5c95d1c6fd1a0a604c49cd2be7d41d6916355ffceed4ae24424b32ec0e98f83b68f56fb8cb6aaa7a53833f89f8142626a93b35001d84227427fc42d2ee62696df667eb0ab2680ba23032388fd978fdbc45b7a9dee9f564abcdc94de4681d98233127a1e947e030d6aacce97dba420f75262fd258ebb9a754a28d61823440a0fffb368b461f4371525b43d5d081a5b0dad0044bf92c5c70cc8d9bd2df232b501583e674e6e3a8f7961cf564fb056740c0fe34fa3ed94475bc2677e505e36bf02bce10048c08d403e49ae642f9ecc824b46dd5c118b59b1d8c1ba98a673d340f4ee9d9627b239fdc80c12bf55e7aebed5d4eb68d5408919e7a4937618d3ca53891a7ae995848ddca18971f1861f826262183404fede2cfa32ef9a16b604bdb197116ee4f08bca6e92b8d5ed6fb1ea0834d323f1170d500cbe7892f9de90332a5c3ffae5bf2b574e5ec90113eefa7b9bb5a4443256e487a97c97fb85b1118f8063f52cb92405eb32cdd4042672fb9a30ba106d06f683a5300548600dbb711f8fa9cd1d7fcf0d9def99ec6cc8d285e250e98f3f48c1c28f5458c8eab151d141df73db91f1404145d6ff78fb978d095153dad8acb92682fb19dcbfa37367712844b6d3b7d41a1889959d4ba399f2eeaf9f2534a92f859212ea7b074f88e97dd9c82d41444ef27fbeca6a4fb810dd25933c276e6930adb7d33b28df851cb61ba3479e100fdd209ef6d8e58deca41c70a386f62acb7ab1e2b0b15ba1c7e590bac7038cfc0efd8b97d31382680481474077abfbb459a5b9c6e0d33449f8dd93baacb781b3bf9bc74d7ae8a0114ba99943bab701c95c346e8806cbe0c7298468ee821ba4a6b5463417a73fbf1023409cb505b0c7cb6819b1d9cb40f5437e063aa6a5cd0ea9b90e0961f35ce218ee1e9a3773b7bd8c558e5fdcc543eec064263b069356194aa194d89895ffdb156a84449735c74937d6dac6afe0e5142dbb4fd6f8c6a41238c0538e36fb664a282d47785ffd4656bb1f2883dccbe8020f9255d66e59a43f6c2626d85c33e1fea4af8295d9e78f1a07a5ca59a4805af45e53fe72c60cb0dc1e83a25198516d22f8c95c88b33935061db03713de08b0b6df2cc08e49be4b1defdb612750a3995df9e1b09a197fbebc6593697b07c3bf722ca5bc5fa6139808e3f1dbf42225b5268bb03d40920deb55b67a8cb8ba61eafb900ae62abcc613b78da9ccd7f276ad9335ed55cfe1b0d4e727b2dd1948f74480f696630e24db697c739a2e7439c204b82da00defd0c13030e55b2b30c4fa044eb597b161c340cd7488d2eef32450f9e93cd06cfc2a7442fbede1edbf718b7f93ea9bc760d3d497991e562ca0390c38b6b06ccdf7225abe83782fb32f3046eb4bc5903f6b0498d7673e9572b2e22c687d47e08d5a6ebf35470c01b68519ba0f7bd159d616c1c360d2fdc91140af894c7090b5053eedf8b761f62039727d9820eaf4fbc0836be35219a3e6e8fe812b150ef963b0eebfa2968b1832982ccb88428ba491d92489869189a0d61b0ca97aed650b631b21b9437c8cfe9abf5af986682c28b2fcb4f6d7bf9d81773a5a087b073c77de4eecc47cbc12f4784abdf0ca2e6e2c70a81443eadaa87e4a5869839c673a1c45a7c7a0b6b3cb414a4a0f58732ec467d8c4958006de15ed71d1931ed9e91cff963f00a5d4c4abceed5828c9e703a11e23c2eb69416bb28514a229438365bb1b20470c5e8a9a48923973714054ce7410c2dcb42e1afe228d9c6bf4eb1ec2a72690ced740b2cc9fe6fcb70bfe7e6b7d5d388e187f0dce21a51653b75a7347042228a14820e5eaa246987ed61580290761d9c2977db3c6e424d71a831f870aba69f9025920668badcd64a945672a855af3b5980330c8ec3327f276d110683e41270954d45d1de465c2c6ee942433b4d72eb0ac9c8d8bcc60450d61370d9a499e8ea952fa7b3c2e6d80401be4663a199132a9153afeb2fc4414b7068f4603e4d07c6a9e04d79aa8f75f5b6eb31b7fca7f2d17a9aa0a7ba4f6a9507d9e6fde3cb249640bf25c081e69737241953357ac543f3d5d8d9f804fe32f7c944cfc1bfa9664b29179e13d4cc9a964c16bffb8a58e61c20a02e54c74a23579c004b6c202be13ed8851d734473857c2931a8b07c305577c6fb7fbaddc7c446fe8a3fd8bc7540c5af8052fa7cf12d0a8925d8812beaf6fe76b2bd1ef72fd9332c9d4225023c4cf967758b5fc7258a822d3a44b276c836a0ea46faa969f9d7c857526bec2719028c0c957beb3c857a74e1b1fb5758ff1061e03be7373e23a46d2860d1b013ec923b41ba6d9bf4121200187efa62f10816ce3c3258c56ab3c9732d91097ba9ab226888bac007fe225e4773d1ed762fa27f04322709cb54235bf58d78b0d0ca15fdb467c40d7d4166c3c21bdbbdbf99d8d5e929bf60176f9c8f736f110f5e608f310e793bd86274db141f6b0aa805bd02f80f9a63e3250f85b9229da10eda472fe90dcadbd7a8a2c4bd646bc59505211ebdd8f908328a0ded3e93ae83c5b24dab706be72820a9f13ac65d7c059545db21eb111a0cdc5351e3b78b39309b02a54ea1a14e988938bc71e5b713c7e441af165f36fb747e6eb5525bf3f8d3373c70479aa327b9d8d2da3abf5802df6d5c2ce254a77795273af454e6ccbeab4dcb10f3309440b07491621cc77962236e0f80e09442fb503dc54396421ad803fb999a76e116db83c391018a3716fa67e552fad14717119ae522dca39930a55db1a2b97472c2d225ea1d615256274bfd1a0b4f3b55c1b8064a1d5dbcedbe852f161ee4ac89129bbf2cc7cac7c245fe9c25a92d3affb9ba37dbd7d1d9db7e92822e56cb2738fb304f8d2c384629c73fd3a2e3da4b88352aa078fc17786d36aeb0c4788e24daefe87df840d5537c11d9ad414d68dea6a2af707bc96ca38c418360b430be8b633a9244000b462dc4a7e254ce36d33bff403f21492f6e7038ca8ccc5b0015dec322aaaf8d48df7276e096d95c0d9f82908cef455a141004ffb99ed83cdaacb42a1c0b1f2a0ad3af898e188b93c245aaeb6b051a8869107da1f7900a85eca0d8532975d3c393ebd924cebabd4a7e4feaee9bfcf9f734a059a4265435086c4911e9916ac23ad5c571e0a3d5c5f7420eb13026e0ea485ad19cf406dc3d2c927084545cf23d455fe4811f6817091034131f22e807224eabe155b5c8f05053ee4a861b3e022ccc8fdbfc42369fa93e7513f1a6e83c4dcc4b34c45d2aca1bfd4266967e79058be49b938f599ed4cc49bf0d17d5514dad6acfeb11fa74da587a8ebf4f27be7fda86dba11253d5764063fd8d03fe680334079f2614f1b7f34627b66efed7f290ed92eeca5881364383164f3b3765f36c9163aa6b1e10a2d05a06e62526b229fe294c2c46dd52fca00c7bd66c5b84147c91cf1672fb9bb1851165a578f82b3e5d2b0afe7d6614ed2f1cc1067a83f80f5c197a9b3b879a020e1064df7f43f6c74b769b2ec9c8d292521b3fc5945d7cf60d676ef79ad011e537562815b143742e9f2faa60c6f5f54f65ea6641079bb582353b92a10c6d9e267fbc110d0c209ee4b676cac8cf5a9dbcd6db2d7e09dc737148cae2c2cde44d7ac66cb813abbe6f94066484603c35380d9a51c83ccfd1cdc769ab8e8e46499734fabb805c5587c073aefc6f1be03c0266b40227d6f767530d801f892ba76c7e6a83b760cd0ce3d19a3a7d953b794bfcfcbf4118f92f7348c05a1bf60c963443eab3bd47537f151910676ca9d72432532205129115e05a676784eecd271a3c11ad40907a4ad236c68c226a13cd90bc067b7a7a888c21277df959d6dc4eb4ac3eb8a179678e27d8a05eb17f279c48a1cb3e8f60ebb717d6c053263fc9589b2a61d7dc2d27a0cfab05a640eb9c04198a5ce0e0031ce5c9af241e1cccf9c118fc63a678e1ee82edf9a3c1c786b50b39a7481e168b95d9b6a43320e3992d745c1d7faf92d68ede0950a2875b6d0bbfa4a78dc65be91c37d9b575ed5d7a1e898a4c96dc3008ec9fd8ac670d8450ad9f83f2e9f9203fa6aa1e00c2097fa999fee3d0ff35865afd6058c13842c47d90f3e9ac8dd7fcb5c15d0445d5dae3cb0ebe4a68d893ac9a54b4fcaf10e07219cade7ade2bd3c753e84b15eaf1fb649dab13b9dc9a64f8274056095301844a23993f2c910bd3a43fa882081215dc06973ac7ebfd48f98085a4c245f4225e94b75c2a639b12ff40fc1ac320ad86edb2668af26017deb3da70595efb30169533d2961864642709bc5c373e8886afb262ba35e87179405b2c8b6311c482b6a4b510ff99c69ab2209361fa7db240f732aa2c94d088ccf00628b279bfbee3151d6cbdec27ea52b5ec8492a17956d097c6307d7acab08668c30651d6eda5833cdec34d7755444e59eb59dd8637a21200cf7538135ef5bca9f274f420321238915b5c0af83d441d9e3cb87a5b8f7f4032cb09799120c7b5573104f0c3a83d97299584d38f3eff8ee296b12cd5d9e32af8d3dafaf064b2a3ba7aadf8b417106e4f038978d50297542e8009b05f7457b6df815e20012e815bc131ada3d01411b8aa2a5a71ce1a7e8b28626d7fc786c1c57a2d3ef1fb9ec254b916ef15b088e8f0d0b3596054a92059f1e87a4b94b9db7b3f13469a0c65b3160d53832dc55e3018ab77f8d56c680163d0105a805e8cf0ed13c0192185cbdc6796a76c804b84cc4ccea3147c5ab608235e69cc3b983d2423ee6284ea362d70539319d097578a782d6da56a7c7fa9676ad5da88b58be82b493e5e2ea040e52bb4ff4b2c7aea2f4d52306bba49e73a9012481a8caf7b7b928be931d63e09de4b46ba1fd8b189dfb3b20ec7cee21f765e07e5d90c8bf5fdd9107e8683605427154df12d71dd98257ef6896ed8df1823e33a53a601ebdac755d27aa9e308e73bd97eefe1680c6fa4a6a7bb2928e90f831fa9ec9dcfa6b5437df689451b6c6c772970a4ca059a2da56bb3106285fa2506e05a7a790621469ca9ff9e60c5788f929e32bdd522f82cebc74287f137ed63503713e368453f4b247a661f3f99cfff5c6f248ec5ab2e4bbe4ea84b51257b28519847bdb29ff61370c8af56c62797ca4a6230874bfd0eccb241a31ef637497e36e9b5134fe72e1b7860a997f1fe1368e5fc49d64062841fde7f28a8e0a82a3cdf923d55dfa8631ac980b83b0c97a0087428e2171f34c9b4049d8ac014159f573b0947cd317dd382e4796bae340b42755aa15a9ec3f58a423b7d350a19c3ff8b578b318dda50969d6d05e5be223222320200741a5e3911da5158d2e4ecc3e3eee237b8d7ebfcaec7ffa1035d9104e7497595016448487ef45725ed3d55d7351920c287f4453f90ca5dfef8f41e31f0148edf3b69f7b8ddb7fdd6e1ff502a1f9b7d18caf9be2b4e621ad4d05eae465e9aa4368cbd7514c25baac1ae197d0566eeca0bed489aabe294f24c7fafc6380d64ab806b15101048862fa0553e829643045902bb15f00a012578965b9f3ad8ed0f3feab92503079f6fd9401d0eb453fed26fc51db6ad205feef841fffe9469cbd21a19540441f7002058700676d1f0033e4d8ba62fcbb69c0bdcde810bd61f1cfd89c50aa158bbc33f7569d70a62a35b9202e2aa7c4fdcab9e8713f95dbbc177619b2e906eb2d039fd1a4130529f479b0838bf546b6ea693c159c370f30382a7eb093d7f859644ab50dbe2e83913eb74fbf42766b0b415061191f7a8d7cf96b85c1a3169a652b2678d36aec292f67bbb2e6bdb9f72c6d2d2c34b09c087a3aef26c4e824af8d4657fa496a2f5d4125abda645957480ef934a51dd163a1bf7580109d4299d23a9c9416cbc804ecc70e02b4e8a60cde02a0cbab43396391901e8aa9c7a2c1f7d07761a1f0d3f0322b9f66c72277c1d7e740a64500decb431053adb7e7fd18df9995fa3e597b31665a834cf7e2fc5cb999f88f7eb79a4f377ec059ddf247241ee6d67db06e9ed9694427046ecf926b341e8ede9139137d944e677334eabde258ac419b2ed082234b48db9a73a3bddccd2b9ae185c6ee3a1d957bc0517b82678d6f60fd5fa3213d1237817aa4049420e2ad2920a5fc7a045d8524b42a4c442ed72719357debc2cc329b3ba582769aa0d6febe062ba624038b62cef9a28f4953a47cce790473f0df80db181a81aba689dc91671b7f19b36ad4f91645302a85ecf8a45503628760830aab72cd75a3a1803f50736656d44190493fce2c69d191be62622ad7a5776cbd1c403912fd5c8f9b54518ff24755c1acb2481bb1a69bf60842aab8f3be916b63564bfc38fde9497f825b58fdc71ef220a751291314dd950b21d2ed71af848f1ccef458b4f3b87955eaff95197e7992ebb1456d82180b0b15e70459818c757a4de0871836f0c39de4172db6b967cc5d877837dbf72459bc5944d6889c5f885c666a8ae7d5729ba4796bc220dbfaaf432f1591d1cb8a19c275a752b12150dc187ccd4e2f20542577e8d520b3b34a0ad5bc1687ae55ed83699c4e2ed92b9b348abd10266da6426ce6b6335cc6875be370f0dc59010a420f2ccc0cfab175bbc5f98154db6e209d4a6b3e0daac43b3d8ebb186772a86e3e01c66f03572790c4b5c537fb8be63e1622fdf464062904ebfd5b28562410963fd396a474a3ba59094fd366d86b94900a457140ebff7e08d611479d825649201020b8c15d74168a3f15587edd07515c99471beec27c63b6ee01b407e9729f643199afae5a04a4eee7f1f9a3e532f696104489a74f625074534c86a2bfcb2351f209af5770a9e5b65f914a1e8e2ef9658f2e1633f024eb282cae9a768b00c6bec115728ad432efc75d93f14c0fd3e5cd09d64e28a1638b8265038d211924ce65fc4f05eea90349584bc2ad6c22024f838e431b91427e8347e5ac24e8a343b9061af3d070c7214d5feedaceeabdffc62ef8da42bef6af48616a7baa4b1dc8ed68371a10ff64103d907f096f485fa6fc9f2ae87d976878f16177a8f18347b6b15e934c0facd2cade6c17d6d395c33b8eff04853aa9405529654a99215b1910728df46044166fd11222f43b02b4499a2a62157917123582bd38c33f321ae7e943e79a3bf91ec706dab865c70261f0b1c43b32f3b94d0c375979e7709b3acf40f8141e8577242fc76fde0ea8666a4514fa2cc182d646f3a7ded95df895447b40218c3fd7b81e302f492af6db9a5614d119e18ae28caf78ef0a5c7ba0ce7cf27a67a74eeb2696829b009698dcc4b5340ded7ae1d1c68d5b37f7bc512c4e9ad8f3bdf9aa55392531e6536651a14b11faea020e75d571f9163ed6856b7bebc4121dbdcdbd924ed4121d77ef35eb4f5759d6a33f9d9a9eabaf2a9f883b32e14f7b1854fe1a35a8ad54a203cbe62a144f21bbb228910df0317de08ab4b0abb5e3c39c14f79b8c71dab60533895be4fd9a550e19a8c6060cdf4846edcfdc58a6e3e86381042660028ac30c812ee63a61d2b6e40c339783107e4bd21dc5e6b9f8761c8815f76d544d8bf79a174d261764c5faa423a71bfa76b4455bc238d3248220cf9723167f0d912b454dd45b20683f138172794452e05c2eb85e6a1d03db8637dd921a7b4987091c03142f6e7e0f4a3a92cd9c0d514ace6057f2eff27637314882e6f0af2bcdb951310f1a0fb04162f28a70be66bc592d1e7cdbfc879015c2028836d16900d2f672ac099d5aa475257f04e41f36097540296a232e44a1f67c371c2dec18e4717148b702430697b4c05aca2003d2038c2523a14c42809f9a441dad05514cbce96361bbefcf3cdf13d13f15294ed11a6ecaebb77356db2608393ce6904c633a744f6e1b8037e459c231e9113de022ed76c2219a52b4fc0e128ab6fe7190f449141f9d382205617d79eb578568b2c470f065f91bb86d7f449a72251a4daad77013018640ad4802b19b3696f2050c300020f03a82ffab87cd3d83b3db04238ff4d51819dab4ada83bda46f46cf3a67ba2f2584d3d884d8688f3867162c53d479f64eac8e641b5bd991332e9c85c09cc0f17ba5b0dc1acd2698c53b66a89f764a67f08ee6a9fec4268597de3537af90f71768fbdfb15e05ec94eb780b389943fc5de3a9f9a7d6e1e8cd6534c64fcca174f74627f38476bb283c2078bed76ca29df83b5e335d2fcbbc86d5ad4a5ac6d516cf5d2a5ee0d7b31f335d9d3863835f349ed36ee488229d10abf073d82066da50112315a02b8fcfaf3273893963a0f3dc0c6cd95a85500fac53ba4e569781f1b753e28a4da063c0086d278778dcfa057ad93a6478d150a0528a8d61056d83635a01ec038ba58d514881ccc142edce117355eb7d699d2f8376cf2c5133a725faaf4fb5a041130f50e1bb251739dcbd88087e9af17ad9e29354632e7a482bb74571e2faea034de7e9470c22d4f1c2d831b0ee694be602153b098feb3adb75f0e797db9df3749df2ef1b001f4a56fd14bb22813c7eef697e478454999284933035129d949932710fd02bedda4ac3f58224f28f0d0d4f3044631c9e6eeb021672de93e55fd9140dc0a895dbd1b9e3deb69cd64843d49c2d8003573eba0279cc511d28d5768075e9e1ab986c25671dc2831284d31bf2727f514e4687ec8d815b1cff060c5e1702b512456f5ceeac207a3cb1368f305f3a282ab4256bd8190be3bfa007bcdfd909b273269f33d6b1e2438e352500586d4c6b93342bf6bcca2ac694989c4279eafd822849348104aa6a0743fd2f0cb494ee2b0225c36a34a5e512f28ec7c41b60944c13028434feba5e2d7459ae3297343617b59175f0bd78581fa4725ea0e81d01b78e2adc248dca0508b660ebcb9cac5ddb6775f801cea521d963a426787cbfd84858c15c90faa9f7e6e9ad2cd7bb8fcd1e77b44015d4a7e1b5d8109deb9caececd475798eab94d999e7fe54d97ec65b029a017f42e4b804f842fddfcda98a6ffae1320a8fee6a29cd3f5d23ab61acd3164fc090569c906d5e547cd4ce97fab2232d8e5dba65d09b02de127aa0a63d6fd176d9d13802b25939e5ef3c80db06d82c6cb741fd7a980cfed72b20d1a636615aaa8459eb4c141a1b2d1b8da711e1a48aab8f5be80ab23b4064a98c5c4ba61ceb8ca2267e8a34e0fb1dfc1ef27e0e0260c2d9a4c15ba07af48537adef78ddf86e6d053d8f3411b5a754c7f3997371f3af85cd7dc1eb1a7532e032dba18396f44860843e28f582291a469f3915c5b2315329ee3fde5e35ecd81bcc34ce84a6faa666d335c140d09ebfc7db70872a45523e242edd5dd8f11676926d0cddc04da13953623092366329341053f414d2671c6efe8d53ed22dda3a33c749f4cfcc7968d774f3e21fa70ec1173ecd7663d77c15fee67860548e5dc9c88834d841ebfc238bcf787f401a03bd60189360d918a236edc02137fbc7e244b45824e460dbde8451741cfb45c11172119946b768617595027674203ca52eb1c01d2606101b8017344145abd1e096c56afbe6a3eeea8b69e280d6f200f7f704163d4a084167284c1657e908556641644ef22cd47e2248aaebbdf083250c2bd124c635ad5264aba710a25f286d0759c9588f1f15661e85f3ae96098b257f9ba334f01ed9d464db898f4bb39104445bc1e0700c75899696ca373f3b6686776d1029e5827f11a5268ba22168cbd8978f9838570a3c2f61adc37e9111319cf5fbc4642c6a458aa00e94c1373d661ee1d8e2ff1f139d9b3427cdd04a8af794ade2a8b36ae91232668ed908198cdd9d3c847dad0091c2b50fec413643b2cc1ce0aa00f1752899e55cd3d0397df34ff91420b81a7dc42e6446562f6f43d468079f3b805d20e597118428c59eb1a77e507748adfba7f35a525012ebb75373ba8bf4d92cd1ffd3f48cbb383551dbe24d805633838f0e25b6bc670f0877a4325ff21b70f3738e82b5c6b90c9a5c2934719029b5ab931b1b12f5c5b8b3de354856494ad9e52eaf678ec1bac3e524df8f301993135233e9b5f6b893c5f5e2ed7870efafb1e0ff1d5402913641cf0b1c4436dc804f1c9512759806ccc5dc1a6a5a35968a81fd855af16e194f18dc61d929aba0aa0d5e44f05da9a2218c5b224d2151e3310e60ea2cd582e882a95174a45947ce5b8c58f307c29f1793a1ae20c557539e581fefba775ab604fac7b82f44699d00ed353f4c73b7a5dbfc0f571368a961b8bbabf5b3338c6fac73a8718736339fa8c23c0dda529370696194560753fc0ff7846adf22651f895c96efb1981226a6dae079dca744ccf949aaef499d102d4f95f3d579bc6fc7fc8e6d300b9bb5b29a9411656a8215c6e76d524f115abf27fb19717fdcc0a13ef79444db3ef5b9f7fa16d0a41457449737cc2b9981fb68dfdc21def3191c05a1c292b65bf1568f748d24e1dbedde435cfca744a90810965c08fd7e897a0e20fb42bf5704cc89bacb2a652c2b4a058bd72b0be10573e6eb347cdd113c2f6e47703746e38964d308568181ab2633466d91b3da53fb25d74c5cfa72d3e99291254e84b0608a4ffac6ea4e372e24e71fec49b45b545ec6a8cc3d8aef4e79f9edf5a9e6299d12ec97d56e26da9a63b6a4262f0f44194f419163a05444c9b77b0bbca1aa809fc7ddf49f954733a54672b209977c68d517a9bb15e2062adceae498dc7d235ef80d9c6da68646e597a82469de9dfe315ebd6d6e762df0a57dc9b92213aecb231d14f5bf2bfe275f16a4ab1a60ae579ae51b9ab9a0fa08ea2370cbe1caa978717c8789f049c9614713ca7e85a1045ba366c0ee35a4c9457bd286efc0f27e6909ba3d0ba8a1fa278ffc2b92e84b350ebffe3d3bab0876098f372691e157c721c19105d2f4ce64f3b65f8ed35e222d2fa9027b8dcb3f8441a37b2016909ed3c85557accb7ea8f40dae24c59cf6e783f2def8b48bad32d33b32012eb83f294ef3f84e35f2525e6bc3949bee3ba3ec9d1532b743c027a3a3003979501f791a1bbdfb5fd886da5792290b6692e57004d3fcc2b25f3b87b2dd2c93dea9965367e031d81657bdc3f1207becdc0f9e4e6c4cc89efcde0aecc379b9376ae70cec2e768d3538bca33c42c0b1b7721cbe43feabc0a2ba65dc4e6741aae8e32409e550e18c22262848b6329fea96db46afba4d0405f0a1a245fac866738dc14ce33519dce51724e31f0e2c5421399d638c864710096fb11293fead4d289f6bbf0fd34394f04869ed0a0eb27fd4646398520167c8122642996cd85988592c49d79a4f217249ef53cc4c7a31f376c8c360943cbf215fce2931a310dac07a40fa689ab3b351243877d0df539ad7d61a80fab5940019788687915667b215dd18757dc87686fb9c494443b0941cd53e737c9d0aab67efbaeb8cfdd8d68a0335529340d9169e579aaf1f673de631bdf3601ace156d9a18940cfed889c6e43bf7430b2e7ee1a0b3395abfebfcc08593b018cd49e20b04cc339ab5d7691817674a593a861ca1d7344815cea1499170866e2c6c18cd75980b97abd5078826158ea990bfd28d32dc1b6f1df6c590971ba8f3f0e068be97512f2b3178f002cfe05bdf5fda26c58b31bfdab4aaa6dfd945d8609385bf14391a5eb2d8c51189e79e3446ab2a052c465a67cf687f9b83ee3d6cb3d255bd43b19139c68c23f0c34541151ac5dd31bf593f6575ab005440e70bd535432c12a88eafe5881ff0858da154d53f0c4dcf392edb3e47d00dfb20eca93f09eee2bfd0e821d01d5fa40e411766cdd3877c62e4179e83703261f9fefe622a099d10a25e3f89899fa0f0ece8572ecf8fcdda07de9c57197b624dd1f075e3d288834bab0a662df52dbf1484566f259d68a0d3ba636abba1e42af1092b18d79adafbced25e0b32a0bac4b76d5f4d253ef45df537fb8b5cf7b0471ebebc83a5acc5c52cd675db3435ec9877e84da844873aa2bdb0c153316221659e6049bc17396aed8349452185ccdc3cdb29454994632ee9e719f123969a4004b8a20218c42cc181e03e05423c1d35ee923b6cac509e83e13a99000598e2ba2cf12df3a24765b2034d111f7b1fae6f1b867223285b1af2294bda23807e279023ae2daeb6940ad491750a588b315bd82b11f5a907f271198d56744dc575825fd064405110c9503e60acb03fd53a777e57facf5812641c62661e477d81dd5aa6a3660fcac2964123c2a5b3ca7a2cad6df8cdd35f9e9579f4019a3764f833f7123c396573b3bee4d49b4916cf6982d29debf2965a0f79f63e81a54d42f3ff25fdca10dba0ebf6c956c3079e61c4937370c3409e78b6670a1c4f820a249df9ff2b0489f71401646ebb245aeb14a10e8c9660e26bf611d9c5c4512a3dc41b1a340d2a72eea2528055386723830ccc8ceef5374664f3ea4eb32baac4bc6c6d672f796b6253613bc31f47a6aaec96b61573e9d7fa819ee7d6e64e3679a44c581edf1c010a17db0fcf889390ae2899f72c58ddd91ce20135b93410efff19aea7a7e7a569d94c27fb12984b8e8831b66ebe34dd51550e32315fb3d453a95ccab2ede23d70c5736055be6baa6e17514cc31130e7075d2c78dad84bea547e3acc630c70c16d7c77192f86e06703cafc0b3dd2581d48e94898a2e319246dceed5dc34af26721ec3251613963f7d5096bb2d963966c7ef2d61bdcfc22ec29033f65a9d214961bd9a59c01c9c381420ed5857625204c9f3651dc5bdec998c415592454269070f715f3e9c2c29fb582c657e8443bfa42f0c4a8693f504984725ecc42cb4a197a169102fa64b40a8a48a45c4362923be1a136b3914d068771b264a2081cc12b19fa02ccad1a6e726418aa8913679e68eed5baed1256b8d0bb67e4b450038df382fbc399cdf5c51583c480e25905d365e58c8865a0404df3e63fe99ef3496ea4b08674fde4b096afd184ceace6107fb40dbe44ea05516ec900a22d0bcd8addde9b3b8778fddc0a6b2f2678e946adccd797a2f70ca6a6d096e9404e637a5ec146308f1bd9eb029bdd9c97e0df20c9ff074c22f7c3d8828e42b950294bebcf973f9dba6be2b7597159b229cef0c061d1be47dbef29146b872fe56e2aa716dfbbb2093b3a37679a5bc3cc5a8b30de97c456c904e052d02eeb440fa0ab6a20e7c2e6f1ebca18ffc01d3352d3f5b62d5df7618c3da3927275ce787b31791d3ffdd92ae37e49c3efb51806c66d6263d79256ff950969fd147d2106a339d41bec11236f682b76c0c79d8b1745c65c7cacc32bb3823aeb36571b21367a5b8fb2d145adf70c5988dd9cd4c0c288c757808a4fa3fe4d3035738276c6bd6082caf361e68a701db9154e8653de603007b6ebad93dad0ddb11ceff9b27681d775c8d1a417be5084749170371373f5ffc79e77caeedbfd3078e4abe1a7e7744cb93132682315f28b388fcf2628d21f15e6fbc4bc6704b3aaf94897ee8cee28742c42ffa4163b1585d998af853e0b19c629659fd5b223c457f324485e4e34a2a428cf851eb8a7e2abd3537f2ec43920fb211d1a6fbc718d75becec70c29221f3e545b7594578d4ef51b122f9e15b705e013a60bdf2cdd4b0e815f682d13b5eb755fec46eacb8610c5ad542084964e3b556a8d143a172b28e213d3832a990438a69c3d0a774635e3c819006b7e79705856fda8a3424e6e3f0b4c23837e29154882d80b55ea33a185c0ff83b6825395c0b46504bda8684ce8685a924869c5a784d2978bfc24895242570bdde8ce6937aa0a69cb86f826731ca90c4f5ad567364ae107a0720f223564a37850f819cc48c106036b4df02ab13c8d3716f8b5d3ea2e2503360501bf7af422df73042b146166d01931b26e5f3cb396d2d698ed691c785456d8a8c7c0eb7148a141ca916297e69fa1e2c67e8f7bf26d3ab8b3a687294aaac8f9bb74fe1b6c021676a38c06097788157cb147c26c00a9092df917f74c10d7dfdd472799f0ba7c460d91ce54fc5ce1d2e030e7dee31356a1087a72194e477a90335902566d1d83ee8093abe66f162989cda96d23ab4ad60baf57120b2b96d2d557f01af11e437f7769123687218646ea0c551caaeabb5112f93c8fb984627134aa1a261016155d2bccf227406eb5d3aabeb39a83f2c945332db34a0bc75edeb3a65356b7fbf9bb716f296c821054ad44ccd9e3d5f6e73d110ed431dda0318399f4bdc7c869e4040838a2d43c609a5c6eb8fc8ad9dc3751f6b6497d397e643fbe5bb3658cae62f24b47cfd10900c0221a64d9bbfa126d551ad781b04165cf98b3a76989b808225301bdb1bd4c3f5dd7df2f7fc1b5fa7c26a984521c24127fef4dc7d3de454eb1da1275b8a10d761b9cb62c2fc0f6a05385d1d6e3eda4dc6c96dc656520f0fe555fbe1b0aac7495aced65f07a35f5de1b6c6f9086cee6415fc65ab7c523be20e27209da0aabe746e1453cf5d4f6bab2019ae05369b41e28684a70810a621ab97e1212a7dc701617377f1debcd9549f0e32b5311e1e0143492926f1d1784f7e8d2fafd452a04b4c87f48966b579023b05660238bdbbfb969b3a00022f2227caf902a7b88e2a909e39b963527e00723505f81ba8733acf85614df082baa7986360a3e2fcd44d0c64033ea5df19af30bed52199119978b184318abb21123641f811fe5833be7e92023cc0b8bacd5fd30017eb67f6d9b40443a83b0e331adcd382708b7ea1e655081eba94fffc588335808602b76ecc245efbba16e477b43d931364e0168bc90489f4e1238d32644a9cf4ccc496d1895acb2bf47c94601ff468d254fede6276d64d987f437d75162a5835a82e6fd6b66ca34bcaa422071c5d261010f76da609cc411b0122734fa1b9b077f26ec5ee03fdbe28fb2d4d78120f72fe85dd0ee69e090ee0fb8dbbe4db9d0a0a5c5a304ce089602c12152e814cd124e3380026118e0148b7b2821fd16d58b268418f81027aabec1de7b43aaa5549331237a2b9eaecd417f4b191a3f31276612f6c188e89eb7e0597a6fa285f29246d311ca381046fb0816593ed5144873cfebf68cb2ba3d83565cea80fd4382e07e7de52746821d9bb0885853414a8a57da3cf6e31204715baa95aaa8d997274ae23aebabf0bfc591b7462f15a1b045a32d4340b8cd0b9029f5834f8b5415f88a94d7c490bd78a49934b6d38cebab5a461423c83151212020a28ec9cc432f6a5beaa15014aa3a82a4625eed3f1b1b8cd88f22687e51e39b8cade27a445e89ab324111e38b50b930ca1d41450653acc5ad6803c40a1c43b3a4a4e8eb84621cfa31cf0b1e1ef04a1c93c90b3feeb8f161558052765bd70ba4d618dad4f193834ba526019ab013d3c2bc9bdc2cfc14c1ab1d7fa9f1cc42716718be64cf182b2c65a3c911801b56fbae2604e087ef29da5a62a406a9cee7ef3ee19a35a0dc9625ea1ed3dec627e7c94784fe3f415b479a2253cce0e2064cf8cae5fa94532b9f2cb6c0b583e01cf635db5e243039d49a19cbd7d9e1b7a078a33723a0d74f8f1025a48bcba9748c15a1c8ef970816118aebcdaca189fc9fdcbcdf8c2ed981819ff9ec32c97e59d0f896cd8c3a3466159f40e1ba8ebb142201dcc0e0b34b6b42ebea13415e69c9e2a997536028d69f69e928c03cbf33b12a2841b09631530333928368bbe85564a7a52504985dc2c1a5a5f718602d264261cafc2455fa2b352ac25fd503adeae364ae1d9e78470e6eee3ec2e2341d0575001f2bd405dc427a680edc5cc352233934d81e58be70838b1a933887648a97562dda9a248499c51f0141970cce481c9c951f676f61b83f81cdcd7b4faa769d973b5f49bacd39066b9165937b622653f4f5cb358624850a1ce3f313c391e91a18b3cc5288028dbb6945574660c5a2fdb8a845e54840d3149903689454755846de967233bffd66d5efd50d9cbd3084db1e205513579c723c1035b5bdd1720c9f0819e62ef138d96ad64af6dca32b8982970e15fc6ce55645700c2264e89a787712e2087239db909a036b6455d389d3f013ad727ae690dba315a740857b033277179e4816d233c6263a0abeb0d400c1a70506367204a86f6c97cc35006e23152e4390af35879f63ba924f044c16dc2f5c43803b09ab408b87743022250853786b2f64bb8831fd450254ad8a6b95994d0a9d726a367be737e84da93560c9d070a70fe6d8cbc341708dbf56d62b60d23276b7782d546f72fc7fa43e081c61a0e01e660ee570151146a9638c69459eff71fdfd63e78e42e9cfafe141dab4051f306d937cc71ba123d2c6cb159a9dc15f9d6a0c6ab0f5f9812c3e4cb89afd46b50d5acb41ed0c01a667f9da938ab166d3a5dec0b3d5c7683cc7f8a0e7843648403e912c3fe2750c6102c2cb6056efd5a8795f0de6905abbbae2a18c3e1de136c36d98f01a4349d4b5c1cb48dd550075cd2b853f44e9ff268870444385b7a501b67dc1c92edcb7b44328e1ccf45bb67ab430cd014ba94cc95c5b4c0c9bfaaeccb262749ed1e478d184848ee854047348e3bf1cc60fbdecb06fb4c5480a3a100f418a753a81e27e1176b5a9ff5e01f2f5d42accd1d9d0b6ecfaf01b7a9a1e5dfdf68719045fd91d8a5b79e2f1f420023cc296f49a0342b64435181c93c69a11211fab3d5722204c9acd9bb77c3a5e079847e60b00b66ad374e7bba16bbe7f4e4ebd0d3e33098d47ead16e29ccf63afe0d6a34f4e704c91ed13a7fd1e0b88b20816fbdba0ea28828e3517a7648c58cce6a7e2bde49759c3873fecbc3c130c0623e033cf2ef13ba6fad30d0e9a65179cb6e0802762bbebc30575a4ae6e8374b726745c6284291da4d3a9b80060a013494022f602d141d410151949088d2ab1e9281d04ebbe5b7f3fe2c4b25da3cad21af8a83b64d992240331b0f0e955868c5dd40bf099de39c6e8e8bb6b6ee32f8024cd1676bdb9b65968819c0c381e99e7757e5cfd24ac4ce2040b19adab03167086be7e57dfdbae23ce8214ecf2d41a5404d895c447f187b7abf8233f13c241df14c903f913304547f72052543eb7a14833f3d30689f2c69f463889dc37d688b936d45fcc09f0bbbab6d2dc640300ac5076725a116dd814ac27876b277a810d2a3cdd3bed1a4332071bce6966a613c93028f5915f0540baa57911ae73117b5387d3d4b5192d805ca915a0a9a5a7077e56468cfe7cbd74835178696790860ebe2833456c29b52ce754e9070a8779d83261b60e7b9d708071579683f4bcb63916b5b36b1359ce98a5767d6f5d1d34f19c6f7ad0801c39ebd8882605c6cc6d0b9aac3bed33f9c838aee64471ec3fdc2d2aad5c06cfd35af771b67dc8bc78475e6e930afa0a7546c6c661d9d196c4bb0b42c7f37de58c2d4535d43b025b54b192b2d18a2da0ab05766b0521980137cc3d63b7ebe883420d6be65e14b5adfe787c9ecbc43e1c0f6dec6fea44e7c15ed92085ecac8b57dc9c7958f15e5a3b74804c0d17c46a841b66bcf6ded8af338ed41c376bd0a983437ba59949ad4531105768bd61e21527fd20479e2d0ef8b203e3db7fae81a901292ffdfe58c52b1267392ba22a63e031a8e8b6ddb5c1e3d6391755e247a6401c0e051b4e0874d7c9faa692feffd9f0c6f9de697ef9af180d73cbed110dfd35bdcaa40047d43a20d5879476ecb663a0d45e6577ae4523071e3ceba95b308572ee9565f0153db299de990b36161b11b4abdc61291422fd3a711ae0a4c93ec95e67a351111fd44a7f02c68bdfb917a197be2129ce96361be2f021199e53b9c73cc6a1b2bae4a3cf79314be6666daead20f20bfb56a5cba9d1a27277b6054b29fec1aaba48a1fc4474e37fadd9819916532333a5a4ef5de3c8c6e25f6a8b61c99978d198c24e579edc52246cad767f8f88eb140aa6b75b9986314454469733a6a02ec2bff5092ff26eddd3cd12d8b0ce21c47f7cd6c667fcac1feac431ad5deb31097b7cedb5d51d06bb356fad70c34a0bcb116eee05a139d926c42e9a12641e4c67d8ceade36730645d4654d274fc972905d2f6352242560e6c560df9d1eff7362b648b920ddba456c94253b9cbc58df73e15cf725e5e88c943641bf93712e7c32848eb8a670c70d47b7dd898498b1471dafb8f19022d668f2a8540ab0d0ccb402b82ab52838e925a92cde60467984458c00effc90c35cf168d77c592da50684f8004a30551f0550d5356ef5329e51e639a545f71f195f7a4c904121fd332b9c01821a29d54c7a1905807282dcad081a0c748b88cb6bbb0b80e217f65520e590387888439c102a8f1a403f8561cbfbad1787cf1fabe19b5a5d3a5dcafac752a6e600639746258925ef3dbf8ddeb1648d8f45f04aa2429af509fe080b91c0624b1a63e102c089a544b050f374c76c700bfa3eda91accb5d66c64055e9d9406fca4648818baa45a2072e43fe43d34600c9a290fa19d57a6b6649b9e26d8a3d2778ac87214142d4e97dcbb09e1b3f624a8102dd2a72e275736d4214d78a9271fdce4406162dd71cd1cac1d79c2127fb119a71700f52c0bae64cc38ddcc94ecfcb3857e6ee581d68009b9d7fdf133a76b9187a84dee59f93ea034f8dd9881edc98d2dc4caac29f877e5dde69f699faf7689708b1c0664d10f1ddacd051ed38cce095fdeff573e0c9fe80419d12703aefde8fd0b77d84de59bf87142788c1e0675000994abbb92c10f7c9e79cbb9f5538040c4934ea1109bc30ddc6341264ae7e21fae5d1671fd725785e30ff44735cf68db0a8aaeedea0a69f9c214b3fcc2a0e88ee4b363659437abd3918616e946164a20b72852dee2a0203bf164c93289cc648edd154616c4bb4fc32d7ac55b3b1f4d0aaa1de9a8b869ce982b48e8e5d55889fdc284afc519fe2258f02c4c6472281cd279cbb14a8e5bf4fb9838c7da5fd61fb7cf962154aa65d33e3cb96f461966b38dadbcf1b0cb00c5eb3fdee34850d6157e2218b3165fb24e5f060681779883aaaf5f6cecb3c930818ab302c647f97f86ecdaf1941cf2a5ab141374d4ed7b6b6d3c1728bc98a00b481d31b382699d950ba0309e699c70834f5a04461abc3c4ff5cff71fe91d34fb14006a9536604dee7f5f8286057b48d58af9ecb3faee7eb6e1832ba9ee8d4968ad0e4d8d4a4ac762e6e1eafd3185177fca84e7fe467292af1a01088394124dca0d91b5207f9fd91c089217f99f2e859b1162e5ba22664b0a018c0725d7e7c8400ceef84da0f973baf7013a455f7ff669d115117dea3d7833a701ad2c4326056efe085e9abfcedcb4a14d0c7495b564e81464815694ccddeb6b12c0805438f4b42c05f39b184c204ef523ec7a8d8a6754fa0ea1bce21377dcbde73515655b5de709fef2a92fb5b56c8b467a39240221cf4fd7562017cb08b4567151d349aaf5e8ca7280ec57f691cea2a5e1cacc584cbbc8de2975673b8f0d0db53a7cd4514b8f81bc60d2195a3d957e37b1ff5701a08a1d31c76ffbd8d8ddb1733b028fadc7715b79d88b594bf6df418e306f9e5f758e1fb2a33b127ef03b73a5a5f59083fb620d98d66eff1aad582ce6f223a92e8c281fc0ff398303b84cb8ab89599abb6a0908055a332deb217c1dba303b888efa863142b47938f1a242844a54212c778ce246a3fb30efff9daeee5db76ccee9df6493f92c6cc3a954bae549f1adc91406f5d5f7dab45885edf6efdf689caa575fa192360569db9e632cf6a35f94359bcf2c588ecd88fa5263fb390dfde008aa1b626ae4d9b8b5e74d2a42e036315b0b8efe171c1d85571f59bea01c9fff8aabd3b6925bef7b5a374e7b360f5e45cb662916047fa027151b137e712eddb6ffa1db23a842584cdcda2c5c45d64ca2daa743d8de305bb34a38d95c4ce95990b0163a99367cc07ee3d331e49a4e4b88d865315c633108ef90880a45162790043db6cb90ce03b2479f48b31bb02d567d4b1139490460ce88a5ae709ff027110a57a9985ae43828f43b9b54add6e5747793946b081c882c34b7c6c91c7e9ecb3b86e316b1df7948e8ed034efa1190183dcd13cf943e4c70ca83d9125c8d2312e2792adcb238cc04a3e83d44e0f14a06fb09cfa0156dee7e0fcefbec868013334da31d4ce78ac6b81c533d1d30c14ac5c3e25844ff4bfb0a96e4498f043532698abb5660d27e909f26b5efb5bc6bc304d7b2054776394d22530b171f0df80ae015117c8e2fccf5557f78f49fa7d93d76d0afa95d6021e931fb3f3bcafbe0076b9f252e176439149d45956f7d510f627c537bbed0971007c322d4f065da8aff57bce1f3d909a3a93a50016be934a570e0fa28792aebc6d9fd2839ffdb7dfa8a4cea717812136c08eb94ee588abd3ffe5295339bfec37cf495a1fd3398f1e9003247d0422f5ed464e61834470818279f43a8408aecf29b06021f53799a9825bb3baa0d52103f94b2ef6661c0cb1a566a66e19412244db08c8ab0ac2e4969bacf33938dff63a2021f8f7346e69c90e37fa4404ab87a63a165b4e3cf85397da51b56f4c58d3c3af42196673e1d42942c1744c4a8f638f17e257d036c23bd8cd15c715c95c09336005e94a7e6c2bd71ca0ac1cf4751f3f1ae3c2be924721a662569e52053c8173024aba64ff140561314424624e041053395fc4a6c2ff9da458f9dece3e6bad5f8b023cd055bbd07a3636e00f3431028072ecf64e5c553591f86e467cd5390563fdc416d74f1eb8b60382ea8225380f77e306ae0d4432fade7d5af6e501125acd07c924149c95ae2c3e9a14d92ac4acd4f997c7c6a6a97cfb6dac1ff4b15f616fa40a396e294d3496fde9a49d2218e7708834fd1d5191a734a6921fa256cebce6adeeadfb6cdeded0746f0f8f5b7881c6932cb746a1fcdb48e04f28fda959719cadc2029e7d300454a1c0fc12abe4517b95f72907c265c9453a6b4cf6e77c76537bb13e5e25e8bc86c66098797c2b820d63049775475c894c0e4c2ac2819f2dcdf02b3a7f1a85816ca200c8aaa708237fbc468ed7824574657e673ce87b4228fee39a357e39d725d9b43d858742144a605baafeb87a083e571e33319905dea336049b8d58335255aea42b32e90acab232b3f728e1d3683fec095c58c0bf8afcf55bd53663ccbd6c56a85638522fcf8a40e7b131d34d13971a0c06c5ce87791283314313d1a2d984ddf35499aa2876aff68c61e340afca4f1485b840f5cfba36cd7df2f124e9425754c334a1f026c35c6477e890136a57b4991f4d3304e93df6e268442c8cdcfe294bc6d439c506485fc3acd943d5bf20ab7b17148a20b2a3fc4580deadb1cf4809e8e718617e92e1e7edecddccf3329cd79e785ee79185fef000c7d056a89b1fbe2054b69e13fd9c180c114ec75cb100c2bfff8095681abdbd1ff282a9620ba15e5eb150bf82f2467dde023e819d2cea276d690fe48982869988fc8aee88e0403e61a88397aa0b1a5a0629e01f07e272d6065d63d73f516fb45d2adced2a828d198e4ef00b3dc9a50fbdbae4401c973f5da61079f0ba6594c05ec66a382e4060b6a6c8a27cc75aeabbb86804ed3369be5baa2375be7f1e973823c2614c51e787f1932cf74ffd6174946fed71115cf04ef6947dfbe7f3c3bd90bc45ae256cbd956a99c079c834ae3ca8e8b01216eeaadb74d8012b5b0ac14af7d57801f2ed40f5db893caf95fa15c70dd58e9b5040b38b340190f7a4b90a79f27ba04df73de540ef43fd9aa3f630d8680a7eeaa54a63250e6129032e591ae41418ee5405d13c37519b94eb1ea71d19681948eb2872d9c89109cc6dda3d81a891e53b859b2657534f7db92c5b9f8be75ef67e68bffe744305610102518a1fac09c0c5b913530c3a3099a4e5e478d09aa39875b58068cf3fff7f8126629d31a6113836b4761e296471ed95cf7b274c4d7d1a389abf7e81f8498622e599a434197bc9a49d1b85582f793cb8c4d2f2bbfaa9c4db84cafc7922c7e9b24ddc33538dc22e8c14e75c5c566ddce38101d1df571b0e8c137b267c6b866e2de8f8e8792f8d2e849185177042841c201aa46303e4fb7b47905e988084bc461f3a80548e62a1fadbf675d88307e6613787539d700910b8afba7e4342902fb27f545a9ba7d490d0b77cd3f8c3b698fb719f6a6c459055b628fda4b5e49cb5c6388434e7b7030cbeb5262c1311b60dddf4eb67876a6d2bd3f41c42ad00a6b82399bfc10ea6a8da7b90b99194cd83334370953ef6542923de51e67dca0c75e0fca6ff6ab09e6bba2df594a33ea867fd15811eb7788209c32e321bf44875a07b4382ab57f321c45860e8f99736c093fd91e09183d450e10e54444a2be2942f9254d0ac391c78e7c29dac5401a59aa5c1c82ca3a2c98798b3ab52c0f60d91e9d60b0ec0195b1c302e11df12aadf770a84ea300646acf6ea92571702ca47e28a327e4479ad81a5b9345b0c80429897059ba0dec44e5120e566735a91810585b50c81610e9f7ba85b1fc7d2d689da3e7193519accc663f951ecbe1160c006c8d7edb01e7ce8a13bdc6ac9bb0be6014ce8e00dfa3f8f20c09d208efc1e33eb5357057ffde2fc17be3a9554612bf4f3f2ed7ac7463af5c60fa178b75c8e9030532ba4ede645b8b40b303ff0354f39b3861774b021893b541b1468ade8705a36722b5a56088ba2d4f7c9d0bc28d21daa6ee01f56e6a97e116acad4d763863c14ad808c26a36864255a5698aa6172f15c475f10246a9d8047bfae1f2d1dd4b3731852ccb396b0a06b4464bbab53144ddfe2c8b312cada2c1b2b7f456c09aa5c2778e876dca3b3ae46ff4cdd209d1d5800d2e90d45f2925bf126139e746078799a48eafe8db7420eea0c5c01b3df61d5ec8fdb26665204f31c8a96e1d3b1543f2846d3b4c20c402a24abcb435a0d8ad0a6c9c02ad450a9c0b71594fdc5912fbe282944beaa0f9a7c95ac097b190e353b7988cff1d93d136919aea92123f33f9563da635bb6eb17f2df048915c11ff2f857ef4edbaeb9b2c4a15e12dd5891705184f26a3e24b79b2af22b831eb2641c3437f65e821d7e2289d50c88d6fea93f02e9181d52c7bd80f945f19910fbe97afc3c84903b7f5970ab928147d711dea73e4455e1d7d687c9081d361075cab18c57563f57ae58f507068253565a0c6639b718445af0cfac64c79a753121f577629f471154db5caee9807cfa99752e6bb888ce63c39fc906862f1e88ffe33db8c34109bd9d61d7e41100cd8bed86e3dad58eb1326f83372fc4ff598bd8205d3316087c95ff2910784112b4d96dc29eccda3950005dc99c07570e0e131fb8d7a3bbdc888ed8b7f4a2460ad3cc572756831fecdf72567ad90ce489b4ed667b905836ddd0de3e8b65c9b062972a1d1740685e06249e97a8b71e64a691f501f07ffe1d8425a21f1f35bc68845b12d3b9cf2c128d8858c3c571356faf8e6b67bbd6a978efd8ff989d2602869a1d8df6761231bc57abd2bb627e0b7980e168bf75e1f2c806dbd22df0a9828329831caadfc30e565430d42cc2f4f22de394ba163d0c705fcd999a0239f922555b9953e7c57d2515d146305ef463fa3aa3edbb3cc28382af2309da626d5c71a45e18aab09a57bb5bf16aade0b1b1be80e707eb81d0948f8be2dcb9d2989492c37515e6c5d3c81885c533af7f75e579fcf3770984e81ebf32ecfad7b4284cf5040e063b74ae8ee63c30d0616db893f1ec6f00f29adf03f86db7adaa9a85edb462b1f93ae1fec4f8c49b7711f05b04146a9a6ed8bb1721c0490a739134fe500c20699346073167151782bc3d3a4a86fffa6d8c296d5eba576043f99059d75fc2bb1884165fd9f9ffd46412cc84df7a36104815ebfb05786f5b2364a027e3911b4d0fd660df0a33a69bc53cc4638dc158093c2c8376833f39dab6c325b9655247298651fca8e23ed678f7a0df0387bbf12a7080038b026ce574f45431a4d68dc3018aeb0b42d778863b10937dda2d6151ce37309347c04583329d4e4f9520bb69f10599fb6e7b17b26240adf0975cc3536478ab6682bc40b35e361cdf9e82cd6e4840b7594024e344712daa3f050d202918aaa8ce5d191589fee8c78c65820e94b18579e58cb3c036b3089e35545c8cadc858400d95ff333b3e5f6f0c1097fcffb13580d1fedf3a7a7b0c2e15017c9f0c35658f640f2b742ad559e2475352f56955ded6d3e10588f0c230bdf04e8dcae9268b64c083287fb902a3b797a5fb8af1fdc25bc25af8fe9e5aa2540b0cd7cec0bbecd70aa4e64abefa5a02307d3a1b41949c7e324b41b6737edd8c6318a05fcb1661fa4cbf512d2e2f03f307225fd6b2d888c30ff26f5f487d39966b0b8b797d0daf4bcc5a1daa0e6f3827a33136e85a4ca730edaaa65b9bbf6d52b249a9a8c196e66d9e69f1373f69617c421e33a05c019052923c9e498b9d9e638c406abc4ff2f9f165c6b735b5c8869a421c748a0ea67c900e12f7182f440d83f5c6aaf09007f36aac6b498b2bd08565cb4deb4fd480ec36e635b4112d1c941e3ee9ccf4dc9e0c239bb20cfc59b66e7297dc319ddc934f0c93bb70d24c4d51195ec78d0c99f16bf71ef0cea33e10637e2706bdaea84fd3a493b0773598002faf2260e0cb26d872c610ecc7c938d22e2dd21c01dfd24fb8977c04528660a7e0a8f51f8a33b99fbe930720c56c8f642248c356667bb9058eb4e7fc77c8df57c1602d149fbbc14952d587d0c691669d49d7d41e342e1d7c13b98eff953144a9876ce93d0dfbaf1c1abfed857e70d048c51990e7d870ea7b1b9ffda33e521b32cecf20f9a12198835653e96096cb062405d87bce55398da0ef8b5941a7676943117996aa23a3105fb52ae45c1c9a639d971a9adbd330a13fed005e266e21d5b5b474b59a87cecf6032fea452e13abc8e6eea76ab5e1c63ae35c71c94f9e46b0551cf9ede76ea598e5710ad923829d227d740fe1ff9993442bfdf5a018339ac865cf7ee0b3acf8f57da9fc59a1db7f37a96a215d5238675dee5c9324e3d7b98ccee8e78ad5a751a787e20a226f3215b2af48d1f60fab85852555b2a56f18e2dacd0b6e32b8d9e8f5c3198e55d2a49e5d8ceeb639b4e19925dffd259e5b415ae676c9e885a12c91823eb7847db0ca531af382e921bc17f490b9dbec3cf16f1a86ac0acaab8aad0b04ed3f89fbf135f060048ccce983eedaf94d8517c0eb8b3c0aa01eaabdacc0d5f7a54d640eb0bda59aaa803666c643a9091ddb96a5e4c86b80ffa994c026a5eff3a5e00daf2ca6efeec773e558810a5ff7f82ff6050f472cbf9840d01a62fcad5596fc843d155b85a9f14a5c691d1db0683be407b50543ec8d4d83c698ed7bd35adfc838e44a5d0e54941694bb1823513b02a01787a95f82384363d94d6802246e2fc2c495bacd1df3f17dddf7b0348368524efdf1f8eaf443ba339e1d9a88155d5da5fb5a528998ffcb2ed81c238d08ebb8188b3cf4283040d338b316aa95e512bcb8963c1b6331c745010fdc5b234cf8ff68dea2c4fc44ed84dee8938d10fd8a2365db2c202091c8f318f3ea17ce7d1e70f38953c2d1b77655ff8a179f68fe011f333ba5ea7223e86571d2af5fa1bbe7010b93ceb7efec9021de446b0c0473af172b8463a2b4a0fea87e964f172247c917310cae4f32577bed2acbf6554105c6727a41472d07baf4ad01e7875f9aba3748b9ad730013ce3b9b77563dac6ddcc2cb24440c10c9e72c6e9e70733222fc562966e238462016ea6f4c8c290f368852c5e9ea01fa39a9029b9fcb82033b798482f1155f12640cde06c1c1560f60a23d76db1b0f0b01347d5edbfe98c04e374818c94409d4d44531ba407d10929f17dd756348680177a7acd6dcd11b116f88cbc0563e4201e26c1c779268595101ce4d0297aeb719ac76cd48876904c6cf80e01cce1a7fc6071481bd60a51fc955b66fee3e888bf074e8e171f405718aecec1636b31ed7df269624f01084bf3b6818eb13704cea5c6d6028fcc4c4724e77a6fb9c187d91d7e669b0b0e3649fb5c6ece52704fb7b6cefbd94b1f0154758d1e3b2b7290820c853e25a4d8aaa131878a051e8e908ea3bb0269be9c6c1fa1dca08eb93e00948bbfe880b3afe7e408bfc1e2fe1a7f235c510ce1c84caa18bd177ef725b8f7d03a3c13376e7296b9612cb210983044f0c34dee48f943ef91ea4fa87ba2fb7dc102549c09b79550dcddd96631ede3804a9b5fac537ef8c6dac91364f153ee97661044c1c8c29076b7666e38cdc9f52d99dcb7a0115b9f2a9e6fdf036fdf5652107e102e0283c544cdc837a32ae182b36cefcd9fa749d620815f22c2ab9e7de401ec1671dbf2269195b4c2758bfc439f615e461635fef5e9db0c69e7dbb498e487766361f1c3ab01ab8b2af648d15b7a7a4e681da642a0f57a7384defb518ddb7b022f883d5e01ffdc4f2cc40a23e717f857d12aee4bf3da18a945357dca9d9db786a149dd9b66d1619ee52852181b7b140876c755566247243776842c2d6b900a8cccc28a83f3aa9d3edddf2a1f18d4368b0225001110090aec5e7455b45a53e894d598824084ebae98f454c6ec2732fb7829336dcf8f5ac8dbc81708018a6f9c9317307de3a803374d3784b8ba5242b99af1171f7f7997027ea17f6aff0229d59d3066a40e42768861fcb2fed2a9ff5acece038739bc89e81ad8480bda222e085f6420eb622f070da553142277f02b80e21759e5723a8e41d91e372b191ebb95173fb5684bd7cfafc9edfa3ad16acf74d641e719ebd162af8945450013c8fcb5e1909055b7846a03b8a5b58c0329915a8ceb8ef81c9523ee6a636981cfb7b0405a269ff5824ccaa0d9f49322807008b4ac77c9de7df0ce9a85c34e60b14f158a49305d32783c928c58c689a11edab051fa8ad85dc78f9d69fe7503cfe180ccf1d0d2ece1f49a178b3b8cd9b18ac05d81269d6df341bbc1433dfa765795e5c0d037b61a9c004b6c696b71978e8105501d7274bec0cad27a5b67cfcdd1bd6b6c8d29e68ae309f39a6354e65fd8e1ac8f78001df254a50f9c67aa07de90900faf460ebc90e1ec4389011527417bea2de06b0d85e3f8d78f5a3eb9e25cf7742f689499b2dca4081a8d75dab95462ad3f27bffad919d3175ed8a5a608faff3c9418c39656d63f5e6122cfbc21b074f0ffe82a5049d5c92542d2e510401c467546886993b52c38cb87d669d8b6ab9d3a58abff358d716fdb136f7c3edd78aad21a2b2760890905206d3395df16b5f67d061fbbb5c63aa052c5ed66bb1917190646ffc5120ec42d9213229d50d5e379447727468660d10024324d9b714bd1e0bdbebc22401ae5a82747b9efa38aee7f8a166623e0f579fd745b27c7d80086dfe3b6b4e5ca90f09dd44a2574d80dd57b218b59f5178887c19989d4cc5c0c35f5e356b4dcd090234f9d42591c6636afb6e924647eed1b9484bdec47ed98ef295ea6586e9b6b2c824b991e912c27d85001f4db2fb939bbaaa3fcb8ce5e573d56f1ea0a00e2b9110e72215ee51965b5d5677a5d059eb890c1487ce53b9afecddf3ff3eb01a800101e9d1734211cb7567843b20c85fbc91514bc8965a6e7a6a5b10bdd3d3442e16eb79387410987f8d194ebeb35e389009054372d1559821300a361481f4b0dc8d866a078f8f46b0930bfe95fa0a72e330b7f074cc4a8d1b58050021f78c3fb08212f6f5cac54386f0ed9640df081a4df680a06cc40e3e95948f90a3019e01bc79d3bf961140b486c8b7c7cda9b24dd44977e8e269e0dcf0a43b15265e1b77dab9fdf718f73e5dadf5c275c27779623f73308737de99df9f7642046604f5f52b457d9beb3aa7451f6514d426018c74b9097cd957b2b047d3d086d07ea787d4f761ffdeb8144efbd98446eb2ec6920737cd74b9bb950f0a2fae853d86a0a0b7ad11526a42286bd6c4189d76473800d3285246857e1be825d3fa6543273e4c01555c98f6f1d066bfff5e2c9ea08ca8bcd6c1fcfcb6a96bf964a50ac587dd5d92c4de549fb5043733c8f59e526f42c200ed32714cfb6002eab6af3be3fe37afcfb4520039d0a84a09e403d596f320127aa6227ca75992facd8e4e701fdfaf12bc47e7adbce197c6b2ce5b9c3284923162e53f2fca4e6ddc86f73e4388246b24bec188e201fea3cb38cd737ca7485f030516ced57761c7d03b2cd696d9a9c2abed5ec0071f0bdea70a2cf4f1aa850d0ad581d7bad81acf1d1bad7c21f21d0e5a8161621c2cfd83378a52629d012ba7ffaefedb581ab62c86d45b8c3899a481335776877d4609cf01535729945426d9b29b943a2972af8cffe8f62102a4ee6e3c1be68d72b99369dc5b69cec7bb86e2b5160d89fa36615114be6a388567ff01c027a19c2f7d9a79f995bec6350a2f1ebe0538297d18876b440dc05628b26461a7c709543c36cce6207e950f3fa43d2149d8289acd3a60131e263b3905540e985f6a00d94172118d698c952104216ce8085230b2fd386a14cee52268f364a3ff9971aa6c6603a9c87c8d61f3b9ab380391249fe2bac0bb3bec8d69e8e2d083be31f89ba3e6e4379d7db6a39c3c63d2c8ec75c81cbc5eff7dda12d8508083e3bc4e78b389599ecbb002a9ae935ac49df6822a1e0910fcc420f6e302ec0805f865763538aafe68671f5bd23e55dbad174a2670dec05bf52f739aaeed9db51982c7a67f20a90460fd916b8a9ae07c8fdba084601942b275e1d0333540f9b93c58ac5092675879c8cf7704501445e3147786d39e4481170e510d60180a9e56fe4cf89604ed83906111519ace148a149498766f182ccee7a45a29b55ca0368c49da57d10ad206caa1fbc0b992044c9ada8415b3c3ec1c681bef68bbf37f8ca371ec03f2fe3fc62efa1afbd3bf038244e17eb8b25fee915c29f0265bff95934ad27d8696df448b5a7dcf806923b1c86d3f0b3faf92445769d7b4a76295bd2d63e328d742413132640434c282115bbe10cf5445624ba9d9bc8826b1758a026c0df18fd5d02a48b2896a4979c9183f1c85c9facf1dcc90f73350e6eb51501a15e5a68d1fe808eceda43ada599f19ade2e6008e327697e10b364ff1b41f1fded0a56905e18af6f90198643d4798ccf1cc499e7a554d8c1e590621b8a5c1d0ed43832817018a1b743beaeaf12188d467c7134c617d5f638c1644713a63baad591c0b8a49443f2a4913a146a308302b43d8bfe4c06fac4276d9b9b444716e16ebe7ad0d59d3a05a9584fa8ef376f08a5f0ff8ab927832fb223e3cd8dadd572cf0858a0a99f4f70e91483f2b0a98b1c8e01f3775608e9ed357024e02bc7ea33ba2c94d2c95e1d70054384ad6db0866698076ed572eec92817129d3f2f6758594e203f023b71bc2770a6c0f12173026fe6284e68b65dd74c27f60ee04d6466962dda859ff36defc362dee0ff4e191b45734b8e6efa25e1d87e3356c4a348eb0c2a3c0b13a843c068c8f271dc1acb76b97e07b0e6114fa099776a96657499131336c4d3ac82605c57fce6dce63009497f5743c6d30dfdd7642264937b2f8fd0e29b1361a254b890f365f3765f6d86da5548564aa3bbfe205900128f996f97b861fd6c663f6f84d83c789d0e865f9b4a08a75415efea926b941d42c8a4a7ee047f84b37c0ad38943e7d73ce48d0cfcc00da68a7c6e72fd6d8487b654d7ba032783018f0e44ce081ab6f55d639a5199e1c9c5bf133dc18203178a098922075f2d4e4dfff8e52c32190184e1e0ec3162a5638d60f4530278f44cb4cf5f8318dbfb0c515ea8e8325c34d5d8aeb4b8cfd9650a8ad8c31c2377fa4b98b599d5637319d6793f0915ad00041f9fbea8ee7d70e84ff67dd3052653f6ea64ecd6234ea9d4e377988a71d60d5dc7e21a2c13a22aeac0c517631f510a9c42168deb5665c35212113cd95af3bcbea23211f86ce1943816747e5b6c4d7a69839a290407760ed4b95ed04541ad20c71cbe10478a6d6f2913be6e4139b22e272482996a86ed85795f52bc3f985b47571617fb105dd34a4db2a2734726a1b353a44a9e8e48e380203adc5daf28c0487b427e3dfd12181887ecfacef3552b6908131edd62026603149e433ec66e324a001cf3b9ee23c6a265f84d4cfbc7a770b00be5b836d60cc22446ca5987c654080b18c31a36c12a2e6160101a5db72afeeeb900ea8bf4a5e29246fa99f2ed74eb32b753d4414fab27cad898ed7ed640464a40972ee35c488c2bf8de38bd268f89aea5d0600bb306a3103a59f2f7e113a954fc4df2997274f0f96d417135fd05cbab515fe2092a28e924363146734e48aeb6c301bd636ddc652582073ec374749cf627f61347e1c2f11faf0f61cd48ce33d0d557ec019132d552f2610102b64732acb1ac5629f748ce7c2cacf61e66bf8fba5b95c96715e8b6ab47cac9213b817b9f8264f58aac36178f30bedd375c4f99a64a9e56213a61282d1369a82649bdb1320a794ace8a4de1d59debaef8dcec736c7a5d17b9cef61c27d8cebe8d05c6e03ba821c1c5f2d224419453850ea1067df56f03f651a460509892a6f1b3423b6a2cf55f4d28a3acea99c4c8eaed3e904329da7742dd3afc1470aa0ccb3b857e52c334befb3494d48ec9e3e93e1b5d1e02b953f643115813e216aa06897cf49567ef1ffe0774f83d890115f2a00ac93f10546a4abaeea5268f3e47012865292dd270ab7dcdb4fcddbc37653c374537d25314a2fb7f55494d7b05e679d1e8d95b02b29e78351248a81e1c58b1be07f0202a91c8ef40160502d939136661b84eb818da1c58d1b92106c1190f0c82cbd50423970aa2946cc2e03c21b51c6edf0e5ae3310352b602ea4c24ef688db3875e780e00ca109f05ebf8f7b9bd89d9da55aa7ccdcf63e059cfb1f71bdd974d6744c351eca74552b56ddd6c5a783b4ae702539d17d98242dc4547315ab3deabcbaed66c3e9925f1a04ff236201c6e99179e72b434e270275f15732f07f36ac98635e954ec66cb11aac339d671147be73ce5b62113bc8cf4bf2382250febe7f3f879040d2b55531c0307a62e5e31dde7b0c96d9ca65b73363dee5efd688f870a0f16bd695ff5697149626f830c682a600cf4eba8ec8057ac3005b295dad7449dca5b5e9ef6961e03d398749d655603e335c4715c9621833ea18d8e241407a24f86fab6e9bc136fdcb6682d055102b58d89213f3ecf0f30fba43a98e7f85c7b0b6e5bb86429292ac21d865e8a34c8ba8c264a6032bcfe1b143ddc0391791466a1c87936c72ba462cc724639ffdc62e3a6a5f2a1fa02ac89066b9fa4e10f6350ec2bb30a8e605ef6530b7fade8d0775d4fc2f96093a1d9817f6d2bd374d7b40a489220a2add22f3a4c69ccb815fa2b885cf55ffde0ec7072d9fd476a809cfca199a2f190cfd8be36f831f0b3d3a930800678832c60d71f6f2c0f7f4bc55029c00a840e404e4e2117939faccace55436c6e7d9e63dbf101f5a46e6a9fdab57e429e450aef5dc4b4a0fb230df9788cafac61e8553e35405ccbd85bb4bc63c2274e5e94ab5523c5e630a2e52f1ed7d146c49261814812b2e42a42bf2960c3cf1c3620b75c2f986664fdc560e9ea2dec734b34838c3923ef8c19cfd89257b3ddfac4e1e45848f176a285522f136aafcd6287b5adcb2e8c454914c25db43f4eaf1ffa8bb1289adca706edac664fa5968797242e8d1f98bc053efdd621e4f450df0e5fbd858841328c5d76b775296755be5fd12c6ab948a534cbcd4a86fb11e702576bed806def767875dbfa503bf036046120c3c1412e6a2c280edb8cd34fb294594b49914c501e55e6ced871d2f86d3afc8e257cde068107e0669c7c3a1d8d314970bcd08d27c245dfbabf1f99d9bcf2d04bfc3a29b1298ce53cfa62e05738e363bec2a3a641f085484ef4118b2d66da5bf76c9ae44eb2b984a117f64b9bfff7613d31f46d65b8f47fd2227a8c32f370d8ac54e3f928b309231f642c6775841f20c6d3398571f4a9e021516a606b60f60c320f1ee29e3b4b3444f5d17208f74529ce4d81df28b79dd14c884a05beb62edb7fdb7c3aab40732a93ba794ae2ec7178f55ebb135ad2ef3e1a9b2d47c9335910e52a0a00525243933b8eb828e4ee1d242184276dc37c3f7bc30884d98d3c4c4ef3f6b243d2b656bbee307cb8dab5ce5a3e8dd77c68320dad049aa94b295a4cc5226b74621b6f2f810d6fe3c86f8d9dc14fe8f54250c2de0cfb00b2bd9ea6940e30ab586aeaca3036b289df449b35c1650f428621087feda04837c463d3181b380d7ef2eb985fb9a0113bce37a5b9e140c035e321d35c47bb018a68cf6eb9ca44d9235ed9389c1dac2ea21fb2fa2834cf4e5ea47bb8f4887cda84e2ebc7262db1161cae314eec78163541646affd6800f24f4266954d3e5b5f65d68b0e38421767e9b5cc5b7c51c41c28f84378b7fce117490a53d6b18416ca4c17a8662e64af9c50a8bb9f44e7b1341082ca57de9db1cdbe679e0f1fa994f236c5ad6c9b73d7959cf662a263758b466ce5714cf09b35e194d3bb089af8beb50a8c2d46206eedcd2b7137cc6228da213ae5d08ab06a925df75b3609e69550a341fc85e3df1e7087697d8765bdb3652806b3e4221b3d2dc42fda2247e8d6b2ce87b3687e717ddb5e903f5f46b202e2c4cc6c3e262f8372a5cf360b5737e36669f02bc5fb7020eab2cdd62f2b29f8c90a07918f0b67c18586eb570da5de949eb0c19a379966bddccf6627424b7165efe89106cafd5bee44e4239709408e83e31bc7ae83bd982100d1a4685ca4ec3d0b03b05c7acff29784d176e2aed3b7a9e40872e43dddadedbfd8d9d31621a49fadb54730d1b66644411dd617bb9e4096c0e08a18a5ccde20a58e056bc06f30da5d95aae37015ce1cc9b31a39df6cf9b7970d40944d2f7ada65f055daf78676f5ff9f100efef8f2822a53b82a665cc660e22b440726d51aa61f2cd7c05ce8cc84f22a4c83223e37d254b76d00f231e09292ac97707fb30a49dc3a552044f2ea3f7f6905f338e2a452cba9878d1fd9cee4ddc2e105737304199c22bed7a835e04bd8b08559508ca45d6c5b72e186c4b6cdb958d835510497740fbc45f4dc06534eed38548726063c1c562ddceb65e239c27afa484a88e31fef6edc5a0eb345c1a991d0bb75104938e8ebf610696ec9e4a33ed419dce7ee4cdecaa99b849d1bc979b812e7485ee38c1061cde38d8d0764876d1f840369241a9e0b196e90db54691c6cf6d9dfc094b280eb118daa60ba7210fd4490ead32495ffa34bd34d864950d33819633768b158a844f15f30f8810bca297024de0261031c9fc27c05b12f0101cbf6bffa829cae32a68f965ddfbfd2dac74544c12d2173c4ac003c01c36caf91d9802ab311f5226fc94df22b59bd011142b5dadfd1796dae5354e359398f75de6b8c988d9f942ce0d5c07a8a8633d6c24848b2dfdc43e4d0a9590e67dfcd3d2296e2a83f240c46c23eb5439c0bd083908cd2899d6af8cd3a2a7c0194891c54978177bcf3bb8516cf7c0a6c0705cfc42dc4a0026f202036e95021cee0fb72ad190afd7babd986cb210c2fca07aa604d1a2d525900a40aa143fb40e20bb56502e66f76b758825758dd867be1df57df5924f31d407d660ec318ed449390f33ee2ac233ba04b84d861673fdeb5f6741bc6b641d03379c6e72b87f1d631cb4b36dc6acc826fe50a8bd8c94e8209f8c568abe13869bace15f7d0bc494f7227057640574abce300aa0637813eeef0ac153c27259a76ea9a9c997aa0764d1ac1d6ac3ba8079e147611088156ca56bde9dd6b5f508b2ea6fe546aa34a61159b71486f28000634d8f628836a30cc0f534e5566c112c8e087421def89ef0054c41ad977315e4c2a46adb192a71e4843f89cfef6ccd583f88ba34cd135406b8d6267b576d1a9cf668429d4673fd4623c4b86671f409cf4c6ba5df2c4fb6a2d7e9fd65cdd02f79546172f4e12ac458f323df23470c8e2f1cf282ffb1674d54be0ecf229d15045e43cd24e3f886c9d6ac309e14bf5b79951e2154769aa0246e68f31b932e4c578eda1b344d669c3005370df8b336ce07f569b3026c748746b08290b48b8bfca11196c265e06af010d850f166ad33a374753caa80713f4d3fb9c4317146c61dc596900d4663cf1b4dea56647327745c892c89c98b5a597771014ce0f19510ab2f4cc96b687911f32c48aa486e083400e2089534b6b3050209441cb4b60daa0bf82a26271a27623fc6c40190637fbfca026ddab371e7241cd7964ecf4a7f538fdd1ac59dca2abacdb19424a5482037949a9a6ef6e9251835a094c4c8106540134765f6073240bd89fda5ab30282b880e695811ab3ac981a7bb1c77ef5cc342287140625f9106859f319d4194b32961160b573fa6353bb44d6f3c49f7c56f530fb86e89282d39c0f6f3f20c1954e75ffc631b743478097fd9e99ff023b15edd0a9f3c64e6d1d25df5a3b608ad22736b1c1940604796b502f8f087ca33c2372ce5fd2a18a72fb8b8f29edd71ce0f9f17ecc5a199f4960f865a3cf9670ae33bf493bd256ef7f4705bb5b323b54a56bd9caba30316edd22ceea44179dc0b7174163e44a818b5687a37fddccf235ce4369aa9c6e24cec06c0b30bb996eab571474d8d2104f2d8a1f793cbf45ec50d4a0274f9e3e52de30aa27b8ff82be9a5a45582fc40cc668c8c7df754b46c191610f41d5982677bc87998ab869f5c431e86332375b3c7dda2de7243aa665a285d93f906c602052d968594f73352b81018fa53fdee5fbf9b5301e306743912d9a0a700bee05d7f957c0f9b1f3f74ffde76fbf46443cd92e3db359e8c1e3a414ab49bf9d990a9e9665dd784afb14fb4b7dc2351d6f4d2f1d978be0f73eb5336f13ecd26d08dacc7acc04c98682faf948543822cbdf843fbdc26848693e070c8a43124ec7490f838a9bc20f4181ce4db44c6e49f8bfe854f4093d09ed63fe9a59725571efa1f7353690b0ce319d76c01c231004dbbdd4e5f8c02986e052c6338dfa0dafce72cb78ec589e476ced234217e0cfbc3e5962f4fb1a24b382f5b3bb79ad02c3078db609e952bc6f22b9e032d4f3941b88622de61fc0a33db418b99eb7ae25a780deedc96eeee98940de0e9820977a9ec4bc3f197c35384f1cf56fe82ebafbc91c88235e2f8dd575aeb1226be4730f08e801ae55123bf72144658c05da3694316e45a4759d11ceef455a63d3c93dcc5d8d0637a30f7077795a6b5696d1cd2eeb9cbe001f3a41b12430bd141e253063871189c6c49fcce99cbb25c7925930cf527265a1e5b735dd670ea279f479b9260aa571059f3df19009001be17581c31713df0207bb802cc8b0f2a5e2b6941909c4cfa8f2b16a78b7fa4d668dbe722a7b037ce5ace405334eda8f7a0838713b0fe9042db9e3c9d03eeb5939c553202d491ece36839d54cd96e4132f51d03d452ee3446730ee4b2263881313dc7fbd8b5a08ad5d2785f1e0fe9d01dbbfc66440e0fa609a209066b9fa4e10f6350ec2bb30a8e605ef6530b7fade8d0775d4fc2f96093a1d982db80a2b5a263ba73a201b97d456510e8d1f17bbc118eb18c07933a4e64d362fae129b83684fbe687a297dd1a6d71d6052ffe5f2a2fc6a5504b4d92073d351497fa355708532ebc732568249fd75e61a02b702dfd08a62d9ac6b35d847eabd71175ec85ef8a4a51e140cffa36b94a5e8c2de7f59d144616298dd822274e9a8aa6b62ea850bd16311eea28e238787dde0c87d67bc55e010017e9c4000a8822ba446ff0531a202731fac773cd1d3718784290df416b6b60733956da1f3f847382149a070c8f4953cedb926c0f5a91fa6634f4029d34066fca67664d4b5399c948c4e6e6dec5909d0a4d984341a135c40b246a45215d99a32ff6b840ba58d4d3a8afd0648a730c1b12dbe3737dc5b846cb2aa5bf2db4d8f48f7d8f83a59a2d5fe4075f5d79d31224a41caed7d8ac578e349bc411f16497fc208d0bcf8c86a60d5e3b5aef7c05c8bd07cf9d7d61025ad0b8f15e4f4020f63c5c10c567b02313c621c930c0ed370c7328b296f17174a5e3b0b79e422feaeb92a39fc09875a7a0caa5b94a03266cd81be42a0d7c00085756b2850ff146e162ce387ca289c716ac2890fd12fcf3c4cd16326cc351c5a594c2c01cb13698ab5c30b91fd49d5526c2a81100bdae9fb642f9567a1961bb3343aeb7bf4c7ffe2cda92d127a925e9dbd1e625bcc3619518f50919df60557df3f716e6a19e25588009539452fa6268f881d95fe26e1314b73e2d79c99c3176a4eb3c0b28b8e6f28168d03575d6ac9827e030a4bd07b0c1e88bca283170c301da064d4bd01ec4687294ff1a58057c1eae33636bb839231b01895f59fe26192b2529680d6342ca711335360d87709367725ca20c14aaef426c02bcce3f76b1bafebcc3f14e52a04ff34bdaa8c06456fef988ad581f405158a3655f1d6d05ab4d4b59a7adc32ef7f50e761bb12b2a687d2a2db9f3b48d74e3287b0b33f996ba5109a5ba56ce1daa50844cf3f2c5021c68b1b199c53427461196414fc2a681d0132fc18363c5e33efd8ae29f45998be48738bbabe7036ca8e9ba553d8216e64a9e8aea15ace5bdf103fb27cdd74de66208857ca133b11b624c696979d3a3052b6bb89bf88083a461ff1f0017ceeb85af2773bd5662efc2a14fd05b279a95f6abda0ddeb0da83602199f0153e06bc93eff62b63be52339c290106b3570e14df9ecee6096e4bcb79752b0936bd50e56db23768890783696adb31ef2dc532ab3e3b370b474cca5d3c9c297144449d8303a83d3c3082cd453998d99362f70a2ee7ebe03c9a3865947ae80dca89ca10a6fac357396598dc2f9a0f3c43820c374597d518c9ce947096e5dcf54d6cca274c80f2b02bb54de73df43f4e079d57363d1201e62a014c5e716252a3370090325f39c882a0de8e6a213db16a73f80f68bcf3623b11edba8a8990543d05b40defea1b7a02f5a6565fdddd3fd5596f52d6fd219211296d0cebba09e84aa53eadb1bb06d75710d265f2bc08f07d014f31c13cba3940f21864bd02113c1e679249e06057270bd2e3b446dc87233a89b49bc908e2915b8d9fa45f1d1800c55cbfe7bc6c522d2177cf4633f7ca6d6bd83600adb23437bf179dc98ef1936d55506738d342910824a60f701597bfac2ee5471f55846df566cbe04ad7191899e6efad29df4ed22f842074220ba89570e68ca8e105f888c069d83fe265010dd6c9cc0e5ffb97cd7a835e04bd8b08559508ca45d6c5b72e186c4b6cdb958d835510497740fbc4539e84d3eda02dbdbab5544aa84d085fdb8331a3b6d1a84bd7418850987f5fb913c7fb8f6cb2fef9ddbc5e379c95700f4aab134df9d01c87bbde08b6ecf0040638ff458c288506c78048cf40f4f21c3fc8de727550b9583b8145fbbd424a074835375b34260c41eb3a6ee343a761045db632ed516b9ac5d18e826cc1269c26d5d5bd0d944f593e8f5bcda83524dae43286f3094776464aaebbdb0bdcf8f50174f05b4b5f31126ded77fff5ef0cae560614331da7396f0d097dbc5ab855892c6f5c0e66db4e5faa218643750b7c314d08496d1ef29111ab28d10c569373be4f89ad4b62250e9ed2421563a2971e1339f9784118cfbe8e826df3538e87b3043e8dfa9c845341ea12a6488b6277fd19bec387c06446e93057f11d058bbbb89710fb377891c54978177bcf3bb8516cf7c0a6c0705cfc42dc4a0026f202036e95021cee00e6095c434013c3095c664d45d689d337de84123b1892248150e6ebf2353250b5a7f95bd4e0c6c864b5625d0cec411bb468c57e0d6f7c31b6bc6ec3c9347f2c7ca5e11c74661c7e26a885f5b611c532395efd4579f1531284472993718423e3c16453a0d56e87782f77847d8c18b2c4c411a5c08721377ce15d586ed4a2642fb05b691be1ab04795dda969d2ab992d3293d1c0752c5f7a1d96d5f3f652e59e408b693d20b6de519a9e8f9ccc6db31de4583ebb8221aaf45dcda118ae116d73e1b227de7824efc423a07fdc4c138ed0ab84d2c3befd3fcf04609aeada50ba8047130ca09e7cfe4be557253366c18fa5926bf644137a2df1320b45fc246c4c7a475f76d1a9cf668429d4673fd4623c4b86671f409cf4c6ba5df2c4fb6a2d7e9fd65cdd02f79546172f4e12ac458f323df23470c8e2f1cf282ffb1674d54be0ecf229d15045e43cd24e3f886c9d6ac309e14bf5b79951e2154769aa0246e68f31b932e4c578eda1b344d669c3005370df8b336ce07f569b3026c748746b08290b48b8bfca11196c265e06af010d850f166ad33a374753caa80713f4d3fb9c4317146c61dc596900d4663cf1b4dea56647327745c892c89c98b5a597771014ce0f19510ab2f4cc96b687911f32c48aa486e083400e2089534b6b3050209441cb4b60daa0bf82a26271a27623fc6c40190637fbfca026ddab371e7241cd7964ecf4a7f538fdd1ac59dca2abacdb19424a5482037949a9a6ef6e9251835a094c4c8106540134765f6073240bd89fda5ab30282b880e695811ab3ac981a7bb1c77ef5cc34229161b5ea36dbc25f0a00bfbcd3de9ef235349f32577ca9b2565fb6e08e587c956fe2d427a160ef1d3ac65659202854f7345b8eed5712c69c78d40f59db26d4aadf35d0e4df52733b3e9ceb11e885f21e20cb9e688a01c6beb46c4cd7c6541af4bf915102b0e2b582ff5fbc07dd233fa36f0a42da24ac1b6cd89cd7bd689f8763f1dc17eac522724c06beb5982bda869319c0115489563939a1cf3aa76315d24f56308771b5146b76531c0214b1f2c692ef6d65a88a3c767a16834f5fc881f9add3e744930ea3b7fded9fa12457bd95bb5eaccd4785ad23d16aef486f44adeaaeb44dcc0290e52b53fbc6e02bbeeb733e52e3a9590230152a017f496e8936a6149f19e8e260e522f1ca2dd710dc64fcd28da02723c8945d2243e00da8e4bd88c08e4d1ea59b712a569424c3a3afad5d8dbecde20545b00288efd6c9e44e5bd8add45a815949d52513040ab20012f68a1111c97b9d027ceb5f94e4f849399fa9a5a39d1c426eba9acbf39c5ad5b366b029685620cbe941480e1b683051577c966b137bcbd798cbec4af19e73541a9efdfb5114449b432a01f1772757c026fad5af70ce533ded69f5c5aa6f89548b60a559c227e50eea22021f1e102c74ae17476b7c6a533a47caf2061d7a9a74b00d8967cb90705789e5e19ae7437b92800a75116c4429b22629a6b7ce046f63ec5053a07b2d8419324699ff512ee3eae1138c0234ec0a1f041202b8c42cd2fb7960eead9a32b8d05bf29a2d67ad7abfafc8145d05986867415b34dbe155ca8dfc847c28b34d52c047bee6d6123f99a0a1a5a3593e71092adbb2fe784d133b8d9011f25be2264bfe167c2d89ef301d050064cf5accfa1f812e5d689f5303e2e769e87aa4453224c92dc882d96bd2a20bf1a81cb0abe217b2e92272ee72af56e35333b61294f773cce5d226e3909b9172e99e8159057c530832dec527b50281295338f3ec28a2bcffb7657712ef0d75b936a11b32bb6b7668a113bdfd03495eff8c764481358baca8a1c0c28d29befed9d97e9b71db320dc7e7eb1b1e42c0bfdb7a491bf0912930b7fdf52ee65177a9713b950ee88a70a765bf716630075def76a2233ab294efd79cf0f0e584862a3d9b752e1f335377e8bea5971ca541b4b9f98f896dfc0e95feb0b29979fd5c64bae52a44c73b5e7bf73ef674780bd95eea38d39c0b5fe048f53ae271e75bfbaf48b43b8cc0b3ff810ff35f097346be7cd11f7180bdb8bdf208e252cd5b1f23c722d66d62f8ccac2b3344b693a414f2e21c7461aa1870424fef6469c31a8183fa3e7c4d3c3bcbd76d95cfc16b0ac2626f25773ca329019d824853b4a6c63e47fd4bace063976ca8a1b9bbedf74f83acd6e8cc47f65652ad0adb1357ddb966e7b9b16cd3d3031ffddd3a2d8db5a8822b5fda80dd69738c22936d5ca338fae7bfbae82a4d0c41b78dfaf9b3433b240de3cc8b5bcdf383b35e885ced364804332d222cb1105e83566fa673a877f404cc2bf2be936f2012781d2f7ac0ba1cd4ea040b4a1d6ddb5a274f1224acc707cb4e9e5b07c7d6b365245277fdc722fd6471532cd68feda69f95f74f9c51d0af8dc71b3d0c6fe2f078343b841d7d356bcf7f6db9387fe4000612166f9deb1545fa477cda8831e5c247d7d4be30f27136c8c0be62deca1b279721ca7eac249905124138c82c5336872e36a6140ab20150ca62785d77d6b1800038948d1287b1e60df8a723b920e6ef96f30ad371f9c20cafc1be92de8a52e61a8e4dc7fc2669cd05c0cd3cda0931b93e5f35c660bd75a30099d17fbff3c2dee729c452aa42a3a13e51339f7fe57359cb6ee012d8f5324d38d599e037347abae8d123af55c35e4e3634034a29717f9510d392313efc962cc40ac6d29161da9c88988ae2b77180f2f3cd5b1f8cafb87a67add8042e96127115eed0866f3b8c5009221b09b90c8b89816ecad411dceafc5403230c4d137d85b7047e4c6aacb7aa7598bd313d420c520ccbb035a4a1ad232e6323fa5bac1091d195464f379fa992a233720a23413b46096fcbfad333ac07c0ab1b3aabf30668745f6e878ffc01a0344f01a85e7e34593084f6c69f360ae306cfeb0ab388596b131e681a7c5d2e04e171051f0d42e2aef4e392e1100460d3bee06f4544add4f4121285435804c6a39a20f704979044277c142008b46eb8f8f4a587975de700b157cb949c70d33f5365a9048ff7c62278d0f6a99e00455a45c1412b54eb9a5774f25b409cf910c5c0c5fad23cc9846a362bb6ff68615081e5f1479a38a9ced8904c7f66611c90206391a7f30dfc72525b8e72cedafe1a42fddee8f8e65304acbb164e6696e0bf758b68003e875cee22925fdbb8e79174ce9b81579ffbf752d20e330ce1cc2b82c88eb6a8dea84d3982e77a5542987f2fc49885ad8124ca58c4ec096337f35a2c26f9561f76210fd25030a2443e14730af857db5ff1205196b3dc01e32ddf4207adf27ba915a266b69970f83a08859d96f463e3fc34801b8e88a0cfd2e4b1a69e6aeba96b8f6beb6308b18649a46d0a5b2af68511d64718c5461060668fda2c417c81bfdbc80e3bf6b2a5c61ae3f63a4ec4668913ae55291942fdb770b2ccae3bc8b0244f6891fcc412da23e6fb51515e40f65e2e68d1d404193b544560528297bc61a85bd84ee97491b7f5758d12ab9a36c009c48c38a5af295091851505cf9a222eac0688001d834dd3e63bb3527abd23e4defbb4ead6eee6299b89920ac9e9aa7a5a843018efe822cbd3e30791c84f937c2c8712795ad827b7576e8c4a1b24fff95a004eaab82a699cd166da9b97fa7d34c451408f974b50a32147c10dddb9136de439916afed526f3612d19a5e76b64c3aeef2f20582a581fad4812fc2b7c13ac0b381d2f77df27d005de0ef290bae7b0f49c0c210fe88926b397deaeb801eca3af54e3552475b0b0b90c25bdb3008fc3bb1ab747387c813ecc5e56163ff66e3942ec3ef7100423a43b3ac959a2cdc06430962143aa11f15ad7172d1c286c6bb21ee60d68c312983dd56aa4a8398e30423c5a8e747f62ae3ae33c3fbe4ee6e6ec902560de60d214a52f77b3343ed894c85c36a25f9dc88fc0cc9e3701245763c4ec18d12f54be06f14b061721caef508f2ee01ec94b73b08a0422a70f0545cc4d78c47347b78310c48af57b5ef4f18861cc1d39e49d20aad77a888a9957382fcb7b781485b0bf5cd7a2f1dea4380030f3196f05db78d36f3be357f719fe720383e8b8462494e673b20ce2a96a48801c6d278ac9056c22b0c2329781c9226fccc3dc2399102f68506f8c0df94b59c9115a88cac9970bdba19b9c51a32770f9c26ada4b4fe41b73c8376a31922ed4481458af4dd077eed241aab11d492297cfff7fe2d9c2d266514065e6cf759320ebc3a0514e964c09dcc432c3bc842997267aa7da70938cd74b2bdecb98c36844daee481eea027d741d953132992f1c9df387c9dd1227ca6bf4d01a223462aee151eff322bd11514e8f74fe2eefc5f2706b77cd0633e11e924a12a072ff4ab1058f5766ff25cfb8580975c9de8980647a0503c78b7a4bf115354906c1d58a5b62cda60f071ee3d1b1c6a1376cc5761ba4e6972ae73531b36863424333ff5d5f3f1066c84a35a19bef2e6830323e5f27f50058c8057a1d745718964780f8723ac59fd7a257e867e97e89cb4a2c3aeb15151ef1a5de4a9b23db39acabc7dbaa4331d48076e8f5dfef09737859c5d7d8e8eb735c504291ac4c3146db7c367181811c49c7eed546a9b43530bc575d97292e2a1f41e81c3d1db503ca8d8241f1920d653237cb9b65601f6f4275a1cf490cd6cd1f56a281f197cf0a3cb89f80a61f6308eee0d9b323f0bebd80f7a50a2f132c7af1ac7f92ebd5f827b4c6ba309e07857d679da58a42b76ea0b1f93413f40af2761e00b9285c81518c744419c4fe64d1326ab826722e5b918e38afb550b21b3b2cb3a9948ae73a74c1ed0f530e092b858f98e8b3309838889140d8e231e0540b35c54fc67ac7d14e17f434e52c479de5fdda121a168eda2b33ef626b3ad90e7dc4b2281cdeb2a5cb5926e26d8bbb10fd6121cd51babd286bfc61fcbb28cf17d95ae72b5f1ba4fe837feba801fba3b96c5b14dea62a7df1e016d1d95bb74df5f2f3cf0198098aa240c38fdd0726ac89ce51700fce89e655a9ba910181f6219560cc0a9dff5cc8bffc9375e18d1d31388d7a6b05e5f6f5d61bb8e53dc13167a5a5fa0ed0a56626dd3da00321ab611d46ce55ed0e60b85f5ca408fcf2a1d2ca6dafa4eb26ff5ca945d202659149ac256ffde03331a8f12c69e017f4d42df07c4db2e1e5cc9c06bd2883bb12466f3071a64fee7db453bb8d9246fa6344bb9b79b6484745fe0f804a466a7a75d5c7041f16e56120a0588498a7493817dc0cac52d9c94c5048d17094352a45b53c760e23e58e8c75fc983c037d987ee3689bd96ca0347c88bc633d6c2e800fe0932a57fdadb795aabb963350fb3477f078252861b6b75b3b5d07c08673f922b263973be2ffb726e047a4d8076b9267687eb5374e8ff11ca125cfd2afebf4ce67d44131863437ec615488baa672190bc8f7b2ce8ba3eac11e42c7717c44f507611b06cd6f4ee3aa560504265812e07426ffd9bc8406888424470a2c8c95673097f18df76c0f0540172b81a54a112d9f881dbb53d9b05696e47b1c03786ca75532ae13e859f2bb2e6ab6478943e606418cd1c47ab8e82560eac12d6edaccb488e706b511d1c092bfd4658b2da0b5664be52f845b69e0bc1d5eaf3e252dffe93529cd777bd633c74517085e83f97dde0f2843b5c0cb8b1643a9ade77a1f2af218666603d8244ef1a5eaddd44b9fc1922ad2be8f6697398281630ab0bfc978176df9c3beb7d79bdf77300f9a7086e1608596e6b5faba24f7a1d867d3c88716f943085f6b9a384c0512c0c13dff8954bee3b2c9ddc87373ffe6633d0d5094cfcd19554897e02d2a57de8277c2c53ba87d77e213a7a90d941ffbc4b0aa7b32a588e32283d7fbdae74ab87ecae6479d4fea59dbef67fc31591fd94f7d483915afd66c0ebb95d4f804e5bf163fd83f0c58cc88bfed72af23ca79bce2d6e3f9505f4add27e8833ce9477827ae453fb1c6c15faca617ce27ca7a1d41f13d05be0d0ecfdb34efded4ed488ecc51b4b2beb53a5d40aa0c05c5d5e38acebd79e6a09c90fa5e2b0bf4c0b8b3e910fb4a08add631741cb13334285f9b560b3b3bcb34d9e45844f1408b3531b02935259df1017235c22f67bbdeb220bb1296684a45eb2106c63fece9d3c4fd2f7ea1d51326408bb5b52418d80be0850feb08c1007db055ab0804d5748bb08626b7d9801766d0fb30e5d2f4e3e2fc217d54c0a40119225df923beb2112e340d25cec4045148aed8a0f2f270029970ea74dafb16cdcae46ea1e3ef9b3ed6cfa616857a536fee11705b0edd57701d6827ca007b6320ff796f0e6cc72cf7fd3fbc0439ab7feaff994fd753e12abd02d84c893382b0feb2302708292a425bc1ef09da0780f97c0021f273c67cb19d606ad8f711eec74f8847870bf2ed47ca853521595d0c9313201588f53bd3c0639981ea645568ea635e390d8559bf07fdd1e601b7aca7a05c02483e598b44debc927f787411fcba0157f7d96948d269ff91bb4e4f564220831b524e5a5d2451c4447a3ec1011d9f4ea478c87dde714b9a99ac08dc69df7f43fd586ce9eabc593b97e1c10e1e6ff0a90c19c89484fc1592365c8afab2489db96ab3a7681e43c01b846d0ea41e9abcce8b2ef63d5f6d52874a493ff1c9c9150493530366a3cbff769d8300c426365950efc7ba86cef855deaa81e4020ccdba51934faf4030be72ec74a7f8418686671208dee743cea8c60416d8ccd7f1ee346eb1e50a48b134a20ba8f5379e57b009451393d213152ed3d0317a29beeb6288dcf9fba0ecb9e111899993c63a1089c6cc08ace72b4c6c839abfc5bb88328531745a43ecc0e9c3e06a00e3e92652e25b8b3fa9ceaea87e67abe57855255f8af4fb612819ef4367e0ae3a62230b13ec2016f4061fde2c1cdd6e4acbc27731e01d7eb9b0e0460b699f77c63916a8721ba380edde9b5cb394ab3d7de3f50ddf43bb5ccfa235bb15e9029246b0c5b5afd21a63afb46b00b06e52c82885e773e4e48a43a222b17c9c075b7649420b02cbd3351ab8dc625ee0fc3e6e64f50f99e4e39d698344fd0916969864edc9878bb266459d76cfbcd203568441533edab19ac049c449677e05c763473bd78c6679b3230d0f2fd66f06557f0369e7d4990435e7ac8b7a3ba84db3340ba7c223e7f4254f5043a0a19ebdbbf7f6ff864d65e946ac9d5c00ebc58ffb12815becbedbbbc1b16123144cb8a9214b5b7da103da91977d53869005654729cc6edb9f225aa33fde745a03aeac58304adb947438fe9f61a7b25fe4cf5cadc7ea364eb64cc1af9a74ed378ee2e989d65028ab29e6d7931edcb25536609d337fb13c25e7fcbc3f82f9853e59a7b66d1eb8be30ea9a279eef9a045637fa03101ae95bffa21c82544435c47115a50ebf53240be332d824f72e01295d3ab9b5af2209cedbea25a73c61e339177afeca00aa1580863ef767406fcb78836052b795bc40c10d143e36f6135413cc9ac47682f25f9093db364f4715030b10e2437e133c97f72cc19cee99bf03b10da03d14abfb6f730b300eadf481f30c7f2a5a2e81daefee41e037b467b2f9ab12c4d98c889c7ac48130ede6b3a85ce4c10f0c7082f041fe46d06599f42db1615af95e99490141476192cdca95724f338e204857df37f71d5dbf0fdce65e462636f5ff9ba10ab9ca8f3da1d66365ee30081c4c57f8eaf57696c47054476caf295a12b4f2a6b72c0f2ec2ba5ce433e7dc5cf324557171d73b8a7705a1cad477713008686c0cc636cbb7c9c7840a2942446bbbd503d4c2f27043d119171899f76f1b80aaf181588cababcc48dbe50690f8bb4e0e5ed6ed2873f5a75080794d3e5667ece32218dceb2903bbf07ecdfcfc7f09b83a5292f998f59746eeeb04a86eba2742f1cbd3cf7af0e8b3deaa9d159d78522454009e58f956dae33d6b819fe8c317bf81d3c3f5e770831c39a9a48a0910c045a7de06a8308500903503e5cb989cc421df47dc4e172e7612da8f277d2da60ab8f472d8c12f3d8ebc996463c86331cac1998b5947bb32e90506ee0b8afa4967b3a39aa3c42336f36864cca189d7501cd158ed7fb7aa781045eec71432d04ff2169faf5c4bb314eb66bf341bdad7ca8d1b23d94a6d334af87e86bb5c69248d1f50cc31dd858d52f2e61212987cc2c454afa1e7fa64218ff9ea2085f5c428dff88454d29a3e9ae9ef2b27059e6194d340db34ef4df6c2b0e25d3b98f33edddf1acd9325bc3dc782cf1e120a6c9e6d30f2c929024a7e0dac98d1d5088c02a6a6ed997fb930662e002a0f17d800db8cefdc8f0a8d20062f588cf1b05d55313f49cd262a9add74593f530d102787dafac20049dfc84a2836a761ce6d539dfd1ad941f2c0eea1bcd15b334525e9b124899f1235f2e56eb770e4585301f5590bc2971082b004a835321ff93a90fe2fe726715fd168505e6f0cc76e19191b567b9f911eb4dd23402e9b90c712530bf034f722f524770c9571004971e813bbac31764a12f4057e6eafe23524cccd57a7ba3707a5aa45353e974a5d0c3c634a51cdd2fcc870a8ea458bab61a9ca8e95da474539ddb8836828f72e5a527077620424f3d7867665e799c20decfa64f3e005d584847f5b268422af7db5742a517440b25170a6c9c644c6998f0bcc5e5cded7d28fd89b36615d8ed9bdd305a58d1a3a08e61354d9a0706d606672fc30393acce1e140c96d52c7e676cbc304f4b7f7168b98b013223d6d62bf512b1e22d20f92097f4f71c99e1ed2287ab5c9fae2535d54cc577428eddb3da4b17a198120e7260135be5805d025daffc1f9b932de0d2d7e65920a13fbc59658cd8b33a924a4883fbf78d066693777267a57de08ab28eb5b386b352087fc2e5c2165d2c3bc799f05400ea8ea4eb143578785373dc398208c7ec8dac6f018022001b4f99fbc2cc9271369caa002f7ec28a949f55f5fb6f525b5ae1e3b42035dcf198f684c31f422024b6e98ca437bea6dfa149e2844829cf0d8901fce9d4dacf7ffbad55cbf2ad82bea5d565dbdc0a6336cdad1e3bd874f6fec1a02e663d225f05bbd1f8a32104aba52e6bb8298c2bc5895ec4014c74d8bfac769a03884cb1842528b2e41333fed9edc203b9412d20f57e83947a2d8212484f42e773fa32ec884f8319ea7c819f3453ecb07ee23480eb81c605aafc1d20d1cd4019c554796acf5028b52588ff38b01933316d136dff93c783c91efd41baea34eac67e341a5da35f2a8bd9c2ee9296278df4964eccc60f93c27ea4d6e3c4a05d8f2892a0f38338f01319aa103b108db5b0a7abbf75f392bf103e7b4e6789f8b05c55ae34174f6cd38fe28e1f54e3cf5c1caa084d3dc375a0730f4113032f24a735546570a00d6d7eb691a8e59ceeffd1ef6ea970e96ec7bd614099e40d7267e5659767b5cbb91ab17202d2813240b0e2791c11c8db5ff3b1ed3f58565b2ebe725045f26e2fbfc9da5c8196936015884df574b63bd1a6d5eb3c2ab7c24ce96fcfbfc557bbbba32b388d6530c73796cf255788d94ed837dbb9ab4b845933def406c828ec979da3a8ae80065d89abd894191f5e4735f2421e4b34d2491a657924722dcfd7283652e96aef7425afe742485100694b70917b0dcdca709ff8a5ae3c9269219d1b4348795d665e0dab8892770fdf674641988631c2eed56d9fb3103f719b746f6fb41dfe31edbf1909d927c225d44797abf29d347fbd53a663b4a966aa69e4f90ac6baecf0281c69d3da273ce7fc451dc0fc1d910b59cdbeb0d7fbfcd10eab4cbe16e3314f6679ea06d26718406ec09fe5b44c82652870747edb27539df67e4d50b416b93b3c162f51ea510d9d9504b7374c0b4011b3dcdc086c5b21604b55b9cc01431f9dd2392299a872d0dd043f8732cf46aa73c7d0e57c1719a7a802e5d34ef6428f2c2b8492710a968459a1ed40dbb4a091fe21efbd1f41876e5e0e8aa7b6db93e29d0b5ab51563f1855945184774141a53aabb8b2eb568d54c99b5d932da0464b76e1ed1b0ce6c7a5ea5b8ad90397ac37f0e3670819e2df07460e035356effa1773b231188800a0b79d5411bdeda31aa304a19e9bfe592257aab033f74df3a1af2738086d3a9e0ac0144e88bad3105d691f809e725685cd45843283fb8fe6dcea32d36b2b20b674339a1c24f5392860a593f771592997f4cd0c6fcd6eda3eb52d5d605172da8f111e8e05857422773704865d8a8db54d7ef045ce8270ba0b6650f60f4d1eaaa30c04ca3f78983cc58771ec9822428f6d300f3dd6c7ae50a3b8d1ae46dc96a9b4ebc8fb64f9704e22d123ed8d9e66db7c7ed408c520596fbeabefcc1b185091c72352f699ad78eb78a9d68a82608de4813532aae5f16a9fc99c68a3dd2d55499791db6808b055ab0804d5748bb08626b7d9801766d0fb30e5d2f4e3e2fc217d54c0a401199e5e85da58843fe762498ced8e05d8cc4b73583f5bcd1a5cbbf8cd23e4a5db2f379b0569709a62d2a269bc7a7ec6acdc538e187d1e1fbadddda8a673e2462e667a949d1ea188e9128c693648346cde83008eae7de1fda6904e37c91718f9fe2d44b11cff5aab2478bad9625b038c43f1eb12024324f7eb75f7bc29bee46996ec7966ade2105c6390b390ecd703d0914824acc4fd5634badfe773df49917adc522fbf227db0504f634ae0bd3be89ebb655f699b6d478758b1f98a0b3e51c1e4d644f22dc97a273e25dfc09c5a48d6dc377ea5c35d2668418edd15d2d6bb8ab0025e96ab7031db5c544b9da9d29f6f4fdc05810f25d2751051b85e652f9af72d14010bb865294434f51736e64c87d2f66a54956067931332f2ff9647659692a28c68729fc3e10606466c5219a0ce5cc7d40f0a63d5e654a26552f57205a0edd9c184d6608d5d9eab13e386a1f6092ead306c172a4bb89eae25595fd4752f1c6bd664ab871cde556f64b87d5cd4392a874126aab795cb954fb89780d4457ca18102f0ea44943faba721a7c8ed4e35d8cc1d520824df499a00afdacea790ccfc219aae19bf4b4f7bf983477d6d9951db34e26c2304ba8b8fc8e8a10162e9df080255a016be998d64ac2cd543eb07f5717e13651b0f77cd5e12ec82ce82c82643e8a7ac14a7bba9ad62cc831433d9d35662445a898065166120d5372ffb997f4f33fde3be0c41620de41757672cc5f4a90f5828df0615627b53a3595884f0c68fb2794ce8d1bae179771b937ec4d45bc4b3630282515bc1f6763e91163dd8a7a7463924f5bbe1b04768d0f9212ab273e0857d3193c9bf7d6afce1fa17a96c006215f608063a5a7808ab4ca2fcfe5c08d5a7265ad86e3e2e4836f27327c1bf956479dbc8db5bc37760bcba0aa07bb2206a248cc3f02a1063822d1d4f8a191a0196fc1ba9ff4c2344862e3528f1d1ac3ad94805cadd74cdeed79d96f64bade08e0fafba9bffa88a093be452be44f3e345094b2c699e5d9ff4e5b6a3d34c5642d3d214087e4a8ee70b0adc04b7708115933f1643d1ffe58f66411bd83a2c5ee8188a81aa5aa115bbed9e21f40070c2e5e385831089e0503dbeb9a329379cc23c74d09a423745376a2bd44ef60a5ba5b7ca4a77f7e52c0299df705e4275bc7c08ab4cedf01feed0e896afb9b49bacd76851f54bca40adcaa7c254a26193be76cbc04594920954c5bc431c0ccb0c3c8c73455e9f2bd97a6dd6db3e489f79c5a5f7c42478ddeaf314c4b81ec83744fa7dd5c810f955a1bfeaf87cac1c25b808c9550d7de4d8600a6b6f5a8d010a4b95aa63e4cf336002f72f58b3c9baada3950af14bb40116254a43edb864d27dc4a2025d1df52141a61946e356e50c0d73d8b5e58cd57bf4d359c78f9f16e0d216d0e5521f9ff454845ddccc612bd394510ab977dea216784e2213961bca044215506721144ef193e00679605dc229416cc5dc1324a26d8c0db91ef4106a9a79f4bba2f964ddbbcc47956fad0a67644405b8c485bd9a9af25a6b0d27f2190be2888f5b6d5d2b67601044121de3da2000e2f2bd9af92122cacbc7164efbac50015d9a75be6730c96b1453e50c0bc4be6cdb34dd353b6953886e069eec381241a2bf05f64542d7e0f0d2e98e704354bf55bff5b0273dcf538418ce6a2bb20a854206e4c9ce2dffdaf4b2b1b9e93a54851ff26b819fe8c317bf81d3c3f5e770831c39a9a48a0910c045a7de06a830850090355d57bd345b9608de167437f9f25aad3ce21f287a5352124af69c570b6f44768b95e08980eb10cd2be6494bdd7a9aeae37c6cb382ed7352bd1e0bd98b354586423ec1daf25fdb347ad772bed68bee24b961df208b19dcdd92ac3ff4111fdb0602449c89a84383ac633436c5e1a12c6ba44b5acf3a51f9effbac824c69c033309ddf0d3f66306186a2ba16f096dcad90c68f63fd7d19323a377fb9b053b7f17e3305332e22f421a28b31ce05de4d2054d7f552d31485cb86f075f9159eb1b033e703de5836faa93be37496f41fcb55ba197202a11dd810643fd64d8fcab94666b7ca353f77c2cfc582dae754c2dce126d3688d6bf6cfd8f795ecb49f8dd2a1bcdaecdc065977c2d6904d8167fe1a08216ec1e6f183ab5983af4dee1c644a1b9f1ef7b124899f1235f2e56eb770e4585301f5590bc2971082b004a835321ff93a90fe3b7366cb6b36cba9093f4f7c2afeb301918d76b96479c92fa2853079e90eafb300bedb38a192e5132079c1d7be6aee49bf90d0659de7f9c46abbb4b754dba8587367acea8dc441550e9a8d4c989d83b6da2821690cb54052c53f796808da9c91da4173cf4a703fc89327146ab51d5072abc0d85660a728b65e83fdbd9b5c8f3305d3964e6673b61d91c16a29c59e7c25752f8fbe09266b1048478e1e07aa175a14fcab1ec92733b6d57d4bc49d1c6fb2de72abdf7bfb6db668228a1f8ad81ec385f7eb52fcae21b847f438734d143bcd11e921195d4e5a84d8f5f2972dd02e7b4adbff006e27900f933cd1d5684fcda8fdb7bc9aa928a792ee5d161c76f70318630135be5805d025daffc1f9b932de0d2d7e65920a13fbc59658cd8b33a924a4883fbf78d066693777267a57de08ab28eb5b386b352087fc2e5c2165d2c3bc799f05400ea8ea4eb143578785373dc398208c7ec8dac6f018022001b4f99fbc2cc9271369caa002f7ec28a949f55f5fb6f525b5ae1e3b42035dcf198f684c31f422024b6e98ca437bea6dfa149e2844829cf0d8901fce9d4dacf7ffbad55cbf2ad82bea5d565dbdc0a6336cdad1e3bd874f6fec1a02e663d225f05bbd1f8a32104aba52e6bb8298c2bc5895ec4014c74d8bfac769a03884cb1842528b2e41333fed9edc203b9412d20f57e83947a2d8212484f42e773fa32ec884f8319ea7c819f3453ecb07ee23480eb81c605aafc1d20d1cd4019c554796acf5028b52588ff38b01933316d136dff93c783c91efd41baea34eac67e341a5da35f2a8bd9c2ee9296214bafbd7c1f444c4bb02937092c4cb0e59a60a57fca7fd989d00dd832de1384a3fba05aea37493ac7abc8907777ad9b59fceada5b3d046d19f402c18854bbee02a0d1ee37988185e30c36e4c6a936f6139ce3693a38ee13f0679be51007813c24771bbc8acee613fdfa48b51f8b0793c7bcabd891e1a8d3d57a1c2a99936e12a0dc017cf8b0f7972583948c0eb0efc699b21dee01d04550c1625aaa56c0d37f5c59807a16eb456df2a6a473c55313958f7f3ff5af164bb035e7498909079810a4277116e8dc369b56011ea55119515e6b50744a404e699f64b824e50ae00219a3c957df43e1de126f3f30b5244ec32ad9d2b1aea7799d2627ee93e96987599056dd3c41b5f065c5eb84962f4cad618f3b1e34c6907123cd9273f23bb66784295313602f849609e02853a9c716205adc88105a55e3a6003e9c5a9f3218159c771872282751d0bc44aab14b7b6ed8103795cb73e7d9da804915b3aee04de89e5ee7e4181d2a136ead980370d02c8c2f2c75c821ac673e4e4c833061c2172160f0b8e2f7045b8defc563efc642af7c5495252687ebbc25b1ed5460e650594f9a57d94a2b5c10f66070f68f9ea44631ad5185da247c2e41a5457d5e72bf75a0fd78df8e09eca51363cee9547f317c59060f2a46ba510226eec65fdc1bbcb5228af6209e1401e78e7fdeaee54406ca960b959ce7fd23ebe54e6ca94dc203a2ab9a23d37510f06af6aa12ff8c291174a015b0fda72578be8d0b031a043503fa15d61703fcb8bde2fac9bc4b13a460b82ec3c8fe22db095971c1742bfccfb06016c652e0ee2808d97b4eb02b52da1bb38f8048973c0c00e704c5a7c12e0e93d08aaafc4a9459a537fabb0aaf191f9439612dbdf312983f6061e276b393f7c9a0e614a2fe71d9b117aab6c871d7ec1cfb22e1bccffebd2decdbecf2de8cff85533d5570605eaa88cd8fb174b6e90e6e11cc9e42793a870754f1473167966abc71c371cc3e29b84778f8dd82c9f753c4a203ccd11cb5d7caa0ef5c00bde9418a8da20764cc53d72e4fa1e33e3dc33a4f07c432dce06abe0f9d650c5a5eec1a5749ec6a348441d768cf7a91a389a96615ac5c94a0ea1262e79681252a238a6fad30758139b369881edc98d2dc4caac29f877e5dde69f699faf7689708b1c0664d10f1ddacd05a2131ba0e4f304d5cce6293dd417be47a856fcb315090f7d7471dc6a00605f36a7032185f73e0b288f998921e1ecddc827ab0f08eb3f5fe757359bd8f768a01cd1aa7ed8581ffa542539b66c14a7f8fb9891a4b8c758422874b6120b081b47dd3007642fef73e4f6bc64169fd275a89b50fc028ac6877644950576078a6bc09b1dfa41b6fde48a2e27306aeb99ace4d88e06a0d4feb207f784cad6d6f2cb2c978f030ae1957f5f918f5455e2f18a8b3929af73fb50656d8c164c123dff11398e6a03378e13722008f2f88cd0d726a803cfafb9d39eb2cfb3de8350ea7498e0ffb470ee570ecee4f7ee173e1f22bfee4509554511aaec1ae03394a0de4ed4345c4681393df8c614907ed9a5ca316cbf57fa8116780d8ecfd63a60c5e9ba7418256876a81dd2f1b2a658aeaa7ce5ae3b0ef76422c445756a43cb713281fea6998c4da4ce648fd363e6dad9698029af3789e61ac2f6ae91716d7c089128d65035b1425a68f99ab50dfe66b4277f23772bbae304f4aa9b7909ac205c3983baecc88a4e6541d66c6894ab99e09b39e90901afd0ae09d369f1c07c2d804db3f7ebc2f2148b7d5009e7fa629e50b3423d45456da68bd094d45f275215c2978ba5f32910f48c44b34b40ea9e3acd4c62a226d3ef210ed5b5137a868e69fb1dfa70ebf2109ba6895f5a5205a2dfe20d7471dbedd6ac1c4148553e9145be8da0018bb74fd971d50be85febe15a1e63161933b5a1596b77458d5e2cf21cdc47ae6119d95f4868772d4431fecd869563acddcaa53df1627361acbdc13e20041f95deb9f043cfdca12956952bd1157c55ca0906a97384e6c362a173c132f842763e67d28317bf239054d77f8b5ab02a8f080df270726295fddb178130ab836dc03f80080aa279f7525936f4b36b56e2f44f443854c87737f294af6edf0a63c2a50ab2b266896e0e4c2cd0cbf99322ac5d138dab005f645fa17726dae095c2eb59022721c2865a0f0bc1610ac4ce7903f597e98f128556aa751f6d2c4613658205b3786d54b642a6142a30413d72a3d080942f530f730d7dd0adaaa0370c1f91ee5d00fa98643c08b9d20645bc5a35cd512b48d4d4373d83af2a46f4b37bd27273b2a059543d737f2ff9d1479617aa4987b531ad781865fd5b591c41a30df5d2eccbfc3814115687ecacb0e7447e424c6a5ef50907e2e77056b813b22ceaf72f00cb8d93c9b5d7705adb5279093c48380d2b01def6065a113366a3b0f73bff9194eff6b222b13bc0dfd163d3b127a3dc0826db120b93d8e9b04e94c57dde5fa2595b33d7409f30299ce3934084f157737f844ff539ce3e3079726558ea53d44363cf5aa3e585da318b02a2439b722e20925616b2b6f1f73f7cdbe0d0fb6b65121c3986e44426fed68ca45c1e22d9bb3034801534e35ca59d2a5b6e8c783e72a57893ffc0a862865fb2b60ed9c8724e11691c1deabf7fc130e721936c106696c2abe88cedf4d13fd14246b32750d431cea157de56be266d15c5ba6c58831e8a0582713f2f2c35fde4bcd9ada58913ba29b8b03ae0b7e7d11408b9b34af4d53123cd9d5d680efc4b9d427e4ca853f25d0eedb85cc351f38afb45563b39a2ce942f3ad0decb76c7778d30e837e17fd46bc725e4dbe1ca85ae39060b61012a9fd29af4626f81398a6243fdbb6b9f78fe15ebd16c4818dbc6396bba3179a41772eb35fec37cf495a1fd3398f1e9003247d0422f5ed464e61834470818279f43a8408af97d03cdb71614ee11437f1e70e6c9c840ac71998473d8f8e7227068c2014751eef890b155356c895867b88a21e120c20ba998513e9dbf20d174c162fdf70d3b09f16e983f6ccef743df80d7176d7c4478053582ca4be9b5db61887cff417c3f07f5c24f6ceab2de92697314e753f4865015087deaf92e03fbff2affc704f867abb435d96bac3bc5ee03c4970f5f22abb511a47f3aa7c3d521357ac64c17135c053151df7da71485f767bba987461c1a03c2a8c157d221749a745ccb805eef8ffdd0d46bf18e29aea854f0405b07489aad97b9164457ef3658b0d9424f4fcc0923fbd2a6acef031826fd4dd3d9c26b1793d553efb6b869019c28cd957c06aaa18bcbf2bb50ac30c579df08f91ab88de51a1b6f78bc662f46d105d12979c516f5b208834fd1d5191a734a6921fa256cebce6adeeadfb6cdeded0746f0f8f5b7881cee914a1ae5fd3cb590d1d6dee683454a9e2fea7bc50b2fb24b6772f5ddd6d3d3d8dd2a8922accb157f2003002af09a9ad4caf911b18a241d7d0ebd2df40117d037c79d01af8acc207788afd108777b4a1c90076291a4983c0f729efefcf08fd955079f96b77d467395237030d840fa6b07a8eb0f3b8d40df8bf141645e081c4a05020b27197cb81e610a5cb3f44abf645ab7e845b67d670cf49bb5e6c40bedbcc6ec9f2877e1578f8faff5b1083e13f59e765bd3ed047c14b1a1c3646d4afc18a6c5dfaa4c00b1367dffd3fcd5bf2819a5cd39dc70d3fd3512d2041b6814ff73627b7e6d4ad661fe31eba6386f975a04c647ed189982a6b0bc600ae14393c5361a7e890136a57b4991f4d3304e93df6e268442c8cdcfe294bc6d439c506485fc3acd943d5bf20ab7b17148a20b2a3fc4580deadb1cf4809e8e718617e92e1e7edecddccf3329cd79e785ee79185fef000c7d056a89b1fbe2054b69e13fd9c180c114ec75cb100c2bfff8095681abdbd1ff282a9620ba15e5eb150bf82f2467dde023e819d2cea276d690fe48982869988fc8aee88e0403e61a88397aa0b1a5a0629e01f07e272d6065d63d73f516fb45d2adced2a828d198e4ef00b3dc9a50fbdbae4401c973f5da61079f0ba6594c05ec66a382e4060b6a6c8a27cc75aeabbb86804ed3369be5baa2375be7f1e973823c2614c51e787f1932cf74ffd6174946fed71115cf04ef6947dfbe7f3c3bd90bc45ae256cbd956a99c079c834ae3ca8e8b01216eeaadb74d8012b5b0ac14af7d57801f2ed40f5db893caf95fa15c70dd58e9ee9b482755626fc51d206034daae6cc57c2cc7803f38c5adb0ee00640d7815309c8794fe5d5a83dbe23a4d28707fbfa5a7d388b174a2952cc0d1527c988d54754c1100e0ffffff1f02000000000000000100000000000000a1abe14523fb0c35 \ No newline at end of file diff --git a/pallets/gear/src/test_data/fibonacci.hex b/pallets/gear/src/test_data/fibonacci.hex new file mode 100644 index 00000000000..d32c32e266a --- /dev/null +++ b/pallets/gear/src/test_data/fibonacci.hex @@ -0,0 +1 @@ +04000000000000004cb1c9e8912ec64dc2d5dc9231c9b82558cfcb16a3a72f6c6192f7755582e2c4a700f87a9bfc6355c64ad8409d6230fc53c5a98534fa0088b443c8decbb24299299d17a2286f7afdb84b0efad9eb8d0f0cfc63e16348005949c86dd76d9e207384f5917cf1274e3b8ad1063fbd8b92a754f3b04b8a5fac3aa2d5079b15f5c6691ee3ef6fba56fc652bc73473b5ebd7de12fb5cd6544fbfc98f2e6c29158cb065ad7eb9446626dcd64994d91b0c9fb23532ecf2b6476ec8eeaffbffb32b52b1a61cd11f74acde781b9387ba47ccef334c225d7a47cea8fbed9376744a375b4b106bca5fb84d04b89e4142c7ce629250333750442fd592925b73cf5d82fafffafdbd033ba2ac030afb6405c07f0682300a20fa8474def09698ec93f159dc12409c84e67a9179d526093d3eacfc5d54d70ef6133fda51a6eb1e4b1cb49964dae3fdd16382c9c05969c378fc16195e949bd972ab7e3f9cda20ffef2c1ae4ad3bc4963e2f504603aed03129ef20af031d800e4c78b0d140ad60d818b3538699ffb53f4a144710f7c0ebb1373e8a13eee5687733e59fe61fca90c2ab2ee26fe8ef284ed10b1681e8b7387f63f4d4ee777a23b1d4442af12427d881e3a8291aa22c6db7154a2de631f21cd588fb9c61ed3da3acb5ec9a19c9fb22100b9b528263dd389ff0b6e0e027b8be0290aa79f472fc2294c895cc6625e1d5b58c9efcd147c3ba79d34fe74ca0f67009bb612323370a6ac448ece63c53cb8e1d135517a9ebbe27b78700000000000000500000000000000002000000000000006400000000000000020000000000000008000000000000000100030000000000000004000000000000001c00000000000000100000000104000000000000000500000000000000030000000000000004000000000000001c000000000000001000000001040000000000000005000000000000000000000000000000030000000000000000040000000000000000000000000000000000000000000000000000000000000001000000000000000200000000000000000000000000000003000000000000000300000000000000040000000000000008000000000000007b00000000000000040000000000000003000000000000005000000000000000010000000000000000d98577b6185cc64dbcd2910fee39419addbd89852326319dcfd9d95a8b88fc21c6b08f959b83dbd8447a10887e02c16d74a1b73b330098201a2498a6841556abfa99e44d25cc2d1fab04ab1a85975d9fd9e29ceac06eda30dd5bbdc01ea003e72d7c5d9991a7dd39267c2ddee5d7e0eae8c0f558b9ca74499c6e486536b7a39ec456a4125d21e201f3b4494befb24da613507a9f80fcd70d2137684d117e1879a5af810f2865635a302815d7881f34e4191d3a2ed9423ca02c20e61065c2b28d1d3e4370692d9453d0c881bf78435a586222967d4eef46ba8841981e0eb8b4cbf031b2977e32c2c543543cab7dc1f74f799b20830acdbea4eed88a16d1ab1619ce17302c48da3e8e469d4ed2d917440c4444106fd973ab68c1c62ba7f61599c35c35ba00d23b473acafc704a006a1ac44839c7b5e8d29ca02d9a3841beaeb3ceb165f4efee076531c6fcd080b946205ce7c5fa1115e2889b5dd010f92341529e008ac16b34ebfdcce9c3c2fb0a6ede30699629e261622c539e6c5dbe8521ee802897033f55cf414cf74bc14ed67752c2479c01c33787abb43df09cd9e5a5aaf883d7c1a7c5dcc78cd8d5fce0e087fecc8a24177a049c7b2c6c08c583a91ca30da7fe83bfccd92b63b5f32cd4593633b70384b2bf2b1bea928525cbf5c34e6e637b4732aca6393bc54319d660121d90362b0fd1603297734ac67282d1fc173c54f4f68ab239910b50761598242ba07a88e4e33fa38b52f20a659d7c5f983068f58befcf257ad590a91d71d31c3f917439781fd2714e314874400c1c0e0cf762969e7b8c52f4c22586e0a3f2762168df3e93f12555598d2920e19c222a14a4204e14783b3912e798db7752abeee51090535da20a55c43735090000000000000000000000000000000000000000000000000000000000000004000000000000000300000002000000000000000c0000000000000014000000000000000b000000ab0a265b63e9cd7b781e32146c49175a3510d3b1e8cb33b31dfed4545855a1cd732c589e4d20dcbafeb60a77b696c24f1f90de5938fecf1021e9cbdfeaf0fc9bceec2b75d7546144064e87ef77296f0eed011eff71ca578d3257a711f11c358954ce1955f54c7a43e90e2b48b5e040a7316632003737bb7ae8648caa36b1ab10f0e9ebd2e99867b794cab7521636b849880465d42e4e51cd1ae5c8447655f6d5ed98bdf85415bdf725325b6fa083d45699046b0c40c2e5d7fe07b153486145f7b7cf6f4be88c00e0e1da4c28354979f1e8f490b580f1ef77c3add0cca96dade5afbfb780e3a018b2e4f311a482ce512a7208fbb09f707490bc4485b32793e1405f454139fac1d9f2a5c1440510f8a81cf12ad1c2f7dcd4b6f7cf3a7d3a56e7f6e856268ab40a8c586937fcb1f5c07d772094fc66563f49c17664d8d39dd8eea6adc3816f07f063670a654d5a77f47e119bfb4cdeafd62c6c9f6bbc152a90b5e101959c7c3387ce0699c0172e6708dabd0f5b621a7ef4446901279f20bdca5472c3827f01a86be60d1b498d0f4cb5c2872c2c4f2a84e062234ced339b9754cbc51a1a0246d72196ad254c7e1b4759e34442747cebc0e2e2b92328668f7eb3a45bf85acf01f7c22764d645acf33933f9a1df65dfe839ab6ff1d7714dffab274e15e1bb1861e2541eeffc87f51b206d2837176cb9b84e71fb60748863909b718c1cd7f625a95e054675eefae2f7a32fb78d4d351709f00f0e8322e657ae5f060371e33ecafa19862962c3bd12ca6eacabf8c01dab9568c6568dc7f56ceb9b3910e015057dfd065201b4926729cdf3f4165593afcd76ae4b1cedbff3e38ed5b3ee97f3a9c227f2d1a62676ccae3eed766ff3ac3e03c50eac03bb409a1ad0acf6ffe1a65a62828f00af33f0ebc2ecc732c040f27c40f985093efa6cfb7cf08d33ec2324174e53743d35cc95bbee7870ce75e4148ed5bb5f336998b9694bf9ae81f629683cbbf9d3bba70e6225054ef85161ceb61638f10c41c822f379b7fa79e75927f95aca068a8af1821568709489892e496504b44c5cd24dd7b668d1ed0a6078951786e1ff75cb4fb5c08a5b55d6f378c1ff5a56db9b560aa4859625795b68aa86539986c87121fe3eb64d428026f1282781ada990b9add14c5d333f186bf6547a4a6dff26d490af67b9acfc14bf10ac16e37454a46c1cf8e64ced20f601f0a363668b921348f73f170f44b714c90ce409fd965880c2d22f84e0199fb8a251cbb5e3aceca4cc0962bb1ff8efe1712d9e08c4b8b0e1cbbe39333b5788b8e40cea5a22e10a29aa8d32ca2df46f2900f10137ecdd053a6304f3ddcdb2b84d25277ff690eaddcfbec8e7702c5ecc3e40291878428addf4168fdc4c899fbaaeac6f64b48f73b8fb89a4907e96347ec45e3879e49c1e95692499d3007f1e31fb270490c85c094bf4202d95562665b88aaf2e95fab0103060289846862fc447239a25a65c678161d89b95c87a039102fbc5f8061ca5230b9f10863a30e5875b3ce846a3a8223ad22fd4f724f1e2c647397f7ec43ea5a54255ed877ffb2bd4580c79ba87dea47f9b53cedd65ba8dd2614da42cd67da945d7158f9cd6dd29dd854f1c309e72f5b5825076ba77c81492174173fc53c675ee16c3d8a3d15562f6f8020f1eadea0376cb0a309e860f9d125d5d3c499e4a26f02b34e076759c7086eed4c1a756bc1ba5535fc3e55670f0e8df7402f93769019748a57d4dc3cc7d036729b1963d178fd693758222f58bf01b65298d0e611a98998f3e4094d1f18d9ab553a8ee6fb0761372ebdca93683379a7e6515b6663e0f75666de0d504bf7d3339633f41e6d314ff8ef2c518978db42990be4e2385528f67e5b177f823b1b5910ef6af4916a9465ec87517221fba39fa9c8f1f221a2cfaf305576dc1156d821b4e680cdf0a4c768af9bf209d0f0d06c065516483de393df3839d1e160c27250c4fd57f0046893d86bc142213d9e4f5fdabb3861b5ca2c4acc882909007d117f7660a5e6bd096c81af4bab73f36a0bb51d488e6e4748dc565f957cb1316b14aa2b311d85e9cdad173032063c7349a9f272468375ceed7115187c391cb6ef9b64399b392cf4f72d43723f8dce49149d2d20fe2568e944bca4322251a8b0caf900412e707cc515c630a92486bcc157295d14658cd4dbb0b43403126a861d74676daf28be0cd9fe6819e36112a94f1369d40d30963bd83583bb215ce4f7ba9977e907511c8266f4e224030f709c1a7ee579b20967fa4ebb6d0a56c9929f3517f4aac2f912cf6da1de3ad333f170d15711c4593af5164c9e2d38c9d419a84c5a7946a511710f8ded7e0da1ff7959dde6d7320948a703925c4147dc1987aadbfd51fe9c090580d6b4a77d4768b612344d30d8205afb6d9a6072b7fcfc7ef8daa5bbcb2566e81c39ee91698fbf7dab2c2f167ecbcef26d79b457cadec83985cc4b76453ec8c7d72d66394ca15fb9eccb5b079de49386b1ec13640250a0b9549d8cb562b19f54acd21b98ca22fd121256e474992b4975c2081ae248063e7d0a045349c4a3c9e619ba7393e4320ef8391d5b68232c09b73b8a5787328b0e0da8c329de4ddc0160e831798313f78ef66d4631680ff240f5f13d737ecd3c2e5e7635593ff9fb6d941d8ade7195d97abb18cf4316034997a0dfc44085d7c06efc267426c71e85930383836f0a05766ca990db859f5fba49a4cfac5c2e1898a8fb2f26c1d9aebfebfee9e60d1fa0cd86377e403f8860b893e7f2fc01a7845ea2da6ab58ef65a0776085fb9ed708814f87cfc137e671a12c4f654e5d0d3742de3bf44378f1bdbc91188d49858584cf10dccee7f48911634915589509af454eed69a71a5214fa7534e03492464ed0cbbbd4fae625277591bea1dd682e8e816aecbc0cef21854096ecdd201953692e18061bbf161ddc58c7b1bc150d206a3f1b6dda3bab06b2bd00abb7a36f48b7ff47f325ab49dcf0c74133ad5edc90cc71215eef750347123d668868cdbc6a7234ef21073a14dbd5322525389c37135307df1a4ea617aea93cd30c3277c1e81555e7468befd9b0c1025a0ecc115501b8ea67d8ff0b1c2ec53165bb449ab425dd228e821d35ef378bf04662d259767c2b3e11b2f159aa0049e55ecea0780dd4c64407d4cc8a2a2cf2df92324bd136342ce93520d72cf744e0be40026125252aa2d659324994c02ec7f5775596f27ce754a60622cb89d4b91a1ddad4ac0f77ac17d57594c53c7c730993e63f2e59b170d34172bad3f779c970d48c7ba969e0036bf31a1a09a647a1b2ad69ff56fbf6bbf29beaf3654e783560a1c53fc1501761a9770fadba02727b39e5e4e80a18cad01920f173f22c5ef51010a288313556d6099069ca5b9a05fccb236860573bd3c3c817f3c45f654d6d6dd7923cb72041fa5881e4607fa2bf54a8e425f27c27f483365f4f1a5f7fe74c0a490925baef6750fc8450be90ebe84ea253b507803a8936debaf632074144df8026a28a77d7e33f145487ad56511c1e3b07604f077734e11aa060e696494987e0591c327c818c32ceaddaa4193453cf7674349bfd3d3dab79c8ff70b2c4abe2256e06d3f8bfe3e5b7efefedb7d5b1b244b3abc708ba515b62b1537efac6dc081385501da247358375ae45465a4eb6036a572356a332024bbc36a8f09b258a1718f2f38d2a043ccd40ac7051a46702c95e90a257b29fd864159bc7edf25f0650b9b94735e69831106b6f1d5bb7fc5599c6a922ec3bde0a5416989f57c0a2f2198b85e38720a98ae85d9c23e77b8f36fccad8e37961dc56ccad6e2b69117b43334680824dd129094e1f2971833627d988e85b4493247d449d6217985fd61bcf591654709553ea657645428152505b97230c937eb42f97d83a148f196ffd0edfca699edc02d92c961c84468cd3fd5b898c4075a2bb0c862d48582aa87ddb1f7f65215ac19272fb91ae107263be3886fbcba53e40bd33f5199d94d63bda6b7e89ce72e28c0a7a425648f8efe207cd6da65d70ced965f550780460f2ef86d12ec5a6a2b105a1f16eb00f90f2233bbe92a3ecfc1d3ffe550d03db593ead669c2a84023acd42284ba283d71d909024fdb0b7871bf5f2d451283ead89a1ae55217d3ba65c37a3bd596215a0c76e45cf7e09cdf03f76df31cdba455fc3f780b033e8574dbc2f2ab31ac1679b704b4aaf761818933145b3ee776feed77fc53d05e7f458137d5d5d92b1f657f636fef04cd0824dd9b523bad63d085ed0fe12901da50f0c6178370483e16b80d4093a4cc2c53579eb196795e1b817eacbf8b19fb6973f4c676d3c2f2861a6eeda15b6edc8335389f4b4b3a9ab789676b267b8dc27bffdd7d6208ac22db9f23469e310608151951921446e77a1843b203784bfe26d35baf12032e8cf7dc133a0106e7fd300b5d221c4110a04282311310dd36dc752d643e2862a184895a61ea9e9216b8e1d35b5a65294ad659eabc7a65d24b7a4f455c06b1f20518447b0196a727c0cf7c6b2a9b5ac8e6410658d381906060cc6cc9c1a66e56917d80846b0de069b7e2c84d967254eabee5f785800ee1606f337eda53d0f8b2872af9b1c040a6e71e92611082c1f0af97241eb13c5f9f74e71357b191b01bfc0a8dc761ed6834bc3bf02e73c572108e531f7bc044b93105c89751c47d8a9d2c35c50cdcf9ef20702652d4e0c47331cee100d8582e3486f99d0256c6c237f60e5057735462d7109ffcbc312111b16741a8fbda4d758fd66c3d24bf1ba6361f72aff6f48db9b4925152e25cd0a02e54fa890aac2987ea7a0cb15ae0e1c5a337bb814639f532378b8dbf2c8a83832c48a5adb0d63e3a1b048c0db54b9473c9b6773469ff920f249f4be28d8ead46b0eaea1bd0d94e230bc7a5ae7d23dc745551d3b25ac8fddbded74dbaf59b09a687d3b4de9e9284e982fcdd2bf7b844ac3b2a6fdd12905946e1d20ff25f9a65a1b1083776d2d965775446bbcca2db114fda1f079b22d2fbab9db65ba5d45d853aa1d5856fc821910bd8719a19e17c0b4b7453fda49f3e34e0b959a9d3c0baf8e7e5d39a7aa9834a6619d62765bfc27bb0b6e589fbeccdaa25c3ca1c9bdb1905032494dedab333980115b59a4d480f3bfb6e653b34f49a482d9d0e1ec526b56028ee1f48cd3f26eb0396ca708aeb286cfe675efad015407a04fcb61aecfafb6bf7bc59593a003f18482f701c7e38aae891db1f1d8ffa9630f3a32e75a07802db70f96abdf97466a098957460aeb618fdb423bf157f2f08bd9d14a4912dd748f0af0ba8d91e59131bb9247f048fb78193da602d3ba70d7405b7d56ddb4effe38e6caa7fe338e5c62970b9652d0a74519d4ee69dfd838371fc8c39028c4e963792ad86050d5ab05bb92e0ff6046f41ed9ed36ec98af14e7105c90bcfdd0e189228b021c3a7724e84904dc911a64050d22d6a88123e8163fdeba9b7a5c8d26bb3ffcb59940c4fcdd5499d8fe54ef4cebc7de15a0b6a247944e93f42f16b26fb0a9896de2d69ad46fb4afb3f2244e20e2fb8e2a653ad7f2a57b99feda87726ece59ff21bb0e74e4f5bdac587356f28ecc63d22c37785158c21b32827dfc7cc85365fc111992f42e8598183530dcee2d56bf33dacd4d64ed87d80e81277d6c25e124cec37ca5c80007159ad548b8e88219f2bb4d32a913078cd5a139738c55acf4ee834aa6c128f2bf664948413328a2f5537bc86e986ff3349262c13633f1867a693d43189319983ae090dde362bdca68d7d34dfaf633ae7f7645c98abbbf3d906fac9857c9426a40b0c0e9cfab2b03cdba315f6fdef52eaa76432e9850c49eb42adf9cbd2574966fc334381c5c47960073860637ca38c26effb1b0db967a21c0d2c45a011b31d06669528c42d3015cb2e4737182a9510a74cf32e9ad3ab873ec0f8c752d2529b2f43ecde6025fe98ef85c36da61a7ebe8caa0fc3d28eeff02b9c337957275f4f29e47e5c4af2c5da844f63122804de4b12a536a3e100b9c8dd350481fc07c423c0b37b510b3aea3ac5f82d6122c9d43286f5f397c0f9e564c39441624cfe8eeafe725a0099d1e3bcf3aa724748e8b58329a0e5d30d65d1d7c76dc81bfc430ca6e5bacf6fe0848c625a6962f77c19c898f57838fc0ac92815e59c5ad06276de1f42061f03954f2854a791d8799f4bbc49834e962eaa64245f69fb70bc28b2fa740ad5379327cacbbf5bd1f61c37a6a928b7266f2a7c19888e7401373d5a7c084b1a249449fd0d5f5a0ff2ac69b3c3048e22bfca933d0cc36e6cb97f66766001e54bc051b8e9d37647f2c07b4c5e8ce5dcd39fe409b17dbd347ee24b30736ef75f953f53bb6a62257a908d6ab61041627bb3aa986f7a94464ef9cc8514f6228bf641df7ea1e34361fe2840a5749ad61ab042971b5d8afeefc227cf7ba997aac6f347a6a506747d065ab5945a2b70b80338ac0acfb4c99e5f38a5d04653738693ee958eaee64327f11261f6e4c35c363199322ed8fb5dde1f6b2e3efcfb6d2086cae5d33d765b3a1e8353e913a0541bf31be554382a6dc900d1fa9567639d020016c6a6fec5b6dd4b096d2ad7fa5d40ce2951fed672af520bc3754f45bba1433f5fd8723240426991a8a52617345a90a4417abebc437d299491c08b270b4f4aadbd03d6320ba75db4cb9f6989b672ee128a3e89edb7435bf41885b69f9f45a659f3ab28f15b28fbe8ff2dd9dc72855160ad423e65d59bc8440d7f9773e04fd44e4a6fb41fb06f79a7d91eb3583b8108462beb664687967945a1964cbc7642db81ec184a8a91c5b866288a5e6ed44139030c7cf4289830f904633e2dd22ba1d3599980bf9397d2960c81d8322cd99701a02c2d9ac13b42cf3a5bdfdc790f1a3adc02d2f5dd9a985ba94385bbcaa34f3f6b0962b02f0f57f92e1ac12a806eae2a9a0432fd973702859fd30a80f85b9201ff67263045f613847be5ca3fca27d6fb5fa653814832046968eb217901ded8f6d7493282f8b6cfa034f3bf41b7bbaa011bc38191caeea8f105575479838ee5ada155a50fe90450632d678eb93adf453dd20dd396d943c21311d809b21999b8c89f27d9eb3dd62fbfc30e763cc7776b6f29f84723a487a9f3f085a93e70091b78a3aed4256262dd7fe9b32c884c06ce6a8a9d76ba05406afac7659360fff845eb29414c271d2c9c0bc9fc9042e2f6d44dfeab154a339e4725cdf6905a8f4024d0f8b108c0b9e7fd12ce201ea59c66ffdf77d95ecde75afd0e0dcc8b2b1f96ed01fd1ffb87342f820420c2c6cbb955f9c7ea14f0d20736cc721735b8a03dca4ab850dce30677eb44e546bd67839df5ff6dae6458420f6883f46503904ef0462cdb15d1962e9a495acf175e3b10a87ad6f462e249179d4556d3e437aad20755d469f2d1735ef612b4b08f67e3799c11210c501c4d5e2010c06bcd0ce1758582c5d3d27640d95bf48ac1f8559543e3e5fed97937ae10731986668fc083e362253d3337174010b802d55c5c1c451f7ee4f06577c578d7ab0b752f3d0792c0522187d0f8a6246f49603fe8049faca8452597e52c4b46b24d53facac72a9a125c0f5dba4ac0cf58f818a886ae99226a6f38dccbe6b38e0b987efdcabc8bb8d481fa26729c8e06142539af49751c5ff2d993f92c1c16d509197fad50c10faf686cd6f20333c7af137b34735d8ef339d2b902be1e4c80acc8b854b9aeb408772260f9d688396d79870ca878b0999f15327c6edca311c8ca0468639c15d828ffd874442a507fa7dd8880df94c0316000000000000000000000000000000004704fd2ab58053fd01e77221281a172a9b4229c1683cf3e1586288bd3f9b15148f8ad742b9162f0dbc0651e390d3b2dfa61ccafcd69de6814adf2216f3511cf9bd0e25496fd9c47b53834af529936e6d9af39c7344b01addf1e2c7ee279648193f1f5f76eeeb9526d90d41315b83236600000000000000000000000000000000c51b9663c29cebc9b4b53b71bfaed0743fb4d116490f43c627467ea2f1818e09383c6e0c45aad4197302bfa121f5164582e1f481fcdb5af2f247605a557ad53ddcfd0fb3c15184bb77ede84f0a3684727f6236928739e6b407130c8c783b61f627d09ca1d92bc3bef4c95ac8c298b6d4f4de21df20a3b346f399621dbb1da23fdce2e75663b7e27274ea284775f100c5811fff1b401ed8401c4f50389c04d988da894f2fa0df2fc2f96c9db420c53d303345e17a97db0cbe4d7a2708ee94c7ddd94a4ed4fad8dc2d8a402bbc7fc6dde7e692f277ba5dc92b1ed85a8085a7effacaf3991479bcc802543cd0443ec100b92ed83bcb24ba53bc4d67543dae66a4391a013e0454b55f76712bc02fc0dadcab9d67582368638da5364429afd849bdde79d9893e484cfc40bab7745046c7a90f1104179789bcbf40941bc92b5782e8a9d3137dc87c54d8ed647b953c0ae66d963d6f3d31f9a6c562ae0df80b8317ce1a4522fe3f360b8365193531763c01be959e14d71d1c28a8585075cefe40892271f0a98f5834ec9fda47eb396a8abdbc56beac6586c9e315af4a9e633a3214524c88bfc8b015974b15c79f340ef99664afd920a5082e2990a6aa71982456baf23eb90cfc13ca01dca0872e3f7a37fec78e2c55b76371adf9fad7908afe87870e217997c4a05771a38587074d75603eb59b8d021a87acfa007f842f188d4084c1fb6cf73f5df3d59cf4a1c15baeda7a796469cd12797a54abc9af8e88101753a0d4c048fe0df8cf44f0156c79311992a6f711d1b9c35879b6eec307c4ab964e7966b5ac606420a784794574a53ddefbd3ebf87cef5a85de0d3071b20e072712a177f435af42eb61c26fa6ac9bc013b4bc556b5f07c1146ec94f37f9ff0570791020e5b02f73f2939affe1e016654ff3320a634d94313c40afbbf9eacaff9f3842bd02be46cd6f70d887bf217a4863b15ba3d43ee0a0f0353a85dcc4f6cbb5f2658d348371836026c91c17c4102b15dc3fb9672314be504bfaf8839ba0a818e373f02898411523112e3b2783c3c59737f7244526bd1ed76d3afa47f8100a9c2cd027dd4f310099f3dbea52ba47adcb9e51788493c845c535594f75b7bce754a5690d92b51193a452d8b746389f6772bb1c74746c3c30673e49945794707c838d4fef90df182252253dd825caffe466d03dd679d821940ed9202ef4b67c026fe77bb7d2d818db7b35ebddecca3ee905d3498294daf422bd8bac41e0e58cebbb92660bea822e65388d34b09eb467675ced0a1960ffe71296ef2bebc0a1231a602633627d26a2385e3b682d5af314b511effc5209b9f3d41ea4a07c2838b9a842664842ce6201a7ae0271b5e85b370a81f5ac2de3521a7ef30321a7d79ce33c7a59a6acb32bc145a641e85450145feb229fc1eea6750f20a4151656cc85bd7c4d7094b96113bc0c4d8af7b814bef4f71fe58e1f18f18c94d5b9a9e4e3a518922c41d920abaaf54c5c0f41ddbf6557c9411ac27c7c1722ccefb70b7c97416bb403e75f2a03225c4de636138b98b4507365e64f5ced0115429576f7cbc823a26747ccb2e05f68c585a525d25391c1665e5815e26fddc2f4530e9c5482ba9a0a6146f5e03e1cd5134e45a5dd898f9c0c2adcf9af92e95f67a9facb7f79150f39a842f71327476a360de690a2f27109e67643129f946a3cd1b18f7ce3e50a1ffe4ec4c6fc889592ecabea8c7423e611c1c80bc5d46e348accb7babd3531a673a66fc39e80084ef9e185e1a7868f31b3e5ae7912641fcc21b38dab8b76914f96841303899db42a131d030ff8e38ec21cabc3b616e9370e525913b8060a86ba878391b11bef06aab56d3e29f7c17eae7e0d3a1c2cf4d60250cfa278adf650df924bd9967d383230da228fc0c0739f150d8d0f9b25c4af3de8fc54fa245dfa6cdabbf31e7aca74b7d456face454e14bf6b4a3589df4f8834320f7a8cbaefa5e6082d85f2093f6784352df7736cdb3f05ca9d8252ab26c14ecbde6f2f38c3408483501e568cbb84954531c10b43f05aa49fe1714a662cab8234a09398e8e251d9cfd65b012699d85c054e00ddd542a36506b5662f73e39f765e9d2236b88dc6477e37c02b109c846e79bf997c46b9349b0b78b12828701f00be8248e2168f6367d0f755dcf5c81ec4c93372d3d995025961dc4643f497842991441e458c4358fed35c1fc0b72fac5ca2521f473d2d8b8ed7a68dab88d05230460844d1ace7424d7f47420bb093fc2563c62ebb4297c0625c68be6edbcf496514602f0520e3f43ed3353ec2681185c4d1e0edb000a857ee60f133d4aedc7de3c3e5f305a4ad2b8531ed9f89183fd9357386d1b5b7f20bb4b32832bcc67e41e97ca95f0a95afc514930fe2e6269734aa3e71326997b6cfe774a534ab2aeb7000a1c0b0a970fb797118189557cf36c4f098b99b35934d91c5489336557ac30901aec3285648cccf0974839b07cda03c1695a836077438540abce771a6a609613af0fd094336f4c4111e3bcfdc73c74364cce52f51ed3a0a2bd4d22347f0252dc1184888840bb68f1a02df0106a4787cd8cb7b60af9fff40aad025bd5a3d57b212ee83d7a9bb61514b4483574ec52b45e00754c229dd4b24ffdebcb21d948d6628ac22444c330f40dace65ce5e7e6ec599b6a3239f9144745a55c149a0932c0d7b1a1ed1b24b7240bbfa55c9ede7b8b984201c51eaa5ffb5ae965bbb20339e486f71cab9e86e3d7a30f02a55cb785b48e5f6f0ed50f9e9d785a940ab0f11ee1fc8166be7f7b3242254b7b793368ce849d3e0da9e350a8c667f715fb273fc3f741133aee8485fbd0c166abbc514d882d4faeea0aa2bdd4a399a55137d10ca1cd092f7e4704a838f4efdc1029365a11aacd890f76cdb5741f9ee75040ad601ecb0736b762905a301f84f611707d771dfea4db1f5123cc6ccc09da15f759620cfa772f785f4c7409fd7b476b8d73637ca5be2af8b8c8a7e1ed292313696ff7e0455d97626c22fe5eb30fa938966b201b67096da3f98b02497ca56389cf617a053255e0b260000000000000000aef21a5c64c5ee9c2aca0183e000966d9ca8726df642235f59a467df68845c3eb57a646b2139a083662e50465793616ccc3efb46ef0d66a4000000000000000002cc1dcd4f1f9482bb422ca8795c4739daa22c4a957e5e80ca5d0895604a26f8100ccc509f60b3fad47164ec992308dfc0de1c7b125ae5755840cd8231a2a3fd18b2c648f5bac18e2f5152da216cfc40ab67f22b5b41fce56f2f7c541e701d324c1ee912ba4c623e6710bb2203be135ebfc6f20eddf0c0bf47ddc0c9358743c73079ee21691a70f154d9f82b1f844ffc189fa2c90db31554435f836a5a2e92f01c9fd114051633f0edf0b6764af645528bed652e87866a900534f5038149d0bfedaa395e8e9a574a85a449e3e26c4084526e6eba87edb88fa98bb61192a224009c20b46a4776f60a50c76b9f9d73deaa60d2ef5b5c538c6e76fe3d8574e58b830b249e53db24b70c16dc838b2c9f54423464427028eaa65acdbea22f4ee5e21a7d625c1732b6ee32738ab9f7fa684c7491f16f448d06266b5ca34f8ebb1f888b43c1cbac2ea2bccc65baed7836d001f2d893f4192a6f622242a6b44739fbcf50c9c5c0c41cf7b62cf7fadc2036ca1bdf464a70bb9ce909ed6a27c3aa0f0efe389d2dffb1b97996b7063f284b9d73ce7a4c1c54e64c77608aeb89cfd2029b6ff59d4d629766ca554febd2dffb26c78fef038c671b1ea8cb3cfdb857f96d30159119afe83d9da5f5486e6868c36427367ccac3f1fe8623da9e73c5c657823fd3ab8ead1f045dfe96c34ab6f82cd4e22be820ff38a392119948cb1c853d2b4e14840124d4f83f17c31e02cf38ae1df4ee5a05b9d4806a6ca897aa1f17c2d5cbd1f31174c8b0c778bc35f81f64ca27b4994ec9c9982de19cd8d18288a9aefa872804f93ac4cc6acf80ccf62f99804f2b612fb6badb1e7bbb182f52547174fa13622a9f3fc1c66264d571050d772117000ece2cfcd577a150a5d9a83d747c23a31c62649db8198e642a889ae9cd9eb20888acb067e6d1f970e14de272e8ee8b389e0701857e00a4a1ecdade9a575a6a4db7a5643b107adbb2972687179d72e06fa0913197ab6b8f10d5e8646065bd90037e6e853c9170a51bb843a2564ebe3d3477e34902d2bfea2c331d0973cb6eef5978c5c08c9365f6ef1f6624c5396c21f688391ef80899cdcd78f5c19e045c49135ef137804d1c97e6fe8d1bcd12cdba0b0327e763612b97a3828a4a963fe474e35e14ea72f6cfdb74ec0e1576f81496afc1c516783465856e07ee7dbb91eea282cd99c03c55c783c37ba93086ffa77c96d04f87c296c8e49fa7bfccc29312b945005438451bb3a432e8b4762c859705b82bf73df08b5b1c2fe81c166bfcd996c40d109ca70e5016fb2d1ccab7fd86e8e5812cd3accbabdaf85e43397f935747e397b94b61d0ed7fb5f97a629340a2211b3dfe07dcc0e3579a57b8e834d0f57cb36be1d6961c8940c0729919e7b0b40861877343e9107f4a84bc12cd8394fc649615141fae43290930bfe6b7edbf6ab7850453893a58a044e7496503241242268f4a33b89c2c835be9348a17949c404930e89f15a85b7eaeb66fb33337b478418438fbed9f8f309155cbd92f4f1f4e26118b8ec54bbbe294852933440b478aa0efd21623996b38ebd6e47c1dcb7f2b456a2b427576da911b65224e934d31e3ca20b2f35fbe2b16dff4d31bfcd99720955a1f2720b09b4c41b7f54634234cdd992e118a9df6f41858fdd29be74997bac8e33dab9443498c804da08e59f519c50396807ced55baf1f747ad0e801d0808aed02e892ac9f312d15fef87f538fc2c85ccb19055eb2807ee000d64c1f2690b176cefc87195e38cbc29582ebbfb61f16bebde6ed602fe0684973fd0dec8c148aa48e67f68ae728ad86984c6e39f13496cece4be974abc5d61249031cabd0faeb1cff32d8542b086ff37effbb51c42a99f54580578aac11e9a4a1821f1e7fba700a6fd61cbcfe0f2b9ea869bc1d4d8b765d6d765ed2c0c10527f288d73876e0c934e2df7df4821784f43fbca0cb747bb424a242218aaf2c4692192ab32749fe69dfa784b81079538fba81e759a94435ebaeeec7c88c918239e2c0aaa1b885120894e3eb2e0ee531248cd298ae732a21c7665878e42a28cdb6772fd877433f1e2a1d195bcba4806ea699edbeff61a614c46db235b0b1d929124ccf97b2c78018fe60744d1925e08a3389673c683b4562a2718846b03cfc1c4b35b82aba052336080cd2698380a38a19a51a7ad663e28e131e5ed1f9b23da33d987f09f5e04d51890fc8c112a0c94f44f57df36e2df46083cc961ba0c1583d8476aec73a2fe0c7a8a01df74c7c20405bfb04410c046b145816d3401388bce934e5cfa5e0f07139770845f884ff5a332b85de1e5a1f92530126dee4728b7d370d1995eb83b474e9b201b3e30dc146b5c24505aca2b3ed231738e4f2a7329b6a73d2e97b2ba910a9edcc0aebc9b1692bb9709e4c5532e002fd1877ce29bad8ee4d79a978d269e371e2a736752d81481ef4e512774f1961dbd0490a474d29ec67db0ba70f088b57f8900b5691dd4ac74f2a3c49be37053ab26ce2cc251a92f1d52bfbcd2b1f1a11538c243c302ee8a2252a02115736284ec4d3ff350da5fd7278b3b3e31b4620cdbe2ba693e0878c96313091adfa87245531e6bf5ed1d8b513c2336f4e345abba97d75bde05ec4de5787afd19a2cd71b4ae6f0f5faf578162431abf12b3c74b760251e5af57bbd1ad459ce3f9c410b102b9ae690ceaece6ef69bc2c1cf58fe159cdd57555d31529d3428ca0e42bcbb2cdf55b53a0dc42a6b836cd7fe5342ee884638b465829186bb6ceb0670ecf40c0cbc2bc21254fe7f68eb25d45e074faa9a7a981291f15cf74c74c501760dd0802de5b4011bb996b891e990f02c2abe062419738b2b94f355f89e01807b2c157834a9cbc247a6f108afbdf26a33cd18efcddba3118582b29c27c8c3b0fe4ec8c0c4a4e3989c7d217775f20da231cb70bcbe314fc4c348dd92d631f83750904aa9107f68b8faad27c3e8b80ff0a69de93780113029d409af59d6cb0a4029de979f2277772fac30c6b22cd80f4b6c3ce06fafdb41abe53b83325d76815571d0121192cff464fb22c87362a8bf50d13df7c96b40f2078cec0fcd1993857fb17a6f7d1d87f211464c276867e069c25930afd45df36efc1f493fec6b922a12d3c4e4ef39b350d60370f0c6dc67e8200000000000000003683521d5becd1ce7ae5c32fdae39a7111a57b7f1feb6c90915d17e6cdd1026398e0222230afa1f52d5c365dfbe6d8f4150eb11c031c231c000000000000000002e75cc60303af20a5736723444b70d472e608a14eb8dcff4343085c38cef41a4b6bcf5418dc088d22c299d098e3fee745cb81c72b6cb98e24d6d53b2b815abb80553f7453486ea2d0db23569f1bc90524aede20c2eafdba30ca4bd9fcb645f2fd85b9f46a2ad9cb4412214f18b9f2d471c53bfaa1e2cefbed2d3a61ffe35dac96afee5d52f10b2fbbecd00153e7a4ed800107875ab7e0ed9f7fe9d9faa549128f57ae94c1e08c188c2ea982ea614461aaeb5c4c15738ccff66e776142b1f86593420995ac128270292f161fc847e25bbb772ff143de419d5050d75aa5f091afe0439d2860e98f64115fd70df839db497829a3976af6a6460842bdac15f9ba20e090f0c5e222fb8f1a56232ba03984926df3ee24725e7a9e04a1b367a4bc36bd4df5fef993fab3b68709ca0221abe3402b0b01aab552fc64c1b0002f04ba8e881763edcee51bc2f109e130643bf04d0856105bce68d9ac14bdf34fe2d1e5943f8d12cb12fb1492cadb1b984956bbecebd4c48e89fdcb48c1aa721cfdef076c45629d1c4d78a3d142866cf0d69654b1fc53e51fc431a64af6418579c66f56f2389c32f63abc7b8aebc29997e7d6ea026b1ba23a959b8705ec7f339d1efde981cc459d2e70ffd29fbbe743026a98513d12e13f5b07a2c1057140f809f5091668e21e56277f588bf74fe7926bfe5b8ff3d5e4202910ea37141c46360d30e93e3759c6261c0041e444975cc3851e4f44dcfa8d08e3816bf8b8008ecb0025f69e413f4bb6e61363de543087dafd537a0836a9c962ef9a0b65033a6f190488f128812dee9b2be2f31d747f03278d46a2cfc4df0a5d12f0b514df50bd52d5a5b3016d6ec2d172152b11aa86ec8782e7ed9244ec79909a665f6e804ac292c4b04d6f8d7c2980305608513b957cd26d64166720d4aeb4097f8548994436bc8fb1bfb21fe2e1a846bc6af631d10d27c8fa46ffe342443bb22cb18a47644172d51eea5700c0fa552b2183316a8898cbf85983562903ee43643e4eda36b6097da25dc6bdb242e302bf3552b1ccf0f53748bf80615a0d05ad9dbfc1f73245f5c63776e1452c4d304564f6051899f89994cc24eb56587cfc667966c24120661f33de8c34707c978e5c4661de7676b7d05345306d0a951239f812b17c3863637ce3819a7d4de940c02ebd36a400e6856b2041613231e6161bed0d4a43b535d97b4095d87da04dc93b407c01dea774dd8362d60308d183a35e17179fe3ce58f8af7cfd6cdb16e34cc16568e2b4d832174ec235c4232ef27c1c9b1205d8cdcba9ee286eda167f11513e282b247da6e17585cfd47b84ec9bf0c29c3b84c8db0e70ad041c5a45828bdccd1ca7fe8099bdb5ba6547f6027838087cecb2cf55148db646a8a72b2bc711c1a4e6bb5ad30edcd560dd5ff2c79af3f9041b8f194d88071fde9f49ffa746223e6bacc4ad76e2207c613efc41f4ab4280077e24cf3d70729c9a7f453b35746a648a8aea3217c77cab3ac0a1e44563adff42392b22c07ecb0cf0c28b4539b69b1feaba48eef4778eb54e900b35e4df5448d0c6195d47fed308dd34b873fb2476a46b4e0271b71306d9cb3221da2b7a8108c7ae21e634d685859279a92b1ff9d0eb5680c1ad9582401c5f11e17235194b62d3593d8f49be1bb03c235788f7032d2fa8da763feedd95b4098bad27d285c85ef7a2f8df73fdb1d984b4ac09243e2e6f76bb931bc88e3d267caac917ae839ffd9fb851f8b4d192f824ec3e74f655da651802d499ef82dccea682e400eb5be29c3a213629b89fdfbb6554f04ddc2fd51b835a6e6856caf08f6f2fb675d6fa533b4de06a65d8969cc248aa617e61ab5c79f3c8a3da99d0ba5da9e6175f82a57d7a301e328702f8a228b4f29ca36c33c6dff41006cc766ff3b433d9c59ddf2af73a3eb39e09f61fee5de53b104534bd42088a8b89364daa882d3cf4f66a99b2fab8649a17cf95618026bee46240f475516ad723b4654808edec18d9921659d4ad4c54e362c5970aa0157b36355833682af47f4ab343153dbd9b694296c2fe72175b9c1d5e32a832b0ff7c73980279f83c94e5fdc485be5c6e3bd79f3b08dafe767198d64fce18d446fd70f60f88505e0acbd4109d25e47cf968df4079cf4335d9313b22f5d2f8a36ed6c7b960d7cfe6f55549f6744a00f4851995aa0869a6a929c510b27f673a2001022616647f6d2aec7c186769f591370bf1dd1aec0b4c90085696a2e56af4868d66bd7f2e68477c347b333b1540d19bc29b406892114389f2d66471851f2e41c73ba87fba6bd1d7c5896d7fd76f91de85ce1cd77b0798126f580f335a82bcfa0e5ee2daeb20de5349a1c86ac06f55453ca72b9a1272a0df795d870dccbc494991a11b3cbdef9d69cf17697fad3a5d9a4b23cef517ff69e0b64cae4c4db43afd43d60efcb8c62899f059710bc8683ddf81f9ed8c0e0b52302194e13dd1e655ad72c1f76cdaae70ad0d4efa4e5de15e97df5c6664f4f778beeca8f7633b62c25cedd51205d79a869421496b4cf0d6f2a450005d40d98b46e5554646def1457d0387602747525cc2a9d3a090675e00313f33a666d80788188c4441b999589a12fdcc58a94ceed20b5354ec39a86b94e4f1ea6df41a93cefcd92c996a3d0a46afeb935e76410201ca454b1e7e922f3d1a1349a1726455b65ecddd4c15c6278c3a91f76df243ac7b0398b8eac910d2091127a5df9de649b4cc96112b49bd66e7a4cc8e223242a789974678421d66e30d4d10df1eac72bad385a606874e6657db2f7ebbf8487a793103130f322389560e081b909abc806107b783bd5ad8580ad0f2145bddb01e4607e9fdc9e9501373c85232a4db1d294a03d9a3f3efe1950e073b91ffdcbbb4352d79ea647c1eca599f7183db48170bc56aa756a63e981da4a0cfa8260f846f6ba56ae1fcb6cf212a7a0cf42de14f1bdd71e541f8c2053c554bee8c4153eaa9a402b50cb091f6fb71ac74c0bdf31ce6952590de7d54c6ced3298df51d3c3ad85fb946ae289c1918630b32163d2f6fd990e13d0b888f7cc084360c093f9bad9f75b18e8711d7b442f661be6ad7db99a43ff021143f5f7b8015d9d31d8138e250abd75674113c5bf981f1bfae8bb5117ff469bd59bd5ceafc156000000000000000002b861b49b45f02dea0c938d34bed030589135d38d741d804cbcffffabab67d47c20b8410971e523f6ff78781ae5ede8b0e9e31753b0b536e0000000000000000027f5a4941df914a63ff39ca3cf6f81193f6986750a9dd41b4189e0460fd238f32a2824789f080f4d2544f1d4826299b6bc7e6e978f91066e0ebef3f74efcb4dc00e72812e75aa180539fc18b84c6aed2c9d76f6b748b115072a59c3fe69dd937617ba354496ecba9a9d44e26b24b9ee7610f6462d2828b8ec204087b44d91dc7f31ee5525dfd5796b8c2a63e52ad6db6365cbd6254f9d411a04ed5478a01ee51995a39d5fc572a670d4423e6931e0479cfd08c96113be546c417d508e4b39cb93dadc1654a3c8e5bdf92d6391e76b366be6324ce5d0dba795ffe283a977cd68e3d80f33fa0b9f8e3952d6c8013d502514a59e2bfcfdb1dd89d09c23c043912381a3d04fe279e8bd7774841b80a8e985980157d4da463a20e5f074285d950023787f7869a89f1df73de6ca7bfe5e131015b1bb64183225defc55bd62f60152a32756289f7f559ac4f3ca5c1908018bb3bd3dd607243051ea57aac85716e54ae946e7843fb3bcf4f78bf80721f4c70968a5e034b4749f4c80bd6bf8847968749c6574084b24c120ed504ba5918cd43504af0a4aa6aa12b65a1f057d143bd131ebf8af02001d88f755073caaf8c3362e29e68d6910c0c2e2a2ad889a79953bac911e1d288e9c9de0fcde110388064b8e53f6ba54f9242a62cc08bf881e0e604342aef175c5c4f4f9a2397977c5cf8f977c95bb45faffa3f3aecee34c345322740e5b23281ab3f4939e7241ce9492965dc96bb84fb7f63a6845fa660813e9e3ffe2a9b7f62bfbda6ba28e0cef3035babe804d70eacf30fefd979a7055836b7f7b25d0c7c03f9f790c9be6b893f6a8d750894029fc85934c6f4fdd992c5a6e80f2efa7fc18480ba442f5bef678fd3c05583c8756f6ea478e4f0f5236a3984d0af5a3a1c3f97d728da2dd3c6ea6773885c8b4f1494171baf5c71ee3896ccdcc0d3144e001c7a4501205a10d4f3911f4a8ca9e95d4a3a2e644bdf3e628b53274d983c592749f001eab1d0dfaaabd57169ee80e260298593dbc14c4c895b60e26d60b744002d15d9276a75b364654d0e53c9aefa162f23e85694a3a0d6dcc41170ec42589e4b8de493463824c88c28a3ad617c79831e57da4832a0815ffcc2945c3a1e64603a4a49eb023576152ec0c269345d784ad075f9aa032912cba24f06ca2a93340a39505d311cf8c0f73d984ba4276e35df18441597750602bbea86564a2dab2165ba6ad80a4af2325b1a4c072e58b81a157ac17a48bfcecbc38d3027ad2035c3561836a885c31cb10600e87971c6410c05d9040ef2c466b315d73a9100abe2fa740f1875c727abda38dbea125ef07f2e361942402a0d7a51671797a18e2de0394e425f6fa74a42922d656cc82a831627a1bbf380e841fc1b842a64ca88f99b609305324cca6af536e0a93c1c433ed6c104f9b754c9f9cea276ae7b3211aaa49ec01e307f1864c36615ac7acccf6d666c7debadf69a5d172c3e8feecc2dd62666d165faffe2f0d73a84b5c746efd2bdaa89628ba2b367bfe2fb87df3bd918ed00aa103c06877ee64208db1148f7aaa1caff47d05cf0e2dd329c3ec6a130a854d0662d306f886fdac987503ecb52528d03b569d567345d16ce65f5ca0a5d7ab9125f4506bfe64d17b7c5509942f6b2d62bd271b5ee02304bc45b731aa6ab6e6fd4d98f066a8f1bfb9125528860af8c5c96712526f74756294620dd5a8377d8ba41fb9a2ed360648e80af45afd85b2f5591258cde9731fbd3362c475da1a35b74a625829472524ab833ec4f24006fc1455ad1da94effed82f485c34a5416d30af88eff6e79e62e7765c5d0844e592e936c52b8348524d120fbc880416f41722f13c8585b19281277332e22a9ab0ae01440296e9792a6ffd423e1c0140dcce2625675893ed528da8df307cc16d2d0e08ef416210c38f124c368c1edb907bd83922986026827772c5cb44ea4ed5e74dcf4567ddf0f5984a1863877330bb397819dfd535ad447f22c7fcccd78936c1e2911d7457c4d9d40f8cb10d2fdc778957699da05fc784f1f85c4d6bd094b1f5012282f41419662cd45a5eb512a4464851f7d9e05c5b65474311cf1ef72c42d28ba1625dee72558caff659c21e4dca28b0a446095c7c0359962e898478353237cec74785ab5a2dc6683e3104caae2f2579bf94eea090f9a1afd9b476b522f519c9bb4bef77a9fca3a0f76ed7d6c009b11ecf408fed507d34f11c9bac5230eb561fbf500544547aac41bd6f2d217d02e855e25162b5d27b729b7f014bcffbd06614dfbd7c2a40a91054f7f614a9d6f816876e8db4ffc9cd096c58e8f2532bfc777d91d8d0cf1728ce42ae9ba13dfc9cb729f5e10dcd461396fc4f684a9fceb96ab3c7b81a3fc9b7aff96f6d7227289ee3c9ed0c42a91d7a38baa0a07c00a5df3164df143c9e3161b8757b5dc4187d67f8ec18b51bb4f221832b0811d813e17c170d8c259328a06593a1ce7cde5cc56686e7c228bbd48b9510db800871c75888d2c48482ad1d2dd781e473f6d2622325391f5f960397957c5e40efd555a22def17e4d4ac8dc8236eb757a81e31e0585336547de48fe0176b004eeb26da76052e902eab4b0c7d993b91aea60789a7dabfafb2e635540bf21bacbaa92c0b8300a93bad2c9f6b3390233973dde5a44cc02f721c5643de8cdfa625304a5db0f4a71bdd5b3050ae659bfab73339040677df616596fd8ec2bc5177065c5cf42945df1c6abb413045acae95f43df5c5a18a4dd52223027839c204d33e7eb7d1bb14099ff2fad7e369e5c53c99438243002a2ea3c9fb087ff330d2450dcd9963ea9de8021cb9c50acff2ef2fd5997454d9537332f066a14bb9c1a7aa4ecace17da95f5adb610ef1ae41478fab9bda802a958ae4c215b0e34cd29f7c4cb6751c3603b5080e0224c8c8d5fe2a0bb60a568938d8766d5328b8550bc8893001f3907f1002d35b150df772231107edb89b2c1b2158d7024366cc5ecdcc7c1854c722635b08492c341c3e92f3831af5f64f63cace9a1de148700a45f826762d249e8fb116c42ede6a22a0f2c532c444439e114f74a3cd0ec5c89b9b6b059ab4e2cd9e07746abb21840071f48026849436ebbb9155a6a3ffb95dfb3f4b360df70289ff24be25929d04b3ea533e82870f00000000000000000f34906d2adc55d9d380671101bc59037bc4b7934aaf56e5241a01c5bda31dbfd26ea3aa0de68f574d422944ac6395e0f6d41a34c50343ff0000000000000000028ee1f212e16ed60a5899f00d568ae08f7b2661850ed8be7db3aa961828bd1ae843509ac1f928cf6ee4f2d589e86681fb356de6d51294e3002a282fe6e9920027ed73a4c7c87a6f3cff4cb32e422544a94395a3032614f7ae06604b96914831c09b6cb869f5665c5b5c496b38404fc05f39fb4450b1f22dbee7e2b82ece2a73137958a24c2e3a94742649a9f20ea14fb9712b8109921cdb8353c5922f6670ca4a33ec8765368e504055e76ced7ddbccff3b8a5ead0d3d0b88eda8f8efba9b6738401321d2af14ee28d6cd8fc6aa6e0ee14910952e68c999670e07729f6e266335697510653a068aa9e870960fffb9ef478521e7556141b2642ed4c107dfeb862428941a4369fada7547d3ebb5375e9becde272144124dc05015a3b01ec3d497884c5aa845c18bb7a664ac716548ee0a2e120927425400ab7df8d63e2548935f4e89630802eda0b000b2f4bab7443b26f51d7472089ec3f697ae3003d2070640dbd9ac1a953175d1817fd4f650bd931df4ef7f33bfdbb1df3fd4b148d054390627e772251a4326018ec2336a89aab1f442585e16c30850686cefa4f7a16a6ff87bbcf1319300e35db0650a0b540ce09965d24c959feca765d484c61e73067822144fa8ed835bf89f10888613bc0052e459556bb5302f5131c8b64f4099a07424fb98fbafdc0970f50654648bf10d625b1bfe78d26b03a9303200b564e434264fc2a6b4d2c279174a71ed9600d83882d3565a392a986c5df82c3c6b97e13cd465d0bc2e957e3ab7ca894d8bdda5804d215eef7968d98ee87a7937b110d035700424c94905d0d546c099a116698c5357ad1a990baa47f8d17550058915063c0d49ae6cfc5b1d4cbbb92ddd763e6af0512e84b384a42f06fbf0d12652413918d5383bfa40b838a750c5af017fccfe0e71faf8fe2af8da8f643c351b3af79e84094b02d28ca71d2bfd5306ebe893b8789abdaea9119368cfc3e43e7706d786334e653886b4fb9a424bf2635a8ff0395a508a7b7f847824f9a8b3503880a3f6a3672c0e02ce5228934739e3a2eb10f4287f8745ebe1d25b0d8b7843c8804bf29be89b11f313f4a285c86e262111a12a73c19337f03dea101927082156926fa4226aa85d405b04edd586555d9a3198d0607f22e6d12dcc85af283c64b5f1a3c3d0fe28ba5a8e6867df4b7b8617205cc031acac2203b2ad7203d95766291b7a3cba8c099afef9a9ef8ae5b4201325a0b07498e7d3528cf1119ab37318f065091ac73d9a6f532cc831b695665939e5cf3ef41160759ae51c53d270aae6f42871b283a2fc3af4679f2204df54b8fad6a74023a8c96a8e5991f8b435e24dbee749a10bbcaf7d014a6600ccc8eda51898966e7f0843b119c2debc58249106dccd60366768c4d7896c9c40b01648f843a20c03e0c913be9b614169605ac4f9d30bd235121b7e0722f425f60d4663bdba513c1b72aa99c0c3809e9d3115f3f27c1a7f8ed42475e2709745a9c12979b6ccc10d7b5c52b731dab72002a087b68676d3ab7aaed897ba71df3a19e5d166446b7ba4c5c5300eea623c80ee4c8c8582daf7e041269afa595d1438962acd9d1398a38589ae978f82f08bf4307ad64833df1990d3ec8b87714b95ac5c6d4f3d95391e19b575cd13755f8462f5a9f632e32c8e5c709539b88989a7a2d0e82cb5bdb4814ea9d0f973de91ed04605f68ee9f624363b3b310151faafc8983ce5f8c6ccde7c00d1d040c51eabbdb40a17301dff12c2b82d3ab22a9beecda56151773b305815bcd64e110c42ef32ffa25a25010ed2b19566b34acafdfdbfc66d42105892aeda8762766434e84bfed6bdc5b02e13fc597d6790c4b17d1509895cf3a0e0b44859062bb63147d2a9787a3ebf01fb7791d5f23a005f0fec8f1f6800d16751ab6b54528821b92353fc5071e67f93d32f79fdf1c0e91b80950f107a41aa421e179a3aa2a496bce561f448fbafe03515e903be86ee32d824be8ab1e2426d397f1e9b87c89d898bc2050637fd41472134abe530461020f7d809c86657d12d44e1c394cb927b1ecdd41925eab1e2f1e6ef126fb5e6436ae900b1ab683617475e764c90a3c60aba95785654a2954e92c0468657869c3f60ac4742be252447fb896dab883a668a26d1c81b1d9c0d51caaad239bb92e9b47146eb292be94490f0cae1652e0af0937f17a0bf10a2dbafd8f2c45640f13730bde7b46b6eda39b9ea5db4c9babbfda1cec25dc1385c712f45b68fa88ba3bb291e0f1e4e19c4c714279a0e7b1adc385c7f563af9c2f1323885d155d770021a7471c54ae48737d38dae5f475626d852bfeb5181860c655efe36ddf152da39bf2ebb1a1493514f7d99c2e62fc20e749d35ee5fcf20a6f1834f2a9d216c82828387d6364cc78d2db4c9d4bc024cfbffe79ffbf3e0ad3ca9f25f97e782075e31f682cdea481a37e38ade7a3d1548f6b9da57e52fcdb6bfcfe91b5c4c8f8b6ecef089a9fd65cf67c8ef50bea778c82eacf8156983c7f4a74f383c795c02c476b2fd59ef2515bbf591c7995ea4ad8bcaf5e2453a1bad9f379546ef9e44543de5a032ec0eaf2d7a57e9278e531db26ef9435f2364af3bb6545cda771c8731b09559491da7d896fa8fe93a928ac6df262365acf47f95ce466c9c8c72901e2a55d02a38da0e1fcef331ff3aa49897a85bd7f97bf4cdd4422625cc2b763467a8e6b29e60bbc32089fe75f022803f7cee030fcd3fc2fe9b97e970a799de2d39bf0611c4aabe81cb263754b96b18cc3c642ab8bd2982a9d30306a08bcc8eb287b550c3bb36f7112714a24be24b139cbc1de671700d5c3a3fc6846eaa4ab4f213d15ccea96708d29dc5e0c35917d5eda338b2c69174dc6a610264ef4e4b21e58ffa56bcd31350a97fe70f9d741bdaefbf07139e6503d19df3dbadc1de6725060b7a89f4d5252dd0dadf5d6d998cad5698e3db9a10560c7281ca2a40429a77221e9fb9c4a0208fa5b3f9998354d2e22963252ed51940ce3ce9818a39c6cf5c16dc0fb45a4d14e10b0efbfb41317158539166cda9a27f5a703d27a80445777463cb8c1ba8b2ebe0e4235889f2b8784abae47baefa3b100a4ecb41efee0e99fd683965bedb9e763ab0794856c430e94ba16f888baef7e38d113e1e37bcb12000000000000000036deaa6d0af417a8d2f9c596663a473d3b9e3bb9038766ba50d61528cf8ea5760488326ce1ed92e5cda735ca827188ea809bcb66a97ca3cc0000000000000000021729d88b8f616c284e0dfb09ab523fd77c76f04d8972251cab79cf8c5dc1d63a7667f7d76410f610b66453afe55b011e207bbc626b51dde89965265b5e5223cfaff0fe2005677b0a53c1978fe588142f2a8f0ab2451a12a9cb9cad6463050773c8498867ce1780c3b9acc60a85bd82bfcb59708ce5804cf7a587bdd61683b85e421f1ccc0cfacfe3d6403acf18bca7a1457e9080fe7f2f359aa53357ee95061afb04e2085262cd303fb63f221432db9de8de85cf09d69018fe1904c4859889cb9b7b3242fb7f8ffda3f0650dc431c7d1f38c494a53952f43076980f23119d5ec9698eb7d79da3ca4f08a3c134534b2543bd494bea7782539d01598777046992eca75b99883a856ff40dc9f0d17825f2c96dd7c10969d364b442ffe0c5faebc68d2e449398527c956d993fafb4e394642e075fbfacdfeb7d2174c00fcf1e2de9f103fc2361ffaa09c323095504b48ce98f25db26b3c52784c5b4a40a4eb3ed01a59b5ff779f760d44be94f7eb39f06f914080e91cb5c33595acf12cced058f19ce390dfcc514f9bc8867d1774f4195be6ae032687b99fcadf2486725d74017fbc76fb47b2e9b0c134c9ebf5a642a75ef2f4db0473a2cb0a0e46b59e3ba715ca13f119c397cacc46740077a8ad43b3afac004bf9bfc87212b4ded68cf02ec251dd967228bac406251d8a333c633f38129423343aad47b70e0641f6b990b0333640847a9a579a201df1e5ae31e96e8b4678b1411a131b2ba0728c6ae43a373507d5c6e37dd00c729f388bab05d924f71b8bdd75bc06fc1e9a9915a3bdd33d7fbdb11591277a6366ec9fb40a2d42d7bdc51bab2e746c3cd7cf519756651f1b509ba372a5bba4ae065ef23849bb932f9d55e04a6ad1f0aac518f04ae021504b2d202a247f882afe63facfc3e02f45635f8e76f2e808fb2d4dec582c65a9996e32c544a05babdc2e284258523d76df236264889cf1d7d0bbd701e6194461f1661de585b114a49674149637253c6da0cdc938d9f39cf1c3f32b3c045b4fc84ee4e0c91702a97aad06c04858f78b45aa08a66feae70e3eacd69c16adfc3c4d435c96e05d3c91b1f6cf3e99e25008e48784e43905482a4fa073a523433ca5503f92917f875851e3b7ec5c91491acd779150ed806404b76c48d1302e604048b67f8af6bea59337fcf16a23cfdb0bf382661019537304a86bfd2425cbb07153a7fee1d5314239d6f8e2e5c41027231bbe6d413d28fcced3acd6b5a1dc2dc4276cb272a31c9446f01d357a954a0c04ea1122dccd3f2876e1f849d5abfe68eb95210f0a63b79c2a09ad741844884ab0d774f0a5a1e2fb37a2efdc235ccc380d2eecff7fbbc22464b40319dd626f68513051ce972c0eb051c94a034bd346e8255fece92b6bb9cef9b9fd768d029ee3f6f097171f0b5c953c119c265c7203912451bb8a0c8bbb113397ccc9679ba21416f89ea1a38a1c6421f394b33a931c3414a73ab06f913079b9d25d3055cf326f7208255a07031948642d1dddacc96067c3999c9422359f075df55f3888e4d0b58fa7c1fda37ad8137e1118ba5cc47420243ae018ae350ef8c7bb308c6ce791366d877fce2afbf80c0e67610d81ff391156c48febf62307a860a6915c2c7215554649b90affec33fd3cfcd789771ee90b11c8e772ebc420ecfa0ce8191c2ad3a261d7ac04fc4a7f668858b95b72c8638b505d68f34e264fff284089972b93e33c17ac1c35f0b6c51ac15b03dadb0145f5a6091d5a20e28c198247bc4acf8b3fcb4454c50d9efba77975d857279f6ca0f98e82a9a62dc83fae5159eb2e4ea84c41dbdc1a9e5e2b66edafe837d465882de1dcc5187557093138a48d3cfa82f1673b84c3dd6a5ac415a4ae0e6003c0f43cf49227b3a0ac2ad0d6fe1211c5cdd5f0c1fd7724b4553af28f50d0aab5a60e60123a6be134299c01d39b25a33be6439c286fe146f3934355745dee74a28268316474c9afc50821136ba997b802ef7be66d818885af6f1d758e636e64fd29e7fe30782379ea24ff88356686ba58a17906b33eaf9abfee612ebd81881bdb339a66e71fef3e158e6544c03f4825d261262e8b4ebf33abacce590f3d58852e909a1fae8dc762bfc6381fe07c06b294de5a4e4f6f634b7f93f0999820b627e234ae17ac11cd34aef294a60ff47f75d8bb605a640c84f3c7fe927eaaccb3b9272e4a2ea55a1103d4e2e2a2a0a45d7600c4ebc9ac5fc19b4a667890fd0e404f031cda2e71c4c1b3c27380d520c5412c49f1f31a1e430b82646bf2a90f523a800c88e83f433d10586d0b8b7a6f464546bb795a5beec812a25ef234ce75bea700b0512514855d733afd6715e5f64da41cab2a2e8e181dbd20dd23d509cc371fc746291c3a41b0c5176951655bddca658fa384c33b65302c24c4669cd7d3fac268a16252871d284a18e060b244e768b593cda90735e1234bddd5aaf65495c1c21ac7acd124d0327dcfd38bd481db5d71f1ecb756037a9e93379eb952aa62a3ae13db02d017cc02769c6b2919581b4fe091d4a7bc643451b404f37d9b19d244dcb49aa83ec38612f1a12df8d21defe5877a3a4daae0ef321803807361f8ef253867c68eca728638c6426b15fd9ceca98c9ba27895a57fda2a0c9e7fc3d6f08c087136d6aeb72d1302b86fda22a748ec086454101542c781ce30c8b5ee7427d368347cd1dfae79c077dda1b7ecac7b7ad286f4a5e02b209adec8d65b14c8734ac4db694ebdb1025de001faf3b7cf30d4ace60c1905b7cbe1db309d578a0ea2189666b2595385ea2082d45a7527785291f213bf597b2711636a36f66ae57ee7675a731569fef5d52806c3c9577c38dcad32ba952c97d37f4e1fe57f9dc9ecb3bbf9bae8c8f33d43f3c611135c014fa033d6aa4477e1a16b6ef74e1deb60dcddde2f2eef189227925868610225b1b01695590bddc74aac44efa093394c3c2e0caea79c32aed0d7196dcb02481ee369676691e9ecd75f4ce6d46bdb2bae60a2d5fd140eb5609d5aa818c964d078c9a37fa4f03e844f247242ce58d599f5cf39cbff30f38fd287218131850bdd66121911a9f774d181e86d9186c00b0748f8b24c49513d26baf1b0dd878462089a046e36143d9716b7e2c53fd06618164d1a3b51abba180000000000000000a0fce602293717b6c0b9630517f5a3f916486039f0f6b777f82768b930730ae7b283e054d0dc2c8554d280e1b30cd76ad8282d52799e2993000000000000000002b11d9cd7b9ffb07b5e55c11e7c2290d0104d972bc357c037a1fd2de9d41071da57988e6d760f3289d12c0090b4e535a8ddaa0561c461f7908a5b049d4f74c749027b7840842b6895a52821f242e66c5390fca397b6c60c5f81580fc2d4c002a41ed2ec01e87b0a6f850957937eb8be8e01309083018a32d5f747b85587bf74f2358e24842013888f0d450d4d453f37d32dddb62e989e453e80a00f9c65f82dd0f90d3af539c5960d636974ef20245824eb134bf016513a6281c102a0f9f58ef1164c3680f78155d600a6098161bf14c58996eb893c2afe536f7b557a5fd577ccfc064eaf5383c7b71c5ab9976759058b9c9c89cfc931950ed4c00edcb8f86e71d499f4ba22420bc19dee03cb95c1124e11e0793038b76e95771e5bfdcec554f12d409ec9021da8cc16beda8afcd5011e2ed6c83f99d672e2e153634ead69e1101f8b50f1bc951ce8c305afc436dcf07dc2782a9b735b0ea8dc5f96063b5942c27f0367ffac9b6ff9a3afed7516934da78ad67bd73b33b5bd194aa926951e2d610e5940770ef06389af49642cffaf8e11c8b7648d2aad23aa15919c2b1b1916f819ba57338d02a08fb9402ea85f72e77c7951ed2d0c486995f333860b6e22b58a0c33609b97a6fcac43592cd26bf1e16e0b29a4a858c9fc00d06b3edf9d77ace84d92d38953efc03483c5bd5c6a5706809f79126ea3e6ebba40750a050af9bcef9e848fd3e724edbdf1aa6747d883557197c1a7c4b121681944a40669e9105303b638616e579cf648966500c52ec67cc6e5e7cdcccf35582f5deea89e99afbe271c5fff733735c6953b23951e7f9a8add2402aec15953d45ffd4cf87ab210db749bb4d4c6152de4658d56a44707ac49447491a4833c661e44e92c537a645944385558a993ac8b460636561ea63d37aa6d3e49c599284b8df87bff14277f37b594b30520c7953653a58a793bddfe5041cee79987d9a621f756ad6c12278d736a5f9e4ed97c761ebf399b7c2d1a610491ce22f141c894b6f8e98aa9b2b0e8074dc70290ddaa3dfd5ad50ea267eff1536d390445b1c39db8f45ef45c4a3d8f1317100a98e5e6761128ffc7341de0ccf8f113199885dbf814f9ffbfd9fe8038da7aeceb3de42e905d9ed9370c8b7bf40c0fbd928b1d8baef088ceb75af83e66e2f13c341c77ea8dda21777964ed3f1eca9bc58045634fd3449aad58d5ad425253247e004ef618c62e8c1345cc77e2acf113d52f79eeda0d377abd2f56af35621274fb600837d54fa532104745280b306649806415fabdd59fc68cd159195abe4dcd177b4041a98add0db223cd861a64aeef3b97fda87c00e6988e8f8ac1080252c4ddd958fa208fb799957efba60a880b366861289c63b42620d7ef47d4d62ea5b12c686c123c2b06c39641c8fa13cb612b795afacc1785288ef994d7124fdd2618ea68818413e36ac8bc63ff4732d27e7f8bb0fcd7293e9e0a14937f0172a774c6dd360eacf3e3fbb5e20f5342adf7196be9d12318be94a7d652e137d6ff4f184be5d909ff956be8445a6f283c97e0f57af258311bd7068d3824ea9e4a3b91a204697929eec2e56068afdf3b9ca1a114230be58c7e14c36d283e274e02c67128300fdc7b1c4807834e9535cd82c6b1babaaef9a3c04b3871bbd4ac46cfe466124da90205c66d9ca2c5fb571f870bafb030cad3040258e7e55873d55a74c48f250c06a6f42f4589f7c17033fbd100b14d93ca101b3db752a481b59fb1dc668edfe814ed485e21a0463464d998f916a476deda36cfeb5eed8bc0ed12e5a6d0f68c4071e642b750279d55b89479f9469c11dc233108a258c2dd4cf84ed9cf90263ba5ac2ab601c4650786f1cbc255a2b35eb05ba1f75b14c0646a211eb8f1e871579fd9d209a7046c41426bab4242b93c4130f9af1284237b5515d2a5db1e1734e8016bfaa802dc3e2a42748990cfc5c5f88ccd58d31eb0f5f77110aaad9686140cefe6dde94a45133b89801701216055da3b9e9db7b80bdd7f23f82786af32dca6d3ccf6cdfdc28e2d45854152c51e638ea858210da31caa88bd7c7b87ca715f2b512fd0b758c43b6684dda2a7d877e64e21655dd74874f11cb19b8c38910ce8c1762d2a7a9be11389db1e7507f9cadbba751a5a180c3b5add778b352d2eacef63957307b4d8967c5d013d282ab5b97cbe455ee619bc0e6faecf1889ac2949e2ed0867dc19f5c72879a691e8a8530608e7e18bca8a84058b8eb328b36f2ce0f5dc83dcf9583fbb5fea0ab5ca0f23f7c005d5d5211528b56baee1ac0e4f4c6c800300477fe081124edd050af6612501ed56321ad3d1ce502b2d8715ccdc7b93edd830f6c89ac0a057883c147ac64123b8369f23de3a08e2cda0c276c59633a71557d6e72e6bec31b0d7a1b21d2e6a07ee7c40a068c18cc2765d6664bd3e291be28adf9db733cba13c2115df621066c63203d4e16f221851a179fba8843998bb4a8c2f552d235986cdf13501b08bf0a586caa87b6c7a6996a58db98ff43942325c5b902d095a8cc9101911f606042dace2c0b003021732bc057e454b0fbc913f49325eb1c60dba946954ef990492670487e59de6b4cb4f951be099f543957900afc587b0e44bf91d414560256bf7a6fb65694a142c03ff25bb3e0a1dba0243701a65e52c409e1f217b1b984a41e18893b7b73feaccdd4797793caad479f114c85df4e21c9d1ec9c2dda5fcaf41aaa846171d60825662a9c3ac3295e1f1c3a4ffe7204fb61576c92098d4be51a55bbd9c97a53737bf0098725017b986f70a826d0f6a4f75f7e0dde10bf1603f27de7c4de551b9417c95a4baf08dd5bf098d0232fcf0d841e4cf0d2ca338e002387c7d64b8e2bbfba582b1ea5522b2204fde61e38f0012d8352e64e0593abb5bc28d089228c3b8f330a58e1de844a13801f442146562fb2fedea60d8317174466d8802e033b036736858db4dcba215a26b6302c9e9c596d6b55249776fa82aee8524f35ba5680da19ab1ab5afe26e837bf2b309dd44683bd7a17fecd346dfbe56e31f34f7ff8807414fa69169e64c4adf6e20b88632f1a8141d9f1e83fd138401250ff7758103fe2bbf039e9e1e0ca522aa8c417aee795b6d7050417f9bed029749669b73fd81f099758e20000000000000000ca2653a5ac759446235a073422ad61dc90f4d19e9f45271d6676da5d41870248c76e8dd3ade42261c8a399847579f61d954e1a64944b6fea00000000000000000250fd48f5cbe24dfdaaf8fe594c22ab18113b70fc59cc4d9c9d1e7a7f4bae66459c8936a49142abcff66a477d1f0e670c13d4989bb75fa29a4d28634d2d8cc17656eeec0a2a3b2f4ecb4e5b72b5bfb453955d030db23a015e11fb429dff23c3fc9cc4881ff40d8da522d19515b92ce0ada1e3aa131139d838e82870faf7b6a1f86311967a5a59473766c68ac2c392fa3fde08886f05ec358d12d932e5243efbaaa9368819513fdfddf9af277b31a69c2b8b9b866b25979751b87d153198801c0233862af714f70dbbb55e4c1a805a3ba00d20602aa6a434305aaaf911b8d9dea8dde71501962f029f9523938c2f9f5190a86e35d45745aed0096a91f662696769a179b71405ef91545cb35d2f7ea5058b722ebec5c08272e0b20902f12356914474a412591abcf0619d8387c3b64667fa604c1923960bef34e5dcc7e66157a3a515e7fa361ca6efb7fa7b3dc1563c0c24d31cbe5d3a7c098448e5e49703654f68472400265828b9b7ed5d42336ef16cdafdd61289916137143584f3e343dc5688e7c2c9b42a4b27d97d08cf5ca50b66c46b4a02938376124444f910c71f9e4f443e7adbd32a2edf468f7d83a59a742dac3ba6bfdd10eaba1d002167b89a449d8fa00bf347d6c4b2005ab3a3d98673d0498f2949c67bb87b7805b8caca1a9e06021a02694fa0192938fa1a3061b0267ed2df42b3f5ab606f492d8e9bd29f841a0ce1ab3f8ce4bf7c9850dcbb189c1cc47ca1493ee5fd60a8b886d4b5145a8e25913afc0a8eeb28cf3827f029897d21c8c34b3482b7e1182d412a0682b34c93cf6ce6dd747c3bf02ff3f614f963d1c5362b41ea0873af710db4a00dfd8d0506f5da3cb4cf6d9f57aef98311838ea52e81ee8c3e3f6dd70261659318629dc75c06b008e08a35791ee3760cd75dd0ad1c34a9433e1f3760e77aa4e5729ee310a8d501c7882957d1406f44683c2d56c7013ac4c93dd006df80423085ced34b8f2b7651e242bd7134d72a1f1a7295a07b52fbc824c475e0d0f6c74f6d81108d33d4bc9702a235ace6cd6436fe9799a71b978691e923a3b5dbfc2d895337bd121a02b0c2bd09678d2ee6fb9614600956d38bc7a3522f541f7a42bbf08f23dda70fe79fc98ebe814a90131e5f670c06128887a30ecbb8e3c7acda31c861bc3b85e99a8a9bdf8ff82e19c6f62472fa686467aecc51ef3f058e5b97d5cb83d40105f0e30ddead3833c6d12f2f5c3fe606af83645301c40d90a05e7e79864e094a2f031f30e214e7bc9497f3dfb78a2e042e50a60f21616ae32f8596037c01bc0fc793caadbd091009816debf3f84483d8964f552a40fe9313b69ca0531786ab1d14d30e2fc9c591fd5174f78bf42ce2de5dc342e51bc837642fb6e01aea1ecfc280ae491734c63f8be4e3e20c13b4e5f92676446590e7c810ad3b5953ba407e9c5eede854adea266bc0f2df90bd91b359d4b74f83c481e99f17e2f83824188fe50ee537533b64f44dab68dc4a26e7d79426eb8b374c305bd2d9e87f96db68463b69af060bfdefe2f7cff773b3f9b36c7308193c4caadc5c70b6435bee1f8cf5847865174a06e6d86d3293d9f0476f17b813a0e810ad28759dddf91cb87a03d9a88cc34cff2df6c04143573ebfeff55f25dc6629e773c3968a3923ecae4c06f63cc323533efb790bef3e713f238ed01c0bd6ac9685e9ab962caf432e27700db2950fb35bbcabcdd0a83bfea2891345498dc27e835e8f2e0b837dcb121ae9e3885905594144aabea9948efaf0d5afb05d594bd7b971e5d10307a265775dac234ef3bbb4d4d26f44b56c2c9c2f85f18e16b80cd2f68b24ec8a3804c557af20bb9741efec22f65b8910c7003edf21cc2578427e92569c30f1acaf193bbe5a3098b1f401eece72be0b71f34dceba4d89da175230533c85ed413ad97187ac7dde009a4c727c103c5ee9ba698080bc93491ac0ec209f58e365025b71c7c8e4d95262dbe1a9f9fd3dfd0c8cf285b4b9c7fa509e11c523b02c379bac272fe78aab306bce0a6cec2240a9056b884b441cd408dd848f17dec841e10af2d7899d4bb7f84fc922a4a6b28cb8d83a1610bcf7a52a751af7dab2b72b01452061474968e16b454bae851c9432f2b1d793126450f546b508445fa915cb91abe4f5f240e34f72ecf32b75f95645e88dc5614f6fbc87cb3ca54517c1c2fd25e1f7f480f1573bc8fdad7f37433427a6b7daa871c258925d9d5d5c676fbb7d117ab0800249aadf51e875a06027092247492946d311b967951df82c7777e152c4d5d18d93acfa2a7a52bc3487abc8eb7e48da8760e7aa81c40889228ac0d645890e60e6f0b828593861f9b18172d65fc70a1bc58d408e8c64531e15062f257242a5dbead282e0918927393dbfc70beb32720227ecdfdee0da7713381d391e191efde89f13465226fbeb9d9525baebf931f3a3012d215a0cf01eb44e04a06f4787878fb0210b7145c49973c049935ea7ac86fcf8463ed557b22dcae3786f229c721500b1370aa63d2c9d224a89a42c44860e1b39a38584cdd64e4e785742cc406dbeeceae6014536eea3b62b7772b75b7f4b9b8405ed3f8fe48035f748ca2c2b7c66b465e46f8227143722cb90353720a339fd0d398de4b2d69256f4dfb1598bb77f991bb30d6c68df73023991451b1214e12ce2fbcdad61abf9204a7ec9d5d47aaa780323080dffec869ec772ea781687a9af054ff4fe6f62adacc9c30edee7b74ddebb09a551718ccc08df6cf32a05a3dce620e7fe4b3633e1e42b18825333382c33d82833b97aee3f1a4b5614e0bdd0fcc360f7ba17a9e5eb659252a5d4e2cc3bc3194d1d9879e1ddadc9b06d79e552188ae659a657ac6ebad544c54beb4659ed7f0122a05539c597f6b184b8d54f23e9a07b76b51a53b9a6a55c1b1ff2ad64303a53844dab235878395226814dc047081aba4b759c7ed117907b864402d1fdb66f57a2a63117080dc6020aa1be2d8b8b5b3996def5a34ae8f7c8c1466acdc070f8708620190d52da1a4488d1b462ee3ea648ab44cc6da636e3b15b81bfb280450d6b8bef461a7d3497bd91c2cab1a4547b05ce97e11ec593b89b917ecadbe39f4cbf4ef78442e1834c68d7ae62f4385213d5c26911b5f164a3f57ab5bc9dc3d5b89900000000000000001be969ccab8868a85249319c91313fdbf1a0c71b79c9f38b1d4d0393af091e64861099f341a707c08cf06ac7568b96bc50d0e309fde1e10b000000000000000002ebb3d7be4be0ed254f2418d7cb1f459a539c84df402247a9eb964ca9a171bace79cc0cc665d6da0d71a73dc0efdbffd252473f83e1afcda6427390bb568c0844553f7453486ea2d0db23569f1bc90524aede20c2eafdba30ca4bd9fcb645f2fd85b9f46a2ad9cb4412214f18b9f2d471c53bfaa1e2cefbed2d3a61ffe35dac96afee5d52f10b2fbbecd00153e7a4ed800107875ab7e0ed9f7fe9d9faa549128f57ae94c1e08c188c2ea982ea614461aaeb5c4c15738ccff66e776142b1f86593420995ac128270292f161fc847e25bbb772ff143de419d5050d75aa5f091afe0439d2860e98f64115fd70df839db497829a3976af6a6460842bdac15f9ba20e090f0c5e222fb8f1a56232ba03984926df3ee24725e7a9e04a1b367a4bc36bd4df5fef993fab3b68709ca0221abe3402b0b01aab552fc64c1b0002f04ba8e881763edcee51bc2f109e130643bf04d0856105bce68d9ac14bdf34fe2d1e5943f8d12cb12fb1492cadb1b984956bbecebd4c48e89fdcb48c1aa721cfdef076c45629d1c4d78a3d142866cf0d69654b1fc53e51fc431a64af6418579c66f56f2389c32f63abc7b8aebc29997e7d6ea026b1ba23a959b8705ec7f339d1efde981cc459d2e70ffd29fbbe743026a98513d12e13f5b07a2c1057140f809f5091668e21e56277f588bf74fe7926bfe5b8ff3d5e4202910ea37141c46360d30e93e3759c6261c0041e444975cc3851e4f44dcfa8d08e3816bf8b8008ecb0025f69e413f4bb6e61363de543087dafd537a0836a9c962ef9a0b65033a6f190488f128812dee9b2be2f31d747f03278d46a2cfc4df0a5d12f0b514df50bd52d5a5b3016d6ec2d172152b11aa86ec8782e7ed9244ec79909a665f6e804ac292c4b04d6f8d7c2980305608513b957cd26d64166720d4aeb4097f8548994436bc8fb1bfb21fe2e1a846bc6af631d10d27c8fa46ffe342443bb22cb18a47644172d51eea5700c0fa552b2183316a8898cbf85983562903ee43643e4eda36b6097da25dc6bdb242e302bf3552b1ccf0f53748bf80615a0d05ad9dbfc1f73245f5c63776e1452c4d304564f6051899f89994cc24eb56587cfc667966c24120661f33de8c34707c978e5c4661de7676b7d05345306d0a951239f812b17c3863637ce3819a7d4de940c02ebd36a400e6856b2041613231e6161bed0d4a43b535d97b4095d87da04dc93b407c01dea774dd8362d60308d183a35e17179fe3ce58f8af7cfd6cdb16e34cc16568e2b4d832174ec235c4232ef27c1c9b1205d8cdcba9ee286eda167f11513e282b247da6e17585cfd47b84ec9bf0c29c3b84c8db0e70ad041c5a45828bdccd1ca7fe8099bdb5ba6547f6027838087cecb2cf55148db646a8a72b2bc711c1a4e6bb5ad30edcd560dd5ff2c79af3f9041b8f194d88071fde9f49ffa746223e6bacc4ad76e2207c613efc41f4ab4280077e24cf3d70729c9a7f453b35746a648a8aea3217c77cab3ac0a1e44563adff42392b22c07ecb0cf0c28b4539b69b1feaba48eef4778eb54e900b35e4df5448d0c6195d47fed308dd34b873fb2476a46b4e0271b71306d9cb3221da2b7a8108c7ae21e634d685859279a92b1ff9d0eb5680c1ad9582401c5f11e17235194b62d3593d8f49be1bb03c235788f7032d2fa8da763feedd95b4098bad27d285c85ef7a2f8df73fdb1d984b4ac09243e2e6f76bb931bc88e3d267caac917ae839ffd9fb851f8b4d192f824ec3e74f655da651802d499ef82dccea682e400eb5be29c3a213629b89fdfbb6554f04ddc2fd51b835a6e6856caf08f6f2fb675d6fa533b4de06a65d8969cc248aa617e61ab5c79f3c8a3da99d0ba5da9e6175f82a57d7a301e328702f8a228b4f29ca36c33c6dff41006cc766ff3b433d9c59ddf2af73a3eb39e09f61fee5de53b104534bd42088a8b89364daa882d3cf4f66a99b2fab8649a17cf95618026bee46240f475516ad723b4654808edec18d9921659d4ad4c54e362c5970aa0157b36355833682af47f4ab343153dbd9b694296c2fe72175b9c1d5e32a832b0ff7c73980279f83c94e5fdc485be5c6e3bd79f3b08dafe767198d64fce18d446fd70f60f88505e0acbd4109d25e47cf968df4079cf4335d9313b22f5d2f8a36ed6c7b960d7cfe6f55549f6744a00f4851995aa0869a6a929c510b27f673a2001022616647f6d2aec7c186769f591370bf1dd1aec0b4c90085696a2e56af4868d66bd7f2e68477c347b333b1540d19bc29b406892114389f2d66471851f2e41c73ba87fba6bd1d7c5896d7fd76f91de85ce1cd77b0798126f580f335a82bcfa0e5ee2daeb20de5349a1c86ac06f55453ca72b9a1272a0df795d870dccbc494991a11b3cbdef9d69cf17697fad3a5d9a4b23cef517ff69e0b64cae4c4db43afd43d60efcb8c62899f059710bc8683ddf81f9ed8c0e0b52302194e13dd1e655ad72c1f76cdaae70ad0d4efa4e5de15e97df5c6664f4f778beeca8f7633b62c25cedd51205d79a869421496b4cf0d6f2a450005d40d98b46e5554646def1457d0387602747525cc2a9d3a090675e00313f33a666d80788188c4441b999589a12fdcc58a94ceed20b5354ec39a86b94e4f1ea6df41a93cefcd92c996a3d0a46afeb935e76410201ca454b1e7e922f3d1a1349a1726455b65ecddd4c15c6278c3a91f76df243ac7b0398b8eac910d2091127a5df9de649b4cc96112b49bd66e7a4cc8e223242a789974678421d66e30d4d10df1eac72bad385a606874e6657db2f7ebbf8487a793103130f322389560e081b909abc806107b783bd5ad8580ad0f2145bddb01e4607e9fdc9e9501373c85232a4db1d294a03d9a3f3efe1950e073b91ffdcbbb4352d79ea647c1eca599f7183db48170bc56aa756a63e981da4a0cfa8260f846f6ba56ae1fcb6cf212a7a0cf42de14f1bdd71e541f8c2053c554bee8c4153eaa9a402b50cb091f6fb71ac74c0bdf31ce6952590de7d54c6ced3298df51d3c3ad85fb946ae289c1918630b32163d2f6fd990e13d0b888f7cc084360c093f9bad9f75b18e8711d7b442f661be6ad7db99a43ff021143f5f7b8015d9d31d8138e250abd75674113c5bf981f1bfae8bb5117ff469bd59bd5ceafc156000000000000000002b861b49b45f02dea0c938d34bed030589135d38d741d804cbcffffabab67d47c20b8410971e523f6ff78781ae5ede8b0e9e31753b0b536e0000000000000000027f5a4941df914a63ff39ca3cf6f81193f6986750a9dd41b4189e0460fd238f32a2824789f080f4d2544f1d4826299b6bc7e6e978f91066e0ebef3f74efcb4dc00e72812e75aa180539fc18b84c6aed2c9d76f6b748b115072a59c3fe69dd937617ba354496ecba9a9d44e26b24b9ee7610f6462d2828b8ec204087b44d91dc7f31ee5525dfd5796b8c2a63e52ad6db6365cbd6254f9d411a04ed5478a01ee51995a39d5fc572a670d4423e6931e0479cfd08c96113be546c417d508e4b39cb93dadc1654a3c8e5bdf92d6391e76b366be6324ce5d0dba795ffe283a977cd68e3d80f33fa0b9f8e3952d6c8013d502514a59e2bfcfdb1dd89d09c23c043912381a3d04fe279e8bd7774841b80a8e985980157d4da463a20e5f074285d950023787f7869a89f1df73de6ca7bfe5e131015b1bb64183225defc55bd62f60152a32756289f7f559ac4f3ca5c1908018bb3bd3dd607243051ea57aac85716e54ae946e7843fb3bcf4f78bf80721f4c70968a5e034b4749f4c80bd6bf8847968749c6574084b24c120ed504ba5918cd43504af0a4aa6aa12b65a1f057d143bd131ebf8af02001d88f755073caaf8c3362e29e68d6910c0c2e2a2ad889a79953bac911e1d288e9c9de0fcde110388064b8e53f6ba54f9242a62cc08bf881e0e604342aef175c5c4f4f9a2397977c5cf8f977c95bb45faffa3f3aecee34c345322740e5b23281ab3f4939e7241ce9492965dc96bb84fb7f63a6845fa660813e9e3ffe2a9b7f62bfbda6ba28e0cef3035babe804d70eacf30fefd979a7055836b7f7b25d0c7c03f9f790c9be6b893f6a8d750894029fc85934c6f4fdd992c5a6e80f2efa7fc18480ba442f5bef678fd3c05583c8756f6ea478e4f0f5236a3984d0af5a3a1c3f97d728da2dd3c6ea6773885c8b4f1494171baf5c71ee3896ccdcc0d3144e001c7a4501205a10d4f3911f4a8ca9e95d4a3a2e644bdf3e628b53274d983c592749f001eab1d0dfaaabd57169ee80e260298593dbc14c4c895b60e26d60b744002d15d9276a75b364654d0e53c9aefa162f23e85694a3a0d6dcc41170ec42589e4b8de493463824c88c28a3ad617c79831e57da4832a0815ffcc2945c3a1e64603a4a49eb023576152ec0c269345d784ad075f9aa032912cba24f06ca2a93340a39505d311cf8c0f73d984ba4276e35df18441597750602bbea86564a2dab2165ba6ad80a4af2325b1a4c072e58b81a157ac17a48bfcecbc38d3027ad2035c3561836a885c31cb10600e87971c6410c05d9040ef2c466b315d73a9100abe2fa740f1875c727abda38dbea125ef07f2e361942402a0d7a51671797a18e2de0394e425f6fa74a42922d656cc82a831627a1bbf380e841fc1b842a64ca88f99b609305324cca6af536e0a93c1c433ed6c104f9b754c9f9cea276ae7b3211aaa49ec01e307f1864c36615ac7acccf6d666c7debadf69a5d172c3e8feecc2dd62666d165faffe2f0d73a84b5c746efd2bdaa89628ba2b367bfe2fb87df3bd918ed00aa103c06877ee64208db1148f7aaa1caff47d05cf0e2dd329c3ec6a130a854d0662d306f886fdac987503ecb52528d03b569d567345d16ce65f5ca0a5d7ab9125f4506bfe64d17b7c5509942f6b2d62bd271b5ee02304bc45b731aa6ab6e6fd4d98f066a8f1bfb9125528860af8c5c96712526f74756294620dd5a8377d8ba41fb9a2ed360648e80af45afd85b2f5591258cde9731fbd3362c475da1a35b74a625829472524ab833ec4f24006fc1455ad1da94effed82f485c34a5416d30af88eff6e79e62e7765c5d0844e592e936c52b8348524d120fbc880416f41722f13c8585b19281277332e22a9ab0ae01440296e9792a6ffd423e1c0140dcce2625675893ed528da8df307cc16d2d0e08ef416210c38f124c368c1edb907bd83922986026827772c5cb44ea4ed5e74dcf4567ddf0f5984a1863877330bb397819dfd535ad447f22c7fcccd78936c1e2911d7457c4d9d40f8cb10d2fdc778957699da05fc784f1f85c4d6bd094b1f5012282f41419662cd45a5eb512a4464851f7d9e05c5b65474311cf1ef72c42d28ba1625dee72558caff659c21e4dca28b0a446095c7c0359962e898478353237cec74785ab5a2dc6683e3104caae2f2579bf94eea090f9a1afd9b476b522f519c9bb4bef77a9fca3a0f76ed7d6c009b11ecf408fed507d34f11c9bac5230eb561fbf500544547aac41bd6f2d217d02e855e25162b5d27b729b7f014bcffbd06614dfbd7c2a40a91054f7f614a9d6f816876e8db4ffc9cd096c58e8f2532bfc777d91d8d0cf1728ce42ae9ba13dfc9cb729f5e10dcd461396fc4f684a9fceb96ab3c7b81a3fc9b7aff96f6d7227289ee3c9ed0c42a91d7a38baa0a07c00a5df3164df143c9e3161b8757b5dc4187d67f8ec18b51bb4f221832b0811d813e17c170d8c259328a06593a1ce7cde5cc56686e7c228bbd48b9510db800871c75888d2c48482ad1d2dd781e473f6d2622325391f5f960397957c5e40efd555a22def17e4d4ac8dc8236eb757a81e31e0585336547de48fe0176b004eeb26da76052e902eab4b0c7d993b91aea60789a7dabfafb2e635540bf21bacbaa92c0b8300a93bad2c9f6b3390233973dde5a44cc02f721c5643de8cdfa625304a5db0f4a71bdd5b3050ae659bfab73339040677df616596fd8ec2bc5177065c5cf42945df1c6abb413045acae95f43df5c5a18a4dd52223027839c204d33e7eb7d1bb14099ff2fad7e369e5c53c99438243002a2ea3c9fb087ff330d2450dcd9963ea9de8021cb9c50acff2ef2fd5997454d9537332f066a14bb9c1a7aa4ecace17da95f5adb610ef1ae41478fab9bda802a958ae4c215b0e34cd29f7c4cb6751c3603b5080e0224c8c8d5fe2a0bb60a568938d8766d5328b8550bc8893001f3907f1002d35b150df772231107edb89b2c1b2158d7024366cc5ecdcc7c1854c722635b08492c341c3e92f3831af5f64f63cace9a1de148700a45f826762d249e8fb116c42ede6a22a0f2c532c444439e114f74a3cd0ec5c89b9b6b059ab4e2cd9e07746abb21840071f48026849436ebbb9155a6a3ffb95dfb3f4b360df70289ff24be25929d04b3ea533e82870f00000000000000000f34906d2adc55d9d380671101bc59037bc4b7934aaf56e5241a01c5bda31dbfd26ea3aa0de68f574d422944ac6395e0f6d41a34c50343ff0000000000000000028ee1f212e16ed60a5899f00d568ae08f7b2661850ed8be7db3aa961828bd1ae843509ac1f928cf6ee4f2d589e86681fb356de6d51294e3002a282fe6e992002728c0ef19f4d28de21dee2d6c58fad2db408ba54430fcecd98a740e6214d6505291ff778d7d1e041f6da055901c3213d8c99c1ca264c35570c694ca6c9f03b21161044e8f7bcfc80327088988dce16709d9298c54c95119ea5a89cea8993d70fe225f68a8a41cbbc6a7a0b0a60d670d1ab5be1a3d6390f827da4add20ca1a913b4226c7d9c57f4c65e7430e77b25aa399e22dc7521cdd111a15689261c9728e61ac2a99e35181abdbdc463e474feefb23f3aba995104ead926c8db6afe5c5bcd29c5d8c299c9078fbc803a5418df1dd40c06e1d33b60ab48f692fd5778bc8e82dac58eb42e51f4f3e7bb6c145480d2c32a397156c170fd7d1c337f25ecdda098a9a0078c556296e46898faa3df689218fb5a4d053d3754b47bd70755ff3a2db6349e78dbadc6ed78a14619449b07bd2a9af5b49fbeaec9d116c9cf40e0b55825b7a63f91f59323c35f2ac2be500cb271c5f6fbd3df82e7fd4c22e4f717db983c2521da8bc198c67324946bf2862fba0fbc566705e8e5bf96c2b823345dcbf2b95dfdfd8ed9294dafcc54b514291b7670a5757d0a2e1155f59dd7281da5bb99a6d1450bf11186d7d198cae6810e9313320a5377aab41d9aafc486c4727bb3954c7580a9c8336c53812e907c70c34bca3513d5cd68c7cae080d70521685d0cf1849fdd19e749859554beb24f991deba0c552d2535e85d9ae8151b87a120b6c379d43b25ca5bbfe8aef9be44b3aeff91088470ca7b6f71ba61069478d570fd0e38580b036ec26aed7588a2e5e326d6b0db74439e39f01ef84e5ff9d836f11601c07ab1ce09e5cc1d60ff8c24be23f889d31cc044958506113197d5c2dfd00ad31ad73c3ea94e80fe2c8170b934eec2a9d5cfeca1733ae8217ec8edd488d9606cc5a8b31932800f6b8f6ad133ab9f96f4a5ef8708a35131f076b2ba70327be31207120213b0f213b86450540d660231ae2b4d7179bacbd56bc8ec6c1bbd0b8e8b4742d436db5559415d97dc9ebaf5ce607dd5c2b14873ed6524619689ae329dd866ccf224d02e0dda963d471de46aafcde1c9486666ed9104b65bebfe29eb657be6b674fdcd464f4db6603bd870367e911a31d2f6f26539344b0787de152278f969645ea47b0eebc08aab1b7dd85684ee44aa0eea42e8241e97edcfa9fca6c81872093b3fb09c4244d00fef6eabf82e7d536d40e40d80c338d6d7777b98c12626311b602121b3058553ebb6b2585e3a52459c8ae5092edfb66d9da9afb305ac51bfa056dc36c4f9de3f94997881af794fdc73620a0eb551e4fa2c8e6305697595caa5b72c5cab34182f4ee278564b417f95d32fdb13fca9ba19e9c4361fe67e0df4bbf852595500c72862809bf7f1fa0225aa2a8f3e8b32e8fadd0ea1a7d487ff351c07c3db7df9b3d00c3ad39c2313610fd5bc253435874f701b97403a9df3e330c088e3b1f62a36f686e83ddbefbb0755f319c7ae8b1035427db156f44efcb6a26d3ee6764ba59fa1ec9bfe0534a7320cbe7cc205b00e093d00810d81f245d77597c5930fb6e2b75f75567855fd32e4c57dbb5ea6ad847f1b01adbaa04d74c44baaacc5af5a0882fcb8bbf8cf51e412a19a33fe49be5307f2b956204bc632cd3dabfa8da3591d1afe7614b6119944f554c73ec022e8484760737b4c6e89fe9e4ccfa983ddcb17094592d69b754513264b87c2656652615f91d2981d7aad212f6f6e83f8ed21c9851edd9938dc95e7ace8ceafc878ab8520a9f430482a478ed8ac5b535fcd301c73591dff43063de26481b0564d9b0aaf40fdfdc5c471411660f729370e0a92e6001f5d44cccb77258f4c0c9fd4464ebda3140f82800c9f3ebdedc2ef795a50a47bc32fa42c8b1c3ff829f31dc180bb4332dfeb2a2701c75d6ae681c973690b43b843dfeb884b5d352033f7c92773053834b0af747978688029370ad14b719c0762b546924d5cb004503bb32c1217fb43040b83656fee7bf43dd91c348eff7332c833e64fc976ff3771297f4a15cd359f808933ccdc00b7b28fce37f544f49e812275788096de9690e1a0bc27cc5dc0a0595802570482fff68c1a0a345a9bf05a9c14f6ed1e8c99259080a15bfe6f7cd0235840d5a0594e69c588f6cd515f49c8c081e4dbd19212f7bc13adf5cce3b49fc46737fb1a20cfb24c0554d89d4e4303e1a0059734903cb21bfb237e3691d848ff78f96fbdb19b36fbee66b80eed5aff51fc7077f17fdb77190efd76ee1386f98d67c32e0b5d7c3d4678d3385d701cde770db4da333f9e5c385adcea01bd2a88f5644e63df953a80188d9d3531e3b0d60e71f1856435581fb8ba1b66c6dbbc1ab709f158a8559e9d802c14a00dc860d5e2b88a52e6e4d9b67852a02b8337dd86c9cd8ac6a69e7b8a080c2b50a47d7cd5d60ed7de756d35cb3b178c2935b4eaeb0c33b35faaa741e343dc7e75bda4880f1b4a74ddd39cff6c2f7c87da4a3397eacb0a6c43a696019c1f7efe11d95e3864339a1dd725d3455d010ad933631e81023eb1b1b96b8b986dc7e7feab3d95d2d8cc07b26e5a7540ad0d37244402baa34336b50b1202e589e24c48127b59569e3fb5d82a8806d97f8c33150fb219b6a7495893ad5882193dd4bcf696a501911dcfd380343dccd42b5df6ed30188c6cb240974f1651b794e5dd8b8336bed5194a45840ada53b4c304c2a042169a847cff962fb1d32c53619365a811c0b31e9e30c38b274fe54632a17451c5121b7a248b47e6be7c8c674a3c88dec23999df95dcf289f08b001d2b047dedd97c181d1735e08cbe9c7c6c5bdf976117e37c222ff665e3e8a9849a7e6771d60faf0620a9e2fb25982fed8839083a77a905c1c33c356ad806179117816b6e21738f867250ee88a8edb3087902351c004e7b9edf28069086c1afc3e06bef289f7d4ec5c157cc5ba761327ea291b3462fe26564a270aec805b14247537f4d4125c15d44a4ae880b7f10813c69c936b13a5bf081cd726b204f076442e5eeeca2579f1c954b8c0585cbb1d3b9367b86f643f62b94cc61ad36dc69815e510e83e1c112856ea52b000000000000000004eaacc9f9f5546a8fd1218536a7a6545c4c01b34ea32f8dbdc9458affede1a0fb5a9a320fb187369d045155d9f0b029f66557892543e2e10000000000000000029fd8612dad36a11590b249653fb83fafdcdf60784a2427379b3253bd553bf2fcd6d94defdfeeec0fc88845d9e45c4ed5748550f61973e1db31b3510b0d3e2360d8ecc77a240da4bbb9c145d69d561a6e680f5a5ba3d3d042b9e3aef214f94cd47a63bc0a402c5fbaef2ef3a6393bb5970210236f8d67c03153cdca41b2c67598d336b575b75757a46ac9cde660ca2541091b08549e096e37b490374bced2f64be724b298317664c0dddf5dbcdf52e8f5d691df0995d1eae0a18aa55739c160a9ed9d1fd85392c91dbea07f292802b42dca4527eb377e00738d5c1e6e45dae0755eb240d6ff4fe1209b845cf1ab203b874fd08da121f8697c00bbc2266b60f05a9d8fb31ceaa2511c34016964992e6e02305ce430cc3ec2aa681299e61bec62e321ee525d54705688e22f9d22da3a05b0b76887601bc292f02e1050c5552e0edfd998f8aa2e5db1ffe2a082823c0ce7b025a53e84ceb9f14eb5e362b0d5d93edd7cb221e6e59fe727ed2c3ca1c77fe105b9b280895abbed5ad1f32402143ab2da43adda73236ecd706cb8e4925b4c64ea620a6d5930ad4ad4c67d3240f9137d701029d51c46f5bd255d7c4aa6efefbda96af642949c4e517bcb37626915a1efd7696c8e69eef02261f8661f1142cd37770dd82e840f9ed0457287d52fe44bef20a97ddc4b3a8edaa9f762a93ffc10e1ce54daa38908d090685113f2c05829f0eefc33480902498943f93a208415201f1a4a97341c9c51957d5819bea777086cceacdfb430ed34e77b2172aaa61ed0fb67b5050ea308c31e9ea2dc1dabd05afc07aed9f1eda899c170a247d9fbbd45c26656f24b0fc344a78fdc6509e44e8b9a5e13362e01d8cdf116f26aacc59da7db52a5e6a5355f1c885692126dc20a73f009a901528687a7c0877cd477169d18ee69794076ad49a4172e2d3b4d1a4e89f61b4e3f8aa23d6a4c19e474ec330623f5219481f6bee979b5d9c95a816d2b9fb8fe121278dc872ea49997a92103b532d38921a4135bb0c23387da9a8e2c1b0ab1960292702c1911f65219df88dc8913c993b67b04389ca4e90bf2722884778e7b8ee27ffef16a7abca6096d681413097ede2674d819c16215c997c80e95fb8557425becc56793c33367fe6a1500dcfe10ef310eb3fc7e0302490906af702b84dc1672ba1cc29fe66c49f013f52c1b09273f16e6eef71c263ba337ba1da72ddb41df488cb14e27a34cd2ba6df2fda19f7b449b33c7eb907f41ef28d8c68b6e915c76a8d039c13b630959fef66467a76fa14847a52f730c6b56e869b5adc15b34d6247ec02c429deca8d1218cdd9c254c239df1db8589270412d2c240235cf724a2c8b9295352aa95d895c1e1bcb06537c704a8b40eafbc35ece7ad1d53c63ee309861d1db54852ccf3025904816341e2e2b97cdeb093776d3454e0acef10502e3721ba87886dd7d114d258f2f38fbdfd4b0d3f78b4804771cddc338d05ce0cdbe366ebc44472928fa606d2cccdf1102d591e688a170c29a85eea1806e2d3a9ff156f763d87f0d6f07859b5552decf85372608933f1f9019a9d68351474ea0e80373fb746b8ecc1a9fc7620e6385f8207bbf08cc6c796950d3fbae73814e19bf748b21bfd3a303515cda315f67829f26d1cc7ae0134c9c8449a8b6c97a387ba11752432c4bbba72d157c2a67e13ae22177a17439fb8ff75b6422e533d2f94472ad0f247a8b876217347355f691893ca0380521f91942ca8b9d84bf854259350789f6b8d2c53d64152c5567200cb9d7f1083c6810defcdb18bea661127fdc8135e7fe783752d273ab42d22f153d2746fc8f7def0f3ed89b72c623f8a9db1b5f920bedf6e3fd26d4e240608f4c456e86cd8067a694d133ed7cae0b1653459b3d9c8ab945bab8196085703bcf7a9a14e0ebc36c8852ebb14c9e7fe6b8f8b9c903bc86f08c84021527174436e33e8d188b9746e9da1cce3912dbabeab51dd363aba9206e5f8562c768c08fb6511bc17ecfdd444d3bb016c2ae5d0ca998ede694d83876348838a160349239fa76d3b20d58927b7f9922944640075401c306f64163d910484578be9a6fbd233cc79a0396549a3397ca3303adc2a8b8f1c7185c089e364676a23cf617d7c60a230e2b3faf1bb0c0aadb517229fde74813665b97041cc24b9b0c155603255d756d40dce17b15775fc3fa9a5ff7009bdca2d6d2fb361ca635748b7f690f6ff942ac37f39e5fd79aa1de28fc1e7e3502c0a18cea2d31432838ca702b86a83ff61a04e9ee66a0e6b80846526d2f56db3d25279f8e7f25304ed73ed501f72a51e57904e7d09b5763036a0f9a465e24fc83425c4870062e38f0d66b9bb55cabe3cadafa0db2c2e8a3ca005da98b120eaabe89360345cf5af59cb5621937ec99be5253433b8197e27e9b258af411af513c74fabeaca4ea9ad916f5e2d417246e8b2b2065a93341cd3e13bbe44fcbe3047a70b16ec338a32ecb5cb73ef26915a17067e8dea6ded11b7da06147ed7a712e77e2023cc0471518c31aceb025d1b98856111751b32662a0536bfa7b59dcb4876a4ffabb97fa9032eacf469d93a7b22eac9e69e9415c0a7cd8a6c7dce626cb5a8acf68767dee1eca733fd1668b56a4b743e42e2997552cb4b3b1838f9a3545960f14f85250902e9953a4ab84751be0ded536d0f9f8d897d8d2c7f96e83e786093ca189818fe2d9b7418b4a5798e9a69b147db37b5a6598de8c57f1dada24c6cb2675d134350a9dc39ed733374f97eaf678fc00d96c25c0d76e7c1a67f306fde4000d8f29fc29f34ba7d2e65567a70aca47f4421b5b4e17fa6595d4ae586064ec74add2706f654d3d68fab38af12ba950d02ecadf420de82e2fe01b8ba72db05481e0f4011a5454ac29a082712e917481ab890357ed66c20217dcc0e442f383cfbfaa4b50bead02bee70d7961c700d519bd12839244b57f1a463b0175648df9dca1670ee8ea192029bf7e1bd6cc1c6589dedd6d79d27dc4eaa397dc0716a244528f440fdb27e0ff6e593a0060fd8a904af1c40326e844fb852d8b5c2e550a5ac7960c7319782cc011c0b0fe9affb3a5c15b1449c8302f7fc2d824aa5706491ada6e652f47ca79d03b44c6d1d26e0752c07ce3c84654157c4c47b1570ab45201600000000000000003c76b9292cf39a00f36e185fb4a2bf268d6f17864b55a9088b9720d66be863357043628e66f414f57cc0fc31a01d69235c689df68d69d22c0000000000000000025287ea0044c86412296d398bcd9df6e639798215050db9ade94709d79ce7eab9c420ad97fa5195b1fbd26c19e154b58e38d215e49cc8eb891fec4b3192df53f05a8ed0f43646b8f672c298825e9ab81dc6f2a0d36afc81904a7451b6011fe1f794753250e4f342f8d06e414e08674ca9119ce0350a59501fbad564dc9970e4dd153b44de3dff10aad8da0ef2427bfb93843651e00d46193751ddb79768257d19f552b18e5524e14a0969120bc2b1bbd39a526ef9519552612faac4783adc3789082e988153d2f6eaca12f6d2739ab630ddb4813c181c636a199f7649b1fc77d25844acc999646f0fe8346bb4034fcd6b9ea6e76e18d5309c9d8b59d6d882562cc90b23a7daf78754058574102bc0edb79725b5af6098c2a291f3c37291516f397ed972531e0bcd1bf9134cbf7e4603ede49ff2ea9b7bbe92d8abd4a4eb1b82df348221704561bbe54e654ecf9d223d96288f86daddc96454809f72305d7b33caae29d51f290309fd1614ddb090c805fa4bb21a7244bf710a7c93f455d806a22e884febe0af1a543717c62b17b39b4510e1d665c617029b230c0a744ee7c9fb564d13571575de093808a8a75fa466026b7328f26e52b2f219eeb260c2f4ae256d1c9ed83aaf786aac52cfc96a88be6e52547cdea6da8d5f3b7334905071bce56f4ed2a5bb2d01e021900478b51a9a571c59ad0c0dfbea000409db226acb11b0f7cf9b371f73794f799c49fc5f610d6167aeb7e6bc6ab3d33c2acbe66c6a2e9b6e1ce3e121db721cf26278ecbfb4e05a8030ce3e71f9c80c82b103b1dca97c9bac62f4e8a56eb28d764709af5575bd0560d7be5bca9c4c98009a0e97dcb1873340d628e928e45df1278088076aa99460e9182a195a04610449db0a9829b348fbaec975b3c68ea929b02b3da16a5d5554baba056e3b637e00b06f91331725cd8d09103ed5bf1f1401f82399ffe169758f918a6b01711dd0e22084bb2860316771b026602d0033e02b35ebf7878015d16e2fef58d7cef1eccc56a3c3cb9d8c3b9b94029d842c042a707e7a00518589483baf7a6f6f07832943afd8ce8bf32f17e4653fccfae6d090ae2198d87dfa7ae8786a7379f8afe573d646ebf1d083bcd56293f5b0330a5fa249fc2e49528c4b574f46c7dce98e4b379bb584c058ee1eb8a58fe3b3672d3d7719356ff7dc093d6e85f8a57703c3ce262622b59ff42a1e52e27cb6a8316679f99fb30b21ee0dcb61a20a04209c7cbd23a0d01338cfa85378cbb1e23ca437fac9fc3fd4426b65e3162dab882998013d39d35d4fae61b806bfd9cd11c0439960bbfa6c8e683a9b805e888117b791c8d8a64ccfff175a35921f5f27a261a32e3ad6ec4f6a693fa4cd994344381e9f52e1217425c5790c250e308015b2d337748c89495a2d9a28ee89c19c845fd7b78eff9f05906b3b1196de7c9a68af30b720817d014a4638a2cb1a63e6df711b4dbf2dfc8d55e497bd40d8615399b60616e5d05cf612dd82d9da904c6f21953fc1688c00f110b6c5e7949fa5a5c4f4f9e07c6bf81e2a0ab74420414087047f904c4ae6a5a4eeb62ae5ab05dba190e887e37035379aea2172235a6a468c1a78165c0de807d37b2458be586c6daecd507333381ec6ff239dab7e9fa04024c5a96044bc983b82470d2035003db43de4a9edecd180558a704c32109937a54bb24e74667a81fc25614726463c9265713639a0c566334ce1eeff6c381c906afa63602f3b158512b2448c14cc68a423bc8dd03afa6bb604f65c8d8b4247eb1daad423bacbf358297293b5e6b216904067a28ece6fc88815ad50c97f6a3da0a5f9521db7887d3e15a60021ab69a486147b3306882817cb6485aa27914be77d7b30022ed0ac6c87f04bb9c282798c9d0ad38606140f0b7e7921337261dc9e0ee84e000e9f29e7823a287dbc441a6dc72e174a635de4d0f1f129ea1701eb99b724bb061b265d4959b0ba083535e29e401d83143b3df28b77dccbd8cf99e52a8be35cb9072c4b9cba780b516ad29841dfb21f7ba2c71a36f07ffb481ee52384a8b4c740217d6eab52acf3097a88670c5762c934c698d064d47bc1b710a99ccffab6665c9884acc2aacbc54f6e261cfe9f8f2f7d24c61a8755329d24da68fcba0dba05067405657729741ce22f0911d5fe2ea9f1c17ff65417126847d44f6325c81381c59979d6fe2146850c35d0f73e0aa09f1394ba28f99c4daad3ca8ddd28178e942a42542d2b98544d4e0a76bc951ac75665798b0f8e5551c7ca6aee0151a59d3e7bc84b29fb50d3ed11644e2f5f0ef502c580aec0060d4e4ee76d222edd81443d017821a0a86b2faf3d6c5d737adf147b019f02675a7b5c160683dcaba01bf56964fe70dd44a6ad887ac026dff7f8234e1ad3fbbf6997b6341dd8666180bbb63f6405736ebfc903602d964ddab99fa6a313c97f376cfbed77e8e5b225929cbd4a42500bf1da274c1625d146a5bff52e6a130cf1342e07a2807b3567a0e4a12026dcb7b7b0977ddf803cf99b82ec0b139b9adfe72485f822784af0df3107805db0ee0cb3e67e5694cd6e2df1bb2abaaa03f252505ef6f1b4ac31c9c95abcb410fa6e415934869a9004f7d10c5a432eed87e81f12c0efcff3ee262f71b40bfc8b8d5e96da2169dcf46917f5020bf3c02c0e6e6315459933d3599cb0f82d55097f5870b7c74ab020b2a0411ed116636d282c15c338e855ef0627abb3d1e29f54c1e90e29122f62f8077347362056736ab34087b53c1293a0b5c3c66f52e2b2f30cc5ddc884fad82506dd06797b20e52f7cc1ebf0ccd69e34d8868154d548f385a32979f8be8709b1045ae58e9c127c4d5a9c938e88aa41f582fd070407aed9c0ad752e526079197b4720867ce2a89f7d05b7833661d76cffb4ba8590dd9a5a4b2ac55823001fd706ff6d4dcae2981d2f246ce7afef7e33da26d10ada31b50a7556a7a549293e038ad1bdfe421a02f9e000bda9797ef759cd11b24ab55d7d39c035037ddf3cbd543684d5d919f726a789ef4e21d1afe97559ada6e362c9ccbdb0d508cfbb8fdde05557ce4424a876cbc28f81879fddbc14c83bab1e49bf42afb5b4cbce37f73211d5a7cc58f621ee5a7819829a0a53c9816ea48ed4ee4f71f66a3868c4b5abf200000000000000007d3e88a9faaf303c2ce116352a307ed1fd29aa3fb00d6ecd680b1a43aa7cac3ebf17d591e90c19ac9c0863df9484b113e8739e3470d99017000000000000000002cd5eb1380ecff50ce772fe14b6c81e0c47835cb694925a0d8ce034eec657863bb73262e95507cdcb725cd9221a73b906352cc8f85f95d3193283b8727a1984fd8c736296c1cadd144c67526f85b1fcdec09d4d31c563183d7dd7d1f78c3e4df9cb3010f70b9d61a40a5e68be3493514d9dcd2bf6e1f179201acb9e2143348eeeb58dced23c0352cd1097da54521107b124dadad1cf500e0a72c6f090ccd0c569112bf0059d1387f18e8b55cf26dd3a2fa893c0330218202ef0de998e6d1ea5ebe42d5ee1b4945a128aa4f584b24dd300acb62b30f7be9cba007807e70723ffff1f851e818eb9d0fc2cd9e13f62b249d81b79ed292ff6887134c2ea716ee892b6241f2a1a1d41d9026e0c2618f97b76c017d6aff7d513ee7f7bfff3ebea12da070138d5814edfcec0faf9eae97b91f0cd3ad36cba576979e89d020c4c9044b50c8c67364ed5750a2f7aa91680d3a48ddd47638be5c51a9e158732bd32a0acb76385145d4659d085cd7d4685cd436b132933b30231780c2b1a00c1bd75f140e99c8fc3ea98a4f421c915f8d081bec03640e8b5b3ab707385011c90ac1c5c12840af73b020204c45e658e0fabfb27f5d18757aeb0389bfe3364b0ef341979a786a699361d570b2d65c578b963e4f017b8f7947b2aec242971db6cfb0f980aa66531b6933f2dcd68e8d8e5e729f7d27ae113819901069683c435a847ad4b54b4399749dbfefc1f998693a97e74aa19237d1e3d8582de5d458576d1cd454f4cb70ba0d43cec2528146a2c535e9d8ec9524de59dc74b83b481ea6abe841969a81f042f895443c8bbffed5215af3563e11d7752e4ca7fcc6f2a6eea9fe115f62a2a87a23246fa5c7e710459268c4576e3b120aa7e67797bcd1e2bb429ea844096ac74a900da393f0757ad46a7f33d95cdedce1c8b764dbd36c2eaf971132f8d77b9129c8c3b9b809ea38b709e586d333091e84d584aa21b967438064b05f680b3ef09502c5d6d25ad87475e4bb1e56d32cce563a9a528b71c85b7528a14e518228004e902f5263980efe6bcf3001b6f1c1d2990edb5495e82d74451b6ef592cb41ae5a6f2240ab4659c50f6971af99f9edea57a96388e31a27ef9ef6765e5afce493412888e385486b50be807c1aa033acf7c6c2ace6d87003f7ae53d96f113a896820bef034de6455a5d77f7731c006ae59aa16856bc77102672755d848c12deb196974de58db64bd6507e8b4adbf87e8bb35a3fef246c3f4dbec37bce42ec42b2cafb816b1bcdf02d2fc2351a0e2af8c0bb2cfb6ccb9b814fd5ec0e8ed6170fd260729d975b19a1da813cc3438f25b84736bdfbfabbf5365b85f8813b4e298c5dabf4f282145491dd3cdc16080e1a5d1aeef987677c1d58a4bd8075917612136e4ac2fb9214b2b9529b38e66b497dbbbeaab1da131e3053dab8a4ef602936be9dc16762438c1da452daa6724cb173e2e5213775bef89c2a5eb9003d9d7d59ee72ffe61a34ed08efda3229cface265b1287cb4f513fc3a2969833cda68c295584c4468e54d74e619e131ce55f70762d2bf166f61a1357442897cf1e099bafaff95d2a592c39f7c1e3264b988ff6690cc9c791fc5e98e883df22e0db109cec9d3c98e3ac511c57fc158daf43588237d4880ca24e90da030a43c4956eca039b49c5de2999f333667e8b0dc2ebf1a518fe8beaa60ba9aba1693448020492ca3e6042a76f8a3cb6f7797713cc959cb4fb93210a8499cac678316eb473aa36402dd2667c066e57775001c13e0fe6d77296ef24443b1ae6ab62d1e716103837c5077cb5f39f1966db3afd829da0e9c42fcd1da685e304bab0d846c244f0878ed83d5619cae3896d57673a6f0e42893edebb535d12b2862252141e30323860c85a84add58e672b145ec2a6750437b88e7a831ba7aeb05b89b7c8e1771d2020245b326467d932def107e584afe40c2237e07ec0af3314dfbe80e3bc77cedc37e477e76829b8a92070020a7a0b8451c41dea3066058aee7bf615707b8d6981472c6d6ff8d9d8971522770e1c514aca07e4d4b4bedad440c424993c1b5a259494491bdbf030de346fd071089757893fbb6f7c23bb275028db5536cd5315f43e3ac3ed2d81a1f4f01e80474963f8e41d03f32b50b18c2fe245ea4eaaee4748b783fdd590c4111603d2df43e8a014df54f136b4d929d034e11e3a7936cafca38bc0fa61302eca8ab8e879926c0da989dd825828251e7dd12f172a669f8a6eb2a722f48bb7c6793ef1e4316d9f2337348dccd2f415ac479a948e086ada6e71225f076e169ee96bf64651dabe29dadabf834da809cf77fbd4640f585dca1fe7eb263bf19878e34ffc5cf0872ae9e3fa74acd5c50fbf1a97dbbc68cd4e4bffc4a17900d1622a9755c17a2410e7d0f260307161b9e44570af2d1eda36283b00e5d7c1bdf71a6d8aea866c574df92e6d2e25245ca112f1b3cc74f32bf7b067af9711d63ffc9c1aceddc813529c058a2b0dcca6571e92a926cb7c6b8947903f1e9ae05e76cdc3ccc96d73562a920b1989359d9f35304cb4be111e178a343ea675ede9f69f054def27bca98986625ba6b129646260df86dc1ffc3dbe055742c8e1080d1744b1a7547f87026be73129481e05615767bcf259ebc238005194654a16e812d0ecb02f6d909722873d8ac5afd931bd9552a6aa243d8312ac6397aeb810f15d2854f2f3cd05d59e94e64a7227850d35bf323c93de06e1c7654b141468c459385f9b7574d9f7c29bd21a374de0010bf189df782ebc12b6fe626d05193c9b25cf7e1ff3ff17abc8eaf0d3b8e6217640286d55d6550e25142ee2d6549bc3c98474f2167d116bbcff66147d67f9d5ed40787af6b006b812122e2f266ae2ceefc5dbd67d9b1efa61ac8f867c635befb1f1ca7a3ec4d8ad3b248d52434e6257f6dfdf2872b78cc7cf2fa816afe2c384ac096b667965340d277a283f98265a5f5f072e8e6f12402749d9f7bc89a755990df4584e3762efdb8bcda067f6f5133ef85f037770410fdaa1bab17fe1eeac71e68335a1ca44f9c5cd3d7e37451b74a177312d71564f20b67088ba88c867d129a15d471ba98ae1467bd3f3ebe2b00ac41afd8170bdecf0339670ba2e1f734bb23413a1fe918bac1777d4628200fd823000000000000000074d6ae646923abde6a05956bb91cb1691e07358ad5d172ce80de0dbf99a278aa9fa5a0f6769c66fdf6bce493856e6f024e5c55f45d115d7b000000000000000002535072fe5bd43f8523117b4efec941004859580b6c1dcb4a749a7323b717286fab974970e74eb9ba52da97ea98a69e6a09d70951cd8b562d5f956e20513207ab267d95147e194ee09fe845e017ad0164d5288171ce9f4a3654506ebc8fb8b3af3104351793ac1a012ee893762010c69099246aadda7875defef53fbd89f41347b8f6796634a1d4a5e61be0b55a557cbc3244e3bd9f8b1ac1f27b4e40f3fb3c8646fdd8d7600aaf039de95999eec420436be9284d1ebc499eb8f6386da234e24a1a69ebafda5914ca103c5a2a755e1571ebe8148b7a1272adce37be787e80984b661762c6c52ae1769e36667d45aca66d350d4e6521a57bb6ff746823fde7771d33635ef67573c2924598cf0550df0a6af30094295da22eda38d737664d75f4922e7cd88218672a820614c6a10a63db77ab2ea5676e7d6283f47658c8a05bb1f02028698d797cb9eec2c8b93cf21f43926be22ab6584dbcc999204677cd78d87e63d3d8c180d9c8c634a88ebe9093100317cfc1fd223dd0eec41dbab36a268b5014c6a2a51bdc315ad27584e5f825d905637437b6763fb62a8331f931c571d67eead6707ae4d652666654a68fadad9a6b8e4397503a07c41eb9efbaa940f528e99ae29b19e0967bc1ca92b4e309ac87bfba9d13a6b03a70aa7b50e370785daf5fd27a391ba00465957f1d3ace6367b511d58369ed66e8d52c131a6bc26337ce2309aa2fc912a4f49d5da0aa199da52f7a378db434c37c5a0219a5219af051f193f80a0088fcdc8e08a7013c24e453e6470876123af3df07f648219d6418775ff1349524552a08e5864bdf30b43bdb7a24460dbb6fb38d4044c062924b179000dfdf220b97721eaa36e9afbfd7899f9862a0456ee92c7428418e8c6b625814b78ac73eb644affbd9f4eff43f3ea985cdff8bfbe1b24bc6a6a95a4ed9d9f28eee2af99545f7ed91492c2550d675fd26c3ac9d5650ba6399207b11a2ffb61bd8e6fa6a83e0f9c9295b5510366d011c1b0aa5dc1de533c30961cd79891779cad28465029b46ab98ee98875b2399670905662b2edba76742e482a6aebcb97739e00ad34a2929090b8d6c3c61889c578010353d042aa2b80d46d4a071f58c4cd78a7e61b255dedea964612e04108d1b1050c9453b2c22394491de4b6978d50120a33e93a48d0bd392c4a5d6ef77895ddfa98d724f803b114dc94eff3b5218c6aeeb7984b2543d6fa8f9bba7f075cca70db1ff17bf9d57157c2b133cc0d98a95a4d0a43b3309f81975dd333ca8ac7de86acfa8ec15a79bd2d7a49fc26aef980008022261182b40b80a115949f08c6408275e78108ac27a36828cb080f623fa0c898eb84d02058a2423614eddb4b9084164c1ce8e4eccbd73e0b66b862252ed6d9b82bf6cff64bb88256e8aea55dcf847b35a7db111ce282f8f8a65d89c758c40104d18fd801a722fd37f78ac0bc5a784a28dd0fc7f6bf45a5b52132f161846b603d701e8b2d67b533a8cbc288475d4beb59aeee9fbcab4d05c6bd8c5cd8b07b7549649fd50ebd0a39534e1117111637cc4a7d5ce7d9d5d790bb0c8266252741427021c16dab3664fa700d6911b51ee67861cf7e3dd216311bb0a8ead24dd193b33ccff043ccba04c02cc2f6fc48e808883b68f0201dc1e4078049ed9dd660fd16fcd4e5b8ead546133516a05137a560921e80d96f89df526c0a2c9ed71d92188b1d322ab3165ed7daaee2a2e9b5b9d03fe262e96c44fd109fbf213eeb6538715089be6a3d2bfd06b9e5ae6548e7de76a65697ebc727b97ac0b1079737c17a7c2a2e335c707c1978831e6b3da58444698e8a58ca2c62cf119d9f4e6123633af2f04e7fca6bec70562d11a66083f4335558ebe4562407a0fcc4ca24aa41c356e66b95bfd04fad3bc24b9d93520618b10016d3e4b1fb37f637233fe7e8e053954099f6958030358d7b3325a48e664bcb063e998ee7fbc8b65b4214905dd39523ffacc808ca25fb2e747e1ac5fffd5fbfe90d0dca9c3e8f3fe8b46ac58ce55724cdc33399b42c74f312d2a6129905899df58648f80ab2b86d392cfaf33eed6d95128dd0d504c6e1534c74cacec73545bbfa2777cf863ce4f3de50ad5f6bf6277460cf8d19bb14360655ec486cb7e9c3e00e65e9fe55d6fa33948b1e09a8361d304912fa7e37300419a106cd0d1e33b3a43055ffd4455eecdad712590cd87d5c0820e4eaaad9c9d4bf8ec7e746abba66d00ec7cd12184f314e4678e43f77f0852741a733bc00a0a42d35ecc21e2e2f2c9ebe907d30a94ffc3c95cebff27d0e860bba7243f3b820e5b0ff6c2be3c3dc5298b146689c8143e05380ef68e29b31af5ac948f813ee6e6fbeadf2e7b4baa24d6fd581c940b2d1ccc686dcf29fda5d3d2ad4e68668a73fd416eaf86277c866efc42445b9e461617b34fd9537a02ea0aeddbfb1b42f25efe4b4ff7dfaacc88cbc860307dc4fca36eac4b665bf1bf583e1d78a4d445a8edb72c45328219a14eecffc0e6ce8c1401be33e08d51e1ca3c15093420c6004bcc5038b4eb9e12597d6be6f99687325e9d092371dd800cd490fb488b72c7d3e18777119ac508075f0f0d7127762ed176acb9cf236f9f5fec0cc12cb6f0bba72fc28eafe4e30bb4677c50e12162a5197ab530f3d0d98a29eacc95025bb6427f6a3ab0822a3e5e835ee94251275bf0b3a02e2f41d017dd2d333029ca147a35819c49775141886a0815eb81e46e227bb2f68250150862fe85db7c4c5f47ba2209c8a528e71cc37d675ee2f173f42005af788d5e98d45b0ee3e2d45eec5d578a8baf0e2115b780426686a2be8604e41d468a199b1e1c4e258b4fb199faa1480e587cd8b9f49e941d7d2b7802ebaf80674c3ae078c0afc60424fef8968e6a642459c3754d44f106ae728ed7cef2b5d6ac478b3baaeb6f85be2f1eecff6f26364a003748c1c0a7c03e2ed41ddc1a42550a9363c9bb0a6a3e4adfcd44417a028b6acd8ee6a09cfa4f6939f8d9edc26ff7b1bb0d64e95d33a13cbe8acf37562de961ab8bce39f79953eaeb06fe4d456280dd27da825a87102a74b82d2e6e839c6746dc3d88427845c8a8e07e676fbb6f86824a6747a21bfa5289d316c640e0d520b3215121fe7e4355669384cd3fd529dc452a981aed7e0e00000000000000004d9c05b661da84a2632ee1083c858f0922bad491059425036d2fd7e9c07b5cb7a51bdfae464be239515a4087a94ccc6a183301a16782d127000000000000000002f25c34761ce9edda1be2b808746961614c2e3b05116b007829eee17fc69cb575b711c150049b0907c4cbde6423010973d025ed9a9b413d98a341c391186231e9808220fe8dfd644b3b5fc0df6f1e064ce4fb3640c720315b04510cc32df87a5cb616fd794b3db7fd368edd699ea1c7489da7da8a572a5e16bea76f346f64b014699b1154516218ba9870b3ea209f1abdfb6b78627e5b03693bc0127cc9e638f65a467e8f755d96eb834a2f15edf7042d31070a8bb5ee3e5b1d5ff9998bd8572d6f1a177293f62926ebb99b9055aefe2de28ae438a0f46b8b5f0a9953f345881c98def52fab3ac488a8e6e718c1bb02b23c489c23e7b5d045214fb9567bc51f1ed58652753c09fa625e870b8b3369ca46689ad7c3ca9ca2c5056df6b0109dcb00d55f8882b574d9d1df069918c27305304ad14965161d431e4ce820b9efe31dfa383e97f1c1faf39b704819ed324448f96f7ca124d1a12adddee70073908f8b3533e7331eb70fc40edb0f172ac384592c63f458479f7bd9f62c05e0894de647b2304f67570d1faf4237b670606942d5dc396414a79ff2e2dd116b0cb5bfdfcd91b7f4fe4f3fdc04a60a195e55bf8e36b675d359599af6d1c35342c39254d2af29fd10a13ee03c3ab22c3842f923f1d8c3662ec6a6e3c498df31efb80e0539a283a49d2eddac9a57932b94cbc067774dfa9a0af476e81f48ce05405247cf28787be736ff38535aa548641d3979d44f441f8f7e79dc9b1518a1f683da7e6baef99ee484beeaae7bde00b7668a55871fa49def2f072c67b7c355eb2fabca22b0b6524c5a97ec7e51148dd8d47aec3a8f16c9486aa101b7c42594093b1ad82db90698b4145190caf6e017879f4061534576284edbd55547a16c8e721c1fb8c05b510babd167f9c1740286616376ea8c23714a5fdcc09936e24d5e0b92de21cd5f2475cbcb1a335d96ca3101ce48fd5e27213ccb088c11d9027e49d65b89d40c729e409a12bacc2c41a94d57ec422d9460fc0eccdf2e1d17327937ec138ab69545b65102989d4356f63295fe54fd0d4bd175820f74e7bb5f30afeea5bd30fe6d1daab23ec8e514afa2f1a40d1f0f92f5041d23add54bf79ef6e5c8e6189f83194e6a6811c7ad2ddd54786bd8b9a7e02f0dee864300c673d199c69b475133061f4f0eb1ade91bf8786f02c8babbc9b5b984c40c5fcbe890b7f0e7b29db575a6048195268b346466fff074fc402101fdd95ae24da2ba3446396a6660b106107896079782dc4c976483dc8804226e9612c30a079523542adc75609e1223b823da69caae1d8ae0cdad2475be0bed4cc09f56ddcf18dffc5c7930c910540d3be11c4fe7e658c6a5d04d17a7e43a327ef802332d0ff42888dffe03f4d01a17aaea97cbc56b414b66629ea073a896e027da6ee40b4539cb5a62f550b0ec21a0161c8f7128fd78eb0fe33a49cbc67b623646462b582eb63f5df1c21dab5da0c0edc5c7ab0d93d15b94262960cedaa7114a70931bb9090660015675cbb2f5bc911bc090650f28999aeabf1b11624178ffa23ec3efc5c582c8f5acb551fbc586484a02387cc179721d06d9810b4c2293827e64defba6baec62f1db94fd70081778751fd12e152e6a85d87281ce5d872f5c6435d67c1e09162807cb9467319a71f121d9fa414fc8091ea30c10031268f73f18526a24b6de12e825e8f6f5be60bf0c8b11ffa99dcf4ce08f82ffa830c4283bd9a5232ffb33f7a23cad19b4232371707b0e0e16ea6c100b8505c59e890348819e658417ba4bb9aa61ab0006700c2c8b78217f1dae043199b6f4aec91e03554926b2c9fff93879a2b0a1e4fc5fdb3489684307a3d973ed6b4f804b015085b4a6821497015afa44ddb0ec3e535176d796915cf91f0ef301e6b9c26dbd3c4c713f1050433f93e5b37b98f96a36c4cd6adaeab8f50b326420cf16f1e5b1929d6b5c1ff46cb57a1a14d6469c3d06f28391a0e15572ec67e9d2e341efdd5b1d0396e7cf6f0f7b716f5c2c9f5d02bf40378cbb95b243ca4aa46c243cb4136e961bc5be58133b42a80c848fd0c4f80fdbe868bcc5cbc65ea13d32bd3dec31697379edbcae823aeaf7801529a99acae3ac9ea925eb42f9fbd3fcbf42c1812fa90cce15d5dd8ab7ee0af4fdc2eb56a93005bcd188844dcca3cc4e92bee05e74cb3bfe9099bbab51fc46e72e6cf3e6a71b52fbd27bb5636b7060f5a2fb398a4ba01fa0ad0d965be9ede372f5312075fba6452b627dd5d1ad53569239d94b1869f7f6ee3b9729f75b96e8d9f9623edeee7e937d9c774a82e42293a731b4a76b39c7a7ac57977819737f7e88cdcc0781d33a886c411593f50dbe0a953b85d880043b44c1bcc6b032f1294e6246bdce7609bcccc4b64904059b6533ca7669918b802f33c41b4f4396fb8c1413609cb612a9dfceb2ffe04400ee37aa84e99d0e1bb1070ad622df884d6971240182db806b0d59ad8401c3d47307dc989b74bd5d1ed8ca5a590aec4a2be478727371ef80f1298e2879204b7c5be7b165690a25fa55db5567801525cc84be70191156efcc201c15b9c89e252c18931eafb3a47af1746c94197e5ae6196ffa0662f1dbd668bd28209f50ba2176647ddcb8720890bf972f026c278d27b3f637c7c8230f2dd10f0c93d10a3e6c021eb699b9c4e5fb3be12ec8881a45211558056571155a9f73e59df9fe831736f2eb716648e3f02e95b38a839e0b20ef974a46fd86b21b073d7b66d6f5a6c8ee518e73651b6e39c1e4e0498f77c2fef3dc2defad892782589dc1f2ad04f3f40331b410172a25756e47765ce8d1d57e19ee8637fac1026e18a0f6c6ff3c75532148dc5135746c98708e1560d22cfa94712bdc2326e033886f48fe79ecf94d7369aa279fa788afcc863f0cfc4b9ba0b73596a9f8c305ba482ab3282bf4bb69bda62c52e2ae7f4d89c96ae82362f9a1373b086462844480cb6f7d157f2d2dd0f7a6ad02db4c270789949bab69264c56115f52b35cc5f7db7952207b686a78a9604aa9fb41258adb726f2c0924a93d90c55f8717336ad3636a9a8964712b281d87673bbb5e48cf4522bd38359b439bde407d2e6d757a4cc704077a45f92316d0ebd2caf65cf8dfd1b4ab485e99952131b51df8426d9a14c46d0ea2d30000000000000000333fcf560826c2d20f0b8138c2d7f5b97c4408c9ef2f51d87a68785bcb5e979b9901b8a2f9f8678d2305a982bf16927144575c39d4ef66a900000000000000000258afc65e855327c8f5febef486b37a5dfceb9e035263fff0013e31f2216933f6e9c64305af2d0b81700ba31d703a057bb8885f489e40326f35b3ac632edfffa7b22a402f870976055c5ea8486e7db311c1ef02b925e5aaf25247d9dd953c8dd0f55d9c35088087126d1c60f04cabdfdde64bd1618d097fc27512e455f33586424d524bca4f271db5f2b87449bd64a5bf265d1852c65388f0d4650635c67fa16ad5acdbde3d88c5ea8eb469eec5d46b72e96bec2d8b4ca5f5ec0473882272b4db81941d133c8d2f3998ac90f7a689acf29212ba0478e9c0489f7bed7b0fc164eb7cb4f0cd271e569de5e5e4699972f07174697fc44dc6e6cf87fae65444a8ca3e75d50da00e7d68442dc587afcf34577053272f769660809388e1cc69f3851b278ebbf4af7632e10bcbcff890aeafc56931033341751cf1e52e959142f13316d6d19dff3d6aad57bbe28300dbc403a3eeee0b3587a50a9a1a20bdd20b6f170fe2819fd91f9e6011ce4497872f9cebf702771013e34a3e2bd7ea0a430a6d122a91591639b600b9befd8716ce0e34258a9aa64c2e06667c26edbac2bc0fc930feba3c9906bb2f0e4c63b2e515cb1a0964f72579f96bcebc29808fce221225594cc8aef37458c594e7bcd2e537900dd554e48ef3afda67311b35efd0c169af494db964d20b0f6eab4ee8075fe6f39f290636a0b8cde982e49625232e71037b7a0782af8b446dd128222cba0af50b821be04579ff2f2d8e5dd8bee0d09789d7b9c5e0c2e93df608518ef600d6a4d983e6e1c54e9e080b7cf64f74dff2802d19624b41431936138f32e28d3ef6a94315cb97045b4ffcd8f538f340768395d1f394a30cecf3cc501979216c8981ec52c92b8583a5990fdde16a9692709591642cddb342686dc8ddcb38cbe0f69f141893b060c65429cfe9ad9f549f6ac49637b059830691f1962287ccad6743201c0aa1465afa994434e5bc21e15210319f7cbfb821e6b3f404cfd7bf8a7db316dde28f163dfbfcf5ad372bbca35a3af98b988af82e6b0262855bacfd0ebd939d07873d40e00f84e59e200920be89904e39e67fe1d9819f70db35bb43c2ff6793146a02cb6eb0aa4fc1243b0476ea01968a1d05a21df52d7c3b203c2f4f1935c21a0f298046d7355780d6d40a8703d8bb4d7f3f8c4d8ef84a01f2c5ef388fbefa9c2fb83c53190fe71bcaad4c2e7c96091eda97f3c8d74344f6e054b78ffb702e6807603aae7204bee9e40902a476f27371f3e1ebc4fe89bf957639b13686b17ee536671704ae2187617f64e6848eeb7d43b0fd58c72ac447fc37a79852394e27f2afc291a4b4984bd74fb8d9c7fe69c1c7de3255af2029994fb677b87b17fab09e8a06915ea77c3dd64f2a71cb2da842af4d143536c0212a76f917b64385a726e2e6169d28a4c0c39dabb9e87f4beaa40e27fa588ea656563767b322f25c006e0203da43ded5c6fc6c29dea12374f29569dbc51b99890ccec65ba0147cb78ad9a59b0517caa5da5ae0ea4b9a9e53777e804f0972dd32bdff0c88888ad22c9a1bf26f66efe554489799e8e113025f7e0c7fdde108f392978d9a8ba68edfc505b405e31a14e6efe0f8575ab73719e9af04114c7bb35c775e88e70356ac9ec33abf5ad95adda4cae80586bb1ba68a7c13fba17b9a6e2f19d4279ef66b3f734a154527b07ca7da4bc0c664e1687e79f71d12c51af36cc5560c11f1130db95a1ba2a5bd76d6e09d986c9abafa1cdb01eb494947f770184fc40083df4a77f79fe7fe11944e04a5548222e6f6a6bd5dd441336baaa45c7f8b390ed6cd9a05b9f03f4d46ff1e927837bd20d06d456c63eeb66962af78ab118204ee99465147abcac002a2c381cfa7e504ace286a04abb0d8728b05e44123d10dcd40d901b4031b94706324e6a58b41d033cbd489e77d244952843c0a85b0b64d025a4f4fbf8ee57f746ea622dbb1737872629c200c9507d9d43b874551805ee692cddd263fdc91ebd76aea44aaa6f6050c17eff205f88044300de5fa5f849ed25587ac9898b923a2ffc17bd1df03141cf17554290833b89bc2f498a46bafd9c598db8734658f685e1c3e31c095980d94530e3bd69f0e75e5c5f55014766af24bfd6a39ca943e3a24044f327add17a880eedce7a7eeff4147c0e174283a9c9e7b1baaa703c5b34fd90d5864a92dcfe34bb5913611747135ac4da34cfcf9ab7d2ba21ab4c7f4a4595323d475eae6805218389015a02fa23cc1d38a613a769d9b522c328039d551b397795b0a173f0ccc826319bbeb67b811d14d827fc95461e7c7fba405ea94baf92e5164360cc17a7dd9dcd1392d24da671f8873a161dad74bf75405e1d80f3b760bebb0af1d5d03acbbf4f8abb9330dd7799b1e7d9c70b2e198708f91dcafc83d4bb9140253f7acd834aa841f99b9c51e4fd3a4cb74a2f0654e1430163bd9298e411941755c605a3ab0b59a8105ee753d2176874708d77ba6089ee1ade1640245a63a5cf7569e3fc12f97bb506857a1f71853833abd3b95908a00fa898be529e83fd05fdc952f660769ee674956f49a5026d80963ee611b4c4bcd825af8f089fb2a1a0abdcccc01257cef0316df3e27ebb84d55e648f7a576c8f33e4515f2bfb55cd8650171f690671e4caefd511416a06cbb802144b93cc83b41c38bbf6653280790e2d4ee33e4797a7e2856a0cbb6f400ce341901a9238a706e9851432c827d1d40b246bbe6b5036d16a6d6b67a2fb477fbb8b50f7708fa22f5eb56f2cf3112978500519ec03bd5ade973c7bb6968e944554fb6079a92df893bf7af4f413349a9d002a48c9474844dda0143decd05de85ea9277038f7d05c4d0303d2da5b0503a2d63dd330efdf02171ca622a709a22bbbeb50b353ee3abf312257e8c7e0fb340b2264c32f23f4c5f5cd27a04102d7b01e2dcd84dff772c773399020df5f38459d0e1948a894a6b9b58470b9898022c9227f640204a5046157c16bfb36171ad444126dd4c8a4743979c822a357e7a1c0ddfcbfa69327eb70f9f455879547b9e9ab07947cd83de8c1a2b2327a43f0e04b6059ba9c3571496e62db3a237874a395152f999be9654988e4fc3749dc31e36b222933d25b4b6b4e987c14f99ee950b2fcd7d143f6bf1ebb5218d85c0000000000000000d47b6c8cd5cbfe79d5f61e31368bd06eb717ac4c45cf4da8b32ea7c60b2e826e010f5134ea2382fb71b7e79e3764aeb6e4dc8ea352e1b66e00000000000000000290f5681d971306acd75d2ade453e148702af1b11d867a3e2f188d39a2a3c40ecdbb0f123d1619289e7422e30f7e2673c1e2c204dd9e3772b7a8e1b32c43d3f0c6b9a67037fdf738a861d4d7e8a79514907ccbe50a3644e1a0cdba4d892bf017affe71f170387d9f786ad251f269624e92e5a5b921c267a639605dcc17ce956849393283e90a197f47fee2832048e7790b4a3e755682f5905eb93e9149c6a021cb39a8da96c9c755dd4cf675bbee8448bc92c18187e04df5ce958b6ccedf97ecda3f0514854d1ccca98a3a35565ac57d19c151ed4637d4cba9a134753d93598d523b82091271ebe759928cea4c7b35a605def25340fa09f5069739a69e908ba91228e30c74ab4e614a7f91c0739718395fc6211cb0af02900fa2216b2afe99d177f13f7a9f8d599e10c4f7dad50483542012b4d22f05ead16cba7555ecc0a6ab561ea398264b6868bba2ca6614d69ec78843971a56abedce8df775ed9b816fbe6f35ae88e48778c05bed707f9d34471fa06137520c58dd0e0a847b11ad2246ae13845748b47997e8d8875326d6ac4ca42d4e33688bb751a27a098aefb924119796aabbd53525512953d999282843c8ea287eecb339558b1f0b7a548d9cacf10f17703ed1235bcdd69e3cf0a1559f970ca07f56ab7396e37c3a1fb56e96055b946af3b8975227faf431f0007294126f1e07058681a2a3f8f443dd9a3723c9f40a918f7a8f29d1339f88377cc1a5a0ccea3e5a7721828169063cce3b7bda4ddef3d5fe42c4cc8087dd31538b336126a9a11adef31f1948be59b804e24af68e025f8b28f0088d5437acd4175505dd5b44461c85094c6c975a50c5d3f1af5295c9ea7250cbc37383ee39728c2fa79ff6f1e0b802ac7243fe8e3157fb0e5b30350ede59eb005a0aa96968bed87f28da42fc7ef4734d0fcec44f6ccb217d3d2e0db823449bfd4896d5afeb99b90ffb797318cea501cff6e0506b4f8e43f9beaeeb403298e4da83213bea0c8b971d63f48eceacf836a5c7819f5e6d95c4eda92e1fc7ba6020099e2f1f5a45e3914fad0b3051b8ef81a2ae39e49f103eb832ef29aa2e23c8536db5559415d97dc9ebaf5ce607dd5c2b14873ed6524619689ae329dd866ccf2d628a30b42311236e24f1a6d34b937fd809e1c527fe8e1baa08565fb1acd3271e9cddb01d8c958a55b025462fd1866966a2a9afe226f065487aca893c87ee26e5138b6712f9cbb5c4dce3d7381910ab70db315339f00cca4650e8c8688d9f2dfac7400e8e82b2d9601d9cf59632d906a02590febce82098e28fb0fa5f81c20f451d175c217b4275bb6e8cc6737885fc00f0f7cedd20556a9ac72c15c8e7e225eb990e0250b8f0bc70bc872b56f7970062a57f230a4b5a846fd8757041e3bade0ee72f2c56005a78df2ce9501a2604251a3bbd5ca1f0790d138411ea4caaf83b546559c31796ad4ef3ab779d284bb2a1809bcc3799a0cdce96d5da913ae4d3070858cba0ea5d3968722ee6ed86929e53245d6e988f62e8d50ad790fcf36cd35c25f8ec03ee5f5c295c19b1b7f40cfb3fa31c6fff009992647948ec931bfe9414a37c1e13045fddd321039b0dd97b2d38bb7afe82e0ecde8fda59e602036c662de2079b3f0bf18ddf640c5524cd4e7b872820c4f652f9b4302bcebecdeb47b0f63f0dd53ae1824d5620453b49f59df853ecb55ac08000550a6b4580b7169fe2a5915536412e483230a0909d7553de0b7b9b67589ec69702835cba0d4cbe041d1162c045338d08ae31fc26abfccd0e2985392824f5290e562195bc29557bc34553e68c3ed93aba608726eb76bb966f4d9f731b323e96c02c8d613d825f9e10bc817d2706eb88a819897c88e1d0ea0c74c4d5bba75744c345a3023a3d1e7b906664919e19e3b653f37bdc3ae06e7fcc8638447aa6034bd816cb4f5cd5237ab98f5b659bf6848b1f979b489216cbed46ed4ba155db165337b243c8947f5066ccbf8472224d2636583bc325dbfd6b30a001e10e8e0ff72a0b25cf1381978c0109ac4a979ff1eb0c3127a34e3afc3d1b63a7ab1beebc05ba2eaafea4517f7952c2cc10daeb96b90c6de1c878903e414dc74cda830756cb533382ae757b8907e3b10607ea54d0c4fbcc1c7312e1faeefc71a1bf30613268badc7669575af96821e7181102137004707f04b662a409ff8c763719d7b5fd3287c2773ac5daf357948f1416309f4768ef03c9ff79db0d3af1defcc66cfc7d869de9c676b5073b34e0dc05c70969b0e1b4bdbb1259252716bb8e92db6e0cf886a17ee958248b42fdc478bf5b156f88991971b4fe7ec523767b71da6068d38a331f112e5896f048f585db1c8120ea975543613972304d67ac4507a4d2c2e5ddaa38b37b2a7930ee0e04ad4d583c5de42bc5bb96ce9a53cee09d684e68fa25e937bd7a80436d6797681b799f8ef690af165d82c0bd7569f2b3beb21fef93b73073727be47f23a7a56eb3280180adc9ddb0067d49207ade0e0f99fb85a5ef15d29eac8fe57b89305a6fb85531d6d23d56b5a2649ad30e62f96bbef67ff25ab8140f1ec12ef85d9b159f7863d69e148654e902b95db2ba9756d540122b7cce513d6df03dbefc7c542847eb8382b530a08d688b44b71d163173c6e9f2eb2d400c86119b81140b7022d33ff8ae51ba2418d40b906d8fab1e51ff3db7c473f1a8c1f433de3d1cb469f193dd4bcf696a501911dcfd380343dccd42b5df6ed30188c6cb240974f1651b73be232693d212af2e2d403c3cc1d0248ea2146ca119396741ad1d45bd378866143128ee26c27b9b6b7023ee1227cf85941f9ffb118c799f5699241c875187c8042770d6712db2b67cea62bef332ef07d878a3984ec2bb1502a53c4c05c3cc77b2a86f24eaf63e0a404dbb698d98f81a4622d2f4ad7af71aa39a351511ec19d4fe90566f2923ae4638fa03aa9bb85555a3045c2c72921861cbc4980f865d7893802e7f73ef0b2d3482c4b3aefaf28b426d83fc8a1da38e9371b1128300f7b86555fb3462fe26564a270aec805b14247537f4d4125c15d44a4ae880b7f10813c69c9d6d8bd9a445c9a799666eb03be2aa7766204e3a6b6570abda5e6544db4cb17dc268dd3b9377c10cf162e2aec11e6f23f264e51e9ea620e410000000000000000ca2aff5ce7619417ebef246a8621919c8d619d696ea4fe6c5e124e594de1d2142db6812a158b1bad66403f8e8f2a02a1cff9f7c4ccbd632900000000000000000235db33ce1a4b4afaa6c721ce582780000e22745ffa92838a851e7e27b3f1fff7d6d94defdfeeec0fc88845d9e45c4ed5748550f61973e1db31b3510b0d3e236063a5a28c1bcaf026c0bb2c7ac0dbe0e49cebbd13fea35ffc3c97d9350495928c7641469e2b02ca6ac0a6a9564f0e865c754ce775394a507d4809c630f3e8125bdcc258aa1f14d3409310297d6e956389f301f60ee25692fa7e20575415460c4592051827e8c815f2eff8b21cee4625e3405893b2738c9d6765e63bf1ae2864a075d29d76074c7753f6bd526fb087b849ae4af1a3d4529becf8316f4891e8548444ae20f3c37a9ee696dd34b98603325d0b279fc1c5883452e7d89b44621cb8fbe5680b005126ef7920bc1e3ad9c1fdb1afd110837af9e68986e25fae483caf120f62b0da34e5c7e9e7c0f6a22f21441f4ca60d0b4eaf2c03a1b19fa6ea5606d3692453b50c886d7958fdf80e51c5a48624de30d87c2d15a119b9310d031d1cc09b95b27a3dc3631380dae178f7cf494f50d7d891cde720022f7f1eca7355762a287aba7cc2a58d9d8d7fabdc07ee82e08875c7f827b327edb420e366a62f807496f8a5ca9032993a5199a2d1622aaa343bda859ad0ee559b3929a28a84dd65a8e2898dc0d788f1da23e4e2941f570ca2790555657b6a8005d688b587192e66c8c9dd3037eea743a528148d9d8b421ac0a55bc25c632970967b810c69801c101757082c0285afda0aa0e91fbba01fb3170dcdbdec4c15df9876907cfc1767413e006091f1cc8e12850879d2701d4143d8e21a6e7c43ffe0d408ff94d19c83f9117ec289a742e88a263422a8143faf2e1eef7ca41f8dcca1193e738cc1b2b1395bf3de758e32e98d7d2c60fa51295db771d3b45244f9b9952b4a3aff162bf26d5fbe4b3ee584263d44a005f805305a61417f7069ce3f34255c2d135e9d4140d5554eca714157b353fc99314faebcda7a571e6386e5dedda64afb641cfe3d57360bab722ec08dcd675cb6c91baac7cc6f8393348f302dac40fe6545ab535f765df30210f33d7fec2ec45ea87c78da999b7ba00056a0387545598268d6242f9d40b2c8941ca5586f9417a5707007c7d5928bc4a8d9b2e47ed832a50c778226f4fd3f6c45ae8e305571fb26cc45bfff1de0f2ee4659b0cdbd08cc9f32f29ba3391f0c1d036560dff18e87007bc4bdf6640f1ffcab35458d8bc32fe381f3d9895f430c44211fc470e9c70220dc1c42c580c17e3fe439b91318d0759c42847359a410791b72a1e2e079aeffd08f8a4c1ced58f1fe53518fb03da8377c097ca0057484e2aea890ef4bc3816697f4aabbf0c45d1d400d74ab244af2e543ce695ded40238af65be9a24398d47862d2fa543317abde3c9045e1a3bb31fe2e5c4819cfe1c19d05ff4a52227c45f47f4a9330f94c5f6c00e4f4d6c441cf3422bc1ea9d45b876caf3c083a719c841c823d148f802771c141b21df8656d385db2929704a4051a7572f747da5e469f1948e77eb7a66a1fa394137740f936b41e48e545a593d3a7cc4e914f11162ae23f2a0accb51667f714d9ff98d44b0e17f47c17d2abfe3a4bbb69fb6b261ab70025d0747308c783c638e1c36fec58b0b2a3ce463e10ac8e46af43e4719195d1f7fd20baeb70c70e12efc8f323d7f404f8c7d9d0149178132645cbf4feef877cdad293b255d83528cf14743f899ca11aee7685d9323c7d9ad2882d79bb0afd8707d366aa68a3003566f70bc46242d1f24321faf4584dfccff677d851be6716601719fcba062fee864717187751389b16894bec8867d5b9e6c503455f74362db172ecb9e18db91415ec67bd8c938dcfbef697c82f20e26887128e8d09ea419d72ac5f29101374b0811c7444fe86cb7e4537b23ac77da05bbedc7fc0926a379b74cf6b4b890e7e18dadbb67eaac1cdc5cf4906790a7fe6be8cc5e837d87c63406cd01d0415a306dc90055aa12fbe430200a73bd5ceaf583f996fa52f1d08bed7886578e5ba1c494a9358d6343bd6c107f5f13f65329e2735b0978009231948f21fb34d4b4a37373736f041428d07d2dab48f5c3f57285e5babeb8802394d1cb69af19f9a666f8360f39bda2a0ced0ef2dcfffd5812af126d3f182eeacc5fa638a0fabb49af388f6b207807a043fcbd335473af413a1f088118513a796d53e786ecf6647e75dcd602047e965fbd5f36645ebfb0bf27a65d3907edfb1890593024c50971ece217839e58900beace6cdf0464da236431f0ed785dbee530f2264a944922f234ada4e3b63dd5f94cfe9af4f7b4e11a24901a73e97b9d6de094a469a1359f223d753323f5b26cc6c41d2c44ae47ea8c6af63b613d3d35e6ee8872dd8ee86975334e898420da0b85d907935e6ed28f1ce4f199d363ec1bca60a2be8e79e51908c07dbe5650b9828c9ea36770f89f29697efa46c0d6dab69dd4d6e19d147767d1e477d907330d27d373cbb4c5bd34ecd88787318e08a61ee5bd9fe282002715aafbf41c159c098753c8b2f5b70943aadbe2b9eb5222e205f2e15d8f0a17cc113aec36be43ba82ba9a5935ad8d81ccaffbc6067dbba504c8b049a79b7a8c80400eaf6d386c33a5baf17c30842fa4d01307adcc973a007b469c56f2f225678a576866d240cb187ec138bd296f7fedc2b5efb702c8199592ccdb3efd1bc40ea316a295cdfd551bb6c1b280de6c9865ae71553dd447227374bc7a550141dc914485823d795281b3b0f89031039627ab534f41d83b7d7757897a5c100e5eb8528f604908b7b9fd4e70827ff0d4460edfbdd8dd27e23840f5528f8148c9b6fb748168591a6d5c27a08bc1aba3d7f6c765652c30f7b5cfad951b15c1cf3925e5acfd7022e64bb96737d83d9616ce73c4918e7fb5908df14fbff6e220c585bcfb8f0f39df43f0dc9f3937e7b41bc405808b8253089b03cf7e8ba3d27dfaa1cdb9d07657dd3a51beb18ac03222e4ff20d7addebfb67dd9028e522fa120472ca21063669cd86e31d8c36cf3d7ad1ccc0f8081caa5793620c982753a4117820b60857fcf96792fe10c7fb8d410a3e9a740fbaec16e9d6fa0c5c92c7ead6b00b14ffd44e859a81a359368ea68f27591bf1da1e7d8b1ad13646b7e02927416bd75e25696215364edfdf8c7aacb09fd90233700000000000000005d51704cd2877ad105b1c473b378e7b8bc214657085ffd9d0a3b6135ccc3d631e7f2d6aefcdcf59dcf1369c808354a688f0aaf1edbbace5a000000000000000002bdf4954b73b10c9ba03c3b0f4b72356be294bfafaeafe3d4d68d2755bca5c22e6afe5eb0ee83f777b70416b28e43c564a66e8ffc109bfb1f8782a4ca4f01b1b7267d95147e194ee09fe845e017ad0164d5288171ce9f4a3654506ebc8fb8b3af3104351793ac1a012ee893762010c69099246aadda7875defef53fbd89f41347b8f6796634a1d4a5e61be0b55a557cbc3244e3bd9f8b1ac1f27b4e40f3fb3c8646fdd8d7600aaf039de95999eec420436be9284d1ebc499eb8f6386da234e24a1a69ebafda5914ca103c5a2a755e1571ebe8148b7a1272adce37be787e80984b661762c6c52ae1769e36667d45aca66d350d4e6521a57bb6ff746823fde7771d33635ef67573c2924598cf0550df0a6af30094295da22eda38d737664d75f4922e7cd88218672a820614c6a10a63db77ab2ea5676e7d6283f47658c8a05bb1f02028698d797cb9eec2c8b93cf21f43926be22ab6584dbcc999204677cd78d87e63d3d8c180d9c8c634a88ebe9093100317cfc1fd223dd0eec41dbab36a268b5014c6a2a51bdc315ad27584e5f825d905637437b6763fb62a8331f931c571d67eead6707ae4d652666654a68fadad9a6b8e4397503a07c41eb9efbaa940f528e99ae29b19e0967bc1ca92b4e309ac87bfba9d13a6b03a70aa7b50e370785daf5fd27a391ba00465957f1d3ace6367b511d58369ed66e8d52c131a6bc26337ce2309aa2fc912a4f49d5da0aa199da52f7a378db434c37c5a0219a5219af051f193f80a0088fcdc8e08a7013c24e453e6470876123af3df07f648219d6418775ff1349524552a08e5864bdf30b43bdb7a24460dbb6fb38d4044c062924b179000dfdf220b97721eaa36e9afbfd7899f9862a0456ee92c7428418e8c6b625814b78ac73eb644affbd9f4eff43f3ea985cdff8bfbe1b24bc6a6a95a4ed9d9f28eee2af99545f7ed91492c2550d675fd26c3ac9d5650ba6399207b11a2ffb61bd8e6fa6a83e0f9c9295b5510366d011c1b0aa5dc1de533c30961cd79891779cad28465029b46ab98ee98875b2399670905662b2edba76742e482a6aebcb97739e00ad34a2929090b8d6c3c61889c578010353d042aa2b80d46d4a071f58c4cd78a7e61b255dedea964612e04108d1b1050c9453b2c22394491de4b6978d50120a33e93a48d0bd392c4a5d6ef77895ddfa98d724f803b114dc94eff3b5218c6aeeb7984b2543d6fa8f9bba7f075cca70db1ff17bf9d57157c2b133cc0d98a95a4d0a43b3309f81975dd333ca8ac7de86acfa8ec15a79bd2d7a49fc26aef980008022261182b40b80a115949f08c6408275e78108ac27a36828cb080f623fa0c898eb84d02058a2423614eddb4b9084164c1ce8e4eccbd73e0b66b862252ed6d9b82bf6cff64bb88256e8aea55dcf847b35a7db111ce282f8f8a65d89c758c40104d18fd801a722fd37f78ac0bc5a784a28dd0fc7f6bf45a5b52132f161846b603d701e8b2d67b533a8cbc288475d4beb59aeee9fbcab4d05c6bd8c5cd8b07b7549649fd50ebd0a39534e1117111637cc4a7d5ce7d9d5d790bb0c8266252741427021c16dab3664fa700d6911b51ee67861cf7e3dd216311bb0a8ead24dd193b33ccff043ccba04c02cc2f6fc48e808883b68f0201dc1e4078049ed9dd660fd16fcd4e5b8ead546133516a05137a560921e80d96f89df526c0a2c9ed71d92188b1d322ab3165ed7daaee2a2e9b5b9d03fe262e96c44fd109fbf213eeb6538715089be6a3d2bfd06b9e5ae6548e7de76a65697ebc727b97ac0b1079737c17a7c2a2e335c707c1978831e6b3da58444698e8a58ca2c62cf119d9f4e6123633af2f04e7fca6bec70562d11a66083f4335558ebe4562407a0fcc4ca24aa41c356e66b95bfd04fad3bc24b9d93520618b10016d3e4b1fb37f637233fe7e8e053954099f6958030358d7b3325a48e664bcb063e998ee7fbc8b65b4214905dd39523ffacc808ca25fb2e747e1ac5fffd5fbfe90d0dca9c3e8f3fe8b46ac58ce55724cdc33399b42c74f312d2a6129905899df58648f80ab2b86d392cfaf33eed6d95128dd0d504c6e1534c74cacec73545bbfa2777cf863ce4f3de50ad5f6bf6277460cf8d19bb14360655ec486cb7e9c3e00e65e9fe55d6fa33948b1e09a8361d304912fa7e37300419a106cd0d1e33b3a43055ffd4455eecdad712590cd87d5c0820e4eaaad9c9d4bf8ec7e746abba66d00ec7cd12184f314e4678e43f77f0852741a733bc00a0a42d35ecc21e2e2f2c9ebe907d30a94ffc3c95cebff27d0e860bba7243f3b820e5b0ff6c2be3c3dc5298b146689c8143e05380ef68e29b31af5ac948f813ee6e6fbeadf2e7b4baa24d6fd581c940b2d1ccc686dcf29fda5d3d2ad4e68668a73fd416eaf86277c866efc42445b9e461617b34fd9537a02ea0aeddbfb1b42f25efe4b4ff7dfaacc88cbc860307dc4fca36eac4b665bf1bf583e1d78a4d445a8edb72c45328219a14eecffc0e6ce8c1401be33e08d51e1ca3c15093420c6004bcc5038b4eb9e12597d6be6f99687325e9d092371dd800cd490fb488b72c7d3e18777119ac508075f0f0d7127762ed176acb9cf236f9f5fec0cc12cb6f0bba72fc28eafe4e30bb4677c50e12162a5197ab530f3d0d98a29eacc95025bb6427f6a3ab0822a3e5e835ee94251275bf0b3a02e2f41d017dd2d333029ca147a35819c49775141886a0815eb81e46e227bb2f68250150862fe85db7c4c5f47ba2209c8a528e71cc37d675ee2f173f42005af788d5e98d45b0ee3e2d45eec5d578a8baf0e2115b780426686a2be8604e41d468a199b1e1c4e258b4fb199faa1480e587cd8b9f49e941d7d2b7802ebaf80674c3ae078c0afc60424fef8968e6a642459c3754d44f106ae728ed7cef2b5d6ac478b3baaeb6f85be2f1eecff6f26364a003748c1c0a7c03e2ed41ddc1a42550a9363c9bb0a6a3e4adfcd44417a028b6acd8ee6a09cfa4f6939f8d9edc26ff7b1bb0d64e95d33a13cbe8acf37562de961ab8bce39f79953eaeb06fe4d456280dd27da825a87102a74b82d2e6e839c6746dc3d88427845c8a8e07e676fbb6f86824a6747a21bfa5289d316c640e0d520b3215121fe7e4355669384cd3fd529dc452a981aed7e0e00000000000000004d9c05b661da84a2632ee1083c858f0922bad491059425036d2fd7e9c07b5cb7a51bdfae464be239515a4087a94ccc6a183301a16782d127000000000000000002f25c34761ce9edda1be2b808746961614c2e3b05116b007829eee17fc69cb575b711c150049b0907c4cbde6423010973d025ed9a9b413d98a341c391186231e99b039f29b6f1a5b0e62b9f0c14310881f022fb74cf42dc20a0c062b9a1119e124603eb52af20fb2118cf43b61bf5dd3b66ae5c558861b44a45e7ea7bf1c7e66e1c389c5e418bf0c9e95b8afb79020439e9090ec36285df2b3f94ae605d359c48815f04b94ead2b931d92be877b511841024b08b275a7e742df9f36b8913dedd98db453d2e934d5d500ce15b9d232fd84de29dab1db449668af80989657a6b18edd9699cc9b7963548ef31999e2d5665de8cbbc7b396fbde655e18c7927b3fb9c3df047a08e902248866439abada58790294f04bd220fc0d18cd5d17fda33ba22b1ac2b00eff0acfd416e0bbd05e0e68d3faabe7e1870b85e23a4b48d4f7ed836104359b634541d4d803a94cf5102f511459d77d30f53faf8c1b8f2216f9dcc2b9da04d34a832c7ee443b850245a1c925c81fdb43e80a00ac1e397e71890a17804ea870c4e54585d243fa3e837140958d4e5a731c449947bb7fe51b28948d3ecbb9233d273ddb58b15b3caa89bf492ddced842fdb93a0aa993285d36893d8522777eaf00fcf845938e7388c13a115057288e7aecaa2daa57522d3545bdde8aa6b9f4664fe1036c522c00ff7b2c02b00d09929929c7dcfe3e156c1b45b4f5c54aa835c5bbbf6887ecec66475fcbff64b67a6fc4c8d86a2c4f26a98e61c1405c059ff6d19d19df254657e1553a2dff41c9641d831217fbccf84fd1c3446c8a853f3a1a83a79f2a6ed5b39b194f33fcc367602c5e658dd30cec70d9933b2575025b85e798accbac9522acd73ae881d12bdf1f85c4454cbcc433418cc506fa477d6eecaddc0c883bb5d5cec09acbcaa6eddc2828748784c20b364040cc863c2fb8561ccccbbec12e3851372f8ad3e4eeb699db61e9d3ac394813d5fc8dfcb7a1882c7b7e16e05556fb50f5b61efa1071023cb03d44016131e488b786e4f4a2924643b020a3605ad4e37b9f09c2abe80f12fcfd5389b4533a123c41d891aa05fc227feac09678d2ee6fb9614600956d38bc7a3522f541f7a42bbf08f23dda70fe79fc98eac8f144478fff57bd15ed12ef67a484e9e7ab4b7961317a8e441a743320d22ae0a8cae9756bebe579d23299a37466d9bc0c26909847454bdcb7bd02384a174f757fea8653ebfc1e4581d4eb8bc3b155be82149c168c2039fb877e1753c1c1ea00ba0ebdab8a0c80f8991f34d23df233f49742c883a8f5d465db6ce2d42fd264dfdf0e68e54136b31a4525bdd511e85785206ddcf18ebea4d3fb0370e703fbadb092138b1baaa6b3d0ab691b208c89d8a1ea68be8a4f3e97151b97b31f54d2e1455a8cc32d59bdca07e448c8173215757cbca5f8826d822b346723cea73b9122af2a23e30fff6bb92e19e3f7addf34cd7940949a3da53e478b361562810ba6df70991a9be7f02203dd8abe0fbc71d4199cf07bce7be679e361c80af77970b08688709142c569024d43bec92b1575c066ed87e4d0713709285c57248ceff99440d7b18394ad34cf3f8b340529457b0a16678de7170a387f308d6941286126dc10dbb09858dafdbd0ba5f6b8ace29445633d6caa8ce00fb82014073fd3d7b56761bf33ae6e46031abafb2d5e0f499f6315097c4d240d36cc550b0239f931e4ff58514c8043eade271990e36db3117f7e15e5556be791a33c8162e48c84813a2acde0ad1ec05ac920701d70a1681713165ad7021fd9acb0ed0e508f1dce96745b580e3c1a03bce8cc5617a76999023eca2403a5c30b4385bb609cb5e4e214932a18a041134d7053643372a420a9620e12e664bcfddf7ebacc9fbeb29a7d07caa9470b6c2cd7477386350795038dd64acebc15bc0685489b4f07b725f6dd6ef0b8788b111098ef913d9e6e7ebf18d31035376d4f098cdee39d34a6c5b79fb43905f591b5f2a6718e92bd22f7945ce9c5efc74339a55aad3d0290406b4e50d20efda182852ef862f846b2000ca5fd3eed17b5f6afed6f82bae5d83544baa5afe41050af50aae4329f0cd8c84086897b1200eb3687b85026223729cb374e29746919acba286037253030514ba5d6a2afa23f73240b114d2ad95627c0ce1d8e12380f606ade022d6697ce17c2dec3617fb8aa5a8d4a44196a1e3f220a4495539a3efd9ba4d4b20db60538aa10d2ce32fcbcdd8114e67df10f16c02c2fa35e806760fd6156acfc56497952c3945b5eda9cbac8bb7e671a0f2e4d2e47a3b60184470ebd2a77352f98c48b8d4bb691dca2fa6a73a483dcdb81392d2959587aff45a0ce7c704eeb98e5530d3fed02292575376d2219488b65ca1480ca27972d8fa483b8a7e193dda8d44197e61db5e588bd9b18318fa72de55bf29e41ec54f9922ea8c71143480aa1d6b4453e9e0642d8fe0cd8d95c2fe87d4ff3ec736301e19c8b56b709b05a2aba52f6530fcc17ba8bcbb4cbb311630ed5a493ea90d387a6281f2f8dfe4d60fef02cf27c6bbf9226fc61d9fa7555aceb8fa9f7fd0e6b9f4fbdd02e5f250cddf435ee70b27885b947c23f33420765af6323d97fb96f47bb86018fb58d4789b864c9eb139772059415fdb244338c2bc557878c40e22cad902b7312a5a53c52790cf06f9f8373b11b894fb8fd08e8dd9ef081986c52b23a58cc772ea781687a9af054ff4fe6f62adacc9c30edee7b74ddebb09a551718ccc0861f6795925a57781283039c4350b1e1250c183fc3c46aaac095674cb3f423fb80e057aeed8d098bc3b7fe3b503f57531e8961cd547cf168f5ad053325471dd7e6b641e5a4a82f96b6d82f32a264781669f1c6777e8ab62e6c1943e4585ad91e66e30213bf8d567d0b7aecc8c09a04ef7045fbea5971bf45e9470a77e83fd800324205105661238dd40df30c3c9fc9d8d7e3aa0671dd5cc0c5685abae656c237d028fef4d0909aaa3d85769bd422198825e50ff78ece8571b0c94e9bd047b13542288d1b462ee3ea648ab44cc6da636e3b15b81bfb280450d6b8bef461a7d3497bd099bc6d0c7319f44c8821924ac5e0e12e06da48974b2f9a517a75497e179eab7427c11111e739b395699fa3dc58eaf4236eaf80461f4cfa4000000000000000005ad8cca14baddeeef20137185632cba39727fddf18435a6cea970558c16d1520af55846cfa45b67d3295c62f0c5be3f73f828052543e86f0000000000000000025f7206060eae3aa6a7d8e7646cf054be23d40b50b99103b3bdcf99f45176f26c79cc0cc665d6da0d71a73dc0efdbffd252473f83e1afcda6427390bb568c084426c3ccd5f36a815e53b6b18ad9a1ad07c27e617a7a311c432549de216bae05ff0cfd65fe2295378c63a103b2ac8e930347c163ef0e716fdd4d53ada1dd1d29d327e6ea253a53253aac013e8ccea9ce0ed5d2a19b0fb6402a4871b56d91677461507273d73d6b9fbc029b554efdbb526018098a2e1f66a043ef66c57f87639466fad37c58c54456bff0a68934e924a1a12e79df09b362051baf1c893e5ee7c9f23984503556a6a379b06521bbdcd5a0f78b82a1125b80bfe4b8ddec3abdba8fa68420fa79e5ac2610b1edc1178b3a9b999086619f6bd5208e3005034eea8719ee1587066c787f626a4343487c2e763339c5ef0998e01152a81639333efd0655c4ff6812da06718050ab0519746fee5dacf43524fba2b0272523d6050193c26640276a985eb47a1b66d26b2ff8f7bbaa8af295bab5bd0d2292e4b52f8e23b56c989a9886015780336fccf15a8df23d2c7d21ec3099bf64e21565471b4af4de975e752ba3faefa489ea9bd40583b3e5fd1bdd73d1bb9d7b0375f8091653379bfe09fd6147efb90d842892411e09f188a59eea57714871678d4707ec76b201f65bc6587ed36c1e7ee64013c240951c87aded20181b096628f39ff015c3280a1bff419eac680f63201bd82e6d8e1c324c62c194aec2c1a223545e8016aa3825d986be8b64b45d3a7c2284b704a1a944c5989cea8b8f034083e09d27710f4a2261e08ed14124ce75ea9fe582f8abd438c81c9b1213b9ecb3ece66a11c7c06fe230e84584f2a9f6d6a7336e8f9238be49a0d168335d70a7173876304c6aad8bf5fd72325d3b6ccd416e5388653eaaae974db424289c0c1ecbb9aff87ca0c43fee3ca8d7a0e459188fef4a67f5d34b044b227a37f8487b731eb124e3e8febc057ca509a144a9c05880406489bdc863e45b5db6f47e5aa14db1bb9236f9cc1d29b3b5367c02fea3a91931cd02d9f78c55bdd8a2a678d37bd629083df4465caef6c536fd83e464f6051899f89994cc24eb56587cfc667966c24120661f33de8c34707c978e5caea42f0e23b002df9cd9cf0134ca3adbd1b9235474949540235874b80cc8731d12d74c3d1355c41db5a664a076d7be19fc1d1a7308f7824790894550bbeab0c5d2596a67b9bd3361792ef1899e8c84a5a99d22c4a47c819dad0be8ee3db689a5d12309e2909cfd575f276480fbeffa6ced60f6d43d3a5988efdf989c821366fcc4c96ed2fd0c2c074998582aec79b8b1ec9f6f90d48776840c5da433c254adcfdddd2356baf97f92db687723cc521955bfa562c33a62d3b97e726d7aeebcd57824bd4bb4edc72fdd6280770848f6643aa14771eefc5a5d3fe9fcc5fc3fc8dc89a4e23e8a509e127e73d2f7e63b686140e7aeff75afe92233e3fd047ef9aa60eaf2c87e52eec6a1e1c161bddd9a24221573677cb10ea1936af4f3ba8cfa8f571ae8e731c8899b62518a75620c0021f98cb4021d395ef5c128a99a69c61e0a9e8668270c44233fb926aae5aa133609dcf3681bb32eac4c8665ea0a417b14a7ab8845a272fdefd53506e755b33da6edc7153435de0ae4c1c34caed13da1de7d408b08e9a3608e510758ef6c7cb4c1f24830ea7dc1063bffe1bc138cfcba7dcd45f46eba6c33a824f0923c4c19a4fec69496ff2fee6c3c369e15b1285c0c570b4b9143b1f7650d25dcb4bce709dd34f85d826563d6b2f908a69c31d7aa3488d7b848a7200f55ed1931da059cacf8b1e32cbe724aa7f15ababbf0103edb2716c54e03e84e3c5295ff3cb96310066535eb3165629dfb3cee989525fa3da57282ee6602169d704354a727d2897ae9c3af399d638af71093abe68c1308d2f23435fce2ef1b3a7212308ffcda1bf4552a218a93bc33b056af7b6bb7220c7b63e089785dc6c648172d907c70c68523cec99904e6f93abe64cd007bebc603a14f7dcc9e375ec253818512171d8cc0e8e2b414a42c9212f6e8ff8a08116f999a2e953a15899bd675984644f5db9b35b0766004ce7c894e23bebaa18c5ba3be586ca2059f61b9270511529fd870df59c8b1844e0d2e7423da9c3f370d5c4c2b9f19a744db14e858ef0ec9bf4dacdbb36b2c5d49a0905a891564d8f5dbf2a040b3c226010d2a71ce361854df660003559496b5e8b0e34108d6f3f87a9c8c0cd3f3dcdbe30ca20d799c636c24a4842c0941f2b7b96c75d68aeb1d8157df882e9283b2a9c2cf8fc7cc221e24910c47e67c717ddd212fd7ca5ccf9aaff972c654e230443e20a029fe663589dc2a031702b0c4a45064b9be3fa893294d4ce079b3ddac99d752fe3732df1950418c5ace313ecc93224859f9290e594f198e97e84cca13761150126fe3c510e6dfbfb6b7808dc0848cc09018e07806510e08d500a48fe23dc7f6813028286824de8d93e8c1f53d89a2e6c6e8be9b5c146883783e30326efbc9b5c100167f1d85cdac32f52b95e37365d7a221b9fc63f3e674b197488505562d0cb55e83b7e8e6929c6f3fd21c1a517fbc061881e264ab4947acd624db05df28c09754c71dbe56f5217ed738820d34c2cef2789fe5df5865729fc228020fd496b104d77e636374fdaf4c3bebe7907b7b590ba175c0171bc4ec0ea7d8c27b0398b8eac910d2091127a5df9de649b4cc96112b49bd66e7a4cc8e223242a7b06c47bcc8c4df567d4c2c3916d3f3a093080a56b72dfb29e03c48a48b1a05354c947c692f47ace2140af49e71bd939d0f9d0c3592882d02e9897922f62d099036e6984ebae4fe490ff2beb16635dde89b4f3e059aebb0426c6f249715948d68b91d00a72d073944f934da73868e4d36ec949facf6959acebfe98232d12e43f05d35d4ba825a784d19c300a1389de5d2e6381f0845ee06f2141cda4ed2db7f4e02d2af32a51ee6a5330410958d7659fa9f2d29a300e3e8dcf0658ca2b3232999a746ae289c1918630b32163d2f6fd990e13d0b888f7cc084360c093f9bad9f75b10ca3e2a7822e72f506588eea81b8a344dbcea4a638f6cf25d03118ebaeaabb77a9d6b37b7365f6ab52c8c186e4f45f97ae92bb4d55cb98d00000000000000000e2b0737f5ce766262f31ce359f8b0131d47447b7c1c1833aec1d317af11e955c347133c184fcdae8ac2fa832a19d255f5aa701030c70ff4800000000000000000291ceeaafee7930b6739a904c0c598fd8fcf11e0603b4ad2c081df35cbcdc2f09a2824789f080f4d2544f1d4826299b6bc7e6e978f91066e0ebef3f74efcb4dc0c32e351b7bd614313a8c09b718a86a9e496051b34abb0f950f05585a9012e3b0e29077afdd5bc3874b57933b46a885d58ecb5f3a34872880f1fd0e8304c683167c2e8e3278efc1b8234b260fbc1d467110d778e049d10d180f546547484ff546f2cfd285d948022023d328fd2e232874fc53aa328b29886dc9fa4b584a9e56a986f8ee008804f9992fd9e733a1a7f4c9949cefe8d706d5bb4b042b25abab82aff84a10a1b8e8a72ef3df84ee7d03b0c1736fc8997d23028f11a0a0e21abf01832aa5146f410e17ae972632a90b91651ee4ca9f60e45dbab1f313e205aaf021a0b63ffe1e507a3aa6090ae56f8e66c1ec97b5d012d58d5ac6954ace355843ebde024346960455ed9eaf19b151e726d7dd4f01cdcd687c54683b6c6b019888187b528cfce80034ebd1aa0209a3a272c1c9d8b5887ecdc6bedf75b148cf229c1d9111bd62029a2d7dcdf154ed381b9d25c95db21da18fc7e2031cdae1265a2de75fc3cf5576baf969ca50bfe542fc69385322c524ea49fabea5d8415352906c34bbb46298fd035180aeee8a3fab5ead0bc3a1606e5e0a497f9a15a5545a290038f450145bbfc917b0ea1c658f4ea3b00ce957458cff8fa51a464fc2cbc32e5f2234617680fef6de13e56e27c27a08f8984921c9b55824920c79bf76e78a90047850566205f803454530c9de2a5e529d255b9a42443068c3f1832147d229ebff8cd4a5a5f03638de04cf7cd23cdcd365668399b5dbd09c30b7d5c6e845b01bf3c5374bcfaf402c1a93d0a32a8864d3a0677e802ee20ad56450a2c90aa1e281b64cfc62e9a7f4b5198811e75cdfecafd933dd3081aa5195b3717b4bc21fc071caee44d2aa3dc12740ea5b5d86a8304c4d7f04fd77fe95bc5da48b6efc807bdaef2abca1a33cf3e87fb4c6370d569a0f63a04584848f7bb2796a00dda196dfed3e897c02054368fa7db103def877f658aac4e4ad751c2da99ee8408b0e7ada985527cc4da636da38c8bcd01c0798bbb767a2cfa74ecb33016a17b1d4af63a94ba12833ca47bce7bc88afe1695e97c41a3c53e02ed90cba6eab1b85d17ba821250866eff3a0245c4e1e3538de35e85146456ce03740dfd7c2ec6b839ec2ea5639b37acad62294e53396e3c4465345d5ae08f79fbc4251683a9daebf306132d96e4683689548c0d160e02ee0c8a7b96dacf586f8ca4d5a25fa27220554cc5e85643b42d7638f8faf6385b08864dae4c21ef5ec7279768f94f4b83d7a82940fb3f21cedcf5609274047c75c1fefa275afec4e3b4fdfe8d0cd47a6ebabda1ccfb97aafd01d2aa4c687be2d4e53524d7181a0612ed829b3ace32cfe619615ff591694e6a5e20b81def8bd339c905977f3904ce06bc1e11ac70a7d1283527f72af6ca8df8e9aec3ed8dbeb60e8c1647b3da0714b33592a0f0f672aa0a2513ff01004090319048f757f44786bf516f03b395ce1ad85ad7d0f5b131284e39f7d5d880079045fadc363b919772ee81cf423fabeae4abedac19a166913f6402100931af2ade98adfcd2efb1d4832795331b27d704f42ebc981b1ce7ab21542e901eb87a9f3085131f1b82d1a6d60065a1a1290dd133c498fe42ce0de932d6b27299e74544c79c4745c6107996a33101db050a7bf45b3f0a72949e7fea0de0b18ace7fe9d2dafe322f2b62d15f911fb4e7329fc5aa0faa4014463daf63799258dfc0f1a75ed278bb92f7f7c2c30be873d64835d92afad8b4ea91a0f8beb60c6e07a2d2173da5d660bd994c8545be92b74151a7e8943e5e769ec82a46908bab5f2a6ce27ef246cde5e068bb2865cc258ccbbb1801fd7c29d3cafd726a5a2268f3ce968dd99c763ee80df7e8380addee805c6f55a05e3f1ff574a0355168fdda2d29fd7010f00fd8485ed85bd66cd5dfaf4a43fb36ede246eea41011666ee1237a18ccff861586afda6e8d9a6fe8b595469da0dfb91d334549cfa16123a1894462b5b29465400e8bec17106e015ce657c9121be14040220c0f7798cb8f301d510f14a672f5303c14bb1d61dbfd83617fa7272a77069bc3a95c57a81b7ab2ae01355701279f78e98f2ea0ad2d71cc0f4ca9e268be0d9c4bba30bf3fbd5019efd97ed0d68cadabf631c9baff657108a65ee184535fa711b42b85ad22d15a9dc8c71c7c91600ef1e927e0a24f0c5f927007e2189fd4a97659eec06b8a62437e3dbbcac73c2bfbcb8a6680878205bf4a6903b91f3c05eac64fbcc15b1a69f0da43ed26b5d94719b50662444bd090275c4743f7947ecb89469b1f1b8ac61837caf8cae7ebc97424d6823710ec65514265053103d9db4c7d16a9a14f5dc165f3a36aa640ee145a215d39d8ff648238f5d1f53305a2b4cea167e24627207a0eed13d6e9b32b572bf5160d6d45b0eafd13342c028ecfd7b3f83717f480c25a860b5402feb6a4fecc029ce700bbb17ae4403357f537fc35fd228ca2c565beb115ccbbf3eba9e263bf8748f8d50c74cf8cb36b40ee6dcaa534421fe380bdcfa984cfc89e24aa17ea13d0f2afbd34b35e40afeaae4421a94c6703c56e64b945b70a78e51d510d1ab023861ae872d79d0536b19700bc69b2582e917275efaaf6319c2294b44e10185ad8b80e8faf36c7a27c92a413ee6397a7b3d14bd60e09b56749f674fdcf53b016dbc4fd950faf5b632c7f4c593cb5ff394095d953bdabbf3520d8d49cd0824f6191514e6f88630e76ac00f2d3e20d14940139cf4338d66a9469d979e69bb192f16fa5dfafb6ffd34cdf4b31405ffe8958d206d1aa898bff920113ced8fa857c45d6069af13b7ae3da9f3fd412a7a0d2b10fcc672888ad97ff67cd10cf26ebc0ca85def50814f84543ca9ad249d4dd827207c1a91460178a350c4041cce9d8359f702917dd4e2724b6c81b7380cb0c1086fb8eb0e8f9cdedc8bd6b3a0664fb11233309b1fb91664afb3504e5b85242598a327e01ce955570b29c6d96e01ff5d35d5f4f81884a41449e7bea93252df94b55d696004590c7f0017416ef1f5a834796fd9eca5b9736a827097628693d5aef55097ceb64be56ad5bd340000000000000000597161792e8d89d388265f295f725290acc9b8a16a8b38ade519ea2cd718ddcff50790504fab882ec36ff064377661f485c3a43fefdd95c8000000000000000002ad1d1561c2b63b08ce837fc730d5dab1b5b4d03eac5182ce63b210f0b8d6f09a098714bc663b95b2719b359ad041515747299edc12ffffadfcc38a2d6ba54cd2ef4d89c90b432870ab6947b9f2f7488f305ff20cecb1a14467111674b0d15888834474cc4caec800fadfb47ab7f084b5cd0ce60367f99c79fe05aecd672de0b5e349e86a84b4808d2dc803c5f923547fd33b453a67d8e5855ba1638686c97d4b5d28cff074e91095411aa05d1b596e25efa327df73da335dca79828a9cd2da4b484be9e05b1e4f691f4aea89d7e729ec4b6be9d48b1c6a5926c53b5f429eb51e44e3ae67c7c6eabab323034ae71318e321b7b1cc65022bf7426dc8f7cae01915711fbfdc08882ac7043040214cfb7b79ef44e2eef6c9e47672978bbe0e356a2a1c8d293f5084b7e346da260be74e4d295f16353dfba2f9efe57937272cdb8f3ddaf107953a83d482044e0778f75ec26e6b88416c16b4337ed5b0f140925d7c3a1f3c0aea17b178f37df6ebb67a9554ce9843df9faca7e1e84d2824842a4a055b0f29218d6a157224130bb4b84061c0709b3610a5074863e11e58feed8a6003ba23db049a2e526e69cdf3a911430e0a210bd4c9755a6b9425d6733021f473d16a96d8568c45d0f287810321c60ec7f7ebc6e6085a6651bc359cd98b281fb6c2d15f113350aca5489c3223c70a75c84c510976c1575051244bc360bb3ebfacf5d0665ebf950b20644beb08f64814110428c5d9a9f8f193539c0c58fbe9a5defa12a31777376c3e1cd6c2ad78d7c578e141f9a5f62c003774de3ebf14e931711cbbc6c7fdaeb013f4f8d205da7cf8b83a104f2fb7782f807f9e742bb28cbe4890f4c739f47a007a0f1b3f6f9d39598d3675dc9edb4d445ad14da46b693860ca03aaf8a67c25b2d6b8a4cd417ebe1c213a6f11b019b26d176179bf3573d1effb84c11277e9a8c32a3f67ab7457b74ea1a0fcb214b5d4aa1298f8610fee80a70a02cf1d2cb6076d7f64c05bddd97294547b30caae12001a80768bc9b438090e752780026255403205d90cf61fefa766a59c6ee645331c78610b4f10cc87b45957aa1d9f131746095d0c4e28887c2e7579d51c5c04a9e9dfca40bc6d54b2a1b1d07abc51e0b8319745094a149e220fbccee7bf7e2390f97c77ea2e150e04e2328ab3e340c54b7eef554227d06996ef8cadbf304ffe5284542054d20f8ec281d05098da01be0b659f72ba6a3e0d0bae525865450a4a720a9ed53ecb5f79285947f3f9b60ea7a3b5da02e94b8a3de2d79eb578fa5244c729663fb4840182314caa04e8d89ac18890f0959be7b2ecbf4b5063e0fd0578cbbb29b1ac8dbafa28c4656f9fcbb696777ee8c0a8d528f5a9656989004876afb1030ab721a2c0be701f05f9ca7d04980d7af2b0a820ba630debef1e6f8137544332f66a1528bb292bda643d245cd475e28923ac8574af51f46a495e9518b6bb2cb2daa9cdddd12f35f0c0a40aebf0a1b1a51513f7c55ed3eb2c030475284f4d3cf369985ceeafae4eeba1d52c7350097c809ce1f6100a06489f048b6368ed24c35deb3e2528741bb412fbb067a3617f453c09a82585a94e2b3bc68726a05dc3ea897612c9c7751a4967b874a9a01b920be2e5654dbb778f0486d4e977b2c91184262a98a9ceec33ea910424165d0d2ad429a0e7e067fadf01621eee1ae66ffc9ebb1e1aade63ea18b1709e463a93c22f6060dfecc245bbcd095905030be215cdaf0d731f6d92fd9fd4dc195b42e159a04b64b3c30fdb804a1ab31d13cc179a8f693a7dab93e72d45771d6da0148c32b2982bc9074fa1551c35433ac369d963af8ea5772aef2646b4ba75f044d5ab54962aadd084c89fcb92cbada8cb5938ed538b59318e9deea12e429cd705ac7397de12c73bd42c0b2951044f7a050f0f4e95e43dd2ded4d36fc1c863ef34e20aad03ba917b0143597f8a71c398a552622da7db69e694f4ff4ab5490d1304983a9de4bae71ad68fd62b6b777a0519d4ff5a758d4f66b939080a178e3908b8f2d41da21ef508cf1b7027473cb0b967c988657fc8da758aef35803f7fa1343ad850f24a097633b1b659cef6c6e88acaa2e81f013f8aed370e0f8847cc4cfc2ec5d45dc7be7d07ad32a3525f4b55fa807a175a1b20714e7dfe8b0890662b4bad755a4d04b0dc29f5d8987a254f3813aa2055ed998a01bd86826a160b9e2b4c9283b5b96421f869939c8f60631c7553734920c530b0546ccd60ee54794ba06269ccc034d551d635d2aa0a682373df9e6444eaaf01a4fdc5cd51366e5471eb0cc0ac608c93c382b94d5e285af55f1c5d7fbd08b2b6e1a6be5a329e554f7c0cca2dc0087db32cfc8f6dfcf14c330b9f4ae5b98d05e537121962d8e8c2f07aaa767034862dc5868df9f0b234937fa51e5512e1f28fdf0e427143848f689597dbb8a7d443e35035fbbb50e2105a299d98ff9f5436b24a10c6e2f12770ea08ef907e795bc0b3bb8f7f18beea913befb7f238c7843929660c4bde2f37b93d079bb324ac8683c23dd8257361ee2c9b073a70718204bab0b42be9278f8d0c8cf8b37fe951fef8073ec32879771149126fb33eccee4d98669e27045fb3d08ed266371a53af0a6a7a30ad012dc5aa8ca0be8592119b0607b6a7f5c42117946c802cd923b7e8d4221bdecbe4c5247e2fc79bc56a56e144ddbbc06efe3cda773451f5e708885245c9bbf3d2470f8c92996660aaa4c4bcfa2e736b6cb0461be46bd45b1c6e9f26338ebbaf7b2307f32f8d6963a011f0caf0e129e8a8a01c3da928c12e4c7f11554d44ca9772082bc8483aae7b58138b718cbcce53080279a48c49d9c906d62cb77757d3f53843977dacadae213dbf13542a618d9e328f81931d8696111bd67900bd907ad47d5fcc03d31178d26fe71c21043787e3cc479465bd76a0d8d46c2354356d42972a5d6bca6adfa8282890c7cdb93378658ebff413577adbd02d219e33c42204a6f7b0d78a1f94c6a440783f2718e88c6ffca9988996724e83e26027ea84651203205a1060e2d4dcc5575e86d06c0e849e0cf12fb84545cbdd8e2759502cdb6d4da27d27807c1e7a273c9882e2c16fa65a10a475923854b18ce6711687882702e79f497174c3e5868ac488d3aaeec9c168f0000000000000000cfa252841d28b287f74e11b6240587abd6d649bc1014bc9de5f84aba95cbe4a355a91316d4dfdb46000d2ce19e752d3882828427e4f2abbc000000000000000002fa6584e749fb28d42734d6a4c60866fe4ab94df4839c16e0c764dd95f571ed4fd2cfc160880f00f19a08c4977bf658793c217a59f56d2e4bcb5592793888c1366848abde0b359625b2bb6fa5a892195e83e70307486a4d76736d1cb424fa0b0be774d0e096b2992eddc6d741b895dbbf3abf7ad54ae0038806e9240dc98a0aeadf81473d52dfd460ef0894e76fcfdfe0c39fe07c20af3a429e7160c3c0c801a2675609c1db3fff79a1ffae5565df18103fcea24a01bb8f210422b2c11fc1a903ffdff46c690c57b97a5fc201804fa658fc0e14d98ea8dd241c6d5af096e26b53d290f1da1b9b4ffe9b35515a9d9ca2e4431393649ec53152a55797d6a1d7072a9e0073173a8d8082c366ba7894789245a158974fc2267ced48f643eff4c285b81209434e82f317317e6c2f770e5553cb7ef7525166e9e0e0e53e46fb34a387b87e26650748d2bba92d8d87981e083496eedb650c9c056323050a31a04aae626708ea5545e350221ad8cc5665d7cfd683fc5f0f15c09dc97f7020cae7789a78b46bb5b673de299cd04df228b0e0f78462b94d8a1eaf3853721fa3d9b7f85d41f42a186b55fb1419e651da2b66094b9237c7294990ae73b9613643bb687376430b74af36594d41816a2f853b5abe58fca83c62feb254a09c1ebd6ad430f678cb3587fe3ac31e99d1039d62af09606237d4cd96bd04f8f239e1d8d0df0110548e2a5fa80ed1364c2d41ccb0016de2cdc47a80176a523395144e678500498eeb7d9087d2aee25e5fce426ffec3d6a447838bab9675c77cf7c43be709622cdcb218c568273e2e18347c4b9274a44cdfd2fea3e99dd05f7a86dd47784655d3577fc4af5a091f2ef2670a6656b762856f056cef59f2dad67d68cad0d44a1f294a3e8a673d1b3cb940dd9a7fca2b49b8722b65264a09cb7c75052e1c43e9d95ad90e89bb812ed3e15407fac977e1d868119e200831849f4d02bafdea3c3530b990886042378006b65011f3104f3c7143d72bd94b94fcd81d42a2dc43dc5811dc69f8cd1f0234918f47666296787b5acbb55e01b246a55728f554c314fee2665d5b9153e59078c70e4ee30c3a6ab1f50e61025bf1cc1ad54ea2718f576726b596eb7c8c035a1d03a65e1649eb97a7e176e780a1b226d1371c745800055aacad413270dcd669e8839a684c32a2aabf34723583b69fae5e3145893d11edf9e4e46a83aca848b9a5cee7b0874953e8d678bab54b390b807d7102e2d963855bb074cb39216dd977398375a2aaf0116b0dba96e4c120600ca94cc4f1708d63abe3671075ba545ff21358506fc38a1039aedcec353944f5f0532b4d0c220358aa194a4b267d60f1b12c5db0a948d195c0e9e12f160d07ba0cf8e31363e87a87c44879a760c810e7793dff9926d8c745bfc8d3fa434ef571321fc7059ba283f6e88ae86b0858bc85a0e216641980550b2da620519d656edca0e26f8cbbaaab1157af1b2f885a7f4a1b881d83e33b813aab592f1dbc8680c40a043052a1cb21f8e357e8f375867d31394f6b7c708b89079826fea10fd1bdd2c745cea536831e18c8440f86d96ee4d5cf1d21f3f57f111c6e2851c221b98b6efd2ff28a89b602f9a95ea107653ac2b16024e1a9a6bb44a9c605ff629cfff294755c775d775c9806a96ad05a34b52a018ddafdf17176cc3a892337794970f2c3facf113c27cb3c9cea68f2c7a61b7944a8921361ec3ba42d1c7778c0b2f49eb91b5bfd28655505e0d6a27a79b0cef9ebac71ec62591a1188e6f4e5aa110fcf3447c935a97a354e3d872a22b229dca65986a3167a6075060073e6364968c3cfff5d4cc1eb6b96ae8df78061436c1feb547b8a32d038783bd7ccc9521f13765d0fec9341f6e199ec7eac297ab5cdcdc0e9025cb0b25a97e4c205d3aa251d72f91e13657180406061c9b0172729dc7a4814300bf6b336f88e84f67800b29cfd9debc1047162756fdc65d89077a6c58dc2abf9dbf3f9db26cf31bbc90be30bdf987636b1c355d5a51aadf4182c8bce0ca9ae8a29c1213f5436bc91339fce663463bca1eed5f6020b2830e2fc27095c2652c9bb3a234bb3890f975a4b0aea9e988ef9ab893daf3371ca0cce2d25b5911cc1d28f0b2da6c0a941b0bcc9e5898a025d987b38b1d8f5f1cbb8612863cffb8c3d71c0816043fe803ca6857d8cd7efe22b1dc222ef7c3f14caf65a6b617a82e74b8729dada465bd8a25cd163feb5ebbbf3e81b0d4ec681c59192390d575e1f3d0afa02e1931c008dc5c1e60b91027d867d1098710a42beb312e2a3b1e918f27d6d9336feae273efe638a3e644604bb5138aca8a6d6e211a60f65536db456708ad456537ec3e0bdceeeecf60c6f7b45c1e9c628be14b7ec77a510fc283eb8f69cc2ce76a2811d5dd664690e1675275234a4517a7039b5c344d5128feaf382b03247f73b3ec824dfa27048f57e5a58e84d01cec159a10561e692e4d88e02e36ea7609bc2922a7ade8d8be40428341c723d96726433e67a816d2d4e43714de975175d56c4b157f436b24df3941003599830c9f1e066db758f8c8e6546342f5156bc8cb4034092395ff933981777e55f518994b4038a18aa5c3336c20d530d174f97f78525a9bf8d75d44a132c78cab69b9124ccbdb978417d05306f7602708734fce238b7efd5bc5b89943c78fcf843cfad5ad6a11aecd3dd8f668105f3db00fc8889632f64d3ee924d69505369593e274428a053afb5842350e90d4bd4683214e9cd81ce1c63d3ff8f35f062f0efeb4f2059992c7287357477f67333bcc15aadba5231f04c851bf434531aec36a94c6222dd76003fa8fccee32b80800a331d3269dc526254d52dece1f30969d93cd8776887dafd7f3f9dd585a315260573d70826847db28ed43c3bac7fceadd9026213def778001710566d79181af6ffa37a6fcc14f7edb61159ee68a7b2d5e758ab366dca148f4fde274280259bdff302ac8764dd339a375a932d4ade64ab89c8818c44257690011b22330ae3b08055ffd70410e146774da1addfc8a0e38738e1326cd3f93017236254910f1af0039c67327cfed088a5d9d67ce91e0ca6a1b7522b5f09cf9c61c7788b910e76e94cc260893cc177d87d68435aa51c2de05c390c8345476aa42161690000000000000000f6eb1b227779bfe73956e9fec504152b451039b6f6e8efdafd27ebe494c985a16ad56ef906a380bcb6cb2561a2bff2c1a11926c0870a05990000000000000000022748677baf80650cf8d6f4af4171c1c0b1320a075b1e780a3977b2d4fea91bfe501e2b128c99795b022e57a92b7ea7b0c65fa56464399a05c734a02bb6cc00e5a41768c1ea185f97bbb61da5c7224e217024ecfe3d4b5f767ec92c2d4e8ce326062fdac62c6e117ff8cc59fab265240527106e35130f10ce3fa9c97c5619dc1fe57c9d453654e225772205c68554d8d2035c1da27550660dc8b3cb1bbeb60a5de6f00c4ffa4f24afde9488fec44e5dffa5ae98cc810df27a06bcb953a79b4b143cd59ff321945597e0b99bd83c54bd4d677f03e423b93e9324b55da7f871f8051ec9ddb029bacf1af66910eb221a681023bd590040dbfabfd4de7d34eef409e6158f715db70d79c54549641b56a3839824a04d36f4687846532eaf05b6dbe52b3cf0526b1ef782064f0154c3980a2b7b0a6c9d84464b98d8311d304e97515307fbc5eae4bb7d495e1335e93abf905ea4e45bfbe057fe81dcd1003ffb6fe892c27f485153b36fee6cf2909a57c493431db63f991194ce91b9ff034799d053cd4d00a235854f2e0b3c7803d3bcdd71a712db0564bfdc295f3f0ab895b3825da6271ebc1be4ee9dfcccd21d4bb63db1d58914e669e9d873096685a8c7c864aebfac255de1727792c50aab3455701e514028f7f739f44862072c3a5fc541ce08cfdf8cf39177488462ea376d6df635881c956d596561cc67b1fecee416665d6fec7c5299c846e66734b956f6d25701f160d24373b2daa40e8bfe522e6d6462647cd4c8555a50dc022341b006cdaf7e67a66e8f43070923c6768f262820b1c160457058c89bcc1faca9b16a01f8b7303891c4e840da2f0a6b889957e7ebdb675f2033a16e4542cfc886813f5f5f3c9e7bd1bf95874975ceede177aa29b2a0ea5d9106cf1fc0ae1139e36d7fba314a5593f0d0e762105e3100996e74710ee47c7947e3aae0d06d5095310080d38891c61957d0330dd6deab53065824b131e0ae20b953cf0a3d8fcba4304afb760fe3746c3d05f1927929418414d2e1540026a2d349d802941471edf40aa8bcf1d3e6705e282784f649bf85dc4d88665ab1ae852f83f059131746095d0c4e28887c2e7579d51c5c04a9e9dfca40bc6d54b2a1b1d07abc51388a450939293448d32c50e20193b4058f431471e9d1e5218a28e0dc2f10a9b2f2f01cc70bb53cce2fc3857642817a7239ed3a506a2ca614d0c34d6df69f49059ade52667ca74f59299426ff975e7e4ceae1c47ba93fcb0d2aedfc1fc542ff090889a9115258f5a168a0d08800e9ec00fd8488fdc4143172e19785e8dc515da2764840a444c561092b1f179ad8675eb0f5c0ad2aa760661936707496283da544dc3d0c28e71dac6462124c40ff2498842ec03b4b83d7c68c0aef10392f87e3090acff9a49c058caa91cbc454be692387e8cf9806ca6668fca1d34b6c3f64285063458ac21d37295412666f854dd909a1d8def3139dd11b755b303ad99880a69fc744282aa0720623b5c620bf21bd6ddda89d45d0cf32136e6394bcf5ffd63c4c569559be3b94bee7e811c05afec5554b1b13ad4bbb5fe0a62469e9edccb4c32e8837eca9118372bf183784110442a62814d5b6cefe91fb758aa554309095477ebc3f742dfab9b5b0994eda2073f60522e47703164d727f1f6ddd6267b916bebe18050ac2d7e03ae0262f163c94546c6c1590cfea3a17b01b2937f46d6e0719ea8a8385b7c29f9b01d7cde4101d4278f5b926936f5193f204885666a20ae26695712142cd01ec9cfefe4026ed5d5c416c76a0f4f8dd1a18f06fc3680e9bfbd93a7df56e15c5789f09f52a69b6fe3c33bca2911a837b6b16c8300cb527d3d1d6efff93fde2a12ac78ca2c46cbb3bc021766df17ef146fd1128670d1755a516850fd778ecb2b0b7bd79407e63352f0512bd34b906ef5ba7e02df1a7d62d20248153f0a90ef073eaccee13d8297244642fcd2341a06278654dca01e4b55f98dc9cba129b41881bfbcdc04e8692e72d5ed8790699930109eb27601b254140b7fbaa6e7dfaed24ebece18b413eb45ce1e1327d71468a519fcb22d94d4117bed1507fc354457bdfb41285cdbc2b3c006ea5bac902f72b8173ff52c1cd4279a5313931eab15a99bcb760a22c8c887f1cc61867fc95c7d883007d47d7cc5a7d3a13fc13dac6a1df31be29e73ec1b50757319fe2cf928abd39a9493d015c3a92cb396c8d9c950d563290adb680359ee874479b81a418cea9058dd48edefa2ed8ab8c2276016e003339900eb780e49997aa5508131fafc461bde49d76a452629e7e4e17c946d4471b724173299418eaeb20573015fd6111fb169cbf9bc436089cc83e92c12e3d72ac633eb323f30df0405b9da5c371ed9d8d9e3d3b472de796a70d8b64a03d62d5661183074020d74025d5c1719143cea5a5ea8a75ff993246eda1065ee9384ecad4332338f5b85ea6595b0b71931b0ff074c80034e371e02136ae60d63a53abd0bef3dbd5a8b3cdf0bf6a8317da3bd0b6831a5af9118e6ea62d37b342eed267674fa867c8b3a337f83d744ba6580026322ca54e8328f5110b65541f50a9654baa4bfb76742d9aae8c3ede3e31982ee866e146543f9a684d40848488033afd1ef3be91e4c51fa50324e29f9526773f7fa96d89ba893c7902d5fba7fd965b33415404bfe28fdae71e4c2826447be75564e0dc56985ee256d45e708885245c9bbf3d2470f8c92996660aaa4c4bcfa2e736b6cb0461be46bd455caaeef97f2d996de5f9d96c1e624deaf0dfa29e5144e164057796d5620cbaa941dfe7f4ce134b22c4c06b7cf8571ea3e5b408bf57fcb9387c6de0f487b86ffceacaa5bba8996e72ca13bead5373e9e3bf77bdb39fbd656d0211e4764c50fe1aaae1ab0ba12373f62e1ce72cc053f626af77cd1a16d47a165a3715b7c34147686605f96cfffd92a6f9ce91980eccfd94d6a1fc4d8ae141e14d301ae70e90716702bfa6ae9305fe03f0249d615c4708d1ee0e437e417fced9275cfd687b376cf7ff26027ea84651203205a1060e2d4dcc5575e86d06c0e849e0cf12fb84545cbdd802b0b3ff3232921e99a9e3780374b345af7f910c8f2d04ea62633a5bad35dcb261d8297d5425e55c849893dffe67335b322b38ee551184a000000000000000007f3ad58abae7377e5d898df8bf76938835fbba4361425b08521f8a916e4f460e12e17b499d184deb863fc6468d8cb5a73fcbdff3b0aed88000000000000000000235ddc73b5a6a1b8a66464f6c55f7fa120bfde4172af5fdd9527f41bec9af4b81d2cfc160880f00f19a08c4977bf658793c217a59f56d2e4bcb5592793888c1360e72812e75aa180539fc18b84c6aed2c9d76f6b748b115072a59c3fe69dd937617ba354496ecba9a9d44e26b24b9ee7610f6462d2828b8ec204087b44d91dc7f31ee5525dfd5796b8c2a63e52ad6db6365cbd6254f9d411a04ed5478a01ee51995a39d5fc572a670d4423e6931e0479cfd08c96113be546c417d508e4b39cb93dadc1654a3c8e5bdf92d6391e76b366be6324ce5d0dba795ffe283a977cd68e3d80f33fa0b9f8e3952d6c8013d502514a59e2bfcfdb1dd89d09c23c043912381a3d04fe279e8bd7774841b80a8e985980157d4da463a20e5f074285d950023787f7869a89f1df73de6ca7bfe5e131015b1bb64183225defc55bd62f60152a32756289f7f559ac4f3ca5c1908018bb3bd3dd607243051ea57aac85716e54ae946e7843fb3bcf4f78bf80721f4c70968a5e034b4749f4c80bd6bf8847968749c6574084b24c120ed504ba5918cd43504af0a4aa6aa12b65a1f057d143bd131ebf8af02001d88f755073caaf8c3362e29e68d6910c0c2e2a2ad889a79953bac911e1d288e9c9de0fcde110388064b8e53f6ba54f9242a62cc08bf881e0e604342aef175c5c4f4f9a2397977c5cf8f977c95bb45faffa3f3aecee34c345322740e5b23281ab3f4939e7241ce9492965dc96bb84fb7f63a6845fa660813e9e3ffe2a9b7f62bfbda6ba28e0cef3035babe804d70eacf30fefd979a7055836b7f7b25d0c7c03f9f790c9be6b893f6a8d750894029fc85934c6f4fdd992c5a6e80f2efa7fc18480ba442f5bef678fd3c05583c8756f6ea478e4f0f5236a3984d0af5a3a1c3f97d728da2dd3c6ea6773885c8b4f1494171baf5c71ee3896ccdcc0d3144e001c7a4501205a10d4f3911f4a8ca9e95d4a3a2e644bdf3e628b53274d983c592749f001eab1d0dfaaabd57169ee80e260298593dbc14c4c895b60e26d60b744002d15d9276a75b364654d0e53c9aefa162f23e85694a3a0d6dcc41170ec42589e4b8de493463824c88c28a3ad617c79831e57da4832a0815ffcc2945c3a1e64603a4a49eb023576152ec0c269345d784ad075f9aa032912cba24f06ca2a93340a39505d311cf8c0f73d984ba4276e35df18441597750602bbea86564a2dab2165ba6ad80a4af2325b1a4c072e58b81a157ac17a48bfcecbc38d3027ad2035c3561836a885c31cb10600e87971c6410c05d9040ef2c466b315d73a9100abe2fa740f1875c727abda38dbea125ef07f2e361942402a0d7a51671797a18e2de0394e425f6fa74a42922d656cc82a831627a1bbf380e841fc1b842a64ca88f99b609305324cca6af536e0a93c1c433ed6c104f9b754c9f9cea276ae7b3211aaa49ec01e307f1864c36615ac7acccf6d666c7debadf69a5d172c3e8feecc2dd62666d165faffe2f0d73a84b5c746efd2bdaa89628ba2b367bfe2fb87df3bd918ed00aa103c06877ee64208db1148f7aaa1caff47d05cf0e2dd329c3ec6a130a854d0662d306f886fdac987503ecb52528d03b569d567345d16ce65f5ca0a5d7ab9125f4506bfe64d17b7c5509942f6b2d62bd271b5ee02304bc45b731aa6ab6e6fd4d98f066a8f1bfb9125528860af8c5c96712526f74756294620dd5a8377d8ba41fb9a2ed360648e80af45afd85b2f5591258cde9731fbd3362c475da1a35b74a625829472524ab833ec4f24006fc1455ad1da94effed82f485c34a5416d30af88eff6e79e62e7765c5d0844e592e936c52b8348524d120fbc880416f41722f13c8585b19281277332e22a9ab0ae01440296e9792a6ffd423e1c0140dcce2625675893ed528da8df307cc16d2d0e08ef416210c38f124c368c1edb907bd83922986026827772c5cb44ea4ed5e74dcf4567ddf0f5984a1863877330bb397819dfd535ad447f22c7fcccd78936c1e2911d7457c4d9d40f8cb10d2fdc778957699da05fc784f1f85c4d6bd094b1f5012282f41419662cd45a5eb512a4464851f7d9e05c5b65474311cf1ef72c42d28ba1625dee72558caff659c21e4dca28b0a446095c7c0359962e898478353237cec74785ab5a2dc6683e3104caae2f2579bf94eea090f9a1afd9b476b522f519c9bb4bef77a9fca3a0f76ed7d6c009b11ecf408fed507d34f11c9bac5230eb561fbf500544547aac41bd6f2d217d02e855e25162b5d27b729b7f014bcffbd06614dfbd7c2a40a91054f7f614a9d6f816876e8db4ffc9cd096c58e8f2532bfc777d91d8d0cf1728ce42ae9ba13dfc9cb729f5e10dcd461396fc4f684a9fceb96ab3c7b81a3fc9b7aff96f6d7227289ee3c9ed0c42a91d7a38baa0a07c00a5df3164df143c9e3161b8757b5dc4187d67f8ec18b51bb4f221832b0811d813e17c170d8c259328a06593a1ce7cde5cc56686e7c228bbd48b9510db800871c75888d2c48482ad1d2dd781e473f6d2622325391f5f960397957c5e40efd555a22def17e4d4ac8dc8236eb757a81e31e0585336547de48fe0176b004eeb26da76052e902eab4b0c7d993b91aea60789a7dabfafb2e635540bf21bacbaa92c0b8300a93bad2c9f6b3390233973dde5a44cc02f721c5643de8cdfa625304a5db0f4a71bdd5b3050ae659bfab73339040677df616596fd8ec2bc5177065c5cf42945df1c6abb413045acae95f43df5c5a18a4dd52223027839c204d33e7eb7d1bb14099ff2fad7e369e5c53c99438243002a2ea3c9fb087ff330d2450dcd9963ea9de8021cb9c50acff2ef2fd5997454d9537332f066a14bb9c1a7aa4ecace17da95f5adb610ef1ae41478fab9bda802a958ae4c215b0e34cd29f7c4cb6751c3603b5080e0224c8c8d5fe2a0bb60a568938d8766d5328b8550bc8893001f3907f1002d35b150df772231107edb89b2c1b2158d7024366cc5ecdcc7c1854c722635b08492c341c3e92f3831af5f64f63cace9a1de148700a45f826762d249e8fb116c42ede6a22a0f2c532c444439e114f74a3cd0ec5c89b9b6b059ab4e2cd9e07746abb21840071f48026849436ebbb9155a6a3ffb95dfb3f4b360df70289ff24be25929d04b3ea533e82870f00000000000000000f34906d2adc55d9d380671101bc59037bc4b7934aaf56e5241a01c5bda31dbfd26ea3aa0de68f574d422944ac6395e0f6d41a34c50343ff0000000000000000028ee1f212e16ed60a5899f00d568ae08f7b2661850ed8be7db3aa961828bd1ae843509ac1f928cf6ee4f2d589e86681fb356de6d51294e3002a282fe6e99200278c736296c1cadd144c67526f85b1fcdec09d4d31c563183d7dd7d1f78c3e4df9cb3010f70b9d61a40a5e68be3493514d9dcd2bf6e1f179201acb9e2143348eeeb58dced23c0352cd1097da54521107b124dadad1cf500e0a72c6f090ccd0c569112bf0059d1387f18e8b55cf26dd3a2fa893c0330218202ef0de998e6d1ea5ebe42d5ee1b4945a128aa4f584b24dd300acb62b30f7be9cba007807e70723ffff1f851e818eb9d0fc2cd9e13f62b249d81b79ed292ff6887134c2ea716ee892b6241f2a1a1d41d9026e0c2618f97b76c017d6aff7d513ee7f7bfff3ebea12da070138d5814edfcec0faf9eae97b91f0cd3ad36cba576979e89d020c4c9044b50c8c67364ed5750a2f7aa91680d3a48ddd47638be5c51a9e158732bd32a0acb76385145d4659d085cd7d4685cd436b132933b30231780c2b1a00c1bd75f140e99c8fc3ea98a4f421c915f8d081bec03640e8b5b3ab707385011c90ac1c5c12840af73b020204c45e658e0fabfb27f5d18757aeb0389bfe3364b0ef341979a786a699361d570b2d65c578b963e4f017b8f7947b2aec242971db6cfb0f980aa66531b6933f2dcd68e8d8e5e729f7d27ae113819901069683c435a847ad4b54b4399749dbfefc1f998693a97e74aa19237d1e3d8582de5d458576d1cd454f4cb70ba0d43cec2528146a2c535e9d8ec9524de59dc74b83b481ea6abe841969a81f042f895443c8bbffed5215af3563e11d7752e4ca7fcc6f2a6eea9fe115f62a2a87a23246fa5c7e710459268c4576e3b120aa7e67797bcd1e2bb429ea844096ac74a900da393f0757ad46a7f33d95cdedce1c8b764dbd36c2eaf971132f8d77b9129c8c3b9b809ea38b709e586d333091e84d584aa21b967438064b05f680b3ef09502c5d6d25ad87475e4bb1e56d32cce563a9a528b71c85b7528a14e518228004e902f5263980efe6bcf3001b6f1c1d2990edb5495e82d74451b6ef592cb41ae5a6f2240ab4659c50f6971af99f9edea57a96388e31a27ef9ef6765e5afce493412888e385486b50be807c1aa033acf7c6c2ace6d87003f7ae53d96f113a896820bef034de6455a5d77f7731c006ae59aa16856bc77102672755d848c12deb196974de58db64bd6507e8b4adbf87e8bb35a3fef246c3f4dbec37bce42ec42b2cafb816b1bcdf02d2fc2351a0e2af8c0bb2cfb6ccb9b814fd5ec0e8ed6170fd260729d975b19a1da813cc3438f25b84736bdfbfabbf5365b85f8813b4e298c5dabf4f282145491dd3cdc16080e1a5d1aeef987677c1d58a4bd8075917612136e4ac2fb9214b2b9529b38e66b497dbbbeaab1da131e3053dab8a4ef602936be9dc16762438c1da452daa6724cb173e2e5213775bef89c2a5eb9003d9d7d59ee72ffe61a34ed08efda3229cface265b1287cb4f513fc3a2969833cda68c295584c4468e54d74e619e131ce55f70762d2bf166f61a1357442897cf1e099bafaff95d2a592c39f7c1e3264b988ff6690cc9c791fc5e98e883df22e0db109cec9d3c98e3ac511c57fc158daf43588237d4880ca24e90da030a43c4956eca039b49c5de2999f333667e8b0dc2ebf1a518fe8beaa60ba9aba1693448020492ca3e6042a76f8a3cb6f7797713cc959cb4fb93210a8499cac678316eb473aa36402dd2667c066e57775001c13e0fe6d77296ef24443b1ae6ab62d1e716103837c5077cb5f39f1966db3afd829da0e9c42fcd1da685e304bab0d846c244f0878ed83d5619cae3896d57673a6f0e42893edebb535d12b2862252141e30323860c85a84add58e672b145ec2a6750437b88e7a831ba7aeb05b89b7c8e1771d2020245b326467d932def107e584afe40c2237e07ec0af3314dfbe80e3bc77cedc37e477e76829b8a92070020a7a0b8451c41dea3066058aee7bf615707b8d6981472c6d6ff8d9d8971522770e1c514aca07e4d4b4bedad440c424993c1b5a259494491bdbf030de346fd071089757893fbb6f7c23bb275028db5536cd5315f43e3ac3ed2d81a1f4f01e80474963f8e41d03f32b50b18c2fe245ea4eaaee4748b783fdd590c4111603d2df43e8a014df54f136b4d929d034e11e3a7936cafca38bc0fa61302eca8ab8e879926c0da989dd825828251e7dd12f172a669f8a6eb2a722f48bb7c6793ef1e4316d9f2337348dccd2f415ac479a948e086ada6e71225f076e169ee96bf64651dabe29dadabf834da809cf77fbd4640f585dca1fe7eb263bf19878e34ffc5cf0872ae9e3fa74acd5c50fbf1a97dbbc68cd4e4bffc4a17900d1622a9755c17a2410e7d0f260307161b9e44570af2d1eda36283b00e5d7c1bdf71a6d8aea866c574df92e6d2e25245ca112f1b3cc74f32bf7b067af9711d63ffc9c1aceddc813529c058a2b0dcca6571e92a926cb7c6b8947903f1e9ae05e76cdc3ccc96d73562a920b1989359d9f35304cb4be111e178a343ea675ede9f69f054def27bca98986625ba6b129646260df86dc1ffc3dbe055742c8e1080d1744b1a7547f87026be73129481e05615767bcf259ebc238005194654a16e812d0ecb02f6d909722873d8ac5afd931bd9552a6aa243d8312ac6397aeb810f15d2854f2f3cd05d59e94e64a7227850d35bf323c93de06e1c7654b141468c459385f9b7574d9f7c29bd21a374de0010bf189df782ebc12b6fe626d05193c9b25cf7e1ff3ff17abc8eaf0d3b8e6217640286d55d6550e25142ee2d6549bc3c98474f2167d116bbcff66147d67f9d5ed40787af6b006b812122e2f266ae2ceefc5dbd67d9b1efa61ac8f867c635befb1f1ca7a3ec4d8ad3b248d52434e6257f6dfdf2872b78cc7cf2fa816afe2c384ac096b667965340d277a283f98265a5f5f072e8e6f12402749d9f7bc89a755990df4584e3762efdb8bcda067f6f5133ef85f037770410fdaa1bab17fe1eeac71e68335a1ca44f9c5cd3d7e37451b74a177312d71564f20b67088ba88c867d129a15d471ba98ae1467bd3f3ebe2b00ac41afd8170bdecf0339670ba2e1f734bb23413a1fe918bac1777d4628200fd823000000000000000074d6ae646923abde6a05956bb91cb1691e07358ad5d172ce80de0dbf99a278aa9fa5a0f6769c66fdf6bce493856e6f024e5c55f45d115d7b000000000000000002535072fe5bd43f8523117b4efec941004859580b6c1dcb4a749a7323b717286fab974970e74eb9ba52da97ea98a69e6a09d70951cd8b562d5f956e20513207abc7ca9a190ae413e58e75e427a16f6a8b99c85c858c3d18ab5e7a8d1da08416e533c6e0692d10605c194b908cb93059808a2e28f35866ba6eaf1e2332fe91ef47e2dfb79b7f5d2e551099d195297e7d7e7398cbd6f8ae850a02841212a6af01d2aff688e9f64b1b0b852d90891145a74000000000000000000000000000000000c2670078ffffff87030000000000000000000000000000000100000000000000b0bf94c5ba76db33 \ No newline at end of file diff --git a/pallets/gear/src/test_data/recursive.hex b/pallets/gear/src/test_data/recursive.hex new file mode 100644 index 00000000000..988c5b14602 --- /dev/null +++ b/pallets/gear/src/test_data/recursive.hex @@ -0,0 +1 @@ +000000000000000060f2738fdfa203371a90c3fa697e56ba8f7965ef4106eacc162c1b9e04dd02fe5e11098b4c2b726b03506b1a77077fa84502877a28a7049bf59bd29c3ae7b8888700000000000000500000000000000002000000000000006400000000000000020000000000000008000000000000000100030000000000000000000000000000001c00000000000000100000000104000000000000000500000000000000030000000000000000000000000000001c000000000000001000000001040000000000000005000000000000000000000000000000030000000000000000040000000000000000000000000000000000000000000000000000000000000001000000000000000200000000000000000000000000000003000000000000000300000000000000040000000000000008000000000000007b00000000000000040000000000000013000000000000005000000000000000010000000000000000d98577b6185cc64dbcd2910fee39419addbd89852326319dcfd9d95a8b88fc21c6b08f959b83dbd8447a10887e02c16d74a1b73b330098201a2498a6841556abfa99e44d25cc2d1fab04ab1a85975d9fd9e29ceac06eda30dd5bbdc01ea003e72d7c5d9991a7dd39267c2ddee5d7e0eae8c0f558b9ca74499c6e486536b7a39ec456a4125d21e201f3b4494befb24da613507a9f80fcd70d2137684d117e1879a5af810f2865635a302815d7881f34e4191d3a2ed9423ca02c20e61065c2b28d1d3e4370692d9453d0c881bf78435a586222967d4eef46ba8841981e0eb8b4cbf031b2977e32c2c543543cab7dc1f74f799b20830acdbea4eed88a16d1ab1619ce17302c48da3e8e469d4ed2d917440c4444106fd973ab68c1c62ba7f61599c35c35ba00d23b473acafc704a006a1ac44839c7b5e8d29ca02d9a3841beaeb3ceb165f4efee076531c6fcd080b946205ce7c5fa1115e2889b5dd010f92341529e008ac16b34ebfdcce9c3c2fb0a6ede30699629e261622c539e6c5dbe8521ee802897033f55cf414cf74bc14ed67752c2479c01c33787abb43df09cd9e5a5aaf883d7c1a7c5dcc78cd8d5fce0e087fecc8a24177a049c7b2c6c08c583a91ca30da7fe83bfccd92b63b5f32cd4593633b70384b2bf2b1bea928525cbf5c34e6e637b4732aca6393bc54319d660121d90362b0fd1603297734ac67282d1fc173c54f4f68ab239910b50761598242ba07a88e4e33fa38b52f20a659d7c5f983068f58befcf257ad590a91d71d31c3f917439781fd2714e314874400c1c0e0cf762969e7b8c52f4c22586e0a3f2762168df3e93f12555598d2920e19c222a14a4204e14783b3912e798db7752abeee51090535da20a55c4373509000000000000000000000000000000000000000000000000000000000000000400000000000000090000000300000002000000000000000c0000000b000000a5207e145fc8a99bc1f40c5ee9e27854148e7c532224ec3a8da489d60ceaf9782b316a8b4d28306a1b38582b5c41b48b2b5b8e7d724b4129b87c078c4412a4000ae4663f6ada1cc59d53f4b929b3d1c444286edbaf405dbeb49e75dd744886b1b9e4876882e69fbbaef9d46874d42128d94548892ef4b01f447baaf2cf8c5617db79cdd02e8d1eecd94ad0cde320b44deb777f12d02016caf57662c869235e19cb8937bb570b7bbc9346d7258e7487f985af3c196f7288055d4981bd3bc9cd62ac66e4327ba8352c2f67ca7a5f51dca1c18570caaab6e3271120c68e9d674575dc496a86d91d2b6617106ecdd30107221bcb73975859a23a9ead2ddc4f2d25aa6e4a9c679b77604274914e0a1df5e365827c098cbbb3add294ab963cb877da4ceda37112669ceddb62c57770f4c39702ec4674cfda68b9477d92e80f25018f8cd59bee788515572e76ef2d42f780a42501339bfb72123cf2330d3719aea2b55d9afde01a4f5d3e117093e09ab84887009baa422631c51c6a0c97610c282ea603e8fe2eaa8f504d89d1ac4c055ae8aeb7f2f25821861dcfe477c9cc5aa2d09e066c3d80456bbe93bdf5ed19aa984c0c83fdae93de34d5e470c4d4e9ee5e29a3b768b6c3ed285e82657fff5fd262cf953c2e897ca14ae5b9e43d1e26c6ed1a3a77bea2864ed24d2212adaedf4ee05396d73c2d252cbcfb72ec1c36e4bb6f19e047b58fcbdedbce6892c435fb8df9d711fdfe3ccf534e56f934586dba88f78a616c5baa57084d805cc200debad786a3edf0243ea3837c6a0be8ae3c3528f6a116d46da294a794f1f5fa786ea4024e00ca984e7b268f7f43ae8f95138bce2a2973bd4b5ddad61ba4d6c2036be604d6c9b7ca9c70f255c51c8466f38242e80f6269c8e4ce30cb8aabb70a757ceb7d89d5474dc2a1d2f46115b3e80aee6d86a3c31275040813f6b083f9843e292d2838686cb429b1196385a4683f7f590955490dc85c10425563d00970b76fee09cc44d57aaafbc53d85087d3f9ef8713b7e572a1f4f1620bf25a00be3cded250070a9273d60c31eb8349feec550edc70f134441306934fc016f953d2364f876597dd68f6d3ecdd61031916552ad3fd7c261738810ba284a2294dfb7435f220e85425db8e2d555880ba5359d5c3a35ac9f31538b148ca1d4bdab8e10238acaab9ed74ae471c80c72c4c1b5dfd184a4c1a0902d8fc08cee679da0a6ea2b5f13f9029b1d01eb2f82743d9c7823131e8a3cca1fbfbcc1b745bfd5b275d1d83b25f221aec5969bf75fa644288819dbe46c9b8be03fc5a7dfd0bff7f2476bbac04882bd2d2b2337ed056cf8b61b9f79042954e4cae58b77272f14bfcc8ac6cde6e65f05dd00ba45c838cbc8199c761daeddf4d4e31dd317ed1bc90a103609f4e141184987b5028cd9698dba6fd4fa43666b9f878e4d65530d2ee3d36286f6e359244347c6b3575a2cd71cf0c40bcd121a30ec203e2165ecf88796a13d8c4d743f0276818878036f46de01eb445f536501581c77ae85bcdd274cbeca39e1814708e973f220400639d6ed89db8bee31c7866a00a483fec1924b915b7d536e2d92bee67dc6155d43adc8331ae876470cd526e7543c7cf9faa3d74177389bbfaaffae224e5bbf592e82be543fdf3e6355818753b92e27c88a6b30fe2ed09ed6b384a2d1cd620c2a3a919b997ad38e20268059b4dc7a7030ee93784bf66c1d390616055ba6f73c5a86478d1f2fe51005020935d872d4bd43a075958412b6b5e52befbcebb32928a8574f36f31b6c36fa1e17dafd3e0a30dfc7c78cf235c62fc7b388166b9ce310cc16aee2eb02e6eb531f1e844c9c6656f8ae43804b7a8b2ac89cb50e55511b66ddf1e36d594fb5e96517c464b64561bcac26acc4a647638a8316c6afc6e5a3db789acdfe796d775329e304454e8146b1a24df9d274dcc6e4232de115ec840f1587e69c339ab4cd43a2ea5dee841433f04c54c61a875f968803000af794b263c1e2e0972db448555820f813d4b5658602540b8c65e8d2aa91187d3ecf1cb49562a8b935bd07439b24f96962e75bd2ceae071763abf8ec45723a9ea4426bef34a58d95c8c0fd20956be6d24896b9b6476dc360721b86010b7bfccb61d6ed108b284503caa743531e73ca689f5090a92d35678ae87c3731c5b98d507dcb81f5a682001f7e89f5ab59a2c35f07ccddf6be9877f2bbf88d036b86aa67aabdeb23db74e08c3593bca28a2256cc91a97c9b80027ff808037d16d52f8d8cf89c85ec1339d22facb8d50bb345fc80a23d688a8c110a2384a71d205c04e094e5b8a3d36da31e34fe2fcf2fc32f0b540cda8d02168f288e6d0d90880374bd572e36ab4de26030295fe00732149a60d1ccf534fd9448166977241f949d4e49d7ee7c52c9e4195e6be84e320c62bb1912eb41a6310a4d779f2e2f571c700b488145e7a0306e42d46b323240e13cb622dceaa39e4a28bffda10e2acbf03674b532a0b5e71b978cbcefd864a0961d3cd115baa6d111a9516ca24e1c94c2c5cb92abd6d938c2905ca8e08dca34a632ffd82b5c592941363c2ad54357d91ba5693e2c6dbb9ca09f51aa0efc861368df8a1e5ba0be4cbd90b954bbe1c6e982c8735805e44bb26f3fb78ac902f8c616387d79a9eb2b988a03ed92040021d14a99770503d7d4074587a6c899c691efd726104f6979edf9d8eef9207c73fb764d32d828b7b0e32cce08008a1af7ed7fb3c7dd5a2335c5fb05940699b862672fb6f2c03369f5b946aba981d44dae4f4e555f940923a1d4559ab2649f417aa509d9f4bafc6f015ad09ea2f909e5909cd49583cec3936cecc7d90d06525e5f210313d2b657390e62bc9d475d1fe99784ee933fa78e00362d304fa562bdffeb9a4e6484aac8007c3fe84f4c3a3a085737d3ded2f40a6b0ab065442b5a44f555870eda65ade859f9d6e39420b5388544e687b0327c1bea38e926b9542dc18331b1c814f93eb8d7400122a3d0975f3d1d873f2fcb43b265f18ec91ddafa9b4d3a570bad9d8ccb0b4acf1617cce5241fdb186e47cb5c230038d44db79bf93c81e83f7fdbb3641ba46cee252d124f9ddbe0a53122b79d4fe62bda7045090f2cf0486b7ebd69747e69e8805d320ead3858348115686aee88e895e57debd63e92548a109ad64e053b98e07b21d708c3546c3a900668ceab2d467a3c05407c570c0ef152686de10e5a6e7d6dfd67c7db715628cc11e4defbfbde9674cba5bca5f22fa17b712dc74ffff7df68d84ba9324a21bb1e0cdc1769d279c72ada6b4bcfabe5f59ae9ff9b9b957b367bda17242aeb734b40ce47d26090eff3eed7b24c7adc64cb2688dbfe3f6f883bd39e0da9636721e3fdbde710c3cecd79fca2bce3fc00055d36b10d6343a831aa7e303b98d8f558b87fe1ac86817b3dadcb557d22e506150bce68fb70d4af8a141e07b6dbd62da573a01de857a68ac046519f6f2b1516027ad446adf1de7d0549364f7e6e4ba81c13edabaeec46c386ad9aa915744cbafab22aa08e45ec88bd75d4dafc7f58ad0527559325f35dbc5c9ed54598048a8e548a7102c960d448e0cca1289b030cb2f2f060f8c21312a2b7fc30a4e6ad02d7d116c699320c0e2b5416aba2731c53e2e53bd11a3e09477febd2586ec6f837204cd20105ee78553693703dc3244f2ebe0632ba1630c9dfd336e20e179dfc95e839473ab11f53fe25ac629507085a0c215d1cfe0cce9a416eccca81089365772f1514c51b96b0141988f4f1a893dc6c0dd2f1c281e379f72c78a0f66781a964edcc5a7f3ae59e16618e14d5ddf9951c7a7c5bcd15ff3ecbba851642a19df84d4d86d978920a7cf3c34cb50581cd74d753c2dfbccb5d80ffb0b653465ed06c21cd2275db4645a8db6782646d252418accbf68556e9dd67e7319d04cc5c3b16b32387fe1399fc05ee1f735bdc27f2263ad85589321ae7f60ade81a253f893f1fbd3f4f69c1d807e041fd9acc51bd160434f45c1a918f9ddfe4b73c149fa19d46d78f0416311c18250acbe44d1078a8336683eac9f8e50d7879b519feb7bd807296221fb9086e3d682b0c43894479904484f4014f088cc3b4595e9f1e41ebe1e2f6cf2389a0380c6d2441b649082745fb07cfe16c4a52a354604482d010c52233063e506f4355004c0ae21b3321fef71d574ac7c54e77e9da37906bc85497aa2b3fda9ec7a3b5c2206e3138fafb8c6619484c21eb84e453362eb73c4e6d7f488477676a37fc4933c242f9ccddf8677fe2779638b0e5bee0bc34f68b012e136823ccb7e01258308fb4364a1a793407ed193bdd46117009efa5489beb8b6718eaab33a8aac9b4a5f72a31378639894152cd08be18db596a4003bb81f8a86f112b6d2746db25a7c14d15f2f40aebc83f30868d88a5fa2aa7f9719a646043a625366912b154e5e3ebb0b3657c46f5641b7fa268cc5895b8800ef0308e12b3bfc96f8cb931fa1637ec003abb25fb3984484793156891b800b390b641df10e7cd265d9124dbc6cc4ec2a2c797d5c9bb7fb5bedae77d0aae7154351d65005c6c93b60248f6bdf1fec4a686171d6e7f7fc757d83d017eef2c4b28a33f03fbe06a856c2a0c9cc5848419bf025c745e74638518231b110ca6b23c74bcf34148a4675682447bc8e42d432016343999e3325b6352e99406fa0a0d218fc633395848cbc0ac1f69a9d60f6d7a7d470ef9d4f83fb81babae91bcd2b5e6d016a91914ae64d80e0c5092894c2415c7b92d1e8dd97c6516c0337653082e2b3aabb0e5960a1e4249be47817b962df92213dd0cdba27c44f7f43980639a8a33bc84ec9ad4b3ed118701d98d2e4ec971c235e4ad22a22a1a6873cca5c29d12125d0b8e0c483afb585a826345a9ba1a8da8d99da330c2e36945d87e77a9b300e6ad932ced43022879772d98f34436cfca31e0e21c111cdf4e8bebab5ca87c05c9668868aac8179b64ea23b374405510fbb3e1c32969daebc07346dd8f8ad3a4a43123bae8ddc8dfd05bdeb95cfc3bfd9c0de03cd7910119f95a9b403dd6d6bcf4997b8dfa5fdd85e2bd6fea7b0ab52e6b1a8a0bb9171ae3b527fc3447a8f3883b1f58930ce3e01f789bd039bcc5331079a9e946d29d53fb9c4b8e2fa262e8b1caa3711617e20d259ead4900e5c3e823c70eed27af3c3a75e855f099bf71a0103b35ec373bdeefe49aa24f4b065b36d71807795851fec84edd218c05a68a053ab6cf945b7b3a2c360a32cf6160f46aa80aa3711617e20d259ead4900e5c3e823caa3711617e20d259ead4900e5c3e823caa3711617e20d259ead4900e5c3e823caa3711617e20d259ead4900e5c3e823caa3711617e20d259ead4900e5c3e823caa3711617e20d259ead4900e5c3e823c3365445c2307d23b263d9328052e0bb2d2debae53ed3cb525d61a6b5ff1962d0f46ae82a6c1fa37a4cdf83f3d29d460d70eed27af3c3a75e855f099bf71a010370eed27af3c3a75e855f099bf71a010370eed27af3c3a75e855f099bf71a010370eed27af3c3a75e855f099bf71a010370eed27af3c3a75e855f099bf71a010370eed27af3c3a75e855f099bf71a010314b233525fa1b05478e972aca4b4c4f3b2a1a8872cee9096363d050d38d6138e793a9c532897ebc98561fd37eaef3b132fffd7e2d46e85113ee827289bf2582d8d334a2080e02c7c63a6c5a67ed5d7e506b8b9923a88aa340110bd348c48ba27ea68d290c6c0bddd8a61d6c4f3227ca700000000000000000000000000000000241740e482bee71ee1dd58055a83c94be9728f0db229699b9ef5c835bca059a22cb237ca9f07c9524e23b2e436621b2d8208f7b902bb4d921f725bb64debd9204956329af6fed01cb35fa64e650e724e10656b50052426ebc74fb75586ff5186cd06a27379db9f81de4fb074ecc984d900000000000000000000000000000000dc22730da65e6548c11f76de5f8608c41ca4cd0902c12bb4161a2e06c33c5f1d7011539a65461604bfb3222d3c36a5d74778093f1acbe2b88c2acfb79b85d6202e4d68c17b9b4a6b4d986981fb24b033e97bbf194af57ecbcc0099f8c43baee71dcf1ae8f7275cd31f1e4dc6014d05e6f1df033d953f8fd46a78555f14d31b676573af05b37cefadc45d9f64583618c97c54b4cd620f141d67e253e1dff468f352b148bba1c081e4de84e2d0b15c1bb4f37fb6e8788a06fb73c3ec8d2cd2b5fb2a4fea662e1b21966d38250bc9b388961bcd9fdcaf8c24f24486323dfb4dc8d65ff05e8551736bbb810e794b249218b805632172aa6123bac2e38f96b02df399d0029b7bc4dec8e8a8047d10e0822b68f9ea6582a307d1bed35da8d2747f0943a86f4b82892ce4dd5b4cd077c0406a25214d18a9f53a7077b112d4cd1d70b989fe5b8871875de2026bff2bcb0895cd16942e5ac6f26218c87d83a298c1d791d82022f0357b41dbb2ff8c72bf62ff86e2dbd53beedbf84f070eb87d5b270323c627972904136aea7e07ea23438fe963423c3c3ba72396f1ab82f4bc2322481c978255617c7dfbcc8a6253105b359b87277990f1a809ff465a4c34b4aa1a472c8faf7446703f9368133d3ef002d972bbf60bf7c7088c1e8125f8acc6b95e2b4ad081664ca898e7b05f584c93996d1e54c07fa8c92bed5e3e39147862230f93e872a8b28c891d4402db3edbc5df4aac6ccc3e9bd1182dbb4c895d06d0873686157d9b8cdde80c5c102db6a9bf0ae9426b25b8e85a1b56e06c6441cf9e2fd2fa9bc9b40b55de340c8ac1c681dd168bb904901c639d6f558da134e4e7113a039906fe5b177ab4d0257d6411bd2f41f90318a4564de5ac25d9fd9eacd7b7be93ea81868ddb31ce0d4dbea82234dedf92bbc676567790954f43b47fd32b8461739518b50698ec443c19472c00034a9ad6fb9909723728beaa8f71883625996ef1c707ff09495923cf573ff9420471c78b4dee8cf953d8bc66b4f107e37c93035e6910c1aa5c5025b538b45026174425279602916021fceb3eb5260f706e1354973b273e3b6fdbf3d68697df3b147327cf31a5b197767d1f446f2aafde271c0733d4cf3ae1f9bde96bd45733adbdcfea1d76a273869881e6303648dca2d33fea4043b61a859bf480a64a90f53ce7dbdfb28b39574e403f1157dea27ec6c4a919d6a48c9a03cd7fe80545f719145851cc5af0c43990576460d5514392caf24ee366617c0c8c5396a3e4b11ce165e375de0066f8c9ca8bf30fbbd4be68be34a1d18dcf6a5b08462432738961554f5502d3da16f949d089681055d60ffc1210ebdc529002c7712ca743c5aecd4ff9675bcf7564fcf2131df9e9ecf4e1033843a60b78f34c5126009c83c555d5610e9fe980327e39b9c3a7f448fa0256f9345fe1d33e35441d32791c8b2a4e14e08814fe00a076333570d42624f5f6f92593359c7d35eb343b4430a6131955c5c9bb975cf6748dfd3fcb7544aa15789446cabce24af15d92bde7c5a81019715c07e63b88d71d1533d2b5fcac10657138d9e4c3612aaf17dc5b97cbf9ed2d8e248c2f076128ffef164c2ee27890fbb807f9fb5fb6b1412cd38b0150696170e3aeb6760f7578225088d034fd50cdca0648c2602cc6890ce357ddfb04f60e464446530e914df7581fa0579d9f5b03e40872efec8cdeecf2db4b212bfa80e1e11aba133570b8235db5f2510600ae6ef75bec7d0ec4588db71b49ad27c8b5bc02fa3cf951140332e04a4feb6ec2559a00a409973ef6492a020e9d2811894503a9e2731a9bf7303d58775b0784e76729f3cb06950eaeb0a1e0c6365b9507d362364a18041e8a7758cd0659da041b1465e9dd72d2ea000bad72def435faf683103726046e49ff2d7b031313fbd9ec6fe0334745eeae3b82885a7fcc44ab30be84a2569812dc696d4aed6cf76e82d64ee3934d20cbdad17752de5b024461ee9a1c301a976319f4c8d5946227f0662589a734a40304375fcb3bf55a24673e32b1ef768dc73db654046192e3e7e56f85d01cc450ba754aa2c0c584f5266ab4323fde3a88f333db40ef0e77b7619ae35198c24793b12024d6653ecc45fd516a52d0533e22494dd062b9e0596d7eecdc44b59a60b455c14b9da0260970c6579509d5b210c0644fde9c57ed95fc42836f7dc947b62b35fa11a06f77c8fbb40e094a0e35f62551af14617821303b3cce5798f011703a45c3e56b8d5ce4e02d88671927c2e5b07b0804f3284d62f60841754140849a0a6532c6de62b60a2121bae0f09c84c4e95a7d5f6f3e761dbbbf4a693d419af6f5fbd2bcb951c8662353e13c8aca806eb5cda5d5c81761e9842a131e50692ec66567009bc26be5f2e8d22887691c5dad63f2f48c6dcd0691043745d1cb89c10c21546af564f8d9edcf04a66de299d5add633c33081282120bcfbed51c70ddb6c3ce3350cff9f015060c5d4ae786bd6596bd69d087b152dab7da251196f0914bbcdfc3bea00e9cfe7fafafb44211ffb312ac529a78624551de4f041f958c4cd6d536aad2bbb1d75bf156469b391a777522345d9c67440a2e5134c43f6b9e3ec6c884ea148082013720e0357824f9c92e5625baf9754acb7f63ce91f0c10d507e18160b0950a3fa3f65a7407c0f5051565c2731469de948c8c38d47e1b546111e0fc2ec15c58dc98797efaef75a12683ef651f161006dfdab165cb35fdb799ba8dd5af14a3064e0ce45f5361c9009fd4c802a66587fee5dec7226633a951b6f376374460d8a91ef51d3aefad7efa6e531c5b1e5cde6911b9f46793f8923dd6a6a29e7da3af47e622de46b93f763c729fac81edd8c23946a2b591b9d763569ff72faea897ccaffd9df08b5ac77b52163697b4f3d38ee2b1ba19a975b00b952182bcda9ce18a5c4fa48a242a681455db1ebe89e2a39b085ab1a443288cfceaba4c1c55a8e3a7e46948f92c596d6b479b5414e8cff19685a9d4dc9e48f282a21dc73b0f200bfaecaa18f246c0a4541a7ea2979b8af20257bf917263ea443da6b29c1e7d3f94acad3af8b70056be07c1cac86573389ead5bcac86573389ead5bcac86573389ead5bcac86573389ead5bcac86573389ead5bcac86573389ead5b82cf9ebd76ad9dd2b3966f23d5063d4b70d859ed6d30d77eb928a5b4d208ba02b928a5b4d208ba02b928a5b4d208ba02b928a5b4d208ba02b928a5b4d208ba02b928a5b4d208ba0206ebb4369418e85c790419085b50f8527a1fc6d5a20a097d80420a8d11f4a7c4a6d513f7ee90dd84fb2defd9bdd85de704037babf5071315447523c6e42ec55c14739546ed7d864ea50ee37b8b10ad5875925e6576c8cda77435acfe0a166f35795396ffcb4078aa36bed87b1937a7e9964b6f40af4389054e91766378b2f32fa695eb6efdb9a656a65ed85d769019b337ed320f4eb632652594aa33568c1e272007e9c4f53d2a79f6afc585ab46cb17e5ca1f75f2181d5678e5cadaf8f44143a983b74739d610ae2b9ec8c4e33958816815184242eaeb029c8c0a469788f124e28cbb238b1c6c4ff8a52f8c8a1b70e07ddd352f58c98aa5130000000000000000a91a8858da0c8c531d2270319f3abdefb7e55e4fcc069ad8267088e0c6af65ebcc6c1250d296825aff7726cb974fc197f20efefe2cd53a960000000000000000060f2ff4570a28a62f7335731682946150dff05302c082f0064bca9791001197a639ea8e9f117647ce0338947ce64a539b967b4089a25f74b41ebc90b239708149b380cc85f8ef702d2c57e04691173f384eb95630a3f431d518a8b7e7ce3044090b8a8c1de508a6506c461aa53f4fae625d0554cfb235ee619e4622fa9c6dbfe3d0e990f989340aa24f89aaf4f2bf65a1357c2b294f3f22a7b8025cbc1d586d11009aa3ee5e9a8201fc8d0cf0c5045de76b1a43925f78f9b0f368f98994bbfca6cd1c3ac7a4d449190723f08db342ce9bec407d1b1da33c66025d84c00aadc5fc0ce7686fa1c2016f19bb3823e9fb3847354c852698a4c80a879eb440860f36bc2af561bbeb3503c56a3404e841faf6badb47282d5bd9b17ed021db483b7557c189401304e28b1f65afd2641ce1e6fd7170e6f1c989ad69a6fe6e0d6bcaba1f2297c411c2b7262f03578fd8a5ee969a4e1e2c02e792e977bc24d22e5d22dd38dbcd1ed667ae827f6f5749452d512e62d987e91a0ab186b3ecd66a784cb284995e09a878689dc27c3a1c8a2c83a2a6b108a8c90a1137d62ada83e2cd4093906dd3a6ec326d8cbf04830e69b5bece8f001dbe1ba176b88b08c6e1d21add18cc82c7ca08d73c6e2e9e0506dfeff0a82f4f0935fbc61a5d82f0d5b05a7045ec79a5bb6afb9cc736444edf6010c9e52ab5b598d0add2ef1e11419a586462fa8cec6025f01cf58c3afafbb7a96918d763d2d5fe4c73f2dcce6f265dbbce8249dc4ef8062094c938499a3e441b813ce7f13afc19892392625ff1b5ab05499589b9731a70adf78bfd2a024d2eb979d358377b0dd48ab1405b1ca8767887b8b786c6d102e93e07015e65ea02abe049d122f0b0c3a61daefae0714bf12098aa3209a7e19668741f23f02dd95de920fad39d8812787c71eed1d4fcb415ee738c524f6b5fee5968afcc639d4001cddacc742a577fae986da827f1828dfd503cb9b9e92d1a27f0e7ec7412fb494e657540eef025677ab65dce295366ed7a3660f0d8e6d405b90c452eb02edfa5351981eac12629b56b534bc9e36d25376fd1c3a0d96bc76c809e56432ed0eb7c661745d25d89f6df8a03c6201d1fbc9637c6dd8b31fef103c6cf0c97a8850537b52ea17deb7dcfb7207a36a608b303c2aa9a9182807bfbf256af9d63caae82ae18b74440962072124368e0d4b48a9600abeef83c737dce95a457069f62709f352620fe4b73525124c83a6be62aa9dd3953e13a7939d81267c8c0e2ba172a73428b51d5754e98f0a37ab9eb787523155f12d867f9846cc4bcda0c510868fd4fadca94f2956a194cac2f9c643cf4c24f0fbf73d85216a832a713a5428b8fd738508d862240e363568db677131d0a1ea60f253db138bc2c18d67d8cf859043bdc06b026dc821c1f2632f28d9ab88ec24b711528d708f8ec1c5ddee22379d0e4b3548b495c7d1ad7146368450611af4eba32c1731a566453c21cef004f2cac2f38364968a8c1615f33c772408657522d73c0b69162d5ee987e224a0b19f55d40bce6ac81cc9f879d904dcd481a765fe5bdaeb7df04ad4e0f1ddc890d9cc399de46ac346a6840dfb69fc692625bf337bd1ab89d11a21d308af1dc58cd03e3f85a6aa5d1c64b7f9b08f5e2b8b0bcef5a6130035ad5387e30d068020f769913678cf21bb05227cc5108d697ac677fb08abe0a6c72f56d27dfced3785308f4e74b9076dd0891a7bcf46e7c9b34df635c6f30bb819e90fff298414890bae1de2b9071fa1fa0d99125db29ff7fc5192f92c52b677518dd216a2e7772a85c5799d068bb41d9472d203c315b1bf1934609a418a1d4c47d2e5f8cebeba01644e5c98372b5db48a5c79f121d8ff485fd53d9e6195e7cfe61773791e11bf06ca4ffff0140bdc656c2c6f7a13ef9d0bda3ff58cd3c0eb283aace595e2797417a31d6182e37f7aa66c2dd2de10b01da14985d4a2b9028fea7a2f23114eb5cf3f169604180ed25ada0256e733b16892995ae9151439b36767eb69581b0fb9f00d3a29b35c7017a32be9c43d61512ce7a934021208ca4e9056d411afe4c2b340a8572ddc6ea462ffa0b079fcc457bd304c42db2cf4fc00b7f633ebf2b8c8daf03f3b10bd1e8f117ee99387f75a4a75e207bfd6d5a9af4a8655f6dd10d02eea95e95c3c36a28905ba26752e0776289a7b627f3b43681bb828e9a06e1e7773dbd0041e5f41f377429fb667a127030a2dc2127e2bfee05164e437587186e4975510503f05a7cf1520c6044e8fbfef7450a9d9552a14ab1ac2ca096a3bce9eb2c856579877458c73c02554a57e3acc664ff99522ceabda6c30bb704aebec273ef86cbcad49aada0c59369c86742ba723779c3051e5a49abdc5467369e2836cefd21cb9ee58255ba86f81c574b993addfce771670c0d4668a297c3567801a906e523d4c02916d2ce6dbba65039f1bbf86bcbfec24b36bfde21b608e133efca822c54edf745982835ef31bb33191f35d07f5d7ec4950363a9539c7c4cc6f3863f9ab430f6a9f7d4719844d87acd264c5e692adeef0a9c63b1fe5b7f8a10dc515592e90a53aa218f4d6cf57cdad7c36c5054acd7eb1bc9c64f333a4b418587d53969d9a291042dfadf0a123f9154cb275ec0e175b6a5dbb46608005ee9f7573579b50fb1ff79e72b959d0ed832aecde08e495937c9f3f9b40bf3e818b317e7a191f53e1fe571cb2f3d9f8f53bd933f25080b2f3e0e2304c76b8780e85d5c02b0aea41e69c6caf141f098a1526e302fef930d61dc0c4ebd7a2e93b853371b7938640b27171b6ae91b2020d9538600b58f14d90e0060d4107aecdaa26900ab620ba578d16b3829e7f46f3a9fa15eea0968f151be559d41d95200ea867fd93e7904b16e69eb734a4622c6085d523dafd6a6efea4931add2b114390ea9fa4b508f899e36e8b12d098f654fb449d1664b379664968ac8482e2c0c03677b85ab0e0922f53f06303cd8974ad59e56b1b1f0e83001ad39f33b9ce6c2b5367fdbd9e8797c06588b159ca2a3fc227e223b8b3b2a3350d18581e7959aa12fffbfd6a2e87e0cd5ad1bc52dd6cf5e3167bcf3cabc29143ebf2a4f1b620e21dd74cb677e2289bfca4638aa2fa5f017b7e03b7cd2b0428f99210e2d811a08ff79fe37ab013182d2e73af584b2cb6290d504229c3b555c2bc42148dd909bd96b121f75dc58ab9424e28287f3659046c14ed2ecd6609ddc717852132a70aaf24e273c0c9a28d103e227214a2412bcb8eff92ed9cf775e5756b408e7c501b4e293326ff4874474082bdd066443c7becdad96accb1e9e1acb673a8fb3def18382125214bdc642f008835bdf6c7ca1e54f78227b1c6764759425d57b1c6764759425d57b1c6764759425d57b1c6764759425d57b1c6764759425d57b1c6764759425d5bc54fb9f38dd03d37af6ee1ad414368ce009173a7bf09a2ed7ab59faedfcb42ad7ab59faedfcb42ad7ab59faedfcb42ad7ab59faedfcb42ad7ab59faedfcb42ad7ab59faedfcb42a0623a72306a2f008bfafa710fbb486ba2c524f61945befb709f2ce402b0b8acefe9384ad35a2d9b8364e85ceb1776ead92bf1b063345f4dbef4e033cd288830ba28f2b9a09e62e658f1543c289eb7368ffe321a2bd4d79238bea1abab4e58a75de57db3f9c3a69f9a81c70fdfd85789c125b7403f5fb83ef2733629780dfdbbcd52f99747df4ef1ab2caf7403326d29909f5f9c05241a306c3e6d0db443f8a93eb05b46b86d31804036f6ef67e6d3dfedab3d6d5589a08c31819184d320d5d40bac1c4209213f48e1c597d3abb2ae074eb20bea627e24bc002f704c09eaca7eadeeb90900e55c9f8675bd690a5b5865efd3c0f6588e8191534000000000000000007b09deb38859e399af75fceda0655429a97863fe9c05ed7515cfc3abd022bb9bdf8df8c64407d902af398c760c41c0d6c1f12dd663f546d000000000000000006ee5b80819f4d688db777351904f9bdd361d67679f6538821b6b6e0de3f4d94900fc414c406c39b8ed9ddf10c80b87645987041c3b704ec1da2ad23f08750ccad32f9c8b3f380247db7496cb38008364d9dfff1151a164a0a5cab2eb34fcf130d21d533efb027e7c34af3a7419bdb2442c7b8dba41cfb4e227b8e0623a6d16b31898e6223de4026c53ba639a8a46fd8b2717290739d4eb0fd2ad4a81ffc529925c2963106e19752dd86762a7ba005db2529cc0c5ea90048d8e6e5fa4dab98257c8022f164e23d345198ada8b49dff9e1e467f6a4539f33032fb351b8f0da2fb89c6fc67ddb34c1d293a9fdea6ab491b51e1178c10ddf2f52627ab7e579c1d80c8e83a813c247260beb5f6faa1b775726a64854bc832ea75d0581d381722e06b11b4766a68dfb5c3dca646c51001e3942d25060b1af03d11afeb23cd98bc933b741331cfa0a198ea248f5213adca969e88c9e80b23314cce4ddca0d5739fb57429e4b8d5bf29db97099eca610c0fcb36531ec7874aeb57d3fc9a38bb1fa47220454d6ad3f4c9b4bf30e4e466303304dad3388f30f0b45655fae45e0d4f6cd6b21ebad84bcd7871a5e6c61bd26ab79d1f01747a37427972171171c0a53549339805f59311ae84dc5c3cee0bbedff3515f9e5568f68de0bb04e1895eb5e001daf34a42d2f6394d6f889ccfbe2092fc566a6bcc9e5ba5133f2282b6afc8c43e1ad9c191de39ea8876f4f70350d330abc9a4ab4697e6e4739edf4cf3759d05a0b89df0783f2893f992347dcd35027901e475f8a91047241cc0e26b2009922af373e734a459ef175756049a5870f3a65970f61a10136381a63950ed2e0c716fdf8ea305aa7a0e0212ccd405b3be93a292e16187bf395cf502e496e22f2f5565e34ec32d2ee63e46b25db2bb083ff4a7331225f024e0dca385fb692b4de518a551d7bedc66ce5e99613c8102cfb299e985affe5c5fd9504f83061bfb20767273b35d344e36cdd9e9eef6939c94e880dc4d4dcef494b99c5312cddaf5e7bfe0b1e6a90b72ce895c608fbe4b6b5a01d583f3526ad75e9492c77d4c6edec38441b31490d900d2ab865cced7f9ccf989a4bb19edbf15748a413aca792d6f61b817639be5078ca3172e51680b6e6a0c41fbd6ef9f6f41cb6a4c119b088455a220050155f8e5ad9238c794cb065d313087802c11e5248623bea969f22e015651ef79e6a99d2b4906ade67ae60f5e3c73fdfc16c189a3ec6f73c54d90eb340d4edbb1b882a93a0d7e07e95f87c4157642162fdaab338133c2e4db35b9871ef6540dc937d644ae9fe9c6148e36f8f1ff72fb56660567fcced214c00a72179d41db2d45e044bb314f65adfe21bad2337ed4338804800eecef9afd008910f9565ae21e56f686de839c53e513bee8c0895e8b7affdc66f29dbaa4eb7e4b9e1eda57a97cadc1c3aee8969a9bf480a64a90f53ce7dbdfb28b39574e403f1157dea27ec6c4a919d6a48c9a03ff8135f9ddf47e70610aa171090d273de0ce1f28c3dda1f44a48c2a3fc2744cebf0115fdd6495eadd036d5eccd4ae7e92e32e5e7ea9a713f05fa2d7df3d46f64fffdbab031fadc222b5a0c0ebe61c1b4feb07ded3053fe3aedff0a161da1fabded2dee09d6218524df21bcd88a6913e36885715517a5a217c00b60f68748aafc61ad6c74a85c9943d5285eb2d6882580d9ef8532bca2251980b01fd6d45720e9dffdb90761f5ba354acd1d5e554ad2a75fa1463f6b91970876906188ba0c38869be8584601716e15f7095bec9589fb3d9d815b604e0a8386c18bd2f9c8b3fb44409fbc22ec46bf82016bf1ea79012f16fb80bbe8081f1cd693538899b0c5813cbd370345ca1ed0f2864c4126e1ade6fefa7316f72b2ecacfe95ebd218e6d5abd879f081073f5da011786ab939c5e272060beb88080a3e497f2db830e6ecfb41e89656084fdfccd271cca72810a753aebb2f8c279e9ea673bbe8526d849a734c3c78d8f9541c2dbc9108e9ca9b547c7763cd82a5975ec308e33c7f29c3e9fe80c85e5b0b1bb7b198f0646d9870cc1c9e7c17536bbcab05c267b2a870683ac05297b19242e1d32b6917511cec04fdf1c9e513676360fc02a886ccf8bfde37346798c385f4e0f3cb9553679ec5723692474a9915b3e1aec122e2d99d977ec1a7bbcf3c978e9f45c63afc750ebdfabb1a7662897f2646ff32b3268d28953ffb30e973eaafd2a0c2c92da0a07c6e847cdadb51f8e00c9ccbcf37e5bd1e06cae9d41763ae6411367a00efd6f517baa2cd7f193ded0faac3a77aabf4349a369408413c552ee3874ad5f3d04881f751a9f5aec10e189316ff0ca19215ce5e232cfedb2144ab31d256538fef8fd969428f9f72b7a7a9fbe6231be635ee4c5ff711ec7ffb59dd958d0ae5e8b4f59cadc687e93d61c30c514b68a0aaf97563828359b74e867f3d1963d214791f740514a0ad67bb6efce5913a16febfbbbb9072692b721ca478b93adcb2adf44a4c8f5fbd54cef4a031d8a57338ac1b0905a7e2014a34e285297a886c88aefa5f992e086bb9f8358a725356e2eaae78fbdcb8bcef0c017224564e42374378de8a0e5b7b77a34a0e92de1c28a23694433c5dda52ae0a01ad61f4f93c9bf63e2b2d30c11ca347b352f8e18c59b63fbba8f03530b13327aabf8ecec519dbc2727d1eb43cd826f44e7c53ad44ca5e688673f46e4840a1162671105064a0f2476b6389e7ae8886726740d1217b287569bf29c4bc7f5f612a0db41c3ef23984da3ced637501d2f696c537bcabb625fe9cb7d869b87c05972ae894fb59e915fa569e5b514c6d3223caf345b264f32de8868df6d956b622ba89b678f9e572b6ff0e4dd79ae23c41b41cccc9a1753909dd695c29d5bbbccf9e003dead879d7b58f192fa1bfb6752b985ecc02ebd3208105089a6b8773f36ed00f695be63419fa809baa93f73053c920ac92d0b2d3820daaef27558586570553aabc2a419b919c8638502d597990f13ec5abe844eba5629671740689d0606bfcec01af550d4c34ca0aa4e79eb2c3cbf7a5839db4b4b9057d6e74911d62c904f9b21ec350b905324928cc27b406c2daa981b9af2e0f2166266aae051503549736b82d4d802cbcf4545cc0a3f38aca27c0d5ce2a59e8774ecd5dbdc59781527cef371241a83eabe472b0abe9319ee1f45dde13cf9918fdb03b9c151db432270d84e5d47921ca9641339ae390aa990738407af02e976bdbd8df1570941e7a86948f92c596d6b479b5414e8cff19685a9d4dc9e48f282a21dc73b0f200bfaec4eb04e4efde5116db66300b4832650950bde32683e692686551d001c925e90dcf41f26677d7d7e517220536910de08467220536910de08467220536910de08467220536910de08467220536910de08467220536910de084687f32df5f0c6684ba5cd4547faf9e14d693ef6045af45c8323e30dcbf57ddff923e30dcbf57ddff923e30dcbf57ddff923e30dcbf57ddff923e30dcbf57ddff923e30dcbf57ddff9069c03c5aec96d2ab3593f2516ef51579203ced575b4dc77006fee92c1d61ef9d5f92324081bd16278f4449d010421cf7054b688898e34e13a7c9a895a25331f16f2c3dea7d29d7fa9963c72560d7c5e7e340d47db8c6155a8af790b3ccd6d59aa234ee625e18bba7936eaf8c545eaca5f99a0c24ab1b40b74a4cf5734a905687c11729c5fd01baf5f00f1e034bdffb3f0af203f4fbf4e2ab4b238a95d96ccd22807e9c4f53d2a79f6afc585ab46cb17e5ca1f75f2181d5678e5cadaf8f44143a97985e88fa19514f389a1a65dd16ad19555be88986088aea5abc8997b2bff1536f915c8120abfc51315144542f6ad2700d66bfd6a9ab9f4d500000000000000001ed8f868722eb1eb95f0914a3dc3f72cec9ae319eca3398aba0238a678083780d394e834e04255b7c74aa7ade8570559888e1fbfa5b35b69000000000000000006833ae955700b31374a7b1dca69ffe9e1ab475c301843ed33bfaf949ed32b953409bd045d5c6b03064c13c8c09dd3c81b9602759de0496bbd3ddb29d81ef07c06d6f154e05b31c9f01dbdac04bfa0eecc3bc851f323c7c153922d81725113a7c7de2486692dd5d90ff3ac1fe9c08dab0c026302e651be160b1666113f1726ea2ea50bb66c38cbacf17f499c8f68cab5ed91805b731d49320b4f954ee6e7c30a17009aa3ee5e9a8201fc8d0cf0c5045de76b1a43925f78f9b0f368f98994bbfca62ee668335b6038217431370b815a66123316334cf0ff1e5977b5267832971cd7e28ddd7ca8a6b6c505e5410c05514b8ed1a576bb01c681d17f543f18419d624b6489d0e116c89f80b68c77fa6710d2197c7acc8c0ff8f7f0e6cffeb289e7c0a95a49694d7e76b4c66e1d87c548eb8eef4840093ec49edaf240a58281d9722d2cc57e1b7c5217cbcaf156c95dec626d1b09b6529c582e7a5a0aa5f77169b8dc997c0abda488b6c471071e8e2b0049d66c010b02f6b4d6542bb2f34ad2fcc1305bf21b40bf9f5aed3a617eacccfffcf51939e4738d0106c95014355045f5d6148f41adfcac3987c89382fb176fc6afdd76a3bbcfe93a4de5c9a0e9c301f7ec24cd8d0ed34fb9a913728386b52f75f8d147ee7b50a1ebbb85ef31d4227aa4ae0868df8d0549fc5937bd57523afb2bf6f35b78f72d7bf69d50fc9cf8d4be861afe682f20407ab88827da6d510940e071af2682cc165d18c7d0798a3dc8d7ebd883e22e0bdefd7241e84b291e30d7dfc509907550a242a7407200632c7f24e57dcdcfef978d66cec75e5927945fc1a60767c1c04a3c4cccba9de672e9b73052d47d3bade8a7ea8c1f483079e2c9fad5a74c86a575f8e6cb5f0f90f348bbbc614c8e3b3711b598c407cc766c3f1cfc6460d24859528f801a3d17ee058d30955e074a5dfcf597397f6f86f1068c3acb601a19be94137865af29111a2fc77ed89a06cf76b18dd534b619bedbc72c4ca23c453b3595284ce6dd254d34d2379affb08f9ca7d2523627a02e3ffa57a26405fd6cd14b478674510398fa319afc0b136cd0524019cc44af01a63c5f01a9e327914567b44b43ce29bba52ae7e1cf81e17dbc6f9648568a5e70eea22d42fa2e04ab61c07ff0a92aab419b6d2269488582abbb56e18c3a47e290182f1b6de7bd1c95ca4694fca78af8aafecabb52db42bd95bea876068fc40de7daf4ff568a069338a0a9cb748cf37924a20d9a41fa5b4971913b89ba746f04349824a19af5458e1f7d19dac508146a530596c2dc3b02a4b3cc7816d8f406be651dbf2cf8439496a45c888ea16a819112205935abba314642d84d500a2393c0aec8be10752050a6d4ad53f4629e2ae58572f5912119829e803a1d17d8f9bde96bd45733adbdcfea1d76a273869881e6303648dca2d33fea4043b61a859bf480a64a90f53ce7dbdfb28b39574e403f1157dea27ec6c4a919d6a48c9a038b7b7e840acc328c266bc82ae48ba59965d0a2cdf16c835cf6f7b1b6d94c3b514800aac9820318368c30431b8bd5cc1dadaf01455e639894e476c6e861d75d54a180046f85e7f64f0769b99432fe9ef4c8f69ac19e4a33b480cadf0280e371bd643a52e64caf23d45dc74dba5f603b6bf25d490093936fd74068647330557d29ee433f795cbb5ddcf7c9debc052961447afa2455a079c3ed6cc252f6273e7cbdfb73190b6478505e2eadc3d6c6f5eb690a0a2d1977f6a782e9162ffc94811b1100289a02493441cfebb85a2eabfcc3b52dea0a39df98b711e64fd85130c41165078caa7ea516be9975ffc02b9f5b3128a700554eb35edde5d1742a821678f7fd118d637b63b718574948fc95e54f25a4313cb7e12cbdbb0eccf1e7414b9c5265a3595a9323f3b2078df812ae73a7db5dbf9b13fbf03b76d6be611b886f9b021c090297d7b3b994a9112a8e67fa3c00b9126c8cff67ad63018b82a3f1b31327071c4dd008274de769378be2a96967eec993be50e57dee3b417e9fd8192b62ca4da57ed3b67383df452b1bb1890001b55a67a660e037a98b185b1e521bb445e90dfb9776d26a348691f6a2de7ac30a221e4b2afb275ca3ffbd603a51207477d3ff3a32754431fadbac85af9ce1b95df98ea0645aa4f5e28463928429fb224b2ec404b16fb9084930dcc0b6cad9d68f61e55ec140acb006d10804bf39b9c5f402111f98b032901e069638ef43ea64be94c0ea9ffebe73c662033dcf5c58b580cac5ce6008a77ecdfa0c6f7bc258eadb293721a0efdb5ecd7241d4373feab45a1855e7d41dc58366c7f9ccd12996bcba6fc5642f9a5fe0bbcb5e04770d2aed9a409f59dfc5618c9a179a9255e603bc62dcb47a908dab7e20d0c7fc515c25f1ad48d0080ec3bdc9656f0043764aebc2cb9f80fbc20af969e38225d9f434a4cbb54c3df7d8b51e2e2cd68594e75819639b704a0e58c7f7a4e632067e7c2906ca47d9fb967327a7650adf7bb9f6482c8d79d062236fcb9f074f2bd64b3d99cd1afe1d5c5f55596d5a2e3bfd90bb0528d5dcc3300e5dac2dfbd16cda66543d777bf7456293b54af5c3d8b07abb17f5c895abdbf80c82679442783cc10e5f5605e2c01722332c2c9cd39eefda12aaf25ac97d2aa07f1f3bc6ca3daa51a2ae0e46fdd0cd9724e7ae1b6e119e92d1203a8e5751e46a0f7386602325311df0f6c00be0997fb883bca6a9a1e6ef22f19d22a51944c406e0cd805e7f48443dd8fda3404fc247154c842b38b77934863b4a7a6c17bed81b927aaa3bb6a07d68ccb08bf6b15061713b952fdb095fe4b45d30c27200faf451eb6447f2656717ab6f16603d9954b48275f69a0fa091054cb9547d605e0381e52e56ce259048fd2f8b5dacedc722f723a566e27cdce4a42acb3b1bcad189389f59cb98a2059dfbc72eb945ce9e740a01f06f2014318929413f609b873d5f9649e02628a640fe624cae0e87c075f90ff1e235509d9615048a8a1ea7d931cfc7a4b32de0d71130a5a9067b3de7197be76addcdf137bd9a03958d8bd717c01d2f07d022617ddb4b817c8cc8c915e30a9dca0b0d9c874e4cc4a190438f4926d58d5af93120a3bb163f65bf21edc5281d9b59591628a946a238ae27c8c0b7d4b68d0dd3eb4de2a1ceba404c6c9fa77d0555466c62944d71582f067d3358e87251ad9264d9bacb89926aee594fa48a242a681455db1ebe89e2a39b085ab1a443288cfceaba4c1c55a8e3a7e46948f92c596d6b479b5414e8cff19685a9d4dc9e48f282a21dc73b0f200bfaeca55d288a11510b503647af077d70c953df44470bfa6db0e459bee9641809bf0c69e0127b52636dcf3afeae3feb2de7503afeae3feb2de7503afeae3feb2de7503afeae3feb2de7503afeae3feb2de7503afeae3feb2de7501d32f25d4b2602e07d04e6e5cd4fd740c9bc20fc83c01e5e9cb45120f81348e09cb45120f81348e09cb45120f81348e09cb45120f81348e09cb45120f81348e09cb45120f81348e00682123356dbaaaf826abb27b2907700b9c1444bbd00df1a4efbd4320237c371ffcd0659c37e28d768dff6970ae7996b170c5251302635529913e50f6e7a5eaaad094457629aaab00116e4152c1ed9b75a6b1ad068d4ba68762763c07915a0962fb2f894df96aea21d5c5fb506585e91efc4228d057ab40e9ab0de4fe09c1597e895eb6efdb9a656a65ed85d769019b337ed320f4eb632652594aa33568c1e272007e9c4f53d2a79f6afc585ab46cb17e5ca1f75f2181d5678e5cadaf8f44143a921d92ebbb48fdc8567d5837d473d75aa6a0c27384345cc81ccf00e2796e9c2dae612dfd5032467b54118a0eef519e6acb22d615ab880e4ec00000000000000006d9518f636ce5936e19750764c7ba160b63d1ef49a1976cf8c0b95b9a47d46687d4437e4f5c02b6fcf10ada9a108a7a131d54e85f2fadf8e000000000000000006a77adaabb49c7a21786031bb46b3a7e946993c4de27838babaf57152d740d326b7c2c2134e95141f70278b4e4d7a8a734e9e8e8ded20f663c8a273d59130b4303e4a593504456d16bcc281e9e9e7cd9e1096bc711d3eda03d0c010fb5d3c5afb9d4b6549d8eb6dcfc903b5befda5312ba49d1c3a3acd1bbac8a885299487187ed0e990f989340aa24f89aaf4f2bf65a1357c2b294f3f22a7b8025cbc1d586d11009aa3ee5e9a8201fc8d0cf0c5045de76b1a43925f78f9b0f368f98994bbfca62c398c9c8b7783647dc0937dcfdf325b8dd3066b8907210976d750931645680f77c539d62232dbe3972711842c1190b549d02e6da60025f0613281f6845339f0a62d98bd4a54e786076d8f86a071626ab9048d1afa510adaec41ee0b672f30c1efd4ca7a13c13e4fffd79298d3149a108ca769c1aeb4164b4da1f6a20a9bef687ce3787b2f0ba03630710773bd2c4600fae2670b49343c735b478dd2a3dfd426eb2b1cfb4e539d5a3abf894c07ee8a6bd742c8e15021f4975f884a3cceda712bf407e138f3019e8a902677730d74818de173bc6f41453ae0f861d5125333e358a800eb7ba13c1d4957ebcb8d08d0216f126a677788602c44aed19def2b78fcd171cd8373e2596073fe0eefeaf294a00f70ac21d9075fbd4c0657c44957ba0b55d0790cfae3f46d49925cae585a8b09fa2bfdeeee10a4092327a64f0e0e5682ba52cfa872bf7f0b318771cdc05ced7f6528cfad3166d245cb2d9874d9846f50149a8c93e6e84304bec973cec0c29f51ee2329864e5ebb7ac75599e8cba601b76fc0d87f9dcfe43285388d054fd7407d640cb4659a698767e77c6bd8a42c909e7711e0351a2eb02a8c152d02ad3a6c0a62ab0fee98140f321e2b93f0c36d89865632021f8add95ce0840dd89ca80c7075e1f4ddd365be1fe1829f5e4bde59ed538cb3c560014d08c094da712c3e78a69327b12ff5ed80f35799c9b9e3671024f6c4f27216ee4549631e50eaf79a567bbf89d3235bcae67331a906d4e638fcb00fa03eb42425a93b11b1f6ca2faba36f5573dde0607f3165d6b9bbda6d805e8d9f3e5021d046776a1cade95b823ad38b0dbd3f121787863acf319e3effc5ffc3c428a5a18e152eb428fb8de07570ba2f7258b309b83abaaa99c09b8c7586ef5fad6a7ecfa218a713bc66509622b3084c6fb4eab6857afea0eb234d7672c497a853006f5e2f399560ba5074bd19e7a975aa470db7c68269daeab64bcf3626dde6d05215e7a3ecae8c20a9be59f637b4fc00347d0033edccf22a6fc2d7cee0f5ac4f96f6bb798e399f0296e884d7a1091e297fce09b13ddc9b6f434e308c7b31e49e70722125bd7c03764d540a960f862c844c08e2cb51ea4bed42a4a1f05bcfb46177859043bdc06b026dc821c1f2632f28d9ab88ec24b711528d708f8ec1c5ddee22379d0e4b3548b495c7d1ad7146368450611af4eba32c1731a566453c21cef004f0b5add81edd1c3842b8df67986be76e7f428fb7da492c728ee77b5d058cc02f03b3c38eb76f8d4098ea0e08bb977c1af12260886dfbda9f557ccb9c501be27fdd633425b732f1ceacd1f535e06d688d03f8ffdcc9c3b876a25a01764ebd37e403d517c1a0782dc361e1d09379fb0e89824adb783937676cc9a9be96678ca84323c122d04cb61f75fc8b7b96dbbd1afe91c4a4da0dec5ac8cdbf54445180183f3d406b9b17cd7abdbb7771b3f1ad6cccf7a611aea37e0356be2d3a3be3eefd9693bedac3789b9baeab061c4f7e1e4ce23b5607258642599865616504de0a8795f0cdde242d19a7c5367848791bc8e0c394a4c8b0f9e185bb9098264fbd89296858b982041de3ff1e1e27e31395ab1db9ac5e0c604768618f364b8d4db59b4213ca45623fa34392d5581a9344db91c5bf37b355afef81cde61441b3aeea47a6e400059573107b0bcf7acb7dec28de32d99f7b0364c3e5bf1b44fbf9d68cb2cf443a728a8bd3580d4eb20ca0ebc540fbe5b7df5a075704f21c92f2d7aba0f3a5375399fc7dbd5cffb19a1fdc8a77e8c33e200683934f4647933b7cf7177298136d7c504c96134857dde11c95e61317172cf884369e771d145a0eb98a708e24054ff2358d7945fea219a43ec1a5f4f2043e710862369d21537769f23be163565c5d80f641352dd9473ab3a90c7daf18a2895cf53b3298cbe5a10ebedad695f3c73b2dbe6bef0120d68a9de56563d806b18958c1343344deb38ab3615302a9c654ef371c7219a3aa82681826bfbd12b0290c8d4a5c3c2f607e95c7bc2029715d7d309fc326e3ef9a8779ef4e537835f1915bd112aa3aba27fdee42fc69198b539a5b8fcc6d6cfbcbc2064f39c3adaa0e3c0da765a2af0670857e00004ba26caef484a967bd4beef3dc3e91789e3887eecfb9c5f714f0383f9c3c77e0ec0a69b8218f930def5f130a8cc26c888e00cda27af7cd9f79e873e82648076c249c986388c682f72a86d15fc9ef51c5151004e0b827e4954402926c7071c3b8985f3120454831268c90bbac3e6501c59aa7f23a1cb005db188932e0f140af430b2680ef30be499db8c5a7307b1fdb93e2b28908597c0c8557bfe97b9a853d8ab416f2a373fd8a4bfd0f2d7410689a950b467d7aa265dfda19d109b7fd30a296c686405b8cc8215f029d2436ccff186c7249fd1c9857d79d5553f2146439516d68ae78aa587934c9ca8926b8479709de384ec35323dc182d0016975883f6b95df5b597b6901feffeb9c1c8cc7010c0312bfa0aacba2a2fdf05fab7500ba9d71260f5a440fb5c4faef321bbd4bcca10a10c1b18057305fa030236b220b57da9d19488eee7cfd9c27c497cfb2ed6ebf81680ac4ef26714977074c282b6c5ebb15be155f4be55d457d90cd3c7af0cfa4fb768b047ea51798cf9f7958d743b49fe5ef254d7a99f9eb705f86d8020d07f3d107599421546f8c48b097d38e543eb8c5e97bd63f62ae8d334409395d3e347800b1b9eb418eaadabe2e76b273052cd6065bd1014e2984229d0b54bc25bbd6b08229e85ff8dc0f81ea71b1475881818e3035df740eb849423e4b7a96f9c3ea1fe82e574fd9793f8cb10abc263a9cac1408366a0ae34b19286f5efdd78d4ea9584c8935073e52e07313152af5257ded5e811d1b183d00539c462e615d6e96ac42e15b5d3720e15171f479692c7b1ae5558d8287f3659046c14ed2ecd6609ddc717852132a70aaf24e273c0c9a28d103e227214a2412bcb8eff92ed9cf775e5756b408e7c501b4e293326ff4874474082bdd45e6d5b18846fd8466729137d4bbd1750832bc1efa87581b12638914b660d67121e1f7cbca4220dce9c69200a7fd24fce9c69200a7fd24fce9c69200a7fd24fce9c69200a7fd24fce9c69200a7fd24fce9c69200a7fd24fc1b7f285355f2e4a664fc926101b7016652fc35f469c8bbcc465474fb9e70e253465474fb9e70e253465474fb9e70e253465474fb9e70e253465474fb9e70e253465474fb9e70e253061918559bf46903ede52160b1f56f60ac6d90a8f942ac2167d7ca6f1bf24043d9635356a48dd00d5ef152f9efb7ddc609db9df3d954a1c84742fc5b541ecfb4e9982bba95e63954a7c3d049895a6bea228782fed77cecd0266de88334058c3dd1db6003c60c75bd3a594665c4d2747180102467a649a68b6aab258dc1dd7c8eaf2f99747df4ef1ab2caf7403326d29909f5f9c05241a306c3e6d0db443f8a93eb05b46b86d31804036f6ef67e6d3dfedab3d6d5589a08c31819184d320d5d40baea8365defa6d17d488f5f17ccdefd327f4d20a142425dde2bbd3fa9f3b925cf9732dec2858b0192bc1622decc42120591caab92c204ad7e50000000000000000a342081404098957aafce7913a5d16b70b8e5f8daa0a4bafcf268b42fd0d87f176819f809acb5e48aa2716a4b35262470bf53a4d5dbfb7f1000000000000000006b95ef5e761fd7ed8e17fddc94b58e91f8814986d077733b54ea5672d224300cf959a51460045f3ca949924e9050f25343bd624bbb1d4cea86efe572b87b9cd26decaee840f06db7882de27239cd874db39296594ba5c50bc148235dc672a2bc16ab962c6fac062942dfa528937b2f278177ab1c10ff72f68bfbed89f4e652896898e6223de4026c53ba639a8a46fd8b2717290739d4eb0fd2ad4a81ffc529925c2963106e19752dd86762a7ba005db2529cc0c5ea90048d8e6e5fa4dab98257c16a2a29d89e438170da76d58b0aca566380581057f022e21708f2d314be748926426b0fbefb76a522d1ac15277f42db4811095a4dd73e27384add317fa92e72232299496be4b10ef72c866f5579fa3ba2614b4066de17c56011125d308f0129f070e01ec7a053a3863693cb5305884100b573862f901762455d9b433dd5371c8d1d98383b638e86368c16f4072a0a97520e45567fb42bd46b111bb560177c908e1e275d907d8b3c92d5ba071a28684df7bf503c62e9584f19a41e1f882fdcc7eb4ee1e3487224c19b3182daef822df6d68cd80cb8e68ce6ca10ec7fca7bbac9732ae6d74357f08aafe8b37a4d76e3b3fdee7f46ea6f2e42ef5e1804b7692663088e92723d409b9b9c4da177cfbe8a3be3ea8d0ecddc277096b11bd6a5604b4e7c782a4edac9bde102a9d7d41faf9add4fc963db74e287e447d6a5f910c7f34b21120bd73c313ede8a904a0a6b8579349678b2ecd62e83cbf1fe4eb3aec41713b06ef7e5020f4256910986b04e204c8eb2951218e1f3980c5973f1224bee6e7cecc46b3de63afac08cbafe0e682b3e0ec261e26c6dc4e73a0f55b18e2e9be1d47f553f51c0f2498a9f2647d275326c3c33b7c73b8af07c82da55d5eaa25c79d86895acc7d0366bb64200e7ee82f69244aaa47c0b99d0bf7d34798ca3103a5ae7dfbcb9cbd37c37802479d65338c0c5ffd0abcb2a194080d36643fece48267bb68c76a9ab30cdfed271726d101a29d9a9c1526733992269ab51ccd7fc147ce53172a9b13b9961f7d9752b202aab5e8a5d444ba28254bfd98dcff850983672920b367a2570d529eaff3d5f11349a233da7f22e714835295f35ae8c0702fde37cb0f2c452f5c76d116dc7e1782f62fe03fdf5595d598cd361108a542410a5dabf0cb9e23718e8b970dbaf45e0e016523ca4954457c537fe55d026ea1def35e543b5706f2be96d0fe11b48c3d86179b435d5056db8f3c89b018af47c2bfdc993e9c9fcbaa7cac46032d2bdc844471166d0762bea688f6b0eb05a25d9719fcd3d1add63e5ed039ba1b2e29b760635da33d055c58dbea8404a121dd0805a02dca69119525c5cad422ae9db7ff7829b7e85e4d1a2a97b0bc04c434d6328e6a12fce1bc2d8d0bb0febc20db2fd9e4227c7462b4c225e2e3e33e2a68ea6b1ec8bc28d797738a79d0e4b3548b495c7d1ad7146368450611af4eba32c1731a566453c21cef004febc22b704fcdb4999dac10dfccaab157c1c39dc78f052d62c728caa385e40e906acdfb5001a1cc04232aa2782b3c302cae9d7b251584a4b73767112f3208d4d87eee30fc6601cd29748111c91d6759a4636d2088ad8335d84e96f54860d94a635c1f9e45aeee1e829291924f94c9415c34c61642ddbdc04c6e943629305e9b0676b33c3d61594e80706a624ac5cb014618c3b8da15b2cce2f29bbdbbcd315bd1b2087ec486bb61226882bf6a88cd0be80e594cda49c5fa8df81ff183ba87af7e074b3666dc85dcb9207c6617dd0d7a79d8df1cabaa3d57258cb53bfbe5591f93a0b686ed9e98949a8f553fbcfdd784fda63a7c4363ce537d8887bea56ea90fffe762c78b6c774b9a988b7167da1812b7b42d90f9dc6945ad676154f07c550dc556dd50fd68fe2d9dd3d96b706ac52907f4d7465a63af7fd9e07d32506900b3da96c4297027653bb287741de27c388f905d8f74d4b00b62b3eb6ebb8e9a8a6487ae0e687f3af7e37981281a05d2e3b39c55c437673035518d0b6cd946ece0cfc09188f3ba8a6b200473f1d8d16e6b629cfb6811c97435eafadf2ab740acbce1146c0db9592b1608036f88ae92477a438395d7a8fc6469eb011d99aae84ef9a634dfce9c384e4cfc895485cf776e732200ebd15635710450c4ea866ae8fe3dd74673e345da516662f16b12cf5d7e16072fa080c7f49a5d7268945d3798bf864f1272aceca8778029996fb737a2477f14c284d3f6b4516691f734910ebf0580ca9e0149f49d0157f698381113cce5a23e6586697ab2f4e43adcf2bede4d5202959a8b7ddf2b0617369421857d5ce159d1d7269dd89bb65acdae93e156b0ca42b89092a8b4aab85fc972b242b691a7041d7d15d841b75bad58e5b65e4ae0c4a7ce33aea95adde9d3bbee0da5f2be8fd694d32968a57bf6c3819343225a82ae18d5b9a73d2dfba630159c2aa589a3dec216ae88140fd1ee9d974e4298ae9bbc8a7013a7b0613b9da4e33f9169742bb325a1862e5df107b4aa57ed2fdda8c65060d9c3538a252b5916e79e74db50d6b0677f7b21cb7f411f56aa8975d86f63b02381e60df57cfc5eb5f475368e88d70b2cdc64bfaa9a3e03b05995d1237ff93cb8f27f115336a400b975d94699ec72e02f1551b4c4644663c2466e6b5858fa5f66fd69ad29c037d46357850e179fcaee083d975d3c6b02f31cfb1a72cc14150426b8928a7b6be036d5782a9cc164b1cbddec841b055755061d94faebf3a2ec98132f834a6bbd509e8368f9c04b5d95fc62e6020e375471948b7652e4745f2687fa15627da677053fc9d211a4618882aafc981f23b4246d42fdc44779b2e2f5959d7b9d41b55e0b610c3927c86762d3191f0bda39e3ee468973248c62845c2d0ac2300d161f8eaf9ae49e703d885f40b0bc23827c26253055d344fd594096b8de510c668ed1a3e286f1a05310a5d48d1a9204786898bd34629f29045ce698ff276baa31ae9bd3f5babdc7f276a469adcd442922a4629c890443250206f94f8f8917032ca684bdb2126e2a4cfb28c3234e054753cc5f69af0dcc73fd363a9a1a03b87d0ca3be40b6ddd45e82b22e0d4e61a578d07bf852391e0d36c1499f1e6968f4dc973dc127420026931cf57724725fef2664df8b977c35674dfc9b66ba2e2d6deb94d66c76eb830c44b884790740670a5d801b512c19f83bfe49040edcdee7003e2787893b647332a5f51a729a50361ffe52312f2a4e6a1ecc64f4214a2412bcb8eff92ed9cf775e5756b408e7c501b4e293326ff4874474082bdd5659a651718b676094d3578123586ec34ad0ec21fa3f8ba93fde4dffd4623c48d166e9287e565bef53b2b4ebd33cb7e253b2b4ebd33cb7e253b2b4ebd33cb7e253b2b4ebd33cb7e253b2b4ebd33cb7e253b2b4ebd33cb7e2a51a8939f9c7ada3e23927ea22152a4c14c8829440210f6697ac4d8d4adb9ff797ac4d8d4adb9ff797ac4d8d4adb9ff797ac4d8d4adb9ff797ac4d8d4adb9ff797ac4d8d4adb9ff706b642bc847bb2c36bbb191d97aedc3e5b13d67b6fbe312fdffb1cb6fdbab53577ee3c38d09242ebf2ffb8f8c3487c15f7735635e190d69c199811e68bbf33b2609f235b1728ebd8bd347c5483363ed244a11e70f22200055135510524a4276154b6c59ab1b1e3f551c79a403d31f3a2274d4079f4d3c671d879c6ebd253a08235f13853eda97b3eaebaefb6107ccb2d47d6d532336d2bdf0f6a8711196c291cfa05b46b86d31804036f6ef67e6d3dfedab3d6d5589a08c31819184d320d5d40ba8a41ac308857847dc2739c1be8ae62e53646f242def434ac6134bb04e43442db6b2601133add4462fa40249afbe17e2b4e860d2eb9d292bd000000000000000066804840362a3786800b43e38111bbd802297fffe24b488dd6bd0cf8582bf1b0c1944750c0f5782b11dbe0da463ad056a427c18c40584e31000000000000000006b616404220430335c4d4846bd4dc5b7f15a6180af6835b9f2ad2177ab3c9ccf72dbf2abf40241cac13619e89a7202f00a3201891458e05bcb961e1efde057eb0bf6f19efd0c8deba31d135814f2f95aa84fde062dfc2a8ff5cf703ea3b0e6ec0246cc84f019d5698ac3464a9726ae7213f37ba16affe8937da1b665eff1058da72aceb4b830482581246bbde6994ae773d2be6b523d9af087c68b13faae9e85bc2963106e19752dd86762a7ba005db2529cc0c5ea90048d8e6e5fa4dab98257c16a2a29d89e438170da76d58b0aca566380581057f022e21708f2d314be748926426b0fbefb76a522d1ac15277f42db4811095a4dd73e27384add317fa92e72232299496be4b10ef72c866f5579fa3ba2614b4066de17c56011125d308f0129f070e01ec7a053a3863693cb5305884100b573862f901762455d9b433dd5371c8d1d98383b638e86368c16f4072a0a97520e45567fb42bd46b111bb560177c908e1e275d907d8b3c92d5ba071a28684df7bf503c62e9584f19a41e1f882fdcc7eb4ee1e3487224c19b3182daef822df6d68cd80cb8e68ce6ca10ec7fca7bbac9732ae6d74357f08aafe8b37a4d76e3b3fdee7f46ea6f2e42ef5e1804b7692663088e92723d409b9b9c4da177cfbe8a3be3ea8d0ecddc277096b11bd6a5604b4e7c782a4edac9bde102a9d7d41faf9add4fc963db74e287e447d6a5f910c7f34b21120bd73c313ede8a904a0a6b8579349678b2ecd62e83cbf1fe4eb3aec41713b06ef7e5020f4256910986b04e204c8eb2951218e1f3980c5973f1224bee6e7cecc46b3de63afac08cbafe0e682b3e0ec261e26c6dc4e73a0f55b18e2e9be1d47f553f51c0f2498a9f2647d275326c3c33b7c73b8af07c82da55d5eaa25c79d86895acc7d0366bb64200e7ee82f69244aaa47c0b99d0bf7d34798ca3103a5ae7dfbcb9cbd37c37802479d65338c0c5ffd0abcb2a194080d36643fece48267bb68c76a9ab30cdfed271726d101a29d9a9c1526733992269ab51ccd7fc147ce53172a9b13b9961f7d9752b202aab5e8a5d444ba28254bfd98dcff850983672920b367a2570d529eaff3d5f11349a233da7f22e714835295f35ae8c0702fde37cb0f2c452f5c76d116dc7e1782f62fe03fdf5595d598cd361108a542410a5dabf0cb9e23718e8b970dbaf45e0e016523ca4954457c537fe55d026ea1def35e543b5706f2be96d0fe11b48c3d86179b435d5056db8f3c89b018af47c2bfdc993e9c9fcbaa7cac46032d2bdc844471166d0762bea688f6b0eb05a25d9719fcd3d1add63e5ed039ba1b2e29b760635da33d055c58dbea8404a121dd0805a02dca69119525c5cad422ae9db7ff7829b7e85e4d1a2a97b0bc04c434d6328e6a12fce1bc2d8d0bb0febc20db2fd9e4227c7462b4c225e2e3e33e2a68ea6b1ec8bc28d797738a79d0e4b3548b495c7d1ad7146368450611af4eba32c1731a566453c21cef004febc22b704fcdb4999dac10dfccaab157c1c39dc78f052d62c728caa385e40e906acdfb5001a1cc04232aa2782b3c302cae9d7b251584a4b73767112f3208d4d87eee30fc6601cd29748111c91d6759a4636d2088ad8335d84e96f54860d94a635c1f9e45aeee1e829291924f94c9415c34c61642ddbdc04c6e943629305e9b0676b33c3d61594e80706a624ac5cb014618c3b8da15b2cce2f29bbdbbcd315bd1b2087ec486bb61226882bf6a88cd0be80e594cda49c5fa8df81ff183ba87af7e074b3666dc85dcb9207c6617dd0d7a79d8df1cabaa3d57258cb53bfbe5591f93a0b686ed9e98949a8f553fbcfdd784fda63a7c4363ce537d8887bea56ea90fffe762c78b6c774b9a988b7167da1812b7b42d90f9dc6945ad676154f07c550dc556dd50fd68fe2d9dd3d96b706ac52907f4d7465a63af7fd9e07d32506900b3da96c4297027653bb287741de27c388f905d8f74d4b00b62b3eb6ebb8e9a8a6487ae0e687f3af7e37981281a05d2e3b39c55c437673035518d0b6cd946ece0cfc09188f3ba8a6b200473f1d8d16e6b629cfb6811c97435eafadf2ab740acbce1146c0db9592b1608036f88ae92477a438395d7a8fc6469eb011d99aae84ef9a634dfce9c384e4cfc895485cf776e732200ebd15635710450c4ea866ae8fe3dd74673e345da516662f16b12cf5d7e16072fa080c7f49a5d7268945d3798bf864f1272aceca8778029996fb737a2477f14c284d3f6b4516691f734910ebf0580ca9e0149f49d0157f698381113cce5a23e6586697ab2f4e43adcf2bede4d5202959a8b7ddf2b0617369421857d5ce159d1d7269dd89bb65acdae93e156b0ca42b89092a8b4aab85fc972b242b691a7041d7d15d841b75bad58e5b65e4ae0c4a7ce33aea95adde9d3bbee0da5f2be8fd694d32968a57bf6c3819343225a82ae18d5b9a73d2dfba630159c2aa589a3dec216ae88140fd1ee9d974e4298ae9bbc8a7013a7b0613b9da4e33f9169742bb325a1862e5df107b4aa57ed2fdda8c65060d9c3538a252b5916e79e74db50d6b0677f7b21cb7f411f56aa8975d86f63b02381e60df57cfc5eb5f475368e88d70b2cdc64bfaa9a3e03b05995d1237ff93cb8f27f115336a400b975d94699ec72e02f1551b4c4644663c2466e6b5858fa5f66fd69ad29c037d46357850e179fcaee083d975d3c6b02f31cfb1a72cc14150426b8928a7b6be036d5782a9cc164b1cbddec841b055755061d94faebf3a2ec98132f834a6bbd509e8368f9c04b5d95fc62e6020e375471948b7652e4745f2687fa15627da677053fc9d211a4618882aafc981f23b4246d42fdc44779b2e2f5959d7b9d41b55e0b610c3927c86762d3191f0bda39e3ee468973248c62845c2d0ac2300d161f8eaf9ae49e703d885f40b0bc23827c26253055d344fd594096b8de510c668ed1a3e286f1a05310a5d48d1a9204786898bd34629f29045ce698ff276baa31ae9bd3f5babdc7f276a469adcd442922a4629c890443250206f94f8f8917032ca684bdb2126e2a4cfb28c3234e054753cc5f69af0dcc73fd363a9a1a03b87d0ca3be40b6ddd45e82b22e0d4e61a578d07bf852391e0d36c1499f1e6968f4dc973dc127420026931cf57724725fef2664df8b977c35674dfc9b66ba2e2d6deb94d66c76eb830c44b884790740670a5d801b512c19f83bfe49040edcdee7003e2787893b647332a5f51a729a50361ffe52312f2a4e6a1ecc64f4214a2412bcb8eff92ed9cf775e5756b408e7c501b4e293326ff4874474082bdd5659a651718b676094d3578123586ec34ad0ec21fa3f8ba93fde4dffd4623c48d166e9287e565bef53b2b4ebd33cb7e253b2b4ebd33cb7e253b2b4ebd33cb7e253b2b4ebd33cb7e253b2b4ebd33cb7e253b2b4ebd33cb7e2a51a8939f9c7ada3e23927ea22152a4c14c8829440210f6697ac4d8d4adb9ff797ac4d8d4adb9ff797ac4d8d4adb9ff797ac4d8d4adb9ff797ac4d8d4adb9ff797ac4d8d4adb9ff706b642bc847bb2c36bbb191d97aedc3e5b13d67b6fbe312fdffb1cb6fdbab53577ee3c38d09242ebf2ffb8f8c3487c15f7735635e190d69c199811e68bbf33b2609f235b1728ebd8bd347c5483363ed244a11e70f22200055135510524a4276154b6c59ab1b1e3f551c79a403d31f3a2274d4079f4d3c671d879c6ebd253a08235f13853eda97b3eaebaefb6107ccb2d47d6d532336d2bdf0f6a8711196c291cfa05b46b86d31804036f6ef67e6d3dfedab3d6d5589a08c31819184d320d5d40ba8a41ac308857847dc2739c1be8ae62e53646f242def434ac6134bb04e43442db6b2601133add4462fa40249afbe17e2b4e860d2eb9d292bd000000000000000066804840362a3786800b43e38111bbd802297fffe24b488dd6bd0cf8582bf1b0c1944750c0f5782b11dbe0da463ad056a427c18c40584e31000000000000000006b616404220430335c4d4846bd4dc5b7f15a6180af6835b9f2ad2177ab3c9ccf72dbf2abf40241cac13619e89a7202f00a3201891458e05bcb961e1efde057eb0bf6f19efd0c8deba31d135814f2f95aa84fde062dfc2a8ff5cf703ea3b0e6ec0246cc84f019d5698ac3464a9726ae7213f37ba16affe8937da1b665eff1058da72aceb4b830482581246bbde6994ae773d2be6b523d9af087c68b13faae9e85bc2963106e19752dd86762a7ba005db2529cc0c5ea90048d8e6e5fa4dab98257c58f6abfca54c4da3891e683ea40b7b65415eebf43584ed2740775cb7450495ecf424820b02975016b2c78ce6ae04efb82bcc2b14e1a150b7f47047457d3e60e5313f85173362dbbe18504d910fded0f5ef67d6ec25c6032e45ef0f010d565f252edbee0a374a56530fffa9bc6ec7ed87f1dbbfacb3b05c779ba6c90ad660fa0d61b04d3bb62e380891b404b2e6fcfa4fae7c561b29b7704549c9adde9a51eae0a31773936d8e9379ca9c13d1efc3419dafe88f5adc4d8a3aed8c9af09a51c810a1b77d301146a3bfd31102da0a061ef1ca9be620a22812a85221df4e390004983cff02b15c9b03829e1f013a460e94487317118c12b1ac863498058f0bc2c4b6dd5c3c0b8f91157b525f9e12a2e2810bbc04e1abdb04911edbf34a77c656d4fd70889cbebd8029b2576a6b31ea740203402282342182d8eb1bd9c14a355e79c077f4f76faad11d76caa4abfde58723549fdf4cbac939ce8bb99df00dd1c4095f93347d882da3ef05e575f833e5878efdc1e26b572ed66fe374e734e08286a00c57049a5c4f0c4ea871f61aa81e163ea63a50edf0b24c91588fa305d264eb8a20cdd40556b81cf8ede261874d5f0ad66ce596e241e10c28fc4fc32dd181e76c1c5eb2bbd2bb0efe4c1325f0f8ade667cbfc692bdc23b9307ad8bedcb342287ead3d81029af41f649eb0fe5c319d67b979071bfba11f94b57b5e344ee02b593e4cf7939cca3b9279104ecef46cc9b02eb1cedaf56c7888b6ecaa0b72199613dc18bf4b6b32b7573470536ad7a6abc0a40b4d6edea21eff598191d9003deae44bebd8f9cc2e7c7fd630eebf1507889b2ee67a2d6f8cfb905635e6078c9f61e010640c6e6a5ddc634497a06f41f41e95e70f098455358c2f9964f9e5ad5ee61451aa075d316ec0db3934e62486d0925a4dee2f0156dd1255400d9e2b49af725ad05506e99d4f05ce345628f34b347a7714ba55eb34ce77c9f89efddccf3c7447ada3eaee20051adce7f6b3643f0626ae63ea98b4b2a6ef18458e431208ca0a63540266741c78930d0a42bacff13519478b026d9acbfc1ea815131d75b0cdd43fa83818adfe21bad2337ed4338804800eecef9afd008910f9565ae21e56f686de839c53e513bee8c0895e8b7affdc66f29dbaa4eb7e4b9e1eda57a97cadc1c3aee8969a9bf480a64a90f53ce7dbdfb28b39574e403f1157dea27ec6c4a919d6a48c9a03e8f9793dccb7d8b47d4daff0b5a59eeb49d4eb588c30a655244ff685a0286296aea99bbfb9d043a721687a100ca01ca1c8c1f88609fb3070a5c40405adaf0677b5a9594f2f92f7695159683e2559618207a7c5561844de7c4668b9961a09cb56e9196da02c16a5ea54503b12f94a1e4bfcef5dc977889bb5d88d6ebbce7111ed6dc17b495bd2f3dde2abe9b23e09bcd68579ea848021ae582731bf6359e2b081a5ed35f8b5a85c9743fccbac984ff4103d69309b4d4339872c82efd12d725b6691a687be71dade62f4e70c1420999a7f6c21a576ec2231176543342a5bc4622a4114022ac5df3c5813d709b25b6addba1a92a544645cf32f57456dd5ccee0b317f4084a09fc6e38502546238ff3b29d50d8112c54638958cc1f550a061dfd406fec5051097bdfa5ba7b0ed3464dc883b1f430ed29fbbd4f4961a82b480d66153217ed0b5119d97e10b9dfb22358e8ab64aa232a23a35d21f850981f4ed333e1cf2b427bf251f000f3f636ddb4ca7d33099d60a4da0fed1ca2283d881e8ba09c0cd9b020ee6df2ce6bcf9aa9295626fd36a4b48ac450924d5533168820d46d603da2ff1a2cf481faf2462809a6b80d35c7516bb0103f334102da15a040f9c197645af9e041e6fc89ac45cc5ffb45974d829a9292def6c74b3de7d18d8b94d82498ac4c9c85fc9d8a1bc2f9c583fc28f55d1f6a4c83bc3db5640d23c6fbe3264380addb4a8a92adca2b0a544b587972119add192fde5b33b04d7acf8cfe0982f647c8502b10fcd2a9ce57a7f29bf29cb1d1b61223ec9b187683b0b17268901995e795082541b042c5bfd82e773aea68f04ced5ba3b7089d2479248ccc11f1f845520f41317b83a7658eb6d5e314c5f1550b9d5d67e3ccb95d48868202f6ecc5e3f5ddb787826ee1b46f1096046e76133e590d003ee81d94127614ff6b292961ce41e66ebdcbd6a9d380c1691ef77a7768107a64d5279f5b60d14e70fa709f404e0462a8c9459a3e4d6a00563b25e44a242c3f06f46a30c1c3b808a528de12932cf741bfa3adf6b87cedc11c3bde6e5dbc05a9c848fbc1e42c646e809f613c76200db328488b953e40f14829339bb5b4dd87051b3706d3d60db31529b5dc1027e318a4adf11c161cc70ccc4616e8ff3c917fd8817d159a31f1fe49e42ae2ef21eb72ab6be0bdca75fb89f290cd306589e028dd7a4fd9ebf730e003dfe1d004099bcc4a43bd2d3ca151b55adeeebf859077faf5d3a9b7981e3b55bb2124893bfb559bf6923b5c931c3c5cf711638384bd7e0369048c0d89abfba5b2f79ffce05a200085eb42c1d3a3ddd8c9cf842b832f07378856c53a3f993671298e359378a4481be7f6eb5cbc821148e1384b193bb02de80f3497fb5970fa6f9e3dca3598c580f2b46a4433d0f9a78c3080089f13ea1d63a63642a357e8fa425c6465d80d772032c5c4ca14a919cd2a3ea7e3c89ac83cb02e33c1f220a472788343160fdf67bc67a7d3b27e3beebd41a45714910044df0f44f337570d183b506243d9727e5a53d47428c467b6ae99e129556b2f849f1b91e877064d67ec537e91cc236c5387c50d65d425d74b58b8d8960961d7eb6f0b908d5f54d112bd52f80c777526c5e408cc74dd882754c4d659d101dbd78d896f2f9e8df01e388e9f24627cef371241a83eabe472b0abe9319ee1f45dde13cf9918fdb03b9c151db432270d84e5d47921ca9641339ae390aa990738407af02e976bdbd8df1570941e7a86948f92c596d6b479b5414e8cff19685a9d4dc9e48f282a21dc73b0f200bfaec142faa59c7226e36a8971adf500449d06d346af994e95d977bd26d568abc65c6b22b63a38d780e27db3b20ac507ba1dddb3b20ac507ba1dddb3b20ac507ba1dddb3b20ac507ba1dddb3b20ac507ba1dddb3b20ac507ba1ddca2f19f29b5cc56fe69d88f0059f51fcd483d75c94dc8c2119ccb61515bd523619ccb61515bd523619ccb61515bd523619ccb61515bd523619ccb61515bd523619ccb61515bd523606231d995bcc0327f0dad2cf7ffd8b7c1a05763f642d147758572764a4de36ad31fbd28062acab770fc155b6329a36c030c3a2e1fed8fab8faa4780d4c660237fa344de3d4ff1cfd3ba0659ad0d59a7031447f1a161f7d6298c7f7fb494fdbc245234ee625e18bba7936eaf8c545eaca5f99a0c24ab1b40b74a4cf5734a905687c11729c5fd01baf5f00f1e034bdffb3f0af203f4fbf4e2ab4b238a95d96ccd22807e9c4f53d2a79f6afc585ab46cb17e5ca1f75f2181d5678e5cadaf8f44143a9c6c1c94f48f6bb0e162bc943bb5f4e8880d16a7075046b9f6800261baf390887bd4a844980db6f128d3db6d433b2bc2af17f7fa7798532630000000000000000d8b9735658fdb8aa962f18732c92d43d41075be5bf0d8147b01e2949071959a258dedfefbbd83f72bb8afa295b749363c1bebc6859ecd8de000000000000000006b7c999e30a1f9ed4249f9320204d8cd9bdd85645fbbf4f5b69641a38ad1c0941d0c9efc3c4b05133b2e50f2e0802f0af0da3bf706ed7a4d09609004ffe11295c75a37276a32bfc52f5303fb09010460bc555781b4320053b4f85e62067011642de2486692dd5d90ff3ac1fe9c08dab0c026302e651be160b1666113f1726ea2ea50bb66c38cbacf17f499c8f68cab5ed91805b731d49320b4f954ee6e7c30a17009aa3ee5e9a8201fc8d0cf0c5045de76b1a43925f78f9b0f368f98994bbfca69dcb51eb7ec6a9c1c1d26f84f6e962243936d3078aee240641057b2701cef80e51024f47703cadb4b8f69fc53f113b53c14fe1c731c11b65a04510566a842e6c641ba5c8e58b2c0d7c2b4310b704d22133e32afd7601f70938396540ab02d45d7b7a8fd177f5afa8ff76024d2fc26e9448ed86728eea5447153615de7512fefe5b1c70e965cf5aeb985ce3baa25663c365861052adfecb732969bc5ebfbc3da00feba3746f2eede83b47912f17fc707ff7c5b33c01139724da841de919e4c3ba04c56632b304584e6a8366b64be97417ce95104bcb5c65134e260badf5eff79f83a2db745d4f0b3a9682460efec51bd2be1213b1ee737a37cb450cc2f970c0a552868f91c3f49311211ea2e260ed0bbe73cadb041e5468f62075c756b4885eb5c770be806241d2f66e34ea7494cebe205b2022827dcb9e5b3c0b365e3db5afc816e6aad10790de39d051e687530250d31c46ca39b24597e6fb6cd1c40ef2759d6d8e2da382773f288731e68707cc3502dc3a2fd693a81047d6ec8286ca1f0992e904500c65a359ef5a4e1f16e45770f37f49b34c120f136391f264ebf92d0c71fe43b91cfaa97a0e5eba5f0a78b2be930b3ee20c1dbe395c9bed81e7d12e2f55ba1fbc0e442de63e59c4aee60e073ff45d5624b9d323e0dc5b614328234ce5186738f51ffd65ce5e17ab9d67a2ceb299b11d2094045ed9508d2c2c59b11f767217db3b926235cdd9241ecab00a93e880ad5979880993b99c4f3296138de6bfe0a0a2b75794cd895c7db2abc0275901d539241fff205d94924d28ebe4fec28441a45f7c7f32d1ab8645ed889be9f889a4c6c1fb9090738a419d0362e07360b817aff4dc6395a2172e2a041f95bd0b41fbef998c2faaca6a4cff08e71452a120056ca2c0dbcd9138c7d4be935a792f878097ea1255a922bea91a05735ab650ef7906c87e03c4b9f709880220b0e3da20b2375af1c0fe7de0e3069542aebcc558df03f6d00861f491bfeb4f48722b8a4872f489d0d761262ae70964859040951558ef741c78930d0a42bacff13519478b026d9acbfc1ea815131d75b0cdd43fa83818adfe21bad2337ed4338804800eecef9afd008910f9565ae21e56f686de839c53e513bee8c0895e8b7affdc66f29dbaa4eb7e4b9e1eda57a97cadc1c3aee8969a9bf480a64a90f53ce7dbdfb28b39574e403f1157dea27ec6c4a919d6a48c9a03b111cba14639cdb181686a2e5d945e63d4735fdaf7629a0cca5f2bb53d803ae7fc5232b35272af9fcd7332dc804327e871ca9bed506dec11f897ba298ddb16090c6a1b5189d529bdb09ed8e2c6756524cc0d054e49f1f567a82d419a06c33da726a6da7d5d99d38c99c7e2dcfc24a047bbd8920456394b7d9c9759b7f92d9173d0a500bc5e33e840cea3ae90eab6207bd38146ea0a0ec31dba605bdaf1d764da31d9c59cd60fed77497348e94355a9babda5ad8603d75b6a00c398e8d70ef5b45708f3a5bde16be59e110cfac98b8718798ccf1824740d6ab16526fe7d3a659ed9b2e48289013d7a1c34db1ecc485573865cf8521a6d62e86c10fdd97ff3226663bdc60baee8d088f806539160960fa2b2efa8011e3ef579f7de4d5c9031fb80eb6e6396ac80e12e086de81480324b25c26b63250f670c51c4514bcf6ff09f6b9e8ba69bb0232b02177195ec05124ae3080bd810dc9e035f764a06ea6984792b1b5f8eab77fc459430562de67883cc8d51acc668927e0e99c68bde1d3747394db53740a9b485611468005a59b28666f1fc56ba89c3a2165316b51787617eb03ed042e053c8d0354d49b00cf9b5e1b879847227fa2fcdedac132be3d7ccd20cfe58aa2c65f69e3d12104743e896162352aca65e14a020f387d97fcdb0d72b9e465454c3336d210b08850925948d33ce72e209914d163914db6039ddc07e3d4bb8e15de8b14a7b5fa9036679e1aade9e0537f661e7c32987b1c086ad09df0c58ccd5c55d1d88f50f3d4b6558c5eb16c20b88b462ef0c5ed51a71072cc71fe21cd9d61a8fb3c4259642856de0064f7b02d7d2639cdfd25840b392fb0c29614373553534f501454277d02af05e73f2ef34834edf2b26f679fc647b0ee3aef1726dde75360dc7172064a7c1ae1ec17813c7fa74d98b6c99d85eb58e7eeaafa7dbf482206609d7acac93abe5c3974d9706ae39bf1a77ae5d779292e146bbe26f727653ed09d12ae1040d0dbe3865b49e28fd4c69b2166002daaad1b5bd77a358cfa7c7e969c74d54aaaa6980be3b2da698ba3e5870c435b5f23c9f51cdb252f5b9830f806be85de5a1a897a0c1f4b07e1900a209967680115241199e67cec6b6a237a16ea85cb5bd6e10ca56b0212b31fc4b65804f5ed6cb6c550c44447241d233e9bf9eb70a4343aa1b80036fe6d6a2391b2790e24b7e560dbcb3e074da19a7fd9f2dacaceed98ade6fe928b67796d9a3e11798d03f29dfa7433caeb7993834e6008eb2a920c5f4545f87edce30865bea7a9e78f31384a93d6ba3e3e700275773f8f7ca74c7416b7d759de0ae78879f892d11c20c75e058bec8facdb273fde99d4d7167505688d8b4f40935a895664ee14e069e1b6f87330979496c82d67218cbbbcc8f33767ed84705a31a1c3330b083d4a56dc5a0432ab53823b746e7ed61bc942d2216b488d21af9accc09cb0389b13b6c4662d76c1037b7d60f6f1efadac6dacb9f39bdcd61c26a3e5bfbc48067a77ee38647908468deb445066cbd6a41b3ad00ea12bba3c4febd68665b285b52a1eea08fd9b801b9eb649d9f31baf42fd15817036bdae1f92eefc7c7118637079bfe4ed870c0576b6d3e271ec777526c5e408cc74dd882754c4d659d101dbd78d896f2f9e8df01e388e9f24627cef371241a83eabe472b0abe9319ee1f45dde13cf9918fdb03b9c151db432270d84e5d47921ca9641339ae390aa990738407af02e976bdbd8df1570941e7a86948f92c596d6b479b5414e8cff19685a9d4dc9e48f282a21dc73b0f200bfaece341b851fd5b0728141013c977a1580a39a18209ef0c5c9c36812f0e59f9b948ba3c35e754a30b646d0a9bdd88cd01a96d0a9bdd88cd01a96d0a9bdd88cd01a96d0a9bdd88cd01a96d0a9bdd88cd01a96d0a9bdd88cd01a97e3dc1520e28981a10d6b5e08b4484ee17634fa25508263d145bf922d1be23ea145bf922d1be23ea145bf922d1be23ea145bf922d1be23ea145bf922d1be23ea145bf922d1be23ea0677dd26029a828b0f5f259d6bf94c8f09bfcd5c0f3f6ed22bb552620059e77abe071bbf48419cd934257b293e8f6d73db11e25e1eba15f2af98b0fb0cdb6b3fa8344de3d4ff1cfd3ba0659ad0d59a7031447f1a161f7d6298c7f7fb494fdbc245234ee625e18bba7936eaf8c545eaca5f99a0c24ab1b40b74a4cf5734a905687c11729c5fd01baf5f00f1e034bdffb3f0af203f4fbf4e2ab4b238a95d96ccd22807e9c4f53d2a79f6afc585ab46cb17e5ca1f75f2181d5678e5cadaf8f44143a969662aca282f25352e0630cb37e14e8022a523c3ec7366de9cb93af713033fb289a6c8d23ecb5d03094ff486f7f3dd81f0072cdfac74e7f8000000000000000027b2d63f48ad54d0dd47464b132f88412ff3e1976a8cc22991bf41000e88a00782c73cc380691a2089f882520a06fb0b2b01dd7ecbf6538f000000000000000006c468ffad0b7c5b73f85fe92a690741167da439dfa99fde810ddcd06862a67ef25b480c832dd8a12326a0779a303b48214e85bb10c523fa7533308e44744df10c75a37276a32bfc52f5303fb09010460bc555781b4320053b4f85e62067011642de2486692dd5d90ff3ac1fe9c08dab0c026302e651be160b1666113f1726ea2ea50bb66c38cbacf17f499c8f68cab5ed91805b731d49320b4f954ee6e7c30a17009aa3ee5e9a8201fc8d0cf0c5045de76b1a43925f78f9b0f368f98994bbfca69dcb51eb7ec6a9c1c1d26f84f6e962243936d3078aee240641057b2701cef80e51024f47703cadb4b8f69fc53f113b53c14fe1c731c11b65a04510566a842e6c641ba5c8e58b2c0d7c2b4310b704d22133e32afd7601f70938396540ab02d45d7b7a8fd177f5afa8ff76024d2fc26e9448ed86728eea5447153615de7512fefe5b1c70e965cf5aeb985ce3baa25663c365861052adfecb732969bc5ebfbc3da00feba3746f2eede83b47912f17fc707ff7c5b33c01139724da841de919e4c3ba04c56632b304584e6a8366b64be97417ce95104bcb5c65134e260badf5eff79f83a2db745d4f0b3a9682460efec51bd2be1213b1ee737a37cb450cc2f970c0a552868f91c3f49311211ea2e260ed0bbe73cadb041e5468f62075c756b4885eb5c770be806241d2f66e34ea7494cebe205b2022827dcb9e5b3c0b365e3db5afc816e6aad10790de39d051e687530250d31c46ca39b24597e6fb6cd1c40ef2759d6d8e2da382773f288731e68707cc3502dc3a2fd693a81047d6ec8286ca1f0992e904500c65a359ef5a4e1f16e45770f37f49b34c120f136391f264ebf92d0c71fe43b91cfaa97a0e5eba5f0a78b2be930b3ee20c1dbe395c9bed81e7d12e2f55ba1fbc0e442de63e59c4aee60e073ff45d5624b9d323e0dc5b614328234ce5186738f51ffd65ce5e17ab9d67a2ceb299b11d2094045ed9508d2c2c59b11f767217db3b926235cdd9241ecab00a93e880ad5979880993b99c4f3296138de6bfe0a0a2b75794cd895c7db2abc0275901d539241fff205d94924d28ebe4fec28441a45f7c7f32d1ab8645ed889be9f889a4c6c1fb9090738a419d0362e07360b817aff4dc6395a2172e2a041f95bd0b41fbef998c2faaca6a4cff08e71452a120056ca2c0dbcd9138c7d4be935a792f878097ea1255a922bea91a05735ab650ef7906c87e03c4b9f709880220b0e3da20b2375af1c0fe7de0e3069542aebcc558df03f6d00861f491bfeb4f48722b8a4872f489d0d761262ae70964859040951558ef741c78930d0a42bacff13519478b026d9acbfc1ea815131d75b0cdd43fa83818adfe21bad2337ed4338804800eecef9afd008910f9565ae21e56f686de839c53e513bee8c0895e8b7affdc66f29dbaa4eb7e4b9e1eda57a97cadc1c3aee8969a9bf480a64a90f53ce7dbdfb28b39574e403f1157dea27ec6c4a919d6a48c9a03b111cba14639cdb181686a2e5d945e63d4735fdaf7629a0cca5f2bb53d803ae7fc5232b35272af9fcd7332dc804327e871ca9bed506dec11f897ba298ddb16090c6a1b5189d529bdb09ed8e2c6756524cc0d054e49f1f567a82d419a06c33da726a6da7d5d99d38c99c7e2dcfc24a047bbd8920456394b7d9c9759b7f92d9173d0a500bc5e33e840cea3ae90eab6207bd38146ea0a0ec31dba605bdaf1d764da31d9c59cd60fed77497348e94355a9babda5ad8603d75b6a00c398e8d70ef5b45708f3a5bde16be59e110cfac98b8718798ccf1824740d6ab16526fe7d3a659ed9b2e48289013d7a1c34db1ecc485573865cf8521a6d62e86c10fdd97ff3226663bdc60baee8d088f806539160960fa2b2efa8011e3ef579f7de4d5c9031fb80eb6e6396ac80e12e086de81480324b25c26b63250f670c51c4514bcf6ff09f6b9e8ba69bb0232b02177195ec05124ae3080bd810dc9e035f764a06ea6984792b1b5f8eab77fc459430562de67883cc8d51acc668927e0e99c68bde1d3747394db53740a9b485611468005a59b28666f1fc56ba89c3a2165316b51787617eb03ed042e053c8d0354d49b00cf9b5e1b879847227fa2fcdedac132be3d7ccd20cfe58aa2c65f69e3d12104743e896162352aca65e14a020f387d97fcdb0d72b9e465454c3336d210b08850925948d33ce72e209914d163914db6039ddc07e3d4bb8e15de8b14a7b5fa9036679e1aade9e0537f661e7c32987b1c086ad09df0c58ccd5c55d1d88f50f3d4b6558c5eb16c20b88b462ef0c5ed51a71072cc71fe21cd9d61a8fb3c4259642856de0064f7b02d7d2639cdfd25840b392fb0c29614373553534f501454277d02af05e73f2ef34834edf2b26f679fc647b0ee3aef1726dde75360dc7172064a7c1ae1ec17813c7fa74d98b6c99d85eb58e7eeaafa7dbf482206609d7acac93abe5c3974d9706ae39bf1a77ae5d779292e146bbe26f727653ed09d12ae1040d0dbe3865b49e28fd4c69b2166002daaad1b5bd77a358cfa7c7e969c74d54aaaa6980be3b2da698ba3e5870c435b5f23c9f51cdb252f5b9830f806be85de5a1a897a0c1f4b07e1900a209967680115241199e67cec6b6a237a16ea85cb5bd6e10ca56b0212b31fc4b65804f5ed6cb6c550c44447241d233e9bf9eb70a4343aa1b80036fe6d6a2391b2790e24b7e560dbcb3e074da19a7fd9f2dacaceed98ade6fe928b67796d9a3e11798d03f29dfa7433caeb7993834e6008eb2a920c5f4545f87edce30865bea7a9e78f31384a93d6ba3e3e700275773f8f7ca74c7416b7d759de0ae78879f892d11c20c75e058bec8facdb273fde99d4d7167505688d8b4f40935a895664ee14e069e1b6f87330979496c82d67218cbbbcc8f33767ed84705a31a1c3330b083d4a56dc5a0432ab53823b746e7ed61bc942d2216b488d21af9accc09cb0389b13b6c4662d76c1037b7d60f6f1efadac6dacb9f39bdcd61c26a3e5bfbc48067a77ee38647908468deb445066cbd6a41b3ad00ea12bba3c4febd68665b285b52a1eea08fd9b801b9eb649d9f31baf42fd15817036bdae1f92eefc7c7118637079bfe4ed870c0576b6d3e271ec777526c5e408cc74dd882754c4d659d101dbd78d896f2f9e8df01e388e9f24627cef371241a83eabe472b0abe9319ee1f45dde13cf9918fdb03b9c151db432270d84e5d47921ca9641339ae390aa990738407af02e976bdbd8df1570941e7a86948f92c596d6b479b5414e8cff19685a9d4dc9e48f282a21dc73b0f200bfaece341b851fd5b0728141013c977a1580a39a18209ef0c5c9c36812f0e59f9b948ba3c35e754a30b646d0a9bdd88cd01a96d0a9bdd88cd01a96d0a9bdd88cd01a96d0a9bdd88cd01a96d0a9bdd88cd01a96d0a9bdd88cd01a97e3dc1520e28981a10d6b5e08b4484ee17634fa25508263d145bf922d1be23ea145bf922d1be23ea145bf922d1be23ea145bf922d1be23ea145bf922d1be23ea145bf922d1be23ea0677dd26029a828b0f5f259d6bf94c8f09bfcd5c0f3f6ed22bb552620059e77abe071bbf48419cd934257b293e8f6d73db11e25e1eba15f2af98b0fb0cdb6b3fa8344de3d4ff1cfd3ba0659ad0d59a7031447f1a161f7d6298c7f7fb494fdbc245234ee625e18bba7936eaf8c545eaca5f99a0c24ab1b40b74a4cf5734a905687c11729c5fd01baf5f00f1e034bdffb3f0af203f4fbf4e2ab4b238a95d96ccd22807e9c4f53d2a79f6afc585ab46cb17e5ca1f75f2181d5678e5cadaf8f44143a969662aca282f25352e0630cb37e14e8022a523c3ec7366de9cb93af713033fb289a6c8d23ecb5d03094ff486f7f3dd81f0072cdfac74e7f8000000000000000027b2d63f48ad54d0dd47464b132f88412ff3e1976a8cc22991bf41000e88a00782c73cc380691a2089f882520a06fb0b2b01dd7ecbf6538f000000000000000006c468ffad0b7c5b73f85fe92a690741167da439dfa99fde810ddcd06862a67ef25b480c832dd8a12326a0779a303b48214e85bb10c523fa7533308e44744df10c75a37276a32bfc52f5303fb09010460bc555781b4320053b4f85e62067011642de2486692dd5d90ff3ac1fe9c08dab0c026302e651be160b1666113f1726ea2ea50bb66c38cbacf17f499c8f68cab5ed91805b731d49320b4f954ee6e7c30a17009aa3ee5e9a8201fc8d0cf0c5045de76b1a43925f78f9b0f368f98994bbfca6b5bc5bdc4e6ef47cb4330c18c3c83b697d87d7b7505e2ffa2fb159b62901d7ee5d2b10553501f5c0c587d838b7be18a02ac109cc0c89ecf21877f8502fc4d9b9a8abee3342e67f4cc35852e20b720d35f6991a26658654c3be29409ea722a5583a1fe33b2f20f4d053f344276397537b7ba29c3cb5f8f4732c487be00e2214efad33d6c249f4f5754eb13e806d8f4ca36360a0368dbbf6373de2db371a2fe7453873e961f58cb2eb576205e25dbe7c0ace7d73de10947f4ca401fa091ab9052b9d7a56b1282964e5f2e6ce22f1e6cf085096828fe61c15e2a364ab74d5d0cc14a042ca8d08be25f163e0fec5b8f16bb78ee8ee73ec4e5379cd67fa70f33d3b4924a3c3f46f6eea84afa061ed5c1d7ef445fb1e5423fb6ee1260cb58838a92b0291776341417fd64daa9594ce148bfdfcc1dd7dcbdd7d2714e6263eb5c9a1863f8a0b0890542ee289375b54021978dcab6220b34535c6317448620ff22d3bf6a06ecb8277d15c10fa1c8a07cc19787102401d94a8d029901c8d18cb1f7c795ff3aafb65a3aff3b1579009e557e0e9c159c7af120f4cb36ea7725cfa2d9a1475df342bfaa946e307121f9e78b29ff529931c691dbe1df3d703b23cd22e7d1893e3a34d442d43f101b3eeda0f07511998340596d423db46cf852941234cbcd78152c47efd650ae09b615101a3ce61984686fae4045edf6b4a84a3cbb11fd3a6c1b30a6c6335c36d86efb3310b93354fd14e33250a938677491357f48de668ec23e742b494cd47a8cb8fae952859533f5bf4b491215de000a67e7026ffc2141bb414290633d1828029cf1340eaf87664d11987d29073036fa9ca1bf873609d1fef9bf59195a2229cbb686190be0be06a18f0f87baaca72d0669b081a52a118ebae55faa2ce913e24c6cb1bdb792f6ca5b211d2fea922ecaabff263d4b6508ca52faa06a44a7819341352011124fa882643da9ada4f1da5e1041cc0960a6c2948fa5e4cee20051adce7f6b3643f0626ae63ea98b4b2a6ef18458e431208ca0a63540266741c78930d0a42bacff13519478b026d9acbfc1ea815131d75b0cdd43fa83818adfe21bad2337ed4338804800eecef9afd008910f9565ae21e56f686de839c53e513bee8c0895e8b7affdc66f29dbaa4eb7e4b9e1eda57a97cadc1c3aee8969a9bf480a64a90f53ce7dbdfb28b39574e403f1157dea27ec6c4a919d6a48c9a0362fcdfbdc4bb7b70462f383dfba297b63c44b4d9fbe618e92397ed4195e2c8c6f77aceb3cadcb4b181fa0d4fc7220013410ba6f6c99ea2fe4aa59de927265bda1134617b252c4efbb0fd62bac0ea0936a64397c6d65d332fb53c0304c52ca2b6b40edddb83a303729a9c4e56912b0358eba9b04795f852b845e364283d4f1fa7b579a81ee5e8a371f32a42e74ce0bf34db8d0bf2c689c804f1c231b668ae29aa945508f69409038d55a19468dcc4fcbdec9a0f171c77a4bc8d1c2e8d184108016aa028745f1f0ed1705de1004053b86caab1e422593ec320ebe3194127760ed38dee4c1315abf9fe52971a3f4928ae8fe207350f0be5875dcabc4bab015afa9a925c13c5f2a45f1699c927be5516aa22ec2c47b0151e924015e82b86d09c99d6d891b10776d4a4f5f95438dc87d33299abeba5e50b1899044eb702979d33e3775615988dadb300f331fb69ac454bfec30256f1e80c4349afc615d0da98df1f433f0d6c57a74809fad175183f5db6f094ded397b35cf294e886f4ea023b2cf796c86b53e5ad2ad8dd4a6cd9e89bd284d58be1f03ac2baf39f1ee566cbcce5265efc22dd22e5d98d5c72bd3f57c25c19041bd03f6290780dfd7852f15c992dfa78066122c35ae1545e522cb4cc40d3d94083ac0c328f85cff4d3ee59c1602a2be010c4d6a1c71ca9e46b154f44fd97094ec4cddd8219e8b906f25cad140f195aeaae6071dda97dc5131eb88814ba4d6050791848767b4855cb30605814fd8b293c15493a409a9dd4b1412b6b3ae717a073455e1774bdd56bc938a930eddafea3567540df939fb73504b62dde206f3971b381422d876bdb5bcf608d617fc45329bfa2bfacd51d1841e86c707fb7b7ef583faa52f645560f26e22174fec24fdadf8d790811cf8ca6aaa454895bc88c0a64f352ceadc3406843ebcd83898d41de63396b16b8f0b47e2594b3e2d54508590e6ebd8259d067e712868f61af0b487754cec8edafd2bc392d1702ddbf56f5708114f1aa54cb8c01204742ff84923de9f9d5c37b42583e192b4494cce52cc81191379cf4880101b20163d42bfcfd9c9e01f416315a7fcce38d097b64a1e018d10579178f000d96c9f0c49d383c6df63ad312be0cfe0403b963afeaa2134501b7fa36b78e7c36dfdf2a5ca1818a00c3657693fcd95ecc4eb7ba47489f0dfea1e5dbc152ca53bf1d48f4d7652af620ef34945481acd5abcee5f1d85a475e6a76103e5ac9a2eeab2b86c1021ad787d1fe5e45eabfc0ca3c95560ae36332fd898c689a8f4cc836fa903d274fdf9f3747f06a20194185fa24ddac12015168ba82fc980571afa970020e80b2381bb1f515d876b451d89d2a383adf448e0abf3cb125a9a1452b2281b0ce587dca3986ef1cd28c2aca900797877219ed5cc8bd4b90ebc9a6d05cbe4061afbfd4d05e7e8b3102e5e8f0c87a551910d09207cab472545ee9395873d7a4cc5eeba3cb58da090f1433eec0ed4c7a00233c05051d6c15ff7dab74771397543c443c707106071f5e0320d1d0576fcfa687d337559f0b9ea3a79d728d4f97553bc76004c4351cc236c5387c50d65d425d74b58b8d8960961d7eb6f0b908d5f54d112bd52f80c777526c5e408cc74dd882754c4d659d101dbd78d896f2f9e8df01e388e9f24627cef371241a83eabe472b0abe9319ee1f45dde13cf9918fdb03b9c151db432270d84e5d47921ca9641339ae390aa990738407af02e976bdbd8df1570941e7a86948f92c596d6b479b5414e8cff19685a9d4dc9e48f282a21dc73b0f200bfaec7220536910de084623e30dcbf57ddff9b28f2c864149201f930fb789140a61c7d7a15947cb5759fd9b57de391123138e9b57de391123138e9b57de391123138e9b57de391123138e9b57de391123138e9b57de391123138e8de2ea3a2b98cbf5c0fa00290c2740ac6c9bb063dd103f5a0d2aa62ca77620600d2aa62ca77620600d2aa62ca77620600d2aa62ca77620600d2aa62ca77620600d2aa62ca776206006d1c3bc1013d488d90426753f6f8721d12819ba3232033ccce88e1f5442c20eaafbd28062acab770fc155b6329a36c030c3a2e1fed8fab8faa4780d4c660237fa344de3d4ff1cfd3ba0659ad0d59a7031447f1a161f7d6298c7f7fb494fdbc245234ee625e18bba7936eaf8c545eaca5f99a0c24ab1b40b74a4cf5734a905687c11729c5fd01baf5f00f1e034bdffb3f0af203f4fbf4e2ab4b238a95d96ccd22807e9c4f53d2a79f6afc585ab46cb17e5ca1f75f2181d5678e5cadaf8f44143a9cc6c7379f48e187ce3800b92f5b0ee4cecc8d7a31d459134ff41674f8f50f032eca3a339299bf42db5bc229c09cfc8018fe5d4d167556cb60000000000000000f052dec171ebeb593a40a1f372232427b5db315f9a34ce24a6cce718d0693b7c8b5fdc210a82e027da9bbfc3c79d1eca305dc44351012be5000000000000000006fdfd60f5518cd2dac77d57858a68818fd26107c05b0bc37f36825cba30a49422d0c9efc3c4b05133b2e50f2e0802f0af0da3bf706ed7a4d09609004ffe11295c75a37276a32bfc52f5303fb09010460bc555781b4320053b4f85e62067011642de2486692dd5d90ff3ac1fe9c08dab0c026302e651be160b1666113f1726ea2ea50bb66c38cbacf17f499c8f68cab5ed91805b731d49320b4f954ee6e7c30a17009aa3ee5e9a8201fc8d0cf0c5045de76b1a43925f78f9b0f368f98994bbfca68022f164e23d345198ada8b49dff9e1e467f6a4539f33032fb351b8f0da2fb89c6fc67ddb34c1d293a9fdea6ab491b51e1178c10ddf2f52627ab7e579c1d80c8e83a813c247260beb5f6faa1b775726a64854bc832ea75d0581d381722e06b11b4766a68dfb5c3dca646c51001e3942d25060b1af03d11afeb23cd98bc933b741331cfa0a198ea248f5213adca969e88c9e80b23314cce4ddca0d5739fb57429e4b8d5bf29db97099eca610c0fcb36531ec7874aeb57d3fc9a38bb1fa47220454d6ad3f4c9b4bf30e4e466303304dad3388f30f0b45655fae45e0d4f6cd6b21ebad84bcd7871a5e6c61bd26ab79d1f01747a37427972171171c0a53549339805f59311ae84dc5c3cee0bbedff3515f9e5568f68de0bb04e1895eb5e001daf34a42d2f6394d6f889ccfbe2092fc566a6bcc9e5ba5133f2282b6afc8c43e1ad9c191de39ea8876f4f70350d330abc9a4ab4697e6e4739edf4cf3759d05a0b89df0783f2893f992347dcd35027901e475f8a91047241cc0e26b2009922af373e734a459ef175756049a5870f3a65970f61a10136381a63950ed2e0c716fdf8ea305aa7a0e0212ccd405b3be93a292e16187bf395cf502e496e22f2f5565e34ec32d2ee63e46b25db2bb083ff4a7331225f024e0dca385fb692b4de518a551d7bedc66ce5e99613c8102cfb299e985affe5c5fd9504f83061bfb20767273b35d344e36cdd9e9eef6939c94e880dc4d4dcef494b99c5312cddaf5e7bfe0b1e6a90b72ce895c608fbe4b6b5a01d583f3526ad75e9492c77d4c6edec38441b31490d900d2ab865cced7f9ccf989a4bb19edbf15748a413aca792d6f61b817639be5078ca3172e51680b6e6a0c41fbd6ef9f6f41cb6a4c119b088455a220050155f8e5ad9238c794cb065d313087802c11e5248623bea969f22e015651ef79e6a99d2b4906ade67ae60f5e3c73fdfc16c189a3ec6f73c54d90eb340d4edbb1b882a93a0d7e07e95f87c4157642162fdaab338133c2e4db35b9871ef6540dc937d644ae9fe9c6148e36f8f1ff72fb56660567fcced214c00a72179d41db2d45e044bb314f65adfe21bad2337ed4338804800eecef9afd008910f9565ae21e56f686de839c53e513bee8c0895e8b7affdc66f29dbaa4eb7e4b9e1eda57a97cadc1c3aee8969a9bf480a64a90f53ce7dbdfb28b39574e403f1157dea27ec6c4a919d6a48c9a03ff8135f9ddf47e70610aa171090d273de0ce1f28c3dda1f44a48c2a3fc2744cebf0115fdd6495eadd036d5eccd4ae7e92e32e5e7ea9a713f05fa2d7df3d46f64fffdbab031fadc222b5a0c0ebe61c1b4feb07ded3053fe3aedff0a161da1fabded2dee09d6218524df21bcd88a6913e36885715517a5a217c00b60f68748aafc61ad6c74a85c9943d5285eb2d6882580d9ef8532bca2251980b01fd6d45720e9dffdb90761f5ba354acd1d5e554ad2a75fa1463f6b91970876906188ba0c38869be8584601716e15f7095bec9589fb3d9d815b604e0a8386c18bd2f9c8b3fb44409fbc22ec46bf82016bf1ea79012f16fb80bbe8081f1cd693538899b0c5813cbd370345ca1ed0f2864c4126e1ade6fefa7316f72b2ecacfe95ebd218e6d5abd879f081073f5da011786ab939c5e272060beb88080a3e497f2db830e6ecfb41e89656084fdfccd271cca72810a753aebb2f8c279e9ea673bbe8526d849a734c3c78d8f9541c2dbc9108e9ca9b547c7763cd82a5975ec308e33c7f29c3e9fe80c85e5b0b1bb7b198f0646d9870cc1c9e7c17536bbcab05c267b2a870683ac05297b19242e1d32b6917511cec04fdf1c9e513676360fc02a886ccf8bfde37346798c385f4e0f3cb9553679ec5723692474a9915b3e1aec122e2d99d977ec1a7bbcf3c978e9f45c63afc750ebdfabb1a7662897f2646ff32b3268d28953ffb30e973eaafd2a0c2c92da0a07c6e847cdadb51f8e00c9ccbcf37e5bd1e06cae9d41763ae6411367a00efd6f517baa2cd7f193ded0faac3a77aabf4349a369408413c552ee3874ad5f3d04881f751a9f5aec10e189316ff0ca19215ce5e232cfedb2144ab31d256538fef8fd969428f9f72b7a7a9fbe6231be635ee4c5ff711ec7ffb59dd958d0ae5e8b4f59cadc687e93d61c30c514b68a0aaf97563828359b74e867f3d1963d214791f740514a0ad67bb6efce5913a16febfbbbb9072692b721ca478b93adcb2adf44a4c8f5fbd54cef4a031d8a57338ac1b0905a7e2014a34e285297a886c88aefa5f992e086bb9f8358a725356e2eaae78fbdcb8bcef0c017224564e42374378de8a0e5b7b77a34a0e92de1c28a23694433c5dda52ae0a01ad61f4f93c9bf63e2b2d30c11ca347b352f8e18c59b63fbba8f03530b13327aabf8ecec519dbc2727d1eb43cd826f44e7c53ad44ca5e688673f46e4840a1162671105064a0f2476b6389e7ae8886726740d1217b287569bf29c4bc7f5f612a0db41c3ef23984da3ced637501d2f696c537bcabb625fe9cb7d869b87c05972ae894fb59e915fa569e5b514c6d3223caf345b264f32de8868df6d956b622ba89b678f9e572b6ff0e4dd79ae23c41b41cccc9a1753909dd695c29d5bbbccf9e003dead879d7b58f192fa1bfb6752b985ecc02ebd3208105089a6b8773f36ed00f695be63419fa809baa93f73053c920ac92d0b2d3820daaef27558586570553aabc2a419b919c8638502d597990f13ec5abe844eba5629671740689d0606bfcec01af550d4c34ca0aa4e79eb2c3cbf7a5839db4b4b9057d6e74911d62c904f9b21ec350b905324928cc27b406c2daa981b9af2e0f2166266aae051503549736b82d4d802cbcf4545cc0a3f38aca27c0d5ce2a59e8774ecd5dbdc59781527cef371241a83eabe472b0abe9319ee1f45dde13cf9918fdb03b9c151db432270d84e5d47921ca9641339ae390aa990738407af02e976bdbd8df1570941e7a86948f92c596d6b479b5414e8cff19685a9d4dc9e48f282a21dc73b0f200bfaec4eb04e4efde5116db66300b4832650950bde32683e692686551d001c925e90dcf41f26677d7d7e517220536910de08467220536910de08467220536910de08467220536910de08467220536910de08467220536910de084687f32df5f0c6684ba5cd4547faf9e14d693ef6045af45c8323e30dcbf57ddff923e30dcbf57ddff923e30dcbf57ddff923e30dcbf57ddff923e30dcbf57ddff923e30dcbf57ddff9069c03c5aec96d2ab3593f2516ef51579203ced575b4dc77006fee92c1d61ef9d5f92324081bd16278f4449d010421cf7054b688898e34e13a7c9a895a25331f16f2c3dea7d29d7fa9963c72560d7c5e7e340d47db8c6155a8af790b3ccd6d59aa234ee625e18bba7936eaf8c545eaca5f99a0c24ab1b40b74a4cf5734a905687c11729c5fd01baf5f00f1e034bdffb3f0af203f4fbf4e2ab4b238a95d96ccd22807e9c4f53d2a79f6afc585ab46cb17e5ca1f75f2181d5678e5cadaf8f44143a97985e88fa19514f389a1a65dd16ad19555be88986088aea5abc8997b2bff1536f915c8120abfc51315144542f6ad2700d66bfd6a9ab9f4d500000000000000001ed8f868722eb1eb95f0914a3dc3f72cec9ae319eca3398aba0238a678083780d394e834e04255b7c74aa7ade8570559888e1fbfa5b35b69000000000000000006833ae955700b31374a7b1dca69ffe9e1ab475c301843ed33bfaf949ed32b953409bd045d5c6b03064c13c8c09dd3c81b9602759de0496bbd3ddb29d81ef07c06d6f154e05b31c9f01dbdac04bfa0eecc3bc851f323c7c153922d81725113a7c7de2486692dd5d90ff3ac1fe9c08dab0c026302e651be160b1666113f1726ea2ea50bb66c38cbacf17f499c8f68cab5ed91805b731d49320b4f954ee6e7c30a17009aa3ee5e9a8201fc8d0cf0c5045de76b1a43925f78f9b0f368f98994bbfca6db23c8b5d9517806712aaaa701d5e74ca792f6db8a459a3c86e7c6d24b73407643266176231aa15b4510b0a4ac5650b0f69eec5a83cf8140a1926d79e024dd36b49313e6604f3e823d207a76390e5af3afe03270af4c0355929572cb1e6a7bf9625b25fed20c5a6776386d7a446dd6ccc023d3cf8ef81387e43872400b2e1476c7a75c52116de6aaf6166cafca5e9d7e9ddec2b0b5f6ff0d6f4c0ab6cdcfe38ce5313bdaafb002c46f79697cc1f09b08a6cdb723e3fde7f5999d391f4d7f39ff70268c36eb7abf75e5772f8065b496d6968e77e9dd3361d932e09329b44bd544806ebe369effd0fa30de9056bbedfc08a0d3bb11ca93bb8888032ead499ac12ca79f8c7026e4e6e26c5ff0fda28ffaf2a142b36f0bdf250846f4aa0511d09e570c92b6cf6d7a43e475f60591e7b7525b5cf6dc2aa1f81111aa7d4526fcd1c80e81f4ce514eb4a3bf13809a865e4d265d2eba3427a1f3fc6691afeb2c07c5ed84bdfb4199cf97a4e961ae11c81220afc345853822e400165fff4890549a9f3ba71ccd7abfbcb222d0c0829b37cd82b3d77998180b3bcd816a7061887bfb761c2d50d573109060a62e94f59d14990c0f3bf1cde1aa1e20b7147779a92a1c771a6e6b31f73197ed92dd39f8a13fa49128811f01e71e2edc4f5b622ac72893baf6e53073f6ca0ce309147696cd4c317cf5e7f1ca867a2134d8d8feb0939b9deda271ac69ce4e7bb79fe4994407e4756a72a699cce59c9968d6ae35ff058f384f5d906d4ee4027ef45d5aca0aa81ea29752bbeaa2943cf46372f318260c9aa580cc068a5e35e478bece67c84f24ddcd68ffad9d530cd254ce6b7904c3bd18df2c3f6015bd7089455b70535e08dab780ff7c0b5656638ada4420ac0b052908adc22f6f8fc439a65d36e88a7c3904648d28273116f14d4d957709b0867acf3351ed5c490603dd074b448b0913934d348041628724ccac49ca22c4f29a8e0e2d3348fd90a0e4edac9feff2211eeb2ffa71e17965c55e36afd9439d7a615b7cba71c140805690f7bdae49de0b94a34c8f6d5c103ad30a0560e17411dbb9649e901777335e1122125bd7c03764d540a960f862c844c08e2cb51ea4bed42a4a1f05bcfb46177859043bdc06b026dc821c1f2632f28d9ab88ec24b711528d708f8ec1c5ddee22379d0e4b3548b495c7d1ad7146368450611af4eba32c1731a566453c21cef004f0e1db8e8f04b4af22ee4e24b11ecb2a51f0e3b6f791ec8ec242a79edbc76d9e0fe02e930078ea4d3b17a30e52581ef09739ae3e8bf441acaaab5bd68c751b480ce4940ed9c3f3f4f8a78c539b4a7223ceda6634ae37be64faed5c27430ba0daf7a131c55279053c2b6f7ad9ad81d8089d28a8216f3b313844e2414504d934910ad45cee6a3e6367122b29215d61365f918fdd146a6dabb882f7ade653b7c2a2d9cda857e91ea6617f932afc5a9b6037c90c4b8ccb2d4f28bb12f1e1e83defce0854181a7e617c9acbbc0102a6c955f0d8be6f65a5365ac7c1ddecca37513572c0261fa917d368cf7c9459b08a3c676af6c8a206174ebba450845aa6060ae653f834489b35e7eb69dc688503bf0660ade358c197266b904cf69ce80be2942a854310fb0211797bf43aa7514dd53570db71ec5080f2b537e478dbaa34d22ba93293f1d3fb6150ee4b9a17ca9f69a7fae1ec0039beb2d157e39e940762918664bcd34140c58b59c2be959cab6b8215330ad49abb540057d3843f85a43f639818d35bdfad5e7fcf4519e1c5b8f74fd9c9c3170af059cc8315deb07a6bfe63c1cb767456edf462ef77d662ae66038aabd4cd1c3e621e89f5b7c0b3b0d57e5453b8a74ffb236be1449b43dad61b103ef0b2a9d2c44eeea4ad689695fb8897772f8b4ade54cf1e0c234f62e163e316b850c838c7c9eb6f01ffdf188e44baba7bc5ec56fdca8be461f8a1b9fcf2282472dea6f4dace14195606cfdf1d3d95700f62621a9422cba06233753c12daea343ae121ac48804da2b20277ffeb277a8b89a7c323b1f86dcb1c62533417fb2a9bfd3ba1bc2a3f7ff6ec5cad39c1152e35dfadd2484d48694ac2d3ddeb3004a71d27851da0d959b9c1a68c606031422e73261c7a07889c2a36ec91e37e8c6ce87dcea7553bf4f92ed665f9fed53f423ef21d0261bdfd6bb029fb7ce584427228b95d849ec024df0a4439071b0d3346e725fd49670457a717c80e234aab34c9039e3ca6c0303bda4a9ad20c873a69c62a250a766110fe01b9b5ed28393ae60bb0f050ed4cbd97a319bab12bb3c7cb573ace67a2bcaec5f5a05a1c535292b40ee9b19389bf5db1f50c515951af0f09f994da007f8cf024ca76da9f0bbbe25c723f92ce6ea15fa7a17f6ccd6bef1fdf7b4dad0736a4ba152f3ad6bf7acdd19a569398b6c7dfb6f9b30d7caf451bba36ebacfdd091f04a41c433814899bb215ccfa1e839097c8a35023066621dfdef1b439dff1609a7fe816c44aa8fabb555029124cf4c55989f53a3c0a1d4e464ef412cec7d31b2f79148fe31114b49035e4798ec96c212e57e8ee9fd295c94685798372005a095b49bc7131be97ff47806cb3c102c69ae1ce1dea8500f904f5342b251a88ed8023166519a125991ad4d77dcfe5d0b32b82a792fa2aa3696f9eb4ab23a201a06bfff983080c1edaa571cbbd0f831842d1bb1465f25da79304157d4f2e9b04d70ac3003ab5375115f849c73ba3254646e8d67f987b7d115ec3721162060f05511675922a1137feed03de1ae5aa3a7998fc75b241f737a62be3e4b68b594c8708b21ecc600a5cf3d062e9fdc6356987242b2021e350f276fdadcbcb2afd9be36f39337b59add4f32f57d4e2e1c2b536a13090afd25981340be52c6888041d1b183d00539c462e615d6e96ac42e15b5d3720e15171f479692c7b1ae5558d8287f3659046c14ed2ecd6609ddc717852132a70aaf24e273c0c9a28d103e227214a2412bcb8eff92ed9cf775e5756b408e7c501b4e293326ff4874474082bdde9c69200a7fd24fc465474fb9e70e2534f9ee6eccc62f39f0a382be692a4c3be7729275fdc89282bac427019f629a580ac427019f629a580ac427019f629a580ac427019f629a580ac427019f629a580ac427019f629a58046e2aa05d86afa35234ba20643eaa438f57b5c4a585e310242bd95e28862195d42bd95e28862195d42bd95e28862195d42bd95e28862195d42bd95e28862195d42bd95e28862195d0616fab31eb7b3df219cbfd25e3a10a4f27d7995c90cf68bc03fee0d84bee368d6690ec4d2b16947edb9cb68fb9232556904732c8a1d02f99b21aba5bba9adf7e4bb058ddebe8cddfe6e703e00f380cbb318502d25f49e8c611f4e2985b20b2d3edb6003c60c75bd3a594665c4d2747180102467a649a68b6aab258dc1dd7c8eaf2f99747df4ef1ab2caf7403326d29909f5f9c05241a306c3e6d0db443f8a93eb05b46b86d31804036f6ef67e6d3dfedab3d6d5589a08c31819184d320d5d40badde1da7823199c85cd6613a91657c48cc7d7483f16c7beea32807fe746ac0b686ae8ccd67eab6ca11ab3562459154d25f87f241ae438ec4300000000000000009700eaebf7ebe0bade2a7aa705f8ab173c36406eb6983acbce7506814e0a202b29d668c21b9700ebeb165aa04af75140755826c2de246bf90000000000000000060568980bf8e356726714877d32883533fff4bb7b598bf89348885aec3742241f6bd3f4768b830556acb9e01365c6355b48682c2ec08f59d3670e837782e6c20fe747fd824eb71a315fa7bac8f9f3262173d77f9fc51e8a2b9c3827934ae1f9f06ab962c6fac062942dfa528937b2f278177ab1c10ff72f68bfbed89f4e652896898e6223de4026c53ba639a8a46fd8b2717290739d4eb0fd2ad4a81ffc529925c2963106e19752dd86762a7ba005db2529cc0c5ea90048d8e6e5fa4dab98257ccd1c3ac7a4d449190723f08db342ce9bec407d1b1da33c66025d84c00aadc5fc0ce7686fa1c2016f19bb3823e9fb3847354c852698a4c80a879eb440860f36bc2af561bbeb3503c56a3404e841faf6badb47282d5bd9b17ed021db483b7557c189401304e28b1f65afd2641ce1e6fd7170e6f1c989ad69a6fe6e0d6bcaba1f2297c411c2b7262f03578fd8a5ee969a4e1e2c02e792e977bc24d22e5d22dd38dbcd1ed667ae827f6f5749452d512e62d987e91a0ab186b3ecd66a784cb284995e09a878689dc27c3a1c8a2c83a2a6b108a8c90a1137d62ada83e2cd4093906dd3a6ec326d8cbf04830e69b5bece8f001dbe1ba176b88b08c6e1d21add18cc82c7ca08d73c6e2e9e0506dfeff0a82f4f0935fbc61a5d82f0d5b05a7045ec79a5bb6afb9cc736444edf6010c9e52ab5b598d0add2ef1e11419a586462fa8cec6025f01cf58c3afafbb7a96918d763d2d5fe4c73f2dcce6f265dbbce8249dc4ef8062094c938499a3e441b813ce7f13afc19892392625ff1b5ab05499589b9731a70adf78bfd2a024d2eb979d358377b0dd48ab1405b1ca8767887b8b786c6d102e93e07015e65ea02abe049d122f0b0c3a61daefae0714bf12098aa3209a7e19668741f23f02dd95de920fad39d8812787c71eed1d4fcb415ee738c524f6b5fee5968afcc639d4001cddacc742a577fae986da827f1828dfd503cb9b9e92d1a27f0e7ec7412fb494e657540eef025677ab65dce295366ed7a3660f0d8e6d405b90c452eb02edfa5351981eac12629b56b534bc9e36d25376fd1c3a0d96bc76c809e56432ed0eb7c661745d25d89f6df8a03c6201d1fbc9637c6dd8b31fef103c6cf0c97a8850537b52ea17deb7dcfb7207a36a608b303c2aa9a9182807bfbf256af9d63caae82ae18b74440962072124368e0d4b48a9600abeef83c737dce95a457069f62709f352620fe4b73525124c83a6be62aa9dd3953e13a7939d81267c8c0e2ba172a73428b51d5754e98f0a37ab9eb787523155f12d867f9846cc4bcda0c510868fd4fadca94f2956a194cac2f9c643cf4c24f0fbf73d85216a832a713a5428b8fd738508d862240e363568db677131d0a1ea60f253db138bc2c18d67d8cf859043bdc06b026dc821c1f2632f28d9ab88ec24b711528d708f8ec1c5ddee22379d0e4b3548b495c7d1ad7146368450611af4eba32c1731a566453c21cef004f2cac2f38364968a8c1615f33c772408657522d73c0b69162d5ee987e224a0b19f55d40bce6ac81cc9f879d904dcd481a765fe5bdaeb7df04ad4e0f1ddc890d9cc399de46ac346a6840dfb69fc692625bf337bd1ab89d11a21d308af1dc58cd03e3f85a6aa5d1c64b7f9b08f5e2b8b0bcef5a6130035ad5387e30d068020f769913678cf21bb05227cc5108d697ac677fb08abe0a6c72f56d27dfced3785308f4e74b9076dd0891a7bcf46e7c9b34df635c6f30bb819e90fff298414890bae1de2b9071fa1fa0d99125db29ff7fc5192f92c52b677518dd216a2e7772a85c5799d068bb41d9472d203c315b1bf1934609a418a1d4c47d2e5f8cebeba01644e5c98372b5db48a5c79f121d8ff485fd53d9e6195e7cfe61773791e11bf06ca4ffff0140bdc656c2c6f7a13ef9d0bda3ff58cd3c0eb283aace595e2797417a31d6182e37f7aa66c2dd2de10b01da14985d4a2b9028fea7a2f23114eb5cf3f169604180ed25ada0256e733b16892995ae9151439b36767eb69581b0fb9f00d3a29b35c7017a32be9c43d61512ce7a934021208ca4e9056d411afe4c2b340a8572ddc6ea462ffa0b079fcc457bd304c42db2cf4fc00b7f633ebf2b8c8daf03f3b10bd1e8f117ee99387f75a4a75e207bfd6d5a9af4a8655f6dd10d02eea95e95c3c36a28905ba26752e0776289a7b627f3b43681bb828e9a06e1e7773dbd0041e5f41f377429fb667a127030a2dc2127e2bfee05164e437587186e4975510503f05a7cf1520c6044e8fbfef7450a9d9552a14ab1ac2ca096a3bce9eb2c856579877458c73c02554a57e3acc664ff99522ceabda6c30bb704aebec273ef86cbcad49aada0c59369c86742ba723779c3051e5a49abdc5467369e2836cefd21cb9ee58255ba86f81c574b993addfce771670c0d4668a297c3567801a906e523d4c02916d2ce6dbba65039f1bbf86bcbfec24b36bfde21b608e133efca822c54edf745982835ef31bb33191f35d07f5d7ec4950363a9539c7c4cc6f3863f9ab430f6a9f7d4719844d87acd264c5e692adeef0a9c63b1fe5b7f8a10dc515592e90a53aa218f4d6cf57cdad7c36c5054acd7eb1bc9c64f333a4b418587d53969d9a291042dfadf0a123f9154cb275ec0e175b6a5dbb46608005ee9f7573579b50fb1ff79e72b959d0ed832aecde08e495937c9f3f9b40bf3e818b317e7a191f53e1fe571cb2f3d9f8f53bd933f25080b2f3e0e2304c76b8780e85d5c02b0aea41e69c6caf141f098a1526e302fef930d61dc0c4ebd7a2e93b853371b7938640b27171b6ae91b2020d9538600b58f14d90e0060d4107aecdaa26900ab620ba578d16b3829e7f46f3a9fa15eea0968f151be559d41d95200ea867fd93e7904b16e69eb734a4622c6085d523dafd6a6efea4931add2b114390ea9fa4b508f899e36e8b12d098f654fb449d1664b379664968ac8482e2c0c03677b85ab0e0922f53f06303cd8974ad59e56b1b1f0e83001ad39f33b9ce6c2b5367fdbd9e8797c06588b159ca2a3fc227e223b8b3b2a3350d18581e7959aa12fffbfd6a2e87e0cd5ad1bc52dd6cf5e3167bcf3cabc29143ebf2a4f1b620e21dd74cb677e2289bfca4638aa2fa5f017b7e03b7cd2b0428f99210e2d811a08ff79fe37ab013182d2e73af584b2cb6290d504229c3b555c2bc42148dd909bd96b121f75dc58ab9424e28287f3659046c14ed2ecd6609ddc717852132a70aaf24e273c0c9a28d103e227214a2412bcb8eff92ed9cf775e5756b408e7c501b4e293326ff4874474082bdd066443c7becdad96accb1e9e1acb673a8fb3def18382125214bdc642f008835bdf6c7ca1e54f78227b1c6764759425d57b1c6764759425d57b1c6764759425d57b1c6764759425d57b1c6764759425d57b1c6764759425d5bc54fb9f38dd03d37af6ee1ad414368ce009173a7bf09a2ed7ab59faedfcb42ad7ab59faedfcb42ad7ab59faedfcb42ad7ab59faedfcb42ad7ab59faedfcb42ad7ab59faedfcb42a0623a72306a2f008bfafa710fbb486ba2c524f61945befb709f2ce402b0b8acefe9384ad35a2d9b8364e85ceb1776ead92bf1b063345f4dbef4e033cd288830ba28f2b9a09e62e658f1543c289eb7368ffe321a2bd4d79238bea1abab4e58a75de57db3f9c3a69f9a81c70fdfd85789c125b7403f5fb83ef2733629780dfdbbcd52f99747df4ef1ab2caf7403326d29909f5f9c05241a306c3e6d0db443f8a93eb05b46b86d31804036f6ef67e6d3dfedab3d6d5589a08c31819184d320d5d40bac1c4209213f48e1c597d3abb2ae074eb20bea627e24bc002f704c09eaca7eadeeb90900e55c9f8675bd690a5b5865efd3c0f6588e8191534000000000000000007b09deb38859e399af75fceda0655429a97863fe9c05ed7515cfc3abd022bb9bdf8df8c64407d902af398c760c41c0d6c1f12dd663f546d000000000000000006ee5b80819f4d688db777351904f9bdd361d67679f6538821b6b6e0de3f4d94900fc414c406c39b8ed9ddf10c80b87645987041c3b704ec1da2ad23f08750ccad32f9c8b3f380247db7496cb38008364d9dfff1151a164a0a5cab2eb34fcf130d21d533efb027e7c34af3a7419bdb2442c7b8dba41cfb4e227b8e0623a6d16b31898e6223de4026c53ba639a8a46fd8b2717290739d4eb0fd2ad4a81ffc529925c2963106e19752dd86762a7ba005db2529cc0c5ea90048d8e6e5fa4dab98257c35bde9e80be8d433dd97b14ec28bdce5e9bedffbc719c50faa66de1af4d2327471f4a18f61212e9e7e0aa0ffad35f5b1c69eb7779cab65901f090c3af216f384f4af64bdda0176f4a22d324a080451ddaa8cb0ac14050ef91ac4db5a3c25d623ea0e376fae41bfd60d63b807da8eba69b74e14a92b8fb5f283e9325a1c82df6254ccabf96e5274db68ddec616f33f1cc143328369839441ac94ec3e27ec01cbb35252353214f857c6382fbea2fb629d41706cfa45ee5eb0319a48cc9c88fbf363e4f026b6dba6b06777341a0d4091e0f24324a291f2a1c96e30fb4595e0baa49632a4580a4e5f4150374c85b2791c4c023180b91580d1bd10c1e7fb4886d99cf7916d8dc2af646463d25e88303175c41c3572f13213d88f696ee4295a8fb4b183a7d5b12526421efd76282be0406522b0569c248b0d781bb8495a06ef280cb4df0df428c3bec121758fb5f5946a86cb69a74d1329c17c340e21b14c512be8ec4fb1081afde0bda96f16794fb1cfb3714d8aede71dfc67f3a6ac0eddb4019183135b94c219b5053f736501f197c4c1f13dbe1d93922b18c5f0ca4e71d1541e2b80cac0ae3efdb67560f9b82d8abd93c3cc6838c474ff837d25ca2a155d938627978a53382fb99449be1f18117cf96dbb557b83f4661f4082cba6735cefb5a518206346342c73c87fdba629acc72f3a002f7434d5e6af7f2c99dc0131b7c9844973a95654cf22012d8ead92efe5c626563ecd98cc66cd9654ae532803eb731ace8d764ec4668e08268af4dfd5549175a2bbd45d7dab3026723027af67c97d6df4c9a5da8f2ac61500c2c0eecb65ccc2580df18eb7cac6a0ca5193f8fd020c834f0d5bad0a3882ee92383e87d09cf1fc020ac6a2a6731c9eef75cbdbef5a1540f3463dc8e717368f2450da1f1fe99dc35b6adba83ac7f1aa2fd935e210ca0abc4a80671a24bed9614762542c5e6e02a82878a4a24b9e524620941f8c876aa44d52c4aaa7cac46032d2bdc844471166d0762bea688f6b0eb05a25d9719fcd3d1add63e5ed039ba1b2e29b760635da33d055c58dbea8404a121dd0805a02dca69119525c5cad422ae9db7ff7829b7e85e4d1a2a97b0bc04c434d6328e6a12fce1bc2d8d0bb0febc20db2fd9e4227c7462b4c225e2e3e33e2a68ea6b1ec8bc28d797738a79d0e4b3548b495c7d1ad7146368450611af4eba32c1731a566453c21cef004f0901ff43b9eb859bcae43e02b456ea2152528539b82c1660f6c0059c28aa349aad616939a96000fb5973e8754d498f57187ae7390ce0e5a0c64a160db947b537b1b32a44f9b3faae74a8a0e7578e54473a29a3d344eb9ec7148c5c7755998718038950e9dd1b41abf6e60cebf140ccdc48bd631c62076de6d59566fcf0bab9f262f5a7665b90405253106bdf2208c0cd8f746db949acbe5461b116653e7083fed7cb2d94502f606296e7a5d6838d5277ee441d0e3998efd927f21485ae7618f98214896c15d6bea8dedda8809ea647d4061243b8d6296fcab042a1f2c5754424d13528ad00dfa192e5246c9b4919c059431176ba4d3814fe529957e95e7081e3350477871ada2a6a2d972fe34806cfb8199fb2d6845c133ae033b97777e2ca16a395fd22974374183ede0a56c29c4f5563c630783125201a884d3bd0316b88f6fa089447f106de77cb99cd3399848526337c860a9c5f1dac6f2ba1b6a4f3203b425b47ad4afb5f9a1582255e3fedabb5a39483c8fbbd9ed8267fa53437cf0e185ae2d6603b5cc037de6051f78fd71217105726d3ac97bf78a16dc5e7cf5c0d199efb460358e461b05190278c8727049b6e24fabb30fab701a12c8bccf7e4e9944350f63aec595012ac4eb42d9b9e034da731a8de6905a77472f2e06b8ae6775f33df8ebfe2529b8b140166e1b7dbb72fa92bf16ae46821b310b41b6e36889770a57c305a07777a075c13583e8cecba6544dc1f5b5cc24f466598f08dddc63d64daee6b02be886b95187f40883fa88e6a788095483a0534b331feac25c623fc6ae60528cde0294281b13a5d90ca394710b8dfb041f0f98f8e008890f271f3afa3601cbd2711dccd05c03bd6f13b60189abc244a7fc2c2457e374313a9d1b5d1f1574703552ceb776901a51ad8f7254833249b9a1e7dae2b7cd6b2185fed23cf5a83cc22ecefaf825703261a51d2c6a939f64de28cbbaeaf9bcf7c70d2f73929d91ab7919a3072f2f3076bb7afdce0df574e4fd46d1ec15dca605bc8a693cb8a694e76cc38a89a4906decc67c58520a90aeab4101a81b931b45f3185fc9688f065455b1bd5718da032daf1debe6203b865c92147685dda772c46b83aafdf31477b3a2000f370d57953507feea8213e57bd9702a0cb2deb008a24344ec6dbada35105f5ee441eefd93db88344ba0355c48cd52152a20549dc9436cfaa0fb06fc2ebbb90378eb966c2450cc095eb5fd3bd0350a34567fd36b404d6a961a4471020f3e9638c7b0e2e0a81323fe5a0cee74bd988744dc35ce5869dfd2e58d0d3b834d7512f1d25fadb574146f42d65a3b7fd2e2be88071a6f0901e8ee5ab5b6002413fdf5b8851765e1c26de27e449f8dda72cf419b997d9a3f5524ff9fc48cbe2209a91516e887c25eb1a8a56bb0c887e0326345364a988647fb023749134f510864d5087d4c6c20d99ccaaeac2c9bb0a08ba5ee6192f3be27514946a8ec3ab742e4e3ce8da596bab8fa2de28746765efdee8b74327c86bc6e08806e87f5f669a01f16d44d60f7b77512cf22be91d9d3eca5ed349a1e17ff2a204ce3a9a1a03b87d0ca3be40b6ddd45e82b22e0d4e61a578d07bf852391e0d36c1499f1e6968f4dc973dc127420026931cf57724725fef2664df8b977c35674dfc9b66ba2e2d6deb94d66c76eb830c44b884790740670a5d801b512c19f83bfe49040edcdee7003e2787893b647332a5f51a729a50361ffe52312f2a4e6a1ecc64f4214a2412bcb8eff92ed9cf775e5756b408e7c501b4e293326ff4874474082bddb0ea8e0b9e06848adaa04c05d391abb53be99e97200a7cd991d3fb05b1ec32a579108feded87c318f5633b5597b9bac5f5633b5597b9bac5f5633b5597b9bac5f5633b5597b9bac5f5633b5597b9bac5f5633b5597b9bac59c8619b27c8b99fa0fb89f776f00b972f571ff36a0314fe114a203a06858a2ab14a203a06858a2ab14a203a06858a2ab14a203a06858a2ab14a203a06858a2ab14a203a06858a2ab06bc4e7ba27fbe8bc8775aa829eeb0559797bf7f96fc54f6dae42c981ea2a2a022ee3c38d09242ebf2ffb8f8c3487c15f7735635e190d69c199811e68bbf33b2609f235b1728ebd8bd347c5483363ed244a11e70f22200055135510524a4276154b6c59ab1b1e3f551c79a403d31f3a2274d4079f4d3c671d879c6ebd253a08235f13853eda97b3eaebaefb6107ccb2d47d6d532336d2bdf0f6a8711196c291cfa05b46b86d31804036f6ef67e6d3dfedab3d6d5589a08c31819184d320d5d40bab3b0fb07455ffa1bd571089255b324a9cd588d3138f0a0187b4d7961ec665cc6636b2ba2b8b8183a78434b337da31e281f25a543a0b648c700000000000000000fb995181ef693cdebdbf70bd36a3033a41bc011fcf6b15b2c90526f3bd6e395bb351f942bda951511219d2ba8536a53af8cc4d864bbf4650000000000000000063c63ca80ee0b3519bbd75f05c881151e268e9efc976099d9a92ba5238e202af72dbf2abf40241cac13619e89a7202f00a3201891458e05bcb961e1efde057eb0bf6f19efd0c8deba31d135814f2f95aa84fde062dfc2a8ff5cf703ea3b0e6ec0246cc84f019d5698ac3464a9726ae7213f37ba16affe8937da1b665eff1058da72aceb4b830482581246bbde6994ae773d2be6b523d9af087c68b13faae9e85bc2963106e19752dd86762a7ba005db2529cc0c5ea90048d8e6e5fa4dab98257c9be7830be8a4d89558d194876a490d3fd72e5698094f0e250a1049aec7561c40328056edf3c19c492f54fb498477eb2d66da64dcf8ae84f53c1e00b42d0b5ddb0270ea2367e1fec121b7e94ff0effc5ab4dde7a7c8e3887cd9ba275e92c37c4d7b3f922152d70776d96be8ffca0445fbe989bfdb65b587f86afa50a0520efe53de4dfeb545a160634e884f250d2f6cc7848a8786b8df6be9238630ecbb773604c1b42aa72def108b5e76a46d4001ece316be4b0c5c65921e20b26cf5b08c79d874e2f0bc52aa4c133d4eea8da6ebb5167006c4cb7df9baf5c08563318c17a065794003f850741b08f3964a413070ffe243e45e894674f739202de522e6337d3837f728c390d161fafb20100f56d0b0f6cc0439e5a17d0f2a51a58fba12865a4497046338c8bbb120a1ef361ad44a4a6731522d10e0eebe65a99b9d0572139fda11e30a73c40504485896e7289b2d2a01b58c0d233090d9a246317db622b107f9e16b36c7b565c1bbe67ec3180dc503e678dc6d9d9f0e4a54fcb66a76458ce58f54087402d4fdb2d148862ca7c784f22b774ebfa4e25789877a474879382efd16c3f8fea19915fd5421b62edd0e4f3c59e451051f8db40edf6955cdf6571e69978de0dc0fd126a216e1052c6276ed878390112e2b024bea118e73adb093a011a69950339c61bffe3227338bd5a78051679457d80e7c7202afc5464616d1e5d80f1a138bed03b6b19a8cbf110fd9988549a431d6ac981285c9a10f27192afa46f3bcd14fd11f5acae680153ed9d54a94acb6361c92d9c8902e3e5f269437937f61abbcd12f138399e8bc2da276082075fc3bdfe2e04269c8392474ce010dfc3930f568577af9c84ad1608214822f48df85cb59f74cfb3d5565707d7f84030da950649c35517c51e748bdbf69df8cedbc97212b4b7568ff541109c38c82306a5ba806ad893b1b59323195aa409e394ba1d200697e28f340bc46449751a66f7d793544ba172a73428b51d5754e98f0a37ab9eb787523155f12d867f9846cc4bcda0c510868fd4fadca94f2956a194cac2f9c643cf4c24f0fbf73d85216a832a713a5428b8fd738508d862240e363568db677131d0a1ea60f253db138bc2c18d67d8cf859043bdc06b026dc821c1f2632f28d9ab88ec24b711528d708f8ec1c5ddee22379d0e4b3548b495c7d1ad7146368450611af4eba32c1731a566453c21cef004f47483a42ff6392b8560d404d5a1f080d5df1b7b8fbbc5f252cf58d3e114044e47943494b293edff66c5e563f7606b6930b200af05b8e1b2a8b38976f911d476f71547456fcc107ca781608613b99fe29231540e6c998260eb820e6c09d341eb169a46fbbaad5fa0f3d522380e2f2d0a9c497f6557e0493d1f54469976f8e088f65a26cc33e4fbe99a2393c240fb7e8c76bbb04bf34502c0209de3d48210370db5dc07bb4eb0671870689449c595bbb6cda749dc9a3f8174c02a56567ecd706a66a50b42ccc3d158c6b55920e2a8c8daa14d9b7bd6fb1a86e2863172b660f068f77f892377f58b0f080d936523fe292d05948855ea5ac035d2abbff6b1c4b0c72635f82773a21ab3aca21838f08691ffd150bd0b774904691d03e9e4968b7ce892f1319e25585766c4481823ee7601626022c3ad630646c794cd80f3917161b79b2e345c4063e5bef706285677d2c7ba3736b4e59ad74239d4cae728ea61850eaaf12f24b423737e5487ddcf4feb8ea21e14c92582fcaf0e30cdce4a1d57fc2948842532c7d2b03cf7c17e9be9e3a77a2f28d16498b1ac16057fd0e0a0868442e7eeab59f90fead8231bfb2c7e5e6569cca4aea1a5689b47b324a760684f29cb994335c8947b303f31b8baf30cfbe00d0d381046257571297a27b84805649d3078795e71edcc97c8d5a11fd2182218fae2d660ebe12d318b24a4529b77c1f2a07c473b1ac70a3a6e152933ab7bb7907db6bc5e0ea72ba63a35a2b881347ce62a29cfe07a127aa16d3a09db28010afa610a534161672ccb9ea7bcc8a841a05cda956176324caf90ac70846b4039cfa92e0fb8c72989b75b0ad096e77cc9673bf7501d56695e9a38f23c063b9ddc46bfbfd110eab0944e47c02bfbc063661c67f9ac520c9b96617d1ea3b9b5c14d651386fe6bbeea8c66e2623a127f1c8ae1f89d72d91a4f68af60e417ac90aa49cffbcf75c10f4f4dada042585629d6c377de0b8436a3cd726ce53fbd7d02a6b17280953595d76f1b0b327a754dc608d13640e0d226c99c5a3c9df1b9555f68002e3b9235d05b0b6e7c2a4c4bb93206e44f01f0bcf1fcf6edb55fb436fe331821a34c6b0195342c99e97d548b9de5ecdbd9977912fe187c8788ed34f31f46bebdbc4297d8a76ed762f23c1e874e546d5fea450223c9bad166edad2dadacee1082d34ee2b9d4bc0bf0d44876c8c5ead05a4daf82093e5c236afdf0c04302260a8ce0af91176bc8f410c62bcf5c51a057c3d5cd4ea5194c49235541952f97370c2ec718ff5689915831847581d01e7602eeb9b36bbc78fa7fab82f9968a0459e7dab3fcc77675cbb12a25937e9815e50fbc731c4c50e4ed5f296f66ccfb5f2ccdfb5acb8eb39d1a8b2c27fb5ce83e466d8787d086245f267c29d24f257a9fd9f91214bc1bb5970669c5ea3a384dc997839e2ba7ad0c496bea1ef12613838ebd7294b75deed5035b9909a1d18fe07d8b627651f17d97f7995c1dd107b204da2534a799439bae49774f5ff7587690635d29dbd62da09ac2f0732cf0781918d47902290a41dd7e4d399b508b21bc093ad1bc52dd6cf5e3167bcf3cabc29143ebf2a4f1b620e21dd74cb677e2289bfca4638aa2fa5f017b7e03b7cd2b0428f99210e2d811a08ff79fe37ab013182d2e73af584b2cb6290d504229c3b555c2bc42148dd909bd96b121f75dc58ab9424e28287f3659046c14ed2ecd6609ddc717852132a70aaf24e273c0c9a28d103e227214a2412bcb8eff92ed9cf775e5756b408e7c501b4e293326ff4874474082bddf115602b2419141ff4a3c6a3657e8c2eb7ccc5a5d327ac6b8ea4404bdbed527b07d9dcc672c5cb2775c2bca5026a307b75c2bca5026a307b75c2bca5026a307b75c2bca5026a307b75c2bca5026a307b75c2bca5026a307b6f46d3807a70a8baee0ea9ae35c829ec3dbeece77e4014bae1076b1e67454ce0e1076b1e67454ce0e1076b1e67454ce0e1076b1e67454ce0e1076b1e67454ce0e1076b1e67454ce0061e9770551050ce6a440fed0b85349465600901dd749df796323f2497a644fd129384ad35a2d9b8364e85ceb1776ead92bf1b063345f4dbef4e033cd288830ba28f2b9a09e62e658f1543c289eb7368ffe321a2bd4d79238bea1abab4e58a75de57db3f9c3a69f9a81c70fdfd85789c125b7403f5fb83ef2733629780dfdbbcd52f99747df4ef1ab2caf7403326d29909f5f9c05241a306c3e6d0db443f8a93eb05b46b86d31804036f6ef67e6d3dfedab3d6d5589a08c31819184d320d5d40bac5adbee14a335da9e8a259b3231f54ae3f3c7fa5c7b591d88b04dbcc75b89cf5954118ef0554a156cf3394baffd6e79e1e9b2c73348215bc0000000000000000b12c5e199e4fe98a9fc87247044d3312ecbb4797a7d2c2f0c737cddd60f3e428223eee220fec35699c82b676be7f8e4450d07629e521412700000000000000000612d365560f8373ab6e846329fc32557f82d5f04c0d938326a789ea9f43dfcdd80fc414c406c39b8ed9ddf10c80b87645987041c3b704ec1da2ad23f08750ccad32f9c8b3f380247db7496cb38008364d9dfff1151a164a0a5cab2eb34fcf130d21d533efb027e7c34af3a7419bdb2442c7b8dba41cfb4e227b8e0623a6d16b31898e6223de4026c53ba639a8a46fd8b2717290739d4eb0fd2ad4a81ffc529925c2963106e19752dd86762a7ba005db2529cc0c5ea90048d8e6e5fa4dab98257c6fe251daf37f36a8273a6a23a1fd790b2728483ad7823f25e042a211c6f70a76bb8abe567d542adbe33fee8d8364d5730cc8bdf53dbf1c6a8795a2a5feba2b7ee54b44c9a1c3dabd1741e1edd23bb96df844cae6df01b2c5b79a2086ddd09406d4c6fbceffb77d207470af3e51fab92ba2a5ec86baea4400c6c1075baf33e8f75ebf6e7f43561ba369c32c79a3db5fd8adb04d0a906e8bb69048c3d687e9d05e0dbda49f30023b7d3b856556cf296ae10edff3018c03298d6786bfa97ac831f346fc45d00e935c32a956a9e4677c4012dcf5e4f237e23000c05588c93bbca3eb5612d32795a0b3ba6b7db9f1003ae42d43edec4e108c85c836baf33d058f3f5aaf79706e3b0b6ceee0e15d1d9e12f4418e3524fbe0ab9709e18a38a94a77a14a3a8f417f9cbe2d0993cb158b6a3141dfa6dfdd7d813461a4c5f4c9a1c14a5037eb19552ef76f21c631ae1978abfdaf2ce5b935c64cba681906932e3bf00d8a629471d25c7c88c0d77ace1978f733cafd25c5d0296b57efb82b137d7934e0f66d18fbaff3995ca610a7b1e0e91aa88f0c82b64cb3ecf0ec9c700d9b1405d2f38e03bc46e3045685f1a345a0f5864d416cf6c11df3e141c6a366127e182dd1d0aa47e043f1bad219c1a83b5119f0f8c00ba4a9db462bdc1f23a69ebcd7dbb31ae79ac70ae0019a31a1ea5462985c314d6650e2df6bfaa126af74d3d3a64de0898dea24c46d9cca3226dde1354ff46c177f54a68677f56c4663b2cd69ec7119401f615d48a86a3276a3844d543fd7a6fe2ac8dbe000dea26b6db4d7141b003d7bbe5da08380cc2e5479bc1277641507765b3b3e046f6e8c75be64fc9d1f8b9f47e8520b239c695de8d1d7fbe06a41f4be04126673d0543595b302f718ebac5edffa955d3f24316ec7382d416ca585d0787f6a15edaa55dd4156e7fa8ca548af108606912b1945434a5aa803c9779dc680731883d98b41b92c0181f7b53e35c138641df6d00861f491bfeb4f48722b8a4872f489d0d761262ae70964859040951558ef741c78930d0a42bacff13519478b026d9acbfc1ea815131d75b0cdd43fa83818adfe21bad2337ed4338804800eecef9afd008910f9565ae21e56f686de839c53e513bee8c0895e8b7affdc66f29dbaa4eb7e4b9e1eda57a97cadc1c3aee8969a9bf480a64a90f53ce7dbdfb28b39574e403f1157dea27ec6c4a919d6a48c9a035efd60c0423517dfce95317f32716c799b8e60d562290a74aef5388fc01dafb5b80e07dcff085ef83db6d754d9b6d00c2a0dacd53aef0a8cbd9c59be48c12026284400fe5e9df3f6a18bcfdaf402a2a8b9b8532373e1cea9e7cb556804ea3f26aca325bd1cb480fe38d75e65d81e925b22d06447a16f6b9eaf543717f55ade7bdee995d4cb8773e493fe8ffed2a80febd3b540754ebf028b64f45f96e11ce6704e88d480335b0226f502a7519c4f406adbf0ff3b4eb4fadd5191e0a369e00f0b7fe123e8ca1b123fbd5eb88d9eabe59abda2dea36b32cb8f15e290033a37453254183613b24888e79ec2a7d43d7f6045a62ebf63a5263e7d4382b2b9abcc596e7fbd6d8ec1a3c4ba9c0763c94e341a2d153e3f8bb9d052e90e9ff646ce2cf982de13eb69a9901ce48be7f086d603abae7fe4cbae5228642fa33af295e580697ea934d7ff6f1cde1547ca0939dc1f5bc850658375b40482d62901af4e2cf5e62e812bda5f4e6c462991e81b337c448c5366f2dd19ea91343e857575dd8bb37e5a7a61c740510489b9f26a0ed45d914b511fe7b6c3ae52c5c22534bee461918b5db0e50973a5065caf6cdda8a357d2d51bd7c8e618d30b99180cec0a79980920300604c7ad085124c2ee4d5e4be207efc6079fbadfc78714a18964c3db0a94cab45a79ed864e28c9c1ff5843b407998ac9e9642bed82f855bc67e9ecbd531263330571ce8b1360c5e6060661595cef25aa72c056adf78700f89cb4b472ad6e0e08bf72f04b50e09bcd2d9ff282d3efc1557e90fe6d6f8223febcc691077a0551776dd70a92688d1c4237c255ae79e212c079d05103c4026c88b08a4afc560b347dcb7836ce25a1f32fbc5c182be1620777597c7081d203020b861c1d66952fffd0f749d300385c728f8abd72b9e9c98de1e398836520be86cb01e4852accc4fa373fa7a3114d0af2dfd0071087a62ac28c660c14add68c6ad123cd185961aa02337f1a3620f2971c69fd3d326efb288ddb054467dbd8b786b9e9429e6b24dcbe5060782d500eeaa15e62fab35c7f1c25ad8cacd0c8529085c32c871441a001a35b800c57320587a4d77b59a96c94c46f708e40a1f6b6d19ce40fb6e89f01626556fc644bcc00f1b855e641bd8017d445b7164ba4edaf007ad7d7a2b31565557a48eb641ca82938d5e8c632c17b4fb32f80da1fe68a282d19a72b1396304b4f741d2835d7515ae88b9d85105fd5d5c6efb8449b3262782baa290054be319841819453ba2037b684fb573389b4cef0c58fb8ccc84d77e90faf9fa2f43f41550ca60f8b0a8d04675f0ee53440200597a5ffc07aa16220d64b649a3cb18b8e641e898eee83dbbc645f0e1f3f019f788d29197b4b5e8d1a83470b97e36fa9047c56588bde27021a0acfd1a1ec812cef6508ecd97628c2b56013c2d6f1e343942e999552e4c39ff69f29cdcd05e0ebfa199bec05592928adfbb922b3dbedfc8ead91c8b37f21fb651ca9ff9bede88ab53daac9589f349a6c87acd28806b48c3caaef50cdf9535f6b307fe3ba1173171d4fe1dc59f2461b253b7d63879c31baf42fd15817036bdae1f92eefc7c7118637079bfe4ed870c0576b6d3e271ec777526c5e408cc74dd882754c4d659d101dbd78d896f2f9e8df01e388e9f24627cef371241a83eabe472b0abe9319ee1f45dde13cf9918fdb03b9c151db432270d84e5d47921ca9641339ae390aa990738407af02e976bdbd8df1570941e7a86948f92c596d6b479b5414e8cff19685a9d4dc9e48f282a21dc73b0f200bfaec234525019b5d46501056c1faaa7e28becc1bc3f01abfded1b379f5c68d8e2e828c336d7bee8f6d794eb04e4efde5116d4eb04e4efde5116d4eb04e4efde5116d4eb04e4efde5116d4eb04e4efde5116d4eb04e4efde5116dbbfdbddf574d07160ac06b82541b766050979d4254883c83b66300b483265095b66300b483265095b66300b483265095b66300b483265095b66300b483265095b66300b48326509506a801b7575fe396cfc0ddc17c5fc6267ecedee8ea4b128db88760f157162710b2071bbf48419cd934257b293e8f6d73db11e25e1eba15f2af98b0fb0cdb6b3fa8344de3d4ff1cfd3ba0659ad0d59a7031447f1a161f7d6298c7f7fb494fdbc245234ee625e18bba7936eaf8c545eaca5f99a0c24ab1b40b74a4cf5734a905687c11729c5fd01baf5f00f1e034bdffb3f0af203f4fbf4e2ab4b238a95d96ccd22807e9c4f53d2a79f6afc585ab46cb17e5ca1f75f2181d5678e5cadaf8f44143a9287a8658e3d7e75dc86f9305b573f24b1e782434d97f6118d27a479e436f068cf153f12968345c3f0f343124de21649f41f38bef9cefd9450000000000000000c83e720fe0d0d1be61521b3cfdaf185513dc0c90d1f732e5c8d9c72279c2c3d33592ee3b4cb4f445d6af3e2e397c90af798988eb64c93606000000000000000006005f618fcdc51fb07d1ce9ea71d2da45c2c70d401c762bb043fdbf8f88278bcb5b480c832dd8a12326a0779a303b48214e85bb10c523fa7533308e44744df10c75a37276a32bfc52f5303fb09010460bc555781b4320053b4f85e62067011642de2486692dd5d90ff3ac1fe9c08dab0c026302e651be160b1666113f1726ea2ea50bb66c38cbacf17f499c8f68cab5ed91805b731d49320b4f954ee6e7c30a17009aa3ee5e9a8201fc8d0cf0c5045de76b1a43925f78f9b0f368f98994bbfca636c39b8df03898e262afff44753c977d0cc87037855d05a010dc4ec283e93613a467c8702e8136aec6d3414818a5bc07d05aab12a60d9cfe8205c624370376e7f29d908aa5adc9f6e5f6d7238de9bd5e29244111f6a587ed11803b5d5143a5a51aff9113ca707b8ad2e0b2d9b7fd7ca89a535c593f02412b871fced91d8c604572afc1dd46ce7cca399575bd086874e7af1483f53edd1c3f097e7e98e5305fb5f386a2b130d84bcba2d41edac52fa5939cb7588bbe716766205611dc4d5ccd50ff59e10021dd09bbc6f13a838a5de523bbe51fab0a10da1730dd8bc8180af66f8b843d8413913c97f6e5c6afe790b754b6b33b4d2f1622f7e9cef7ec3bfe6829e7c1b9a92cb0b947887775f849d00781d790ecbbae5e26c82ce2a4aedc852a7d3d06fd59fab624182f572cf6c4047db77f77f79dd184916ad427871a2a41ddec6854b988bf851857b966e071f6bfb23ce8d618c7e8a2656c4dbaecd836288ae9c749734121025fb33a67e0c5cf28f71b1543a7405dbde36e4df4e57d80dbb755edbfcec7719942a0c782a707a03e3264db32cdbac2b373fa366c52d447cf3b42f11a8d1f5715c6f71781d6a73505b1750877cc5f07198f2a4af8614c4443ab6a820fc5074a6704e7ef446560e3032f60a76e1b3d707f8c707bf25e07cf6a9c6b1f2b806f67c6849a5489611ac53499598a7faf29879abcc24e4f9b06812736329410b7192acb985888d73c45b35dfc8d9a1ade25b3198db137a7b18f65007d387621a12ec9d88d7e3751fd6c9afaabf0708303988aaeb5e15f536cd0f3ecf932820e02a6ba50e58d4bdc91451cd86d11f910bca531d6f916f2777ebc7d1ea1732e8c70ee74a147bbf083ab61d0fb12a499cd419bd454cebadd63acbb797debfb77fd9018b81de3ac05b195ca41e3fd4d56b4abfe74075959ec3396bebc42186606e9b568092688533570d92a718a8f910ae3bfbd818d36b0e58462cc9cc653226d2ec79d9c270478412932b4b72ee149c2851dcb8af1113c6ad7381d977255c2d1f406be651dbf2cf8439496a45c888ea16a819112205935abba314642d84d500a2393c0aec8be10752050a6d4ad53f4629e2ae58572f5912119829e803a1d17d8f9bde96bd45733adbdcfea1d76a273869881e6303648dca2d33fea4043b61a859bf480a64a90f53ce7dbdfb28b39574e403f1157dea27ec6c4a919d6a48c9a038fc8d90e30f9af3ca4da563698e315302f1af0ab270b4d16d5119b45fc1499ef4193ca04085278b0d7470cd2c20c1f0b0f51471960d6f2660196e7cbec54339ec52f0e51cb64a5f3b797317216c5e5a3f46878039174fab105ce103be43ea1ae6556db54a1ef1181d2bc73991540fa0240729a538d6483442cc3fa392c9118abd53f2b0a113e22fab5292cdd046b8ddf4cccd6df87f6b6a8b236a5269dbf73e45999fb28938ffccf184504d106e4379fc75a783d16b7fba5a4613d7b1e95dfe58d862f01eb2e6c55ae498b9a8917ee6d35c67ac727c8f729ba6f077665535b0082bf05de8a2103452d6293d5137b8a6a03af4a4a1893cd6dc39f0cb54c273cedf0e5a96dc8457df3e55d980e22de0d5972a121817f44f9a56b01e6c7e74be594b912289b1bde55ee72f15958bed023eb7c39a144624f7bde544acfecd879f4113973d3f7321bee2946d8400a938a0204a50c68a9e8097acbbd353969b283145b638cd4b29a7b0a4723bad8d80948f5b1b2ae595e056ee75514d8bb17d1be5b6293930a1e492a46c03987ac4f21950c2a85ad410bb8c9340a10a8454dd5ddbabb825c3a9c4c33101f373462d5b0fd2f934558d5dd8d7069b1697698159bd4700e2033256f583870e13ebe0e34ebccf954e6b016aaec8f39ed94db705657761ae4c056c01c38dd06f8f4e1e29bc85a3b0b4bb040a849348db6c6b0c908e4a562e1fc9aa4c8224d8baeaaef24bf285891062456b30da72fbd2027af8bfe5537e560ca14ed87c5862809ef97e46c413cdc5b81e2ba1e6b609f9b0800c7de083294e6b2bc3ce43476b6e70a0eae1673dc0ead96dff6aa60ce538187ea2968c4ec331c48ee1e4edeecdf4d643a323051d5cd28c4e3b40dcac4e3331504b1ecc18c4b3ccf53ed7403447b1338cd998ab90402dad1e5c087f7080c0e8b5ceb3a91ce3a1bf803ce158e7e0ad2e580a45198bb113fc8d06da9c77d88dbc50512e769948632fb2f0c1a95e506058133b300ccce80c33d421273952fb7ea8d1d07bb93bf04ab0df97300467b5966dffc8a01bab0d260c50543a27ba40c39577d3e699f8769e00abc0551acc8659c19ede4bd6116745903b778236bc24d53a9aeeff38b4423132b1377d6d86d1d4b195136425fd59ebae548d0d3c10d621e0521596a19892cedc6b6464989f1519fec0d7d0567bde9f7a7387ab9f09652c5345b34091fca5aa40510a8b9f19ed3d23e92912e5a03fbeff0e2e74ad214ca61ec28b995740d6c9e9313b84d8182ce5760d9367140d2cd4eb3237b1f21649e302aff9971b18cf9dcc808ca78ccacad68b3c4e893260ea23dae457719817a22c3b87d2d94688486ca8704c7e83653fc6a946cd7810c795d47e84d00bb096c1a7af5cb4fe47466271405189603b0d3a5aecec73edc2e8a7119408295a78787f4547fad6420309f7297c272d832cf6160844b218be8e61008f2ce683f9a38260253516824251414886d97acb6d2b2457048b5667e7bc9f2d277900c66e02c192e68061d1331e9a29834de7423903f08a2f8cc26af95cfa89510fe5b802143b28b27c661e6827d17ebe63b4baf1098966b6f07d4068c0fccc2a73e98ca32a7b891c5b721edc5281d9b59591628a946a238ae27c8c0b7d4b68d0dd3eb4de2a1ceba404c6c9fa77d0555466c62944d71582f067d3358e87251ad9264d9bacb89926aee594fa48a242a681455db1ebe89e2a39b085ab1a443288cfceaba4c1c55a8e3a7e46948f92c596d6b479b5414e8cff19685a9d4dc9e48f282a21dc73b0f200bfaecddc54836b3885c1b3e04910d2e26bf753064da148ebe64b3ddda176541c2b8631c56fba55b0cbaf718da3dfbf6057fa518da3dfbf6057fa518da3dfbf6057fa518da3dfbf6057fa518da3dfbf6057fa518da3dfbf6057fa5519df124082894775d852cdbc8e232b3988cffb2eb9045896d33c1fb57c2235d6d33c1fb57c2235d6d33c1fb57c2235d6d33c1fb57c2235d6d33c1fb57c2235d6d33c1fb57c2235d0616d7839d2af015ae3dc2efdf9729405370ab982b55cf4158500ae28c1e8169ea551c7b6794b1821c848637fc5bd36703810d9f0b711a9f573c69d9d1dba85fb3094457629aaab00116e4152c1ed9b75a6b1ad068d4ba68762763c07915a0962fb2f894df96aea21d5c5fb506585e91efc4228d057ab40e9ab0de4fe09c1597e895eb6efdb9a656a65ed85d769019b337ed320f4eb632652594aa33568c1e272007e9c4f53d2a79f6afc585ab46cb17e5ca1f75f2181d5678e5cadaf8f44143a9693c7585b3fd7b14def79803b1a6b10b689b78d62d09642b5111b4ebc7d666ea4c93b3e2f33dadcbdbd0b21d7aca1e69761fe28524a87655000000000000000016657642489b0256d39d5a532a3cc02020a1df6aa2f8baf547d2d6173cd34630810cb8b8a85d20ed9cc11f68c93ab8a0775d93ba94606901000000000000000006fee2c3d08e876772d94cce3c6e3d47cb328a88109ff710876a2dc7a4b9753a2815380c492f8f00d37342af8762e875157b6a5b7c7fe31cc8fc5690df18fac8e23e4a593504456d16bcc281e9e9e7cd9e1096bc711d3eda03d0c010fb5d3c5afb9d4b6549d8eb6dcfc903b5befda5312ba49d1c3a3acd1bbac8a885299487187ed0e990f989340aa24f89aaf4f2bf65a1357c2b294f3f22a7b8025cbc1d586d11009aa3ee5e9a8201fc8d0cf0c5045de76b1a43925f78f9b0f368f98994bbfca66de085594aab312b42addddb6fd450e024dbf7d99e5086df7c9ac8083e2ee932ea6e544eafa19915542858d03c11ee11acc3905ffadd68523f53a407cd000bfbc52b9e534ae48d9505a314826f5cd53a7f3bb1eef8e001fbb6a63af3badae492ca51538728a5c30cc8679ed68f623845776ae9a701329792d4d1ffa0c44aedb0cfd103c5481a7a7dd041c7788a02d484d33ec5d08f57f908ec0be10ff0d4746e95ccd2f95a8e54142119343bbacc85ff86301f80c52a6628f053de50fe3ce037a3e3301b6333caa141958697dcf71d59c3e49c8c1c70cbfce40029188e197a4b9f08756b0cbd52f76ee74780a09103259fdc4504c4442cabd30c66c1108330b1223717cfe06364c57ed497a786a878e0f92e41f8f13641a481f6bcd2dcb11575d41b22a79b2f608ac095da5a8c3a9dc0c68f8820878820f6904676b0828d571ea31dfdfdb86a74876b32e96a94f9e1089ee7379390728ef3296e277c5a3471c2bf244d1f63cbe87b01791dfe8bf9a16308b0f8dacd8bf5dbfddc390dbba021289615812601148313179cbd06d387858f6a0e543bd22c5324b8e368813cd9ba2205337581502e077e6578d861ee97823501b9a5788f38030ab9d3704b7be0391b6d97ecae07af833f8e44093cb0abf9d9e27eda0a15492e8cd5b52ff7d7108a5f194fa0804dfd0799e2ab3f576ae7596ea2c1c67e060865ed6d178d130b4b960fbcfd24a7f68e641e5493333d8732ac4b45b3763d561ea22d7aea825c8ce50437a4efd29ae8ed150dbe94dab5eb2a30e920939bb7c8c77f5606643640c93af9d2f4753eb317df9f3547fb6fc53ae2a68b735ecb1bf03ea45567f901e380183804db829b5a3cf718d1fde75b10c047e5f92702615526e34b26167e79abc1394594b341578c28f0760e45398921efa45693bd4b8055ba4415506be74ad2c09c167406f7ce306e7bcd6fdacf85229c89650ce867184a0af4e36327fa7b41b3e495e7c579a4a4100ac997a56e5fdffaf3b8c5b694f8a5abeaced14f440900f37f340d5dc6227dc6c10205b54a1da829fa2f30c552341840ca182af1de253ca89ba294bc6fdbf3d68697df3b147327cf31a5b197767d1f446f2aafde271c0733d4cf3ae1f9bde96bd45733adbdcfea1d76a273869881e6303648dca2d33fea4043b61a859bf480a64a90f53ce7dbdfb28b39574e403f1157dea27ec6c4a919d6a48c9a03d5cc2c7594baf320fb4d439cf386c89a8585cebc9957763294e1bddba2036aa423ee1875ac16e64888ceb0c8d500913c05a9ab10f1c2c09bdc3f68b9ddc1ac13a31fbc25ae6cab84ec737650237c45bb92aaf72c29866048b068c51488fbaac9637aba453cd9914fc5ec277fe1dd9cc629d7a5c9fc65813f1ef46957ab43df8fc681611fdb4f0df4e978fc75d4fc658892b25f8f3ca4e93894846db1864e962d8cdcc0c53a3074a135c7e3b297adabf5390b9b84b3cb831aa4579d81c7c3c5ba400ad29259ca102a7a2999f4f682402a11568cfb245639a79969cd3ae55d73501d672a20fa4f85f47140630ffbc288894538ff54d393e9b7221cb630ece5e9f7b9acc15c2aae545bac053a75d4cce10f4c8cc919468191ece57e0d5d1cef0745468b08a1f634ccb18b912700dd9086aa33398b3c9c7efd6454eac0422bdaf733cc3093b7d7d717c1de15808988c116591beb583810930ddf15b472e932d0563bfa23b45f2f88c01572d15c7c17417eddcbb3a20b9b75ae91fdc375ffd8d724e4d6a95e8e6bc93c4e537b3ca26d509b87bcc49d4e1ca46740f15baf764a5fbcbe7aa24f6bf3416d3924cd88d927796dff1d8fd7ffae98bcd3bcbac8feafe0ad2cdaea1090a9237dfdc7640a180e95e4c2a7db62b705bb62a2a13f007f97efba905ef408971b22e81f24b81b906fd3fb0325623bf4f0e31ce5f212af0a00ecf19533d2c8e1995b46efded1e5060a0b40d505b7a96142fa310ef248b2de2fce45df1dc93bfb549ae046e1698ab7da6d476435312669378e0de88753d561cc9f6d1c9cb36f3726b8c953a931e4f3a56be0d70a1f12179f2938919bbcfc51e814d7db9662c33bdcac981dea529e1c0850fef41cdef108cfcb64f5b0ae74ae5583e191f8547be204a469c1f9699e25ed14efb79ff7ede1e851f6a35788ba19d28e4bfba9ed4bb1ff45711cd81076c8ad3a64aa2a9159d82cc0249364cac388fe63342bdc13a715664cc6d43caca7871dd5a4e6f1b9554c00d2fb15e7b7ca6d9a106f91c606c3555458c60823cfbbec4cacfec598bb1112b34090964747d45f9c1117b870194cf3656a89aad09ba9e9df765e8e11ba73f2d676e3a4ec8fcc5ddd66234a17fd47994186a1d4bede73456d2f7a95f0abbc5481a4519a99bebe6d6a8959ee97768b9766163a7c8a3ed8bc49de4f5019d9b14b98a370a1ad686c73f4edf7ded1c3fe71d2dc9abb094c3d29c5a669f5d78e5110479470dde60994fd690ed47502431a68d9d26cf26f50c860574a51fcece31526b9db56fef816c4f6b35ed219ddf28e04b6e92a74bc2bb9f7db4dfdc32b6c5ebe526f39e9512db6e5d141b275786fa6f0f27ca7071a2f06cf78f8dfbc1a34b90c876e22c1bf3129353223fec4a64b9cfb391b993f8ba1ec4b9acad270e46c4cf2a01ec019f3dbb8b06dde349f5ce8d89f7a7231138d73cc1783b3e0e948e74889a51d003e4e36348f857e503242b9f85e1dd5548637f85afe245b9f8f653d0081f028e2ef0603858ad4f3ab93b87a1d0a2a751be55d4b15a2885ca0eb4d0f429fd75457379cecf2643adc85940cf49af9b1a21aa4050e5b0595bc7477d17ae782dd84cba480f1565436bdb73b9eaec580857e2f7f27cab33f63986ada2a41d71873e09eb27ffd9df08b5ac77b52163697b4f3d38ee2b1ba19a975b00b952182bcda9ce18a5c4fa48a242a681455db1ebe89e2a39b085ab1a443288cfceaba4c1c55a8e3a7e46948f92c596d6b479b5414e8cff19685a9d4dc9e48f282a21dc73b0f200bfaeccac86573389ead5bb928a5b4d208ba02323615323cd2d344883ed5d988167b1bb4898e878764fff2178ae338828f441c178ae338828f441c178ae338828f441c178ae338828f441c178ae338828f441c178ae338828f441cd9391b870f75f13c6de5886cb27fc69d96f9cb7de8d4f9e8750cecfe2f42028c750cecfe2f42028c750cecfe2f42028c750cecfe2f42028c750cecfe2f42028c750cecfe2f42028c0640e5cb348a259049504cca176512fc2b94a5e7f083dbad571491cdbff44c4d6027d11dd726e02cd36ba02c630983926e956efca0afb23f56e7f646fca5b1576adf0d4954d2eae3ada88f379d2d93f6c278e1efb23190d89f67ef797bf57361035396ffcb4078aa36bed87b1937a7e9964b6f40af4389054e91766378b2f32fa695eb6efdb9a656a65ed85d769019b337ed320f4eb632652594aa33568c1e272007e9c4f53d2a79f6afc585ab46cb17e5ca1f75f2181d5678e5cadaf8f44143a93d2bcb6a21472725ce2f16e8bab6ae2fbcec35a6121d99d745b5aea6ea1bc10bdb07779377501fa6e57fbc1028ac99d5ec366a6e95c7bf470000000000000000550804e02e631144b3cdb475382bd8a6c7c109d15d7fb0f53ad9a1b5a61b47899143e5fe6a93e07a49d489518bfe5f8ca855d15bcf679b350000000000000000067f37d34bd66e10787cfb0a56d245524e65b361702e7767d1c83b6ec33971b4fa48ff3ad9a61bea8017870b41e0c2628fa3321e68ca0466be11604e263027f7e9cb9fcd7005fc1b5456fe5a505babe69bd71ff40c0cfa94d6c28422183281a6d40b8a8c1de508a6506c461aa53f4fae625d0554cfb235ee619e4622fa9c6dbfe3d0e990f989340aa24f89aaf4f2bf65a1357c2b294f3f22a7b8025cbc1d586d11009aa3ee5e9a8201fc8d0cf0c5045de76b1a43925f78f9b0f368f98994bbfca6e8bc3e42f484073df7284441be0d4729f05ef34e7879e198d5f2284c32cff2b7ed3cb1dcaec355024a9cb04dbc6af2ce47028f42d2590bb46c4e9f58890a41957303de84a847c9b71ed350edb52b7e9815ad75bda949d787b7ef7242f2830c98a1cc3953a8ffcb6df3cee65c8fbe1a160589f76c77cb96f168e87527c8be8e6fa9dc169c3c91603cacac183c5ffccbc4a37bf83b2b0c262a4daa8914a3e901753c21ca4d910d5949bd4a4c62bd18be919a8104d8f128d22a1d9c749ebce0b18103cd5851859d2b63189b9761581bef6dc52da1f32e4988d246c4a9e36660b4f3dcf6c19979b8682c8e5fbbed6ea91434789aca9343ee9598f0d9499ad1522e62745327e4728f327cebf0a38f0f02f110da770bdf4b9053de4a5d11d054faa83bfbb36e7a483086f359ece7b7f96ea351ef3ba1f822d502110f35fcd1b9d959b073114fb430ae3057c1e35e4d65798e32328ea1f3cad83052dab107c513d3678be782d097bd66736cc18b1320ee378c314f81e400c7ddd679ccfb9a9f6fab66179e8fbdb284402780500fce8263c872fa9b753bcde6f44b9aa5a8fb76778494271b3f90608bef1fcaae4f990c61ebd2fd99bf1e201e55f011c4981c7756d56c0f8b0f98ed07cefde0cbc0a4915dc02276377a2edc18e1b222be0e94ba38d70a1b01df0ce30835c3a9c434327c31b358ed5f5322347985ed00370d9eed6b6464616f337cb730b1d8de7a8a766af81bf150bd4b9a681963cdca641f394ff97092b1435d7ef41afd14bdfbd9a29757e1935d0830f5636ac321f9d9a0a580f3654259054c79bec91bfde2248bce68db226a47794b55cef22d0edefd78df2c42e71c10e2dc455b8e76a5e758c380ff244821f88436db449c75cf645977adc2d6f7473822315e36c65913052c958d28fb9bf69d58fd9577b1b254972d7d51ed2fcc28980667667a32c5cdbaa5f442faada18a02951008b2a6fc6fbd689a3d44ac7fe84f462f536ba5a194308fc529aef7d5c3f9fcce543ad87aa03ef36a2319988439e7d490f7bdae49de0b94a34c8f6d5c103ad30a0560e17411dbb9649e901777335e1122125bd7c03764d540a960f862c844c08e2cb51ea4bed42a4a1f05bcfb46177859043bdc06b026dc821c1f2632f28d9ab88ec24b711528d708f8ec1c5ddee22379d0e4b3548b495c7d1ad7146368450611af4eba32c1731a566453c21cef004fab109144e6bf711f591bdcfdc81f0e541e8464dbd2918a0fe12e5f10165aa41ac8dc1f0241ec2fbcd83ce8c6d0db084c864e8375490c1c7d6a7f4caed80ea9eef0782d50e4695010c31866240c5db9e696991fba21e185c04747281b617c5678f088b5fb8ca3baf81194a36b5bde9d332fafca196cfe96b35055284779181216b29114c030b9e1e01d2e9652f027549f5b3f1649225cb93e8d54f0deefd1a24e43f0c6425a5cafbb8dcf561ad9d0ff919649f02f0c83c57614cff6b5555bd49744b06aa1f537ec2bd93b561cc3a55180fe6c67fdb2fa6c7676e43b01362c58af4030eb591d7146d8282a80374c0452bca2098303f9de443558319f7c1d2338bdc3363a39a23b7bc5e96bc6a245bda055666e0d64113de0c16345988adeab68582cdf16293f911c5296b1a2b248bd6afbf32cc68b0e04bc55b2871a32089d754e37da2505a89506a6eb18f399ab9041e28d49fa8c9eb3a39dc25a340c84dbe1e0adf4adcce728bffe735f0e00854d42d67e33b63e36a455974de7ab66e80a567c9c743c881f6662c92df588068f2dc374fb6863825633f69fc64e6bbffc034d1c63b615df0e6d2311effb72448c7ac48df76110ab008ee0c99e7bda25035f3503545551ffedfd9de2ba993276af8a5da275357035de049d783eacc454e3e3128deb0bcdc6e4b0bc8c7314ecefd60f6c47df606a09b93b6111a52e12801ca91fa58e079e3294b4b2831c59cea6873d7743c52fd8a40da2237b8552b7677deb7b07e8960f260112e35796ec68ac2c306be6e5712193d3735138d6e416400eb39bd8ab8033d0f93ae48eae7d4a1174991c3d15be9e2241ff8722badb4de6b57e786f628e7cb50b96d1403ae2952647c4b635f90181184387e8976ceab4744da8182d3d598d13faa6546167cc940227d0ad32597dc46d2d8d2c99a4799b30b007c9a7469a6ef8e8cb2caaed558458586626cd59389d9287b7262694ba1189aaa34130d7c410c19db745b5cff4751796185fe4adfc4d8a06938e10585ff78d138bea6b640020145aa00f73bf300cae8adebd5183d8b064b819e3d5ea3567c7c1052fc357236e8ab90e20ffaa4eaaea8f33feb4380563e8dcf2e4fae0c0d9e02351bb65b5ea1101f1b024e851562156c954a0a398d1185d39f250af97049d90e8249489fa6f057c58fba8ea889be6fd09b5056a7b58a68d2e1e855da6f39d50823ece0bb2dfdd1c127969807562b40796f40a164bb6c5172aaa0db726e1df3737fff04ae67a713532aeeb48e5abe7b750013a7000f712da39a3faeff36cb0f321868a57801398526807047e3f802e74d5aa654697e6cc6fd6502c99d4ff26ed22906f0bcbe0d83f3b2d1119db7aba677c51bd907cf590d51fed9adefa56f94684ed8a5fe3d3f61f6cbd54883b53be5cb67a8c21a6a12a55dba61d1ff4a8bfe24ebdf93238237c9b32159d81d3ee72e61e53b1b616ec874c1bec49fcaef7dcefe555d94da906c61b665185ab7f215601d75790e514778cb0ce10aeca0629aad74a7032707caaa6508799e3010ae57074b6bde65356a6661218a81bc7e171cacab469b36091557740ab1acf82a0cc0ac6ae6339249cf089ec8cc7b628229be36f39337b59add4f32f57d4e2e1c2b536a13090afd25981340be52c6888041d1b183d00539c462e615d6e96ac42e15b5d3720e15171f479692c7b1ae5558d8287f3659046c14ed2ecd6609ddc717852132a70aaf24e273c0c9a28d103e227214a2412bcb8eff92ed9cf775e5756b408e7c501b4e293326ff4874474082bddadd938fe7d667b6297baddc18014b58fc0bcf3f0d92d8e0c61ed1fb759d27a29149173d1a0dff00745e6d5b18846fd8445e6d5b18846fd8445e6d5b18846fd8445e6d5b18846fd8445e6d5b18846fd8445e6d5b18846fd8430534cda8a5f7c653b0bd76d6f28b5fb68b47ef623932b9766729137d4bbd17566729137d4bbd17566729137d4bbd17566729137d4bbd17566729137d4bbd17566729137d4bbd1750673cc45df8c25a29f4a8b22eef00d83a0fe715414bdc6784580e905b073f6f3956fbe1ea3e39c0f344ddf4b177978dae8581daefc3e15d06ea40bc0535b05df9ebb058ddebe8cddfe6e703e00f380cbb318502d25f49e8c611f4e2985b20b2d3edb6003c60c75bd3a594665c4d2747180102467a649a68b6aab258dc1dd7c8eaf2f99747df4ef1ab2caf7403326d29909f5f9c05241a306c3e6d0db443f8a93eb05b46b86d31804036f6ef67e6d3dfedab3d6d5589a08c31819184d320d5d40bae0a2db0bf0c0b055996a0749959470cf71b1f9ed0b7246ea27d3f22e8242c9f83dc478918061782c65811f2ac5ab8282bb3bf8c8ea535f8e0000000000000000faddc6e03277e288481cc57c579c0d703bc21b5376362ae21d39de8fca14f3abf3c836e6fde5cf584654c4f99e8b23c28d62be69ad3aacd9000000000000000006c6039efab068a07479e86153682ddc362ec67b4f3b0291a64ba54a16fc0b140754a88a23f79c1b99ca0bcef82dcbbdb4fbc5e31b3cf6eb07874657ec0014de47e747fd824eb71a315fa7bac8f9f3262173d77f9fc51e8a2b9c3827934ae1f9f06ab962c6fac062942dfa528937b2f278177ab1c10ff72f68bfbed89f4e652896898e6223de4026c53ba639a8a46fd8b2717290739d4eb0fd2ad4a81ffc529925c2963106e19752dd86762a7ba005db2529cc0c5ea90048d8e6e5fa4dab98257c623cec3c9ab42775adeff2a38c99a32ebbb86b122dbc79ea14d3468b0669533cd29b21ce4a0e8f4a16b20ce65e1edb3eb02b9acd40d8fab26aae4e39e21386ea07f75b81771e5b479ea4ca8c6bad0c2dc6c4decca9807318e8eaa533572a8fe5e8874902c1773a5d540c0b404a31b18afcd00fca4c6ee3e4000f1f7592c83c5be0844a3887f005ab8008bd5344b1fd5674d9628c037ac98c4caa2e20249725bb414963e1da5c6c9bf469245d2dea4c9570703503fa288f3e2865dd7a092e0e49012b56c9edd2ab9661d9c52c51c8066de975df48f1f7fc319339c3c33e928ed0ffb1f1eec3cb9ffc0b1a3950176f48ab4b4cc4b2cfe9dd0818310813c30197d61a3e4656d24f46b879888a07b52ff87e2a6f134450a1d937d51d5b51227ad582c4f902a60449dbe7d2a8d3093afb8248828808622d7b6e952dd878e5d4be221399ab46773f7ae7a848991f8e08404dc31929e738165d9a93b4451327c8d775163ab68cbeddfda04cc7981f3a2fd708e4ecbc58bfa1421c91b40b1a827e2448aa144031388d66bd5f3a7d58f85ec1cd9b26cd32453c4c8c05cb93ad2bb730c4bd10e572e0a7ea3908ea7e2958c9fa4e8af98833a0f7e670d5b7079eb3babc54957ff03af8b498fb1812bb9a9f1bfcd09f5a91e4c28e80738f860da1f82f956394e2d47f9097397b65ad769ee539cb66a6778050d67765433db3b064f97dd8c9cd6def48e6d43467a77928c3ba4ba2037267e521da4be6724eca584e7099ff82c78bde5ed135277281caae02936405540f917cfc6774514a1ea2ac932f0b1306cd7ff1fd5944af1a72b6236ebae22792ee08ef435acd2906e90f88814381e15e8cd3738f118a5eb844117c549e2e04ed5b6832be642aab3145249c5344858214048a026fe746e21c53fc4e6a35bd1c02b2ab4b54018af8a6a615cc694142bde799060ffc66275c32f72e671fcec60b3a406d7bec092d8619d37dd43247381a8714362ec79d9c270478412932b4b72ee149c2851dcb8af1113c6ad7381d977255c2d1f406be651dbf2cf8439496a45c888ea16a819112205935abba314642d84d500a2393c0aec8be10752050a6d4ad53f4629e2ae58572f5912119829e803a1d17d8f9bde96bd45733adbdcfea1d76a273869881e6303648dca2d33fea4043b61a859bf480a64a90f53ce7dbdfb28b39574e403f1157dea27ec6c4a919d6a48c9a03a8cbab7282d27dc7ff7978212de7ada85321a189fac506676535346ec1bb369bec1e099fb46cf4104ac6081a255d9a578bb83d45bfc2685c68841fc56480be95eef6427d477cec9eed5f248c1cdc2cfe29e552780cb6c554d85a9bcd1246e5cdd8ededc2899a9adf6bef2b0d151b6212908ded3329e6b8b147c1d88abdfaea68ac077d981bed9c2c4b1d4baba3e0b67ede0d27c4f6d247ab5fd096396affb21b0a0b9c2875a7015182ec110b2bf592652464c5cab7d4ec1d198cf8d8b0d326502bb0952277c00453802789ea29790fbac958ca4f04af7656fe9f5306f4e338e9983d9d8660c998f2929899c2f9f0d984e11029742e45589124dc20ec293fa0d58a5ebf5750ddf0745b5000cc502d669724135f7abe5077a5ec3058ed4dfad86597166d3a1a94cf69a8957ddb5da4ef70059cae4ce3af4efded3b7025791314e0b7a75900808f054ec10a6f114dd32fa1ca31db3f5e489a79f03103865150508e76b1f33fe568f0b4475214b2817df9710467dcfa85d0ed894cc4d8e478ad60d3a4a708985517cb3a9a9f831c46db8e74174739b01778e5d04903fe93d9f6148355b757d01770a221dd41e76ee4a201f54186cf31214a54bc853bc8fb22651a26536176309e65237eb44887588566b9f1b280b3aeeffbd22a6f48938fa939c85b0ac3572007cb1b3424556ea7345c728e7d336876058125ffb9b52b76204f26b3a415e3a42fc23b4e633c9e0b49f79f45a353027826601f6a295bd0762df248d4aec2fde9c731bd7f6097e002a42c6db7ef6d49bae169b4f145efd0a1836fb688114a1cd5da3ee2a037301f45c6fbcbae4499e2ffebe8a67ad8010856ccd3c008cdc1451f39b26c47b6ea17088c57b4f0e5908801c7a64fb0fbfe703dde052fb950abf77eb0edaf442a773f0292614385c247c3d44fe7713c60610a72024e367edda238b6132938af3b648d71442364a0a7b45faee9435aff97ba304f4872287c41151f77c45e5a756c10b05c1c38a5416c305394674d87df5df9e509b23e5e8c097713b1a9663fe4e848fc20be4c20dfd3ac146218ffc1dbd572b252872849435d22a3fe605cd7f4b221f3eae26363220579fb56a3c0ce73c482c61ad88c537c4bac640422524614fc2a34298aa4c5af828e9e787a117ad9d77248eb74e84fa0b8c1736e01029521922cffc3bbe9068e22f11cb293a2c721c2164a8b8c65066e5911611d4301472b84a8ecb2bb381f6f95ec7d5dbfc2d4c427104971a667c70ed41ce9162c74f036ec713c4a465c4a7c84fb0e226afcc69df5608cbc53b17a0b7082b5b655608476dfe47ff0a15059738c320d6fc6d17c342c6b0cdc253b25e1afa2ac07418c2d7c48f944d2fc6b1d132bdacdc5a6521e9c96167e205dbb23cb6e6716f0a204f6079aafbf7d8b77d0cf7b66794814c298c634b25c0051bab712231659579f0d51fe9e1d38ad7629db164cb7df6db884687adc99ba22bfd359c2d3588fe0438515cb7d28b5055903981c9fffd3d7d2f99b3e069bd90420cdfef521a8183762cb04f9f93e9a2a8b6adc9f7c46d2b2761dd0b3aa61e6827d17ebe63b4baf1098966b6f07d4068c0fccc2a73e98ca32a7b891c5b721edc5281d9b59591628a946a238ae27c8c0b7d4b68d0dd3eb4de2a1ceba404c6c9fa77d0555466c62944d71582f067d3358e87251ad9264d9bacb89926aee594fa48a242a681455db1ebe89e2a39b085ab1a443288cfceaba4c1c55a8e3a7e46948f92c596d6b479b5414e8cff19685a9d4dc9e48f282a21dc73b0f200bfaec3bc1aee8d8af9060a502cb56c9cf5b55fba0baf58413e605950bd9db2995b70bc74f132b21a0626be21324a4f40f9286e21324a4f40f9286e21324a4f40f9286e21324a4f40f9286e21324a4f40f9286e21324a4f40f9286674fd5db5e6bb26cf6067bb84ed7d8a5fef12d9e6ad43cfc1e45bfb17c55471f1e45bfb17c55471f1e45bfb17c55471f1e45bfb17c55471f1e45bfb17c55471f1e45bfb17c55471f0656d813db848f350ed7d3ff4b9aa962cccbc3fc0852bdfd273595ab58f50912f0551c7b6794b1821c848637fc5bd36703810d9f0b711a9f573c69d9d1dba85fb3094457629aaab00116e4152c1ed9b75a6b1ad068d4ba68762763c07915a0962fb2f894df96aea21d5c5fb506585e91efc4228d057ab40e9ab0de4fe09c1597e895eb6efdb9a656a65ed85d769019b337ed320f4eb632652594aa33568c1e272007e9c4f53d2a79f6afc585ab46cb17e5ca1f75f2181d5678e5cadaf8f44143a9bea4b2a7b0d63538622f07fb9226044a928737b58281b6cf6d8cfc6afdad5fec70086e8bdf494c4fb8241a573aa844a799e1e01f5091b859000000000000000060b097a1614c688ddf1969d913eae90f7be3441bf0bea87f3b9696b86a858065a248d2f87e9c330c7708bebbae451c8f7a3453ff8bb90dd0000000000000000006c81c170f9b490f5e1b956286994f08e5537dfe785fc62525a9c87b329faaeec915380c492f8f00d37342af8762e875157b6a5b7c7fe31cc8fc5690df18fac8e23e4a593504456d16bcc281e9e9e7cd9e1096bc711d3eda03d0c010fb5d3c5afb9d4b6549d8eb6dcfc903b5befda5312ba49d1c3a3acd1bbac8a885299487187ed0e990f989340aa24f89aaf4f2bf65a1357c2b294f3f22a7b8025cbc1d586d11009aa3ee5e9a8201fc8d0cf0c5045de76b1a43925f78f9b0f368f98994bbfca686d661949da9d3e9a1ef1b877fd0c6603409d6f7634305495d460a2e5369c637b6f65daf4a1bbee49ada884e33a5bc01466078462835ad1c96a68a8b15633d0e0875c0292f4f97806c59727e023afa10545acc332e36b105c738684ddd075621c0d578be6ad4fc2cc909bceccf8c570360e74de3bc0117f1fa9364b33b6e45b31295cd81ffc0572bf88531d9f68436921da2ec62c269fe14a68b1942046816d8c09aee75de67cb188c5da1b2fce9a93e03ec82242b6f6d35086b4e2a4fbbcf994fead225762c72b8e26292269cbe1f4c7bd751c6b8b91d8205afa6eb533449ffe3a47c2f4ced4d2f0f944862d16a7db9b2ac868d3642edecc3c4b6cca435baf392157b2311ae7970e3810baebddfe15d05911e44f68d352457d4fd25b5e08a388129b290f6398f41750203a92092cb1583d8ebc05aa5dfdd5f79c0e5c7c4f4c9d21d768939ea195588235436d330ae193ace8b61e6e4b935c5095f479d05932ea4ef056d279371d2888efd1b0279ce19d76fe33f4624c5d087a00c8c912a137d0d4ea8a9ee17fbaf173ea68ff2a6b1e04d9158c66281b64cec8a2071706f0d9b1df8ed330e02bc460bd66c1e93a245a00d28fc1b5cf5c11de86c1cb15465127e0ffe4ca23e46e043e767cbedf3a73b51ba307a04dca3a9db297ead2818a59ebc20649ec35e99c70a68b9795099e9546295b57bf94f4fe2df5a3e4ca27173d3d393791009d9e924c4b12eb1b280dce13589b6ec329c53a68614dc1856e0b1cd69583470415c605d48c1a40badd4834d54005a81b391c7dbe0e54beb6f41b3d71480d63028865ca0839c2ee612a4bb127791563586413a3e04e110641a1763fc9d644497f42d510b2396e70f60fad6fbe0309964f74b1166731551aa070501f718dc3934f9c6945d3f5b4dee1a802c416c56400dd1a86915ed5bd0556279e6fa8c066b7e0fca9a39016645f295590c3baa4d60d4d6d1bda307c5b7862f14b10a6710360ae31a66906d51955914dab86a4545b8682c0aced77a83e82909d033762028c6148e36f8f1ff72fb56660567fcced214c00a72179d41db2d45e044bb314f65adfe21bad2337ed4338804800eecef9afd008910f9565ae21e56f686de839c53e513bee8c0895e8b7affdc66f29dbaa4eb7e4b9e1eda57a97cadc1c3aee8969a9bf480a64a90f53ce7dbdfb28b39574e403f1157dea27ec6c4a919d6a48c9a03c894d6baf9a44776acde3c71a86e9a3959800afe6e98ae98db3a7c77895492b8349837a96c3e3b64c7ece0e3b0b81721f8d7b5296ed44e7d6545e29ab9196fab03f460f936f3e4a345cf58906dd2be9099dcf712afeb15593a90817c29a30523e010f74383cf603c7e10b6ccee94607bd5fb65bf22248da461fd99acac914e0cc4debbfb7e95bc3b2f32658288bdf61dcdb37a6396769447c9379e80fd05bbba0f24085cf3ae237fe508bd18396c798796bcfd096987574bdb7722d2594f5420278f99dd27e949d91ddf0a2b1b7a1cfc52258b0d9ec376fe55ed1cdb716a40cacef48404e280c6ffb59d25ca5f1b2abcd5c3660a6a8eca0e1ed84104bc954624a5ce85eb28703727cb311c56687d7f199d4392a2842287026937dec1be4fc4b381b57f678eae35f7f38dbd279855d0bb2f6de11970834f212fa82b00f2ade2f88ae54f3e112e9dcf1e1acc89a9c768365dd6928ea9488fb72a9352eaa6487a62d281eb53c16398ab6bce7a5006799186e9a198de5c109eba57ae47c5601adc827feb00d6d67ca3be78dd4a995ba18fb571ec118fc290ddd6d545318842cb6e03e10f1c491d449990f9973e99abef78bb1fa92e65f8abf804b87b3e9db878de84b4fa17f5a373007c923e6a83c648832b2acf5a2a07080de6f76a706e434062e126a4335d4d8cb6912b4fd174acc3be1eba3a956982d33a036995b2c08b8bb31c6bc4ef0a0f0f76c95cb7826f37c3aa7f5dfe7d3863185667c8abbc45cb23925dc4154a058b54b1c11b60ceaa1f370ad6c06aa99d5b59f060a0c8cf40a644ad64176edb70da91e23bb093a1342797195ae16d014859bd5266d2ba3654c1731e501181e6b26e1c482509e30b33f3ad528ba4acce7026a2e1142d804c561b5ac9ff34bd6be078e22c02e7d95f8de0336701d0e05d42212044d8ea78cc3954881906607f9b0be681557fc313a6b7216ecf08039efc37f2eb916a1f20132421078fe5876cad82438abc703a90c705d55a1e8fec04d49ec3508fb92f85dd15df885193fd4f2569c2fa8f158c34b765b3f9b088f93d950a023d5983033a8bdbe476d6a44548dec9e51231dbb979dccdfcfc08006b24e8570d6f217e8c94355294803e592be44a09456b7b0a70b8a586a19382dea4bed32b0d8418cccb71f7e9c3e454bb437eedeeed84afc600244a57f3ca2eab0054f2f5b9480a87e394aa1d5afb96c6d99ebb465c6d7b82d0bb00d05f4eb2e3ecbd7e6cb1395ba5237a2057f017530c79a1fd7faf4039fd633317b677e7c6270810bdec3d4a6fbcb885f7ec6b2846d2ad8755bab87f027a75d36280489bcdd3b422e2e42401f16df78b30c4f5250f1711b3109c88c17a311534e31fad66328e35fd12bf51fd9fd86bb59c77aff94d11ebaac0efb946cb27656fbd38488df95cc6f4400726cb07a470b83f8be6e58b15a22b56ea5fd582060a4af6587d87703a440d23de2378ade68435201b90e3091e1789790a6d8e59be5e6209a04cd917c2deac9fce0b83413606669410a14e2039c12096e035d648b186a1d05ad6434f797e2411832c044acd6803b72d1f3c12078637c25e645f6616d811e9bf073f9dd6a591dd4c080d76faec49736b82d4d802cbcf4545cc0a3f38aca27c0d5ce2a59e8774ecd5dbdc59781527cef371241a83eabe472b0abe9319ee1f45dde13cf9918fdb03b9c151db432270d84e5d47921ca9641339ae390aa990738407af02e976bdbd8df1570941e7a86948f92c596d6b479b5414e8cff19685a9d4dc9e48f282a21dc73b0f200bfaec9b57de391123138e0d2aa62ca7762060cc85411b9a180e32a42a205c6e9e05bc5b4aeca382b077a0e341b851fd5b0728e341b851fd5b0728e341b851fd5b0728e341b851fd5b0728e341b851fd5b0728e341b851fd5b072828d8d4fc4798612a705be74280f1d332f3b9c56d7069b5e7141013c977a1580a141013c977a1580a141013c977a1580a141013c977a1580a141013c977a1580a141013c977a1580a06cebf44983ff4f05b3a9802054bda20269c8a7615c39213b4d33f959b81d4a3abc2464482a597443c88b9045204b5259fe11e4050e2c048c06f0e69633ae4d610f2c3dea7d29d7fa9963c72560d7c5e7e340d47db8c6155a8af790b3ccd6d59aa234ee625e18bba7936eaf8c545eaca5f99a0c24ab1b40b74a4cf5734a905687c11729c5fd01baf5f00f1e034bdffb3f0af203f4fbf4e2ab4b238a95d96ccd22807e9c4f53d2a79f6afc585ab46cb17e5ca1f75f2181d5678e5cadaf8f44143a997a9c30df49d06fc816ec86b6aa56cd82b1017a071572909bb649dc0c7a40db086546e2027a960a9d1fddf8c7d80e6b33713fe2538a6fc620000000000000000cbfd8fdc93bd4f8bc85995612f13c29344763acc5f633763ff541599d6222343aeee8aa9552ee9fe53be4fabcc00ec18f5a2f5a44dcede54000000000000000006686ae3dac418456a3725e7ed5430fd68df00ebd713882b049e92f82f6eb63ad35b7abae6771dca04d966469b1598a68716450b3cced775dec776bacadebe5656d6f154e05b31c9f01dbdac04bfa0eecc3bc851f323c7c153922d81725113a7c7de2486692dd5d90ff3ac1fe9c08dab0c026302e651be160b1666113f1726ea2ea50bb66c38cbacf17f499c8f68cab5ed91805b731d49320b4f954ee6e7c30a17009aa3ee5e9a8201fc8d0cf0c5045de76b1a43925f78f9b0f368f98994bbfca62407ab53e3280dbfbcadcd88b24f2e6164841c75eee77d26f0ca05762845e71c1bc3c89be1f8b36f65fbb4c1f6161fd5253917a451692b0148548e6b318577c0447c5916fb0c72906eea7a122450ce2d52468a49d435fdf6b26c48cbfc7e048ab3cb9b4411d8064f921f24e2442f7d16a39f98ca9205ad258c6f0645cb0ee78367fea562f365ee2d2a6e50a53735daefaf0964a190346e334944daeb18b3c4850e5f88a6784d2931709f034534af659451fc2fd26c9daf8940e8b4a78e1d21d4b9c7a1108d1357ccdd696055814b845502fbb7ac940c58b8d5b61b92167b1a3c1289b32d71cf6417ef5fbc21be76dbf9ef3ea77734952777a70f075cb39334833b4c3f19a64ec8cd1bd6bee0e0471ff5f0418566ef16be4b518be855ba22a03d3816246d67ddf4864ae9ec0bd8cf6fa5deb7ecb97742f123e353fb8a69f8a3918100e99d229e684746250035c7bd9a4c335b74691b43e7f9f7205fd7620f8adb2d78f78305a12f4979c05c231727405e42890a7102c9012cb2fc9120f7353f7760369af51e7b6545517e05371e9c05672bf13031eb769a03a6dfc21051f7ed38a39faae77d20c14c8a27eb3b9f32191ed7e19bc37e3d406e24edf2523139ee3445d462ee1e30db25fe70f043814923514a3d02cef35cb89f35c2548e1c2875edd85fe6ecbd19c61331ea2c9b6864f8ea4fe0950da64468b01dfb61271a3cdb453756d39c66f86e3fb430890e14edd4e4a33099cb9634d1ace068fe0b3e729eba4cd99cc8b9fce8bb8a921550b3452fa51ad245295feac7e4f32f4c18414b01993113bd6a97f27c9da58e9f48159dd1fe0e39a71896aa9cd74007867bf1be597e5a297ae1b88bb6e0eabb10b48501fff9a8aaacc66cb6894022150a52ee5a855feb1c897361bc6cd09ef772082a1b514ea02ae29bfa2bef126e0bf59ef9a2dab906802e42228c1a48da5d4a2a36bda15a8c4bdd4e883a29dc2f928fbcaad8aa400cbf63d1359d107bf4fa5c665fb59397df575ef74d455b55f75a6427bba3a1af8365cc0b6b9c5ccb19058c3282f984717191f5e2cbaff0e899b2cfa09e64cb1864cb1a6769bbff6ad49cb56640bfd23538e19b62c69dc3e8b3b265b91ec78c9661e513bee8c0895e8b7affdc66f29dbaa4eb7e4b9e1eda57a97cadc1c3aee8969a9bf480a64a90f53ce7dbdfb28b39574e403f1157dea27ec6c4a919d6a48c9a0333cbc3020f9c2e97f20fa7c3caf18ea06d9b7af361eb841de1936e3569039baa495ba29786789322b692a7104e734a5f702d3952637eef2db2d8eb967aba876c5b37d87d48b8e5294edc786fcefa982f34e13a694e4957d92ae218e691e261c726e51a7257d7e242660374c299dc7dbe32fafd80fc1a0610c3debed04e63bc07f8408c259f5508e4c4238e72d6b36041191a817696a5868944bb95e1b5be068874f0e169c964396002f03765b7a837ed16a80dcbf0d1f4471119e3d80d88c1aabc0097131f1f95b7bb4efc3d5507bb394a49eb3ee68edd4abd61a236de108ae7d874b3eb5b933fb07e841f29753fab816385db4f182cc44d663f58312f92cee36abd20cfa513a18b0ad7ae80c93958b335797972be8987625f15e5623e77fdcb27a148ad4d2323221f690f278707e7f476a85495962f26b1b5e4de5cf45a3770c2bf30e2960cc05a397694e1cb243308c76619eeafea39248b16611e806012244d6b68cd0669840aaae3881433e9f682c5e9ee7c2c3aed38363241faf3ff412a7cfde2fc0a5ddde2f11b7e2a24efffc7e5f9374aec903d9b67175c7e1b2bea808c8f61ab1cfbff91d2d06e9ffc12f7413df6ec29d24d11a6fd2ec4010743f310b87e6e317b0fae76ee8d78261e4cc44c905f07a47d3dff300f821be1d9a0dd85a75dc7b945c0144540aad4a84392c7a857f2c33944507b3f6dde6bdeaa0fe05f241eec73a14e37516ff8b050609ea3e083c752bbbaf32c53dfc48c73eead6bc17fc5af9e3d83507e07993afd70eefa6496cd70f4fe1afdd2ab9ad300e087fa730f3e4697874aa2ff03a37b3b7cbc985c57c76703424b7fb3111bc2634f4a46e4b8f0af2b1df5f281e91214c77835b387e2e37cc1ac85cb79886b81cc21343e201ef2ba3b01628489d7385bf320a3a3e71037af836d772d3fe130638f6fce4855d65e28fd44b24acf72f3a3d757b79b56a2fa9d8e6e536a3b0d392c19b8bc98838e6980cb1c5606cb7ac0d4d97d17d3cb3895eaeb65f0118af16d720ccbb2fea39fb11ea99cffe765b44628d284c4837227581b76abf532200fffb0024962b02878244976e9f3aa81d9069cfd2e01b72cf33e72732cd8d9ccc82b11cce8624fe76561a4aec3032fecba73afa03682f8f3e54b00bee2c162a934fbeb7aca92dd9b64279790ba148ee1ebfaac764be3a8dca9c18b16989be3bc20928416412d18ee712d051030e69d4df9d405c35827afb2d7b9a2ca04940831416a34d5fd1537557638a6ba23d98b570f608916f8b6e0a1d5076f7bbcaee94052230dc27126d45a1e3e576a7a347044721d73ce90c8044994d7ce29f221696b6feef32486c1f515cf9b058a0cdc0cf1ba019bafe6f01fd88efd1a0bd727916284af842f397b5a7590673c5e3ecb78785edd2054ff43b78109116bb7f359c9b85a359748259032856da033384b6189ea6601ab6d36e2fec35c1bb1f8a421abb8942d9744797ef45a8602563d430a6148925ca0055ba129c8078f7dcd76a0fa520605d2caa0d993bddabb8dde2b7412ddd50903ba4e1105cdcfa319c1ee06f594d84455b2a2a559b8f5dce3343756d73ae69de578e4f6456bef5b598a231543c6513f200a51d03d1f7e66cad40c92d5fcaf864d1583a06704be9feeb115829966ea1f59c59c8caa4f83f6fd875ae951c088181e310931ca91b3491f50f56c77326b70d84e5d47921ca9641339ae390aa990738407af02e976bdbd8df1570941e7a86948f92c596d6b479b5414e8cff19685a9d4dc9e48f282a21dc73b0f200bfaec99f9a237d4d4bcc525664443a05b3a7a6a26e7c40c26d28f772abfe0a2f60c7f8e05c6af96817d1f8f12cff9c204f0958f12cff9c204f0958f12cff9c204f0958f12cff9c204f0958f12cff9c204f0958f12cff9c204f095f58bbd4877057d5466644c2337d55c430587ed7cf23de8e34a8a5e8fc56a9aa04a8a5e8fc56a9aa04a8a5e8fc56a9aa04a8a5e8fc56a9aa04a8a5e8fc56a9aa04a8a5e8fc56a9aa006800c2c3e2f75b65c5817822f7e1a8a061f643c5113da7423cf6a2d5c76fb1f607f611794c1fd4c96148f6af30eaf7847d3fb5ff5fd5e3882d055c1d1405948db4405db98a4c71563fc04cbf70d21a28a586636410f8f0966807d32870e7e511c0164446fceaaaa1b3c7fcc471741d8a57df80dbde948617f78accc3d582a430811729c5fd01baf5f00f1e034bdffb3f0af203f4fbf4e2ab4b238a95d96ccd22807e9c4f53d2a79f6afc585ab46cb17e5ca1f75f2181d5678e5cadaf8f44143a98a017bf0ee6ace1f8abd0819859dec4ad8ce059f3d64c8f824afc453c0967cda29239157f429d9b53843054ae0f82f2525d1e39e5d2015a20000000000000000f619e82acc822e7e9df8ea21fedf861650385344e2731b463efe4ec2b87f861cffe216739f9f52b3d81d8418f9283f5f4fdd2cd82e0bb722000000000000000006b631cbe6563bc3b2d6201e2b5d999eeeefe98e0da6352ce81182863597683ed442e780c4dc7188a108466a6d2a2dd63f366a2c7bc8050112b477c1118a5d0e5f112d58c772f51e2123b0c7c876de798d61e4438b2a571585b3497f9c1f3090f8bf0e845443d0bbe979d90edc170c8f25faac702b33f13e9a544eb922f21df5f0a50bb66c38cbacf17f499c8f68cab5ed91805b731d49320b4f954ee6e7c30a17009aa3ee5e9a8201fc8d0cf0c5045de76b1a43925f78f9b0f368f98994bbfca6410eeb9e7d44af86112dfc8e6c10684e9d0160d5732bdedb45a3389e172a404d44fa08e684d50b7aa95e0c8d9405955f0ae10691e963644bda4394df091ea37e22907807798cb5331f3a1ec222bf85ce0d1a4b4a6b0672d8176262546ac5f663cdabc6ef661331c9f22727c10dd07d81ad26828b6062952509aa0de15498458074e442b3a7559ec45ef4413eded807ada5d82f248e72877db0f2b694b3bcad5b8f419756566dad50735323abfd3912ae3171696991c8f439149dd2fbd0d96a7e4d641d76dc60471a91fafbfd150f3261c12fb079f77ebe4704ec459f7d77bdf876f9311123c9898cb6dbce8f4a412307aa3cb98b5e8989589ea419cce422066176426e2e28c3c7c840faa82f0f0f51f1b8f05d8238e56d821601ed798fba632beca6374462385fc07f7e2bb5361a753a148a1f112d101141c41f8dec9c051baff2443bfa0973d5e8efd564d2e628f3c3193acf6f0d23e51c7c50dc4e7cb668e2097d499a35c796d13901f23ac318f343490e60f16c9d17ebb0f9b9736a764143d92b2b0274022806e12c387b2ca70f0bb8d31ca8bea459a6bb6fc7d14779b275040966eafea15c0e95c9f0b02edd2f05ec01724b051f7106ca71a7e1ccf6c07381d92ed9dc0f5e074d1a89122b6257f3e8c2fdb42d2b925c41a96b5fadb02114ce309e40339cfa0f24c3577f8ad5ceb32642838dd70e10cae1d92e1a4616962cc477fb498aed1dc969a72667110f6558a58966edd5ac3c4492f3d4052719cb09e647dfa54fd1db2b2e7a29b53dd95560543f26371b928fff5b0ac86c259475f295e7eb7cd12fbe3fe98cf6dfa176c44d55e5bc96e2e07d48f8cdf203ce0131705eb40537577aee310df8cfb713828fcab44d04c2f64cc697d82afcf27e84738ae46583ae3451e0edda88721269df49ad607997004b75892a18d5ce958c82392d0616557cb565d5eeb337bb1a435f0a270cfd678a468d1aa7ead6d9bb89799c3e82b54f9efcf93bc169b640ecce43aa07011ceeb070629f1ed7f0af28b0403599df0868fd4fadca94f2956a194cac2f9c643cf4c24f0fbf73d85216a832a713a5428b8fd738508d862240e363568db677131d0a1ea60f253db138bc2c18d67d8cf859043bdc06b026dc821c1f2632f28d9ab88ec24b711528d708f8ec1c5ddee22379d0e4b3548b495c7d1ad7146368450611af4eba32c1731a566453c21cef004f3f28dc400496f5f601e4d87f531efa3eed91cbb0e670fcfc3c8ccb5e50534b2189a7af1c65903c0f8260a542aea73ee243e7585bb20057b1b724f6fd74de5f7221b7e4e363bdc7622671723b853473862478152999f9f79da332bbd36c4bd03bd2f65cd95972a9376ac08a1414ea4eb9c9106cdbc8e82df064498f41d983f598b4c6da11676c552084b4475623e145d79aa14320b818e54734baa2880b49faf6aa8caf56c00b8d739b1ff4f4dbf29d5f429a81052156ed5e464b74cf56b7e412a268107f7621805fc92f6d34bb4f584c0e64ff8a9841f6a558a36c85ad0f3bece02856043547a95be3e6ce2f271ef096363aa46570448133475e4309dc058c057c67e42d9ff0a946b43c54903ffdbc2174ab3dfb6f5b36a744f1137d382d4acf85dc8b677912364b1c4ac7f2d408e544d2da1c07fb8394882a2c86c3443ee1c28c7550dacf52efa278f4ee59c2e99a4c5e7eaa5a77fa0d7e535b301816f7fa85ed6d5f7135e35663009cd4452def4821f4af62f2660590bba338abcdfb77f6e1d57620c2fe0abb06811d0ac3b15128978da2d834e7fbb3e6aedc2d7e5503e82bda888b1bab2eac293cfd1433d7aea2537cf8694480e1e7f5fac65cdc08c753a8a6cf253ef9b982fca7214bee99b6b2c23896abb729fe2ec871903e9d95c5d894cbe3282092cab2c621b73b3e30dd360446519ca393ec9557c0012b22466a45449712b4f521bcbd01163c56fc799272ec36172db749fba4cb2c8262416b19e99a6c4b5edf56336b7d3e1f307d539d290cca99d8468d3f13c0c3e3dc1cb8705b398754e321e1ffc5e72b7585386765a2aa3a9b1cf6356f5775375433a69157995032976fc5b9bd4841132f912efafd92a5403d852a26d2f2288e805dbd7b22558efc9b634df12ca5651d666c5790aa824a372d40116b903e7c236b3d4d69adbace3eba912835a9f633d277dbab162f805969fb92284805657200e171fc3dc5438c157b05f9a009f3ff1598f2954348b0afe849c3ac203de63d75278ed4187d1fecec5c8da26cb2cd2659fc42aa4aaf6fe6347f6e5b1d12b3fa20809774a90a6abc4993bc61308c5ffdc6b66632145c8f18fa602a62c9060ee2e71c78117bbebc9d069f72e83c5facad57635617e91e49c29ae89ae992d4df217c0c0f7fa88a03478cf639628c922b2180cd43acdac6ac4c9152a93dda4a97fe1be84ba84489b735666ce7e462bc0273ec6e3f667713161ffeae5791591cfdd62f8da70db0febcb02a3bc29ccc997da7d8fb8cc68fbd54a49556a9f72b48ce0c73e023af1285f9910ff97425d8be30e201bf626acbbae669fe61d68b331626c3c7291448eb870a56e07987489c13b449988f881d5eef1138e86a98e8569647f1cdd4461d16c6c9351e2982eb3d69a5cee2b1f55c1ebe903c2ba901a5d27d06b867a3737ec14b31e2908457056ed7799c849a830ca320d83e8bac8a96a9ae5f31f13ba810fa782ed2dcbf71449fbbd8e97751b9047ed90adec7621c028eece67d06d07221967da19b2fc7bf9bc747fa61c0cda6bd2c02f5388dfca058140a4af9ffeb7596ccb6fb49f879edd789d8dcfffd093a5fe1db0e6ed7d79b90b0eb837be54638aa2fa5f017b7e03b7cd2b0428f99210e2d811a08ff79fe37ab013182d2e73af584b2cb6290d504229c3b555c2bc42148dd909bd96b121f75dc58ab9424e28287f3659046c14ed2ecd6609ddc717852132a70aaf24e273c0c9a28d103e227214a2412bcb8eff92ed9cf775e5756b408e7c501b4e293326ff4874474082bddfee751e140d1128b0b973c3455d5bc29b24bb97bc30ee1db8b6f4d2d44b349745220fc1cab6b79a44f7b9721fb00673b4f7b9721fb00673b4f7b9721fb00673b4f7b9721fb00673b4f7b9721fb00673b4f7b9721fb00673bdf9c7df3f767806cf032ae314949fb3f8bcf7fde1e3321e25a60cc725b384b675a60cc725b384b675a60cc725b384b675a60cc725b384b675a60cc725b384b675a60cc725b384b6706d1a292eda802d783006a5beb13194f09b67fcb7f70ac50dc3f4f1db4ba3127dcbb5f35cc00072fb58bb9b49c6799d9d8882dc6a1506eedea39c2ce5ff4a237e78f2b9a09e62e658f1543c289eb7368ffe321a2bd4d79238bea1abab4e58a75de57db3f9c3a69f9a81c70fdfd85789c125b7403f5fb83ef2733629780dfdbbcd52f99747df4ef1ab2caf7403326d29909f5f9c05241a306c3e6d0db443f8a93eb05b46b86d31804036f6ef67e6d3dfedab3d6d5589a08c31819184d320d5d40ba7202a6b544b04b93ba412758d538a7102f23de73b173c1daa51408484b76079fdc29dd4c77ba89819d2ad8972efc35ff33bbc076934d3cfa000000000000000050636e20b480757e83611bf0b059b2afd291d767b560353d3e4e253d2dbac6293698cf0116f9251720ff183959032bcfe6d223e30dfc54ad0000000000000000068f19099b9fb4fdd682557d8d1a30590a06d97fe66eeb0a060205f64784cc296ee651cfee023172700e826ed5b460123ba43c88a1fd9a1250afeadc8ade2538df32f9c8b3f380247db7496cb38008364d9dfff1151a164a0a5cab2eb34fcf130d21d533efb027e7c34af3a7419bdb2442c7b8dba41cfb4e227b8e0623a6d16b31898e6223de4026c53ba639a8a46fd8b2717290739d4eb0fd2ad4a81ffc529925c2963106e19752dd86762a7ba005db2529cc0c5ea90048d8e6e5fa4dab98257cfe3276f25d044081301f89dfdd3d7b4baf6309cd10f232a167eb3f00b3e5cdb6f98509f0f8adaa4dc8dcf3a7b120a5dbec2cb407aabdff6498459f12b09c94c01f00e3e06b95221a74a774ceae434747303f3093e3e74473a93803b86ceddba2baec5cd268a36369d8cbe6a3a7f69789f3fffae889c497fb9c6a9d1d90cc869f4f3268706f1823eeee85c1768818751b7feba342763cd3208fb90e866209463df8fd27f898482d1ce2353a4f78d5ca31d4eb87f77b8de9fa0ac12b0f2771c7012479962e614d0773eeb3263a10cf1dd1e5b3167a4bf1f0e70cfefb730ea68f5fc4265d7d7cfc75a6f26bb79d2d9582464f537972c8bd12133e3b49335aca450c6fea84dced51868f1e7ef45141201ea2fc6ee1bb0872cadbaa2b02da491f75c780d64d6f08c670be8cfdfc56de6d34ea7e27143fa45a2022a2863f1a373b0b362fe28976c515e6aa79dcabc92bcf51e6c731749e181b46ca3cf6a0b861fa6cd15d10fa92d76c8e2d797102e4fc8631e62a901cc0b8db3a2f7a5ff3736dd5ec82f4b1575610e80450eac159700c594e1fb46ea7399c7e49b31575df8e8e90f264e40712ccf0fd43b9f62993e16b5dba5ff4d703e4a20a3ee21993e34eaa9aed81f201b35dc0b91fbc1a9834120b58c4ae47cf85fb225c5624d88152d7e65a6143e19b613ca06638f5994686af6516ab9d6c4a8406afb01d20a7c1b35d8d8c2c2c6e86eff62516db3b50d14e4d7e231eca784913cd62ac5979ed23e7a91e4e3296a9cb8fbea29fa2b7405bf4522a7cb2ab01a67e4c6d38241f1cb41490bd4c28eb8129cfd778a35f7c65d119ed5a44ed8870a9ca79bdc5c1fb20ef9be5e79c03629dbb680bd1aef4dc6b18f09f0429041fd1669b08b3ee998cecae55f8f9fe08e725c6cb06386ba2c0a6b211e57ed3be93abbff22e5696ea12a62faa9d8519057306bf7d3185a71b15a4f9a16bd2e6b214c7c8d35fd6f5437f5510997f9ca35046a4360ae31a66906d51955914dab86a4545b8682c0aced77a83e82909d033762028c6148e36f8f1ff72fb56660567fcced214c00a72179d41db2d45e044bb314f65adfe21bad2337ed4338804800eecef9afd008910f9565ae21e56f686de839c53e513bee8c0895e8b7affdc66f29dbaa4eb7e4b9e1eda57a97cadc1c3aee8969a9bf480a64a90f53ce7dbdfb28b39574e403f1157dea27ec6c4a919d6a48c9a030740979bbf14e6b83bbce470428af7f7aee40deef0ae4c04f10cb69ebb858ad35947de01168e20cd5cd234ea40964b8f91fb95d3fcb81cae03a916dcbb6d1ececb0baeaa184507474a55fe7d429072c65fbd4f5cb8d1e2553a63342a9b369e0631681a1a723cb436d2fd75c4aa651cefdc4861d00ada264340abc9df692e6d4123346c09e493e2e91e83c411ecf18a7af86245ea1b9a8a1042be7fdc97946a29b94aa37287a1bc1a63f736eac67716564448143fbcb251ec9d71b53be3d476d14f143ded2e49adb96bb9a301388fafe775d748536563a1d4330d2c271b1acc4d070c8cb86735f9086309f1af52c1aaaea1b703f5707e16399e1f70f0dad61e6c64a907d455c32f55539f74c9131eb8c378c6141a90a06083f049165e4ecd475d0cca04a24c66b9bd9ea40f18570b82353085ff2abc68aeab15b342f065ce02cddea6d0e9346a564b1172a2a4d38290be467af287b3f6caeec237e77818447b6f42e5893ab8080229ce837e58185e33b5c92a1bf4a758c19de7e4790a0192a84a4228699caf555e57f0df69fe56aec987a8eb2c68f9acf72840391d28ce93f2b32b266c4f4d8049d4152fdf8c96f193c6b28f62a9e3dcc09da7daa3b9e857b3a8e95d1c28d9c8e0e55848c2b543bde563aab77d331e6055df05970a52f0030a3a3e306d5534b8d96b2edc37e3c5a1f428cca3ffe2872205851154d0066c4152fc5001e45d40c2c091e1f381652f0040616fa2d3ffb828f14131dd5b8aea28c7a1b142ce05acec2b80ce1d0c6a2329d56f23161ddfe221046b5607ce99714bd88931bf8ef84d03b1a4859f75d8d8f1e63dbee6a7eddaf02e42c3899a7a77ba01ae1bb3bbd3d49d2f614998dce823e988c70b4a4f4da5d7192eb67a16577eb0f4c90329b878e3aae1c48173c86d46274fa86497a11cc5306f907db11505fad131bcb94a17bd696db3f3c57937a686871836331251ef2b9ef17899eec64641c35e8ccffc9886bf5336f563091567309a3006e694d52e0b9c1b953a2869fabe90a20e37f420a2e7719e12aacdb0b2f8858e481278a567c457db21809ff73015a78a2dbb1f50c9af69215cc3b2267c8c69d83de75be3814a119038d366320d252e32afa143e1080c92ce28239cea56fa1e27c5a9070fa70913f2c1bca78daff2cafc8964b8901b50225cf2df96479e4b78d9a82c24bff89473b1486352079f96bd4796c57236ea30897d7b88507c77fe0ef74f86f75b236c10c15ca4d395fd535917785648ad04b1864ca85efcafa1064e145c9c48cedd11845238142820f95b270d3c2a19c2cea7be17213363e2998bd5adbbc151518456aca59e3c1612fb6711990d06f14278f8eb0ed90d0e67462a36f23e6848ba8f745620cca221ef1c1b508e3581897f593ad5a6dfddee533082647126759f4df8de5b6d99375d1c3c27b60002f796fbee46f8df736055aa9c685b9c1592a61a6980433e7fda1d94d742486412f89aa17d8de2ed40c54d8eb0e8c656e369bb3c5a8948accc06331cb7e9bb3345311b0dc718e8b8fac798babeb092ca496e742522cf6a500ddc03b72d1f3c12078637c25e645f6616d811e9bf073f9dd6a591dd4c080d76faec49736b82d4d802cbcf4545cc0a3f38aca27c0d5ce2a59e8774ecd5dbdc59781527cef371241a83eabe472b0abe9319ee1f45dde13cf9918fdb03b9c151db432270d84e5d47921ca9641339ae390aa990738407af02e976bdbd8df1570941e7a86948f92c596d6b479b5414e8cff19685a9d4dc9e48f282a21dc73b0f200bfaecdb3b20ac507ba1dd19ccb61515bd5236d61933211d8f25a691699d859d31a3032686fc54908b6c59234525019b5d4650234525019b5d4650234525019b5d4650234525019b5d4650234525019b5d4650234525019b5d4650a32cfc4b75779cb2346ae364ee5fdcd4d99853f0fa8354461056c1faaa7e28be1056c1faaa7e28be1056c1faaa7e28be1056c1faaa7e28be1056c1faaa7e28be1056c1faaa7e28be06cdb66df8f361761dca0cc2858549b5caf7fdae0ff77c8d64146efb8759960aa8c2464482a597443c88b9045204b5259fe11e4050e2c048c06f0e69633ae4d610f2c3dea7d29d7fa9963c72560d7c5e7e340d47db8c6155a8af790b3ccd6d59aa234ee625e18bba7936eaf8c545eaca5f99a0c24ab1b40b74a4cf5734a905687c11729c5fd01baf5f00f1e034bdffb3f0af203f4fbf4e2ab4b238a95d96ccd22807e9c4f53d2a79f6afc585ab46cb17e5ca1f75f2181d5678e5cadaf8f44143a96f8a03d0fbcbdc1ff3565b49de4586807f91d87e48e54c854f22dfaa391505a127cded18088b24ca05f754ad9e52f83d8fb354be3d85e7d70000000000000000320dfe9c38d87d176128cb2ac1e9e732ebe995c7f0ca0a882351684df6315e3aacfcd5a30a022c16d72f7396fd3c2d5f8a8b1826737b14de000000000000000006da34101fa5dcbb53907e933212522b2c6c22a941a639ec141a47e43fe0e12d945b7abae6771dca04d966469b1598a68716450b3cced775dec776bacadebe5656d6f154e05b31c9f01dbdac04bfa0eecc3bc851f323c7c153922d81725113a7c7de2486692dd5d90ff3ac1fe9c08dab0c026302e651be160b1666113f1726ea2ea50bb66c38cbacf17f499c8f68cab5ed91805b731d49320b4f954ee6e7c30a17009aa3ee5e9a8201fc8d0cf0c5045de76b1a43925f78f9b0f368f98994bbfca6e655325eba49b119d4967a42cd58bef69997dfce81376c0473724f3106550e6949b98425f6c23ac76f4078a2ddf4b814df841d86945c7aa70219c5db3e13bb5aa464fd1cea411339819536e4ad3992047ab4b4b3ee9effcce63c68365c2a9ef183d4f3e5c589a9e1153690fd28729e9f1a0e5b432db5ca7d15a93a04aa3fac29887ea16143a7731506eaa46c23d05dd28d5c4d97438edc7f8729ec1ffee301988e486fc389849165b7159c81d420aebd18877666c64b4b3d620ab03253d04aea03b9ece5bb7d4873704e8d5826464d0eb40b223e73701218c8166c1b0f8e6cd7aeb93f85192c6a4573a044129056ebcb8965356cbb116a671126b6652dadd19d8dacd81b8c70cd83160f5c70effd0eef2788f420b36fac21b7a2ee2faa0557c4064c9185b6cf790ca813184805915cae12c45e07dc2afdeef2ca032e4526a64f8eeeb04bce51cfa8401ca1b2998671cdcf715e0c3427cfad274ef83aeb2c98741a7d2f6841998c934074ecdf10c873ceb27e1bff37222986350465608f5499e86370424d7abfd87fb1f0317d9b378d05668ac432180bb4655c570489877b6bd8e6c06f9f7310e03553b066f39d142d026840e1dfe0aa0fee3d67e388a82a93f076f06712f731021f363f5b6ea13fdd89ca85d123e61e4ddd43f16b45c628f5e40021f31cf6ca3c563dcbcd83cd4ca712a2acddcb857a12ffcaf26112939b9b9e92cc8348ce4e27218775899506e40eaf44b46597e59c32359de0c6b0058f6d4ebea2810be402eb4206265d68a71e6ca2f9cf0a9c943cde06285f5a7f0b9abda6fcb3864135e4021ddd74319723dd95b888b4aa310cd2f121048720d3bc18e3ef1f23baa470895a18a93c7f00dab7de077dc924bb628a309ba888523d2808b8c7dfcea1c938a6ecfad56a72d703640962a9026a884d4dab68d482ae12cf33d7670642120548e52c9721896c596ab5743685bfaba4b27696264eae9fb684d6e75f992f536ba5a194308fc529aef7d5c3f9fcce543ad87aa03ef36a2319988439e7d490f7bdae49de0b94a34c8f6d5c103ad30a0560e17411dbb9649e901777335e1122125bd7c03764d540a960f862c844c08e2cb51ea4bed42a4a1f05bcfb46177859043bdc06b026dc821c1f2632f28d9ab88ec24b711528d708f8ec1c5ddee22379d0e4b3548b495c7d1ad7146368450611af4eba32c1731a566453c21cef004f46a34036988b0d9b5b76ba15ee0e464467b7c2520e4ea8f73d96eb3ffa5100b6ec5657b146669dd35dbcb8759c137d83e42fa6e0faf3e7c89547d36a0af5d4b6466bd371b9f1f6133e707fd0fabc09c79cd9763d53d1fcf8e3e5c0df395a696f33da763f6e8499f8a647cad5ee44dda58ee4f09c419b790f9c45b820c9a129676ef516cf789dfea463da694e8ef374539b9b70388ae2f3c85abe32f35d12348d6447b495b015d50b9be712030178c2689c811297618f90c0534d3c71c9d38ea23b3925456d4b006e61c62593082b48abb5f620924a47993b00e105a2803c93284a63cfe40c44d9aa5d3cc61a9f2f8acfb32995a438ba8d782471b3f28bb89ef75d383a1123303fbdd5d6758daf854655fda3333f19025580830ef44610db04eb48622f8d6d1a9ea9bb9c4b6b44a543da8b69f429fb2d8dadbcf50c0f48871bf24474947cafde781797e448f879b2f7e28518d2625ad3dc7b026c5d362fbcd00f92c0c08ab80cc1d712ba7aaa96ec3c7c16078ca645b72fc9a162a1a0c2363e5fce14ba4abb4a0af353b4bf2c1968a44d3e67c78125d8adf8fc6818169ccb44f64f795045513939e4590c7b9a2b620d40263c04e552d0c9f15eb20f882fc3f0b870e2e6edbd2221b2ebcc226965dd3eba2dbff5e36b049c0ecc02e282f3d4218e9f1dcaddd731ddf9e1998bbc42a30dd4417ea3cd49055366e64f90cd5b4fe2e3ff019a21a1e3854d57709f67c9f4b4f3be18d480948fe10688029b96cd596e52b9b4f3851cda98609fe4c68c5d87aed1c085cf1cce2465289d1d2e0641054ef5535fdc16894aec35c126d8563ceda86c27967815ba5b3ee1f1508700b202d6777ddcdf5899c1b71517244447cb580455cf22e134f02cb12cc9cdec4202ac52545cd543e0d0b308c3d64897e1ec1c63711aadaba83913277f19c46fd4d4d13e586311aaa7ec2b6296662af626d1e6f66c4266269eb37f06ef97dc12625cac8ae6768cf07721e1e290d4c6d3da55b739e0e0a8c46940ffcdebc8422987d6afa0e01d1b6a1b1130c7b375e45df71cdac8c5e72f6a32d10adbebd09f5b0b97ce9831a5b61430879dcc0f2ae83b87d6d83701158e9ee6ddcdb3a058f3f8c90833a8237608014b925f0b713e682e39aa400c451facdad931512c330ef0a7786fd351a130f9413c469f35f9b7059b19a82868e54977bb577acda1428bc426e1f1520af5949ccb28d9994d38e19edcbf9eb84b6ebd426c8007068931a119e7d2bca3c9e580340650403e4f44fe0f49472b0ded0f76cffd6ce16cb8c1acacfb70f50f88af516a1bf29d1729a4ded18665fcd019c6b714b06cd80736a0bea7ddf19e9bd0f788d4d4a6354c4872caac11ee55ec0bfbac395f31d63bdb68c1a53009df55c70d53948740d34f1e3994e99f74e3c4b2eb0eaba53ae2b6f9ba9337a57d127df9bfba6672041fc380f59b5e7bb42c585c4d12439d3cc6f56889e23093f64ff39144444b197119f734338bfc1dc94dfedf603ac60ea2b13caf9a060d6340ce5ddfde100411cec0a72833ed681ab2afb8c18f5e845662159d338bec71cacab469b36091557740ab1acf82a0cc0ac6ae6339249cf089ec8cc7b628229be36f39337b59add4f32f57d4e2e1c2b536a13090afd25981340be52c6888041d1b183d00539c462e615d6e96ac42e15b5d3720e15171f479692c7b1ae5558d8287f3659046c14ed2ecd6609ddc717852132a70aaf24e273c0c9a28d103e227214a2412bcb8eff92ed9cf775e5756b408e7c501b4e293326ff4874474082bdd867ef0fbafb9471ab68684ba7e70ff14c5a0dd7bc23b4fa071ef9e86a6990a2e4b81cbdaa90e48ac8399b4f9baf6caff8399b4f9baf6caff8399b4f9baf6caff8399b4f9baf6caff8399b4f9baf6caff8399b4f9baf6caffc520bd00e90221af7d6bba5854d99ba8b2c3ace59a1f734c018406e5dc4ab072018406e5dc4ab072018406e5dc4ab072018406e5dc4ab072018406e5dc4ab072018406e5dc4ab072068cbc4f3e839b46e2d34623160b5c1af2cd492d5086ac4162b3685297484507cd6fbe1ea3e39c0f344ddf4b177978dae8581daefc3e15d06ea40bc0535b05df9ebb058ddebe8cddfe6e703e00f380cbb318502d25f49e8c611f4e2985b20b2d3edb6003c60c75bd3a594665c4d2747180102467a649a68b6aab258dc1dd7c8eaf2f99747df4ef1ab2caf7403326d29909f5f9c05241a306c3e6d0db443f8a93eb05b46b86d31804036f6ef67e6d3dfedab3d6d5589a08c31819184d320d5d40bae8dd009c4fb83c83178da57fb1ffc200b3bf07c4530e9f6bc33fbe9fc6992ece2b094348d844ccaebc7280d11b516094c3941bdce3edb9d800000000000000007dcbb7673abb9aefcb6d51e5a50811aec15abc962907c363757b3058cfb8ef842395dd44ba379dcd9c469d0f5f8ccb557310c048d0b314fa00000000000000000648644ef438402246aadfaf672ce92f11222c70841edfb617fb0258e3ecb966be54a88a23f79c1b99ca0bcef82dcbbdb4fbc5e31b3cf6eb07874657ec0014de47e747fd824eb71a315fa7bac8f9f3262173d77f9fc51e8a2b9c3827934ae1f9f06ab962c6fac062942dfa528937b2f278177ab1c10ff72f68bfbed89f4e652896898e6223de4026c53ba639a8a46fd8b2717290739d4eb0fd2ad4a81ffc529925c2963106e19752dd86762a7ba005db2529cc0c5ea90048d8e6e5fa4dab98257ceccec651fb3b701234859a4c01cb15f90b8ccfbdeed3e4afb2c3dc7f87592d72a4bd6b8b01c08d0e35b5b06dd92bac108ba69cf80b22625c8f279b37159a1bf0d609dae700ed92772563b72bb9fc214beb62c126df88017bbe011b95574bb4ba1f7f3b664b6913c6a5bc1021843b9f48d4109c669f0e1813f03c4e2d0d5ed233ac6efdffdb9788a7a431bdd106c7ff7245f44f4d8538b4a81cbeceeaadc03546037d27b7dd50e337a2189af4be215d9c9b02b33fbf4cc8bb56c9f77398ed809143362194dce693539e27643a476893988379e05367cdceb65024c653305d7a20db206e174631e0924223074ae20d69b58a89585639bd1ba12306616238e0d21ac4c7c88af9c908d71051f1c0f505dfefe66d82482934fbc6bb632bea43af5a70395fc0142069fb9c1b753a81665f10c9111141ec64cfadd2061baf3cda57646274d5e80e47ef1cf529f3c31101a8691824e51ce7a14b73f2b768e284f8bace82c896d1f7ba1f94c919f343c7e51a813c9e17ebb753882392774143508f04499503280627d1acf78ba80f0b1f2bb879d3a559a6488789b1407ab275451686b8b7a25c0efc543d0701de2f056b58df49d120710614de1caefaf7c073369797aa32105e077f16731f236357f3b3821ffad32c925c181170eed1b12114bfa5728c529dfa0f323267afccd6ceb3dc66d9cc740f10cadaae6ca82717962c1f0f3bb9b9ee1dc93c9ae6ec7410655897487440eead3c445bc95cce291acb096ef25ff0d8d2db2b1ae6442eb0da5560d2ab80eac1938fffac2d4ac9e39575f2a560c2a0d930be3f6be855432e77c44d17fc44d25de17d48ab39c5201d0231700830dc8b317bee31a2d00b97a8838fcaf384a07deb4dc6974c6535a60885738a285090828052e0ed1c489c63cae049ad269743409676892aa010dfd4b483392de8a7f73c730606a3c84887bcb0e068ac9a66df0081efcbf01174779cdd229a592fd74fc012ca46b97cc600b80f68ddae5b6573176afe29b932cabcf01ea5fc4a17011f46c7cabe73a02754c2f00ec3eb46b06b5f7e227f47f58d8e571c0e4469b747dd3db9d18b8fd738508d862240e363568db677131d0a1ea60f253db138bc2c18d67d8cf859043bdc06b026dc821c1f2632f28d9ab88ec24b711528d708f8ec1c5ddee22379d0e4b3548b495c7d1ad7146368450611af4eba32c1731a566453c21cef004f5e0177d135b60b1a5f7ff3d8d113fb1baa0f03b9fc96aa3e39ffe6aedd946aad7116b2abef1ce0004280eeae6025abbbfade661042a05ceb84069a7e3914b6f68099dd1bd8b88675763f3c9c7ed0725ea4742bf747f992eb1d728a92dd4491e64ea1f35ba5cf848922b22e5101f4c6b6dfd7914e435598d9f9b6bf7e9447897c4ee715881e72b71ffbb343ee6ec8f41bb8873630df88b2bd47b90cd735c42b5be8e8ec95d299fa821c83a14ca98f5a9423d6715b62b67fda894a80eecc889d44c293b01561d654e89de702601430f057bf620c2bc7bdc0488814c53f85d5640ff1f2b3e41d5fdef97217937e345e93eb1013da057c8803f380bd2c35f481051d428ed91cb5a5728c2aca83c88832e2e4aaea6b2f6d1d28eac13e350970b9ea68027f93dce93bbcb00ba20ae5024e9846fc26931f1f397459a55dd6e3a8b98f02ed00b07af3fc9d53f38c7e948b10e95d9e9bc70d8d4cb3c52df5b431e2c60dbebbacf18f3af98abd445293ac671282fab65b6c51f8c6180718c54e9d9789cc108c5946e016335e7c15c6e5433ad02c1defaf96788c457d5684864b9c17307fdd68fb0f6b5ffa25f22b783af8a42aaf6f8cb31fd18ff29d0aa0b83811c3279fe435ef1aadcc74502176b2ee2e1013fc2f15bf66569d01f232c6e74c9b9da1a8271222f2e6734ae67409076caf4f00f776e0efe99c199c39846f3a7cbfa681dbb09e0a6ae822f8325a31f2d91b5e7425274788bcc3ef6b995e824b8868d36c22fb62e565fecec15cf5fff130c4cd26cc266d776af203ab1fd4de644cec5058cd4990fc7d18ecf25b306f089c9dacb21b9878e68d095287f7caaf2d0f0cf61d045f9e60628a84b0f694f77f0459129369d043eb2fe49ea16e6776fa38e79a0500a4b05234018e68388fea66efeb45719e0c356d8f1ca12b7377d4bc5ee5378a54ec444721bd282633f711749d5d0cd47a13547f525053c5bdff10dae1a6728907a612d15e75a2a57518396f115dc22c6b694d8ff5f6a0cd25e8e9cec24ead0c2c5d8eaa326f335fe8cdb3cdcd407c07b354f6154f1cc032bda9ea6ea14ac044172956dd8e5d301d36bab887f836fe2783257c338f750ed9209f5c725175bf7e0b30fdff11a4dea911a09f2507ebfc79083eb21769a0aa525048a2e2ab871d57c09e3935c82cba689ab6e688ae441bcaef7fe312e564a348a6f7a631bdc67a8facf2aeb872ad2143c37ade363cd3ad9b4434aea983a67e109b669f9ab157521152fccd3d1136d6d808df216667e0324af3ec38c94015531e587d21c36749a944d9dfd73308f14ab02a10f10dac7b8e15cc499a53391c0ef5aa29a80bc9df525f3e1db0cf64dbaef64ed4423aa22d7875e4b0a10ff7e7ed8d48856fa9fd82200a7d27a31859d686e84aa90ea8d111aa6799e1bbe58de0a7008c691c4a116d40389e8d58b9f8005d4d7296c167c499ac0de7a46ee27c5e3ff42e74e668c66e5eb3228684d993be36f054a48fa8b6601d9745331994e8b34eae3276065be7ff04db67590125d7a252079fed8433329bb012035ccb01967bf8765b267c2c973f6c66304e9b2e0312e4286c51baaac5de13ec8b454bfb71dd18a7a0e5220d570a97b16e3762c564c047d3fae6be041dbb2efcd1f2ba73a213f7612f69823af584b2cb6290d504229c3b555c2bc42148dd909bd96b121f75dc58ab9424e28287f3659046c14ed2ecd6609ddc717852132a70aaf24e273c0c9a28d103e227214a2412bcb8eff92ed9cf775e5756b408e7c501b4e293326ff4874474082bdda7afdd9b75f2699788fca3feacb156a7d9a23f425b31b3e66cb5d897363abdea66e5bd0df121bec3f115602b2419141ff115602b2419141ff115602b2419141ff115602b2419141ff115602b2419141ff115602b2419141f3e25ad50cc639566670a9d4bb98487b15ba0a6dfbcca6727f4a3c6a3657e8c2ef4a3c6a3657e8c2ef4a3c6a3657e8c2ef4a3c6a3657e8c2ef4a3c6a3657e8c2ef4a3c6a3657e8c2e06ea4c72eb70b08313e62040a9bec9ab974a31c0b4c0d80654afab525d9faaab9e14e1abdb2adb1a904928dae76ade542d37f1a03da3205a25eabd9c0cd3bf23719fdc2cbc94ab1c72a2ca4cd2f06a5b4df3b44fdd59b896c30ec9ac0f495ee7fa57db3f9c3a69f9a81c70fdfd85789c125b7403f5fb83ef2733629780dfdbbcd52f99747df4ef1ab2caf7403326d29909f5f9c05241a306c3e6d0db443f8a93eb05b46b86d31804036f6ef67e6d3dfedab3d6d5589a08c31819184d320d5d40ba34b67493c2775e7400bb24d6da5b15372815252c4812cd9c9ded3c79bdf63dd626fb60b4cd92ab3a773b10c76f8ae3de6593e1e1c9da72c40000000000000000f840d06bb75559bdc6660a3f5e5d1317303fd9cfda986d622032e253cac27a5046d2c88b75e8634eccb57ad0ced9e98ac6edfc7d8a184859000000000000000006dc4767fbadb3bf3da48afa2712194512afb9cbf7c74b1402fea6740f62fc0073edb6a8bfc2d4c8a209c89488b555177498c8881c5f2c82557f52e38eb24873d3eacdb579d368c4a649185c4a4751ef69a41166a4a446cb7c371c4687fcc35a2021d533efb027e7c34af3a7419bdb2442c7b8dba41cfb4e227b8e0623a6d16b31898e6223de4026c53ba639a8a46fd8b2717290739d4eb0fd2ad4a81ffc529925c2963106e19752dd86762a7ba005db2529cc0c5ea90048d8e6e5fa4dab98257ccd1c3ac7a4d449190723f08db342ce9bec407d1b1da33c66025d84c00aadc5fc0ce7686fa1c2016f19bb3823e9fb3847354c852698a4c80a879eb440860f36bc2af561bbeb3503c56a3404e841faf6badb47282d5bd9b17ed021db483b7557c189401304e28b1f65afd2641ce1e6fd7170e6f1c989ad69a6fe6e0d6bcaba1f2297c411c2b7262f03578fd8a5ee969a4e1e2c02e792e977bc24d22e5d22dd38dbcd1ed667ae827f6f5749452d512e62d987e91a0ab186b3ecd66a784cb284995e09a878689dc27c3a1c8a2c83a2a6b108a8c90a1137d62ada83e2cd4093906dd3a6ec326d8cbf04830e69b5bece8f001dbe1ba176b88b08c6e1d21add18cc82c7ca08d73c6e2e9e0506dfeff0a82f4f0935fbc61a5d82f0d5b05a7045ec79a5bb6afb9cc736444edf6010c9e52ab5b598d0add2ef1e11419a586462fa8cec6025f01cf58c3afafbb7a96918d763d2d5fe4c73f2dcce6f265dbbce8249dc4ef8062094c938499a3e441b813ce7f13afc19892392625ff1b5ab05499589b9731a70adf78bfd2a024d2eb979d358377b0dd48ab1405b1ca8767887b8b786c6d102e93e07015e65ea02abe049d122f0b0c3a61daefae0714bf12098aa3209a7e19668741f23f02dd95de920fad39d8812787c71eed1d4fcb415ee738c524f6b5fee5968afcc639d4001cddacc742a577fae986da827f1828dfd503cb9b9e92d1a27f0e7ec7412fb494e657540eef025677ab65dce295366ed7a3660f0d8e6d405b90c452eb02edfa5351981eac12629b56b534bc9e36d25376fd1c3a0d96bc76c809e56432ed0eb7c661745d25d89f6df8a03c6201d1fbc9637c6dd8b31fef103c6cf0c97a8850537b52ea17deb7dcfb7207a36a608b303c2aa9a9182807bfbf256af9d63caae82ae18b74440962072124368e0d4b48a9600abeef83c737dce95a457069f62709f352620fe4b73525124c83a6be62aa9dd3953e13a7939d81267c8c0e2ba172a73428b51d5754e98f0a37ab9eb787523155f12d867f9846cc4bcda0c510868fd4fadca94f2956a194cac2f9c643cf4c24f0fbf73d85216a832a713a5428b8fd738508d862240e363568db677131d0a1ea60f253db138bc2c18d67d8cf859043bdc06b026dc821c1f2632f28d9ab88ec24b711528d708f8ec1c5ddee22379d0e4b3548b495c7d1ad7146368450611af4eba32c1731a566453c21cef004f2cac2f38364968a8c1615f33c772408657522d73c0b69162d5ee987e224a0b19f55d40bce6ac81cc9f879d904dcd481a765fe5bdaeb7df04ad4e0f1ddc890d9cc399de46ac346a6840dfb69fc692625bf337bd1ab89d11a21d308af1dc58cd03e3f85a6aa5d1c64b7f9b08f5e2b8b0bcef5a6130035ad5387e30d068020f769913678cf21bb05227cc5108d697ac677fb08abe0a6c72f56d27dfced3785308f4e74b9076dd0891a7bcf46e7c9b34df635c6f30bb819e90fff298414890bae1de2b9071fa1fa0d99125db29ff7fc5192f92c52b677518dd216a2e7772a85c5799d068bb41d9472d203c315b1bf1934609a418a1d4c47d2e5f8cebeba01644e5c98372b5db48a5c79f121d8ff485fd53d9e6195e7cfe61773791e11bf06ca4ffff0140bdc656c2c6f7a13ef9d0bda3ff58cd3c0eb283aace595e2797417a31d6182e37f7aa66c2dd2de10b01da14985d4a2b9028fea7a2f23114eb5cf3f169604180ed25ada0256e733b16892995ae9151439b36767eb69581b0fb9f00d3a29b35c7017a32be9c43d61512ce7a934021208ca4e9056d411afe4c2b340a8572ddc6ea462ffa0b079fcc457bd304c42db2cf4fc00b7f633ebf2b8c8daf03f3b10bd1e8f117ee99387f75a4a75e207bfd6d5a9af4a8655f6dd10d02eea95e95c3c36a28905ba26752e0776289a7b627f3b43681bb828e9a06e1e7773dbd0041e5f41f377429fb667a127030a2dc2127e2bfee05164e437587186e4975510503f05a7cf1520c6044e8fbfef7450a9d9552a14ab1ac2ca096a3bce9eb2c856579877458c73c02554a57e3acc664ff99522ceabda6c30bb704aebec273ef86cbcad49aada0c59369c86742ba723779c3051e5a49abdc5467369e2836cefd21cb9ee58255ba86f81c574b993addfce771670c0d4668a297c3567801a906e523d4c02916d2ce6dbba65039f1bbf86bcbfec24b36bfde21b608e133efca822c54edf745982835ef31bb33191f35d07f5d7ec4950363a9539c7c4cc6f3863f9ab430f6a9f7d4719844d87acd264c5e692adeef0a9c63b1fe5b7f8a10dc515592e90a53aa218f4d6cf57cdad7c36c5054acd7eb1bc9c64f333a4b418587d53969d9a291042dfadf0a123f9154cb275ec0e175b6a5dbb46608005ee9f7573579b50fb1ff79e72b959d0ed832aecde08e495937c9f3f9b40bf3e818b317e7a191f53e1fe571cb2f3d9f8f53bd933f25080b2f3e0e2304c76b8780e85d5c02b0aea41e69c6caf141f098a1526e302fef930d61dc0c4ebd7a2e93b853371b7938640b27171b6ae91b2020d9538600b58f14d90e0060d4107aecdaa26900ab620ba578d16b3829e7f46f3a9fa15eea0968f151be559d41d95200ea867fd93e7904b16e69eb734a4622c6085d523dafd6a6efea4931add2b114390ea9fa4b508f899e36e8b12d098f654fb449d1664b379664968ac8482e2c0c03677b85ab0e0922f53f06303cd8974ad59e56b1b1f0e83001ad39f33b9ce6c2b5367fdbd9e8797c06588b159ca2a3fc227e223b8b3b2a3350d18581e7959aa12fffbfd6a2e87e0cd5ad1bc52dd6cf5e3167bcf3cabc29143ebf2a4f1b620e21dd74cb677e2289bfca4638aa2fa5f017b7e03b7cd2b0428f99210e2d811a08ff79fe37ab013182d2e73af584b2cb6290d504229c3b555c2bc42148dd909bd96b121f75dc58ab9424e28287f3659046c14ed2ecd6609ddc717852132a70aaf24e273c0c9a28d103e227214a2412bcb8eff92ed9cf775e5756b408e7c501b4e293326ff4874474082bdd066443c7becdad96accb1e9e1acb673a8fb3def18382125214bdc642f008835bdf6c7ca1e54f78227b1c6764759425d57b1c6764759425d57b1c6764759425d57b1c6764759425d57b1c6764759425d57b1c6764759425d5bc54fb9f38dd03d37af6ee1ad414368ce009173a7bf09a2ed7ab59faedfcb42ad7ab59faedfcb42ad7ab59faedfcb42ad7ab59faedfcb42ad7ab59faedfcb42ad7ab59faedfcb42a0623a72306a2f008bfafa710fbb486ba2c524f61945befb709f2ce402b0b8acefe9384ad35a2d9b8364e85ceb1776ead92bf1b063345f4dbef4e033cd288830ba28f2b9a09e62e658f1543c289eb7368ffe321a2bd4d79238bea1abab4e58a75de57db3f9c3a69f9a81c70fdfd85789c125b7403f5fb83ef2733629780dfdbbcd52f99747df4ef1ab2caf7403326d29909f5f9c05241a306c3e6d0db443f8a93eb05b46b86d31804036f6ef67e6d3dfedab3d6d5589a08c31819184d320d5d40bac1c4209213f48e1c597d3abb2ae074eb20bea627e24bc002f704c09eaca7eadeeb90900e55c9f8675bd690a5b5865efd3c0f6588e8191534000000000000000007b09deb38859e399af75fceda0655429a97863fe9c05ed7515cfc3abd022bb9bdf8df8c64407d902af398c760c41c0d6c1f12dd663f546d000000000000000006ee5b80819f4d688db777351904f9bdd361d67679f6538821b6b6e0de3f4d94900fc414c406c39b8ed9ddf10c80b87645987041c3b704ec1da2ad23f08750ccad32f9c8b3f380247db7496cb38008364d9dfff1151a164a0a5cab2eb34fcf130d21d533efb027e7c34af3a7419bdb2442c7b8dba41cfb4e227b8e0623a6d16b31898e6223de4026c53ba639a8a46fd8b2717290739d4eb0fd2ad4a81ffc529925c2963106e19752dd86762a7ba005db2529cc0c5ea90048d8e6e5fa4dab98257ce09ff2a1ce85503eda205675082f92ecd3b742bf3bce3392dcff3de520da286ca22d5f9f4ece963d7ee616a999fae689fefccb59b16c904c2363be06fd3769111d9d04a9ab1e61f5091ea821a9a8747acfe22afa49d2caf2ef125922df15522d9f6d5bbd505cee09790b095020d066f800000000000000000000000000000000e9a000000000000013000000000000005fc87fac946bb80d05c257b2f58cf38645a0c77edcb6c562c4d75135448634de4b4f3aff8d95a86c40060915231ef82b459512caf6836d9b2d670d44c52647e000000000000000000100000000000000020000000000000003000000000000000400000000000000050000000000000006000000000000000700000000000000080000000000000009000000000000002d00000000000000 \ No newline at end of file diff --git a/pallets/gear/src/tests.rs b/pallets/gear/src/tests.rs index 40303b83f85..75a28581973 100644 --- a/pallets/gear/src/tests.rs +++ b/pallets/gear/src/tests.rs @@ -15832,6 +15832,145 @@ fn use_big_memory() { }); } +#[test] +fn plonky2_proof_verification_works() { + use demo_plonky2_verifier::WASM_BINARY; + + macro_rules! include_hex { + ($path:expr) => {{ + const HEX_STR: &str = include_str!($path); + hex::decode(HEX_STR).expect("Failed to decode hex string") + }}; + } + + init_logger(); + new_test_ext().execute_with(|| { + let pid = Gear::upload_program( + RuntimeOrigin::signed(USER_1), + WASM_BINARY.to_vec(), + b"salt".to_vec(), + vec![], + 10_000_000_000, + 0, + false, + ) + .map(|_| get_last_program_id()) + .unwrap(); + + run_to_block(2, None); + + assert_ne!(pid, ProgramId::default()); + + // Check a recursive proof + let payload = include_hex!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/", + "src/test_data/recursive.hex", + )); + + let gas_info = |payload: Vec| { + Gear::calculate_gas_info( + USER_1.into_origin(), + HandleKind::Handle(pid), + payload, + 0, + true, + true, + ) + .expect("calculate_gas_info failed") + }; + let gas_burned = gas_info(payload.clone()).burned; + println!("Gas burned by recursive proof verification: {}", gas_burned); + + assert_ok!(Gear::send_message( + RuntimeOrigin::signed(USER_1), + pid, + payload, + 250_000_000_000, + 0, + false, + )); + + System::reset_events(); + run_to_block(3, None); + + // Ensure the response is correct + System::events().iter().for_each(|r| { + if let MockRuntimeEvent::Gear(Event::UserMessageSent { + message, + expiration: None, + }) = &r.event + { + assert_eq!(message.payload_bytes(), b"Success"); + } + }); + + // Check the factorial proof + let payload = include_hex!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/", + "src/test_data/factorial.hex", + )); + + let gas_info = |payload: Vec| { + Gear::calculate_gas_info( + USER_1.into_origin(), + HandleKind::Handle(pid), + payload, + 0, + true, + true, + ) + .expect("calculate_gas_info failed") + }; + let gas_burned = gas_info(payload.clone()).burned; + println!("Gas burned by factorial proof verification: {}", gas_burned); + + assert_ok!(Gear::send_message( + RuntimeOrigin::signed(USER_1), + pid, + payload, + 250_000_000_000, + 0, + false, + )); + + run_to_block(4, None); + + // Check the fibonacci proof verification + let payload = include_hex!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/", + "src/test_data/fibonacci.hex", + )); + + let gas_info = |payload: Vec| { + Gear::calculate_gas_info( + USER_1.into_origin(), + HandleKind::Handle(pid), + payload, + 0, + true, + true, + ) + .expect("calculate_gas_info failed") + }; + let gas_burned = gas_info(payload.clone()).burned; + println!("Gas burned by fibonacci proof verification: {}", gas_burned); + + assert_ok!(Gear::send_message( + RuntimeOrigin::signed(USER_1), + pid, + payload, + 250_000_000_000, + 0, + false, + )); + + run_to_block(5, None); + }); +} + pub(crate) mod utils { #![allow(unused)] diff --git a/pallets/gear/src/weights.rs b/pallets/gear/src/weights.rs index b7b8fe10972..ece0b94f15d 100644 --- a/pallets/gear/src/weights.rs +++ b/pallets/gear/src/weights.rs @@ -126,6 +126,7 @@ pub trait WeightInfo { fn gr_create_program_per_kb(p: u32, s: u32, ) -> Weight; fn gr_create_program_wgas(r: u32, ) -> Weight; fn gr_create_program_wgas_per_kb(p: u32, s: u32, ) -> Weight; + fn gr_permute(n: u32, ) -> Weight; fn lazy_pages_signal_read(p: u32, ) -> Weight; fn lazy_pages_signal_write(p: u32, ) -> Weight; fn lazy_pages_signal_write_after_read(p: u32, ) -> Weight; @@ -1145,6 +1146,17 @@ impl WeightInfo for SubstrateWeight { // Standard Error: 480_883 .saturating_add(Weight::from_parts(130_379_476, 0).saturating_mul(s.into())) } + /// The range of component `r` is `[0, 20]`. + fn gr_permute(r: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 308_014_000 picoseconds. + Weight::from_parts(372_668_372, 0) + .saturating_add(Weight::from_parts(0, 0)) + // Standard Error: 44_140 + .saturating_add(Weight::from_parts(392_124_575, 0).saturating_mul(r.into())) + } /// The range of component `p` is `[0, 512]`. fn lazy_pages_signal_read(p: u32, ) -> Weight { // Proof Size summary in bytes: @@ -3086,6 +3098,17 @@ impl WeightInfo for () { // Standard Error: 480_883 .saturating_add(Weight::from_parts(130_379_476, 0).saturating_mul(s.into())) } + /// The range of component `r` is `[0, 20]`. + fn gr_permute(r: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 308_014_000 picoseconds. + Weight::from_parts(372_668_372, 0) + .saturating_add(Weight::from_parts(0, 0)) + // Standard Error: 44_140 + .saturating_add(Weight::from_parts(392_124_575, 0).saturating_mul(r.into())) + } /// The range of component `p` is `[0, 512]`. fn lazy_pages_signal_read(p: u32, ) -> Weight { // Proof Size summary in bytes: diff --git a/runtime-interface/Cargo.toml b/runtime-interface/Cargo.toml index b46629f0353..e491d624995 100644 --- a/runtime-interface/Cargo.toml +++ b/runtime-interface/Cargo.toml @@ -8,6 +8,7 @@ edition.workspace = true license.workspace = true homepage.workspace = true repository.workspace = true +rust-version.workspace = true [dependencies] gear-core.workspace = true @@ -16,7 +17,7 @@ gear-lazy-pages = { workspace = true, optional = true } gear-sandbox-interface.workspace = true sp-io.workspace = true -sp-runtime-interface = { workspace = true, default-features = false } +sp-runtime-interface.workspace = true sp-std.workspace = true byteorder.workspace = true @@ -28,6 +29,7 @@ ark-ec = { workspace = true, optional = true } ark-ff = { workspace = true, optional = true } ark-scale = { workspace = true, optional = true } sha2 = { workspace = true, optional = true } +gprimitives-client = { workspace = true, optional = true } [target.'cfg(windows)'.dependencies] winapi = { workspace = true, features = ["memoryapi"] } @@ -45,14 +47,16 @@ std = [ "byteorder/std", "codec/std", - "log", + "log/std", "ark-bls12-381/std", "ark-ec/std", "ark-ff/std", "ark-scale/std", - "sha2/std" + "sha2/std", + + "gprimitives-client/std", ] runtime-benchmarks = [ - "gear-sandbox-interface/runtime-benchmarks" + "gear-sandbox-interface/runtime-benchmarks", ] diff --git a/runtime-interface/src/lib.rs b/runtime-interface/src/lib.rs index 4eb02cc090c..bc4a81130ab 100644 --- a/runtime-interface/src/lib.rs +++ b/runtime-interface/src/lib.rs @@ -46,6 +46,13 @@ use { ark_ff::fields::field_hashers::DefaultFieldHasher, ark_scale::ArkScale, gear_lazy_pages::LazyPagesStorage, + gprimitives_client::{ + field::{ + goldilocks_field::GoldilocksField, + types::{Field, PrimeField64}, + }, + hash::poseidon::Poseidon, + }, }; pub use gear_sandbox_interface::sandbox; @@ -412,3 +419,19 @@ pub trait GearBls12_381 { Ok(ArkScale::::from(point).encode()) } } + +#[runtime_interface] +pub trait PoseidonHash { + fn poseidon(input: Vec) -> Vec { + let data: [GoldilocksField; 12] = input + .into_iter() + .map(GoldilocksField::from_canonical_u64) + .collect::>() + .try_into() + .expect("Expect input to be of length 12"); + + let hash = ::poseidon(data); + + hash.iter().map(|x| x.to_canonical_u64()).collect() + } +} diff --git a/runtime/vara/Cargo.toml b/runtime/vara/Cargo.toml index 1b38269eb0b..66d74708dd6 100644 --- a/runtime/vara/Cargo.toml +++ b/runtime/vara/Cargo.toml @@ -163,7 +163,9 @@ std = [ "pallet-gear-eth-bridge?/std", "pallet-gear-staking-rewards/std", "pallet-gear-rpc-runtime-api/std", + "pallet-gear-scheduler/std", "pallet-gear-staking-rewards-rpc-runtime-api/std", + "pallet-gear-voucher/std", "pallet-gear-builtin-rpc-runtime-api/std", "pallet-gear-eth-bridge-rpc-runtime-api/std", "pallet-grandpa/std", diff --git a/runtime/vara/src/tests/mod.rs b/runtime/vara/src/tests/mod.rs index ddf923520ad..aaea752806d 100644 --- a/runtime/vara/src/tests/mod.rs +++ b/runtime/vara/src/tests/mod.rs @@ -309,6 +309,7 @@ fn syscall_weights_test() { gr_create_program_wgas: 4_100_000.into(), gr_create_program_wgas_payload_per_byte: 130.into(), gr_create_program_wgas_salt_per_byte: 1_500.into(), + gr_permute: 4_900_000.into(), _phantom: Default::default(), }; diff --git a/runtime/vara/src/tests/utils.rs b/runtime/vara/src/tests/utils.rs index f62375b064f..ab9d7857620 100644 --- a/runtime/vara/src/tests/utils.rs +++ b/runtime/vara/src/tests/utils.rs @@ -244,11 +244,12 @@ pub(super) fn expected_syscall_weights_count() -> usize { gr_create_program_wgas: _, gr_create_program_wgas_payload_per_byte: _, gr_create_program_wgas_salt_per_byte: _, + gr_permute: _, _phantom: __phantom, } = SyscallWeights::::default(); // total number of syscalls - 70 + 71 } pub(super) fn expected_pages_costs_count() -> usize { @@ -483,6 +484,7 @@ pub(super) fn check_syscall_weights( expectation!(gr_create_program_wgas), expectation!(gr_create_program_wgas_payload_per_byte), expectation!(gr_create_program_wgas_salt_per_byte), + expectation!(gr_permute), ]; check_expectations(&expectations) diff --git a/runtime/vara/src/weights/pallet_gear.rs b/runtime/vara/src/weights/pallet_gear.rs index 43150ed2677..97f18984c9d 100644 --- a/runtime/vara/src/weights/pallet_gear.rs +++ b/runtime/vara/src/weights/pallet_gear.rs @@ -1145,6 +1145,17 @@ impl pallet_gear::WeightInfo for SubstrateWeight { // Standard Error: 480_883 .saturating_add(Weight::from_parts(130_379_476, 0).saturating_mul(s.into())) } + /// The range of component `r` is `[0, 20]`. + fn gr_permute(r: u32, ) -> Weight { + // Proof Size summary in bytes: + // Measured: `0` + // Estimated: `0` + // Minimum execution time: 308_014_000 picoseconds. + Weight::from_parts(372_668_372, 0) + .saturating_add(Weight::from_parts(0, 0)) + // Standard Error: 44_140 + .saturating_add(Weight::from_parts(392_124_575, 0).saturating_mul(r.into())) + } /// The range of component `p` is `[0, 512]`. fn lazy_pages_signal_read(p: u32, ) -> Weight { // Proof Size summary in bytes: diff --git a/sandbox/host/Cargo.toml b/sandbox/host/Cargo.toml index ac7a4949df5..7afcbc12f3e 100644 --- a/sandbox/host/Cargo.toml +++ b/sandbox/host/Cargo.toml @@ -18,7 +18,7 @@ targets = ["x86_64-unknown-linux-gnu"] atomic_enum.workspace = true codec = { workspace = true, features = ["std"] } defer.workspace = true -environmental.workspace = true +environmental = { workspace = true, features = ["std"] } thiserror.workspace = true log = { workspace = true, features = ["std"] } wasmer.workspace = true diff --git a/utils/crates-io/src/handler.rs b/utils/crates-io/src/handler.rs index 446a464a016..c680ccc7ae1 100644 --- a/utils/crates-io/src/handler.rs +++ b/utils/crates-io/src/handler.rs @@ -47,6 +47,7 @@ pub fn patch(pkg: &Package, is_published: bool) -> Result { "gear-sandbox" => sandbox::patch(doc), "gear-sandbox-host" => sandbox_host::patch(doc), "gear-sandbox-interface" => sandbox_interface::patch(doc), + "gear-runtime-interface" => runtime_interface::patch(doc), "gmeta" => gmeta::patch(doc), "gmeta-codegen" => gmeta_codegen::patch(doc), _ => {} @@ -176,6 +177,26 @@ mod sandbox_interface { } } +/// runtime interface handler +mod runtime_interface { + use super::GP_RUNTIME_INTERFACE_VERSION; + use toml_edit::DocumentMut; + + /// Patch the manifest of runtime-interface. + /// + /// We need to patch the manifest of package again because + /// `sp_runtime_interface_proc_macro` includes some hardcode + /// that could not locate alias packages. + pub fn patch(manifest: &mut DocumentMut) { + let Some(wi) = manifest["dependencies"]["sp-runtime-interface"].as_table_like_mut() else { + return; + }; + wi.insert("version", toml_edit::value(GP_RUNTIME_INTERFACE_VERSION)); + wi.insert("package", toml_edit::value("gp-runtime-interface")); + wi.remove("workspace"); + } +} + /// sandbox_host handler. mod sandbox_host { use toml_edit::DocumentMut; diff --git a/utils/crates-io/src/lib.rs b/utils/crates-io/src/lib.rs index 5d7feba633b..6e68437f9ed 100644 --- a/utils/crates-io/src/lib.rs +++ b/utils/crates-io/src/lib.rs @@ -82,6 +82,8 @@ pub const STACKED_DEPENDENCIES: &[&str] = &[ "gear-lazy-pages-common", "gear-lazy-pages", "gear-sandbox-interface", + "gprimitives-client", + "gear-runtime-interface", "gear-sandbox", "gear-core-backend", "gear-core-processor", diff --git a/utils/gear-replay-cli/src/cmd/gear_run.rs b/utils/gear-replay-cli/src/cmd/gear_run.rs index 0858d4465d6..93d3c0e4510 100644 --- a/utils/gear-replay-cli/src/cmd/gear_run.rs +++ b/utils/gear-replay-cli/src/cmd/gear_run.rs @@ -119,6 +119,7 @@ where gear_runtime_interface::gear_ri::HostFunctions, gear_runtime_interface::sandbox::HostFunctions, sp_crypto_ec_utils::bls12_381::host_calls::HostFunctions, + gear_runtime_interface::poseidon_hash::HostFunctions, ), >, >(&shared); diff --git a/utils/gear-replay-cli/src/cmd/replay_block.rs b/utils/gear-replay-cli/src/cmd/replay_block.rs index 02749c27a76..8ebdb77102e 100644 --- a/utils/gear-replay-cli/src/cmd/replay_block.rs +++ b/utils/gear-replay-cli/src/cmd/replay_block.rs @@ -145,6 +145,7 @@ where gear_runtime_interface::gear_ri::HostFunctions, gear_runtime_interface::sandbox::HostFunctions, sp_crypto_ec_utils::bls12_381::host_calls::HostFunctions, + gear_runtime_interface::poseidon_hash::HostFunctions, ), >, >(&shared); diff --git a/utils/regression-analysis/src/main.rs b/utils/regression-analysis/src/main.rs index 772c4953ee5..7020dc44eb9 100644 --- a/utils/regression-analysis/src/main.rs +++ b/utils/regression-analysis/src/main.rs @@ -376,6 +376,7 @@ fn weights(kind: WeightsKind, input_file: PathBuf, output_file: PathBuf) { gr_create_program, gr_create_program_payload_per_byte, gr_create_program_salt_per_byte, + gr_permute, } } } diff --git a/utils/wasm-instrument/src/syscalls.rs b/utils/wasm-instrument/src/syscalls.rs index 3a4ef174371..2527a119a25 100644 --- a/utils/wasm-instrument/src/syscalls.rs +++ b/utils/wasm-instrument/src/syscalls.rs @@ -105,6 +105,7 @@ pub enum SyscallName { ReserveGas, UnreserveGas, SystemReserveGas, + Permute, } impl SyscallName { @@ -167,6 +168,7 @@ impl SyscallName { Self::WaitFor => "gr_wait_for", Self::WaitUpTo => "gr_wait_up_to", Self::Wake => "gr_wake", + Self::Permute => "gr_permute", } } @@ -473,6 +475,10 @@ impl SyscallName { Ptr::MutBlockNumberWithHash(HashType::SubjectId).into(), ]), Self::SystemBreak => unimplemented!("Unsupported syscall signature for system_break"), + Self::Permute => SyscallSignature::gr_infallible([ + Ptr::Hash(HashType::PoseidonInOut).into(), + Ptr::MutHash(HashType::PoseidonInOut).into(), + ]), } } @@ -537,6 +543,8 @@ pub enum HashType { ReservationId, /// This enum variant is used for the `gr_random` syscall. SubjectId, + /// Poseidon permute input and output used in the `gr_permute` syscall. + PoseidonInOut, } impl From for ValueType { diff --git a/utils/wasm-proc/src/main.rs b/utils/wasm-proc/src/main.rs index 9e47f7973d2..a33cebbb84d 100644 --- a/utils/wasm-proc/src/main.rs +++ b/utils/wasm-proc/src/main.rs @@ -24,7 +24,7 @@ use gear_wasm_builder::{ use parity_wasm::elements::External; use std::{collections::HashSet, fs, path::PathBuf}; -const RT_ALLOWED_IMPORTS: [&str; 76] = [ +const RT_ALLOWED_IMPORTS: [&str; 77] = [ // From `Allocator` (substrate/primitives/io/src/lib.rs) "ext_allocator_free_version_1", "ext_allocator_malloc_version_1", @@ -114,6 +114,8 @@ const RT_ALLOWED_IMPORTS: [&str; 76] = [ // From `GearBls12_381` "ext_gear_bls_12_381_aggregate_g1_version_1", "ext_gear_bls_12_381_map_to_g2affine_version_1", + // From `PoseidonHash` + "ext_poseidon_hash_poseidon_version_1", ]; #[derive(Debug, clap::Parser)]