diff --git a/packages/wasm-utxo/js/fixedScriptWallet/ZcashV6Transaction.ts b/packages/wasm-utxo/js/fixedScriptWallet/ZcashV6Transaction.ts index cc5a471f011..5d1e15d3bd8 100644 --- a/packages/wasm-utxo/js/fixedScriptWallet/ZcashV6Transaction.ts +++ b/packages/wasm-utxo/js/fixedScriptWallet/ZcashV6Transaction.ts @@ -71,6 +71,26 @@ export class ZcashV6Transaction { return this._wasm.ironwoodAnchor; } + /** + * The ZIP-244 per-input transparent sighash (32 bytes) for transparent input `index` — + * for a transaction inspected directly from its raw bytes rather than one built via a + * PSBT (e.g. independently verifying an already-broadcast transaction's signatures). + * + * `inputAmounts`/`inputScriptPubkeys` are the spent outputs' values (zatoshi) and + * scriptPubKeys for *every* transparent input of this transaction, in input order. + */ + transparentSighash( + index: number, + inputAmounts: bigint[], + inputScriptPubkeys: Uint8Array[], + ): Uint8Array { + return this._wasm.transparentSighash( + index, + BigInt64Array.from(inputAmounts), + inputScriptPubkeys, + ); + } + /** @internal */ get wasm(): WasmZcashV6Transaction { return this._wasm; diff --git a/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/zcash_psbt.rs b/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/zcash_psbt.rs index da862a67d61..f4ccb017882 100644 --- a/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/zcash_psbt.rs +++ b/packages/wasm-utxo/src/fixed_script_wallet/bitgo_psbt/zcash_psbt.rs @@ -1033,19 +1033,14 @@ impl ZcashBitGoPsbt { .inputs .get(index) .ok_or_else(|| format!("input {index} out of range"))?; - let script_code = input - .witness_script - .as_ref() - .or(input.redeem_script.as_ref()) - .ok_or_else(|| format!("input {index}: no redeem/witness script"))?; - crate::zcash::v6::compute_v6_transparent_sighash( - &tx, - index, - script_code.as_script(), - &amounts, - &scripts, - ) - .map_err(|e| e.to_string()) + // Not used in the digest itself (ZIP-244 §S.2g.iii commits the spent scriptPubKey, not + // the redeem/witness script — see `compute_v6_transparent_sighash`), but its presence + // confirms the input is actually spendable before we hand back a sighash to sign. + if input.witness_script.is_none() && input.redeem_script.is_none() { + return Err(format!("input {index}: no redeem/witness script")); + } + crate::zcash::v6::compute_v6_transparent_sighash(&tx, index, &amounts, &scripts) + .map_err(|e| e.to_string()) } /// Ingest a transparent-input signature returned by the client/HSM into `partial_sigs`, after @@ -2689,7 +2684,6 @@ mod ironwood_v6_tests { let codec_sighash = compute_v6_transparent_sighash( &tx, 0, - prevout_script.as_script(), &[prevout_value], std::slice::from_ref(&prevout_script), ) diff --git a/packages/wasm-utxo/src/wasm/zcash.rs b/packages/wasm-utxo/src/wasm/zcash.rs index 8d8156c9bcf..9ead9c39931 100644 --- a/packages/wasm-utxo/src/wasm/zcash.rs +++ b/packages/wasm-utxo/src/wasm/zcash.rs @@ -189,4 +189,34 @@ impl ZcashV6Transaction { .as_ref() .map(|b| b.anchor.to_vec()) } + + /// The ZIP-244 per-input transparent sighash (32 bytes) for transparent input `index` of + /// this transaction — the free-standing counterpart to + /// `ZcashIronwoodBitGoPsbt.transparentSighash`, for a transaction inspected directly from + /// its raw bytes rather than one built via this codebase's PSBT flow (e.g. independently + /// verifying an already-broadcast transaction's signatures). + /// + /// `input_amounts`/`input_script_pubkeys` are the spent outputs' values (zatoshi) and + /// scriptPubKeys for *every* transparent input of this transaction, in input order — the + /// same data `add-input`/`addWalletInput` would have carried in a PSBT's `witness_utxo`. + #[wasm_bindgen(js_name = transparentSighash)] + pub fn transparent_sighash( + &self, + index: usize, + input_amounts: Vec, + input_script_pubkeys: Vec, + ) -> Result, WasmUtxoError> { + let scripts: Vec = input_script_pubkeys + .iter() + .map(|u| miniscript::bitcoin::ScriptBuf::from(u.to_vec())) + .collect(); + crate::zcash::v6::compute_v6_transparent_sighash( + &self.inner, + index, + &input_amounts, + &scripts, + ) + .map(|h| h.to_vec()) + .map_err(|e| WasmUtxoError::new(&e.to_string())) + } } diff --git a/packages/wasm-utxo/src/zcash/v6.rs b/packages/wasm-utxo/src/zcash/v6.rs index c1538888473..78e6cca6b83 100644 --- a/packages/wasm-utxo/src/zcash/v6.rs +++ b/packages/wasm-utxo/src/zcash/v6.rs @@ -524,13 +524,18 @@ pub fn compute_v6_sig_digest( /// ZIP-244 v6 **transparent** per-input signature hash (SIGHASH_ALL) — the message the key /// controlling transparent input `input_index` signs. /// -/// `script_code` is the script being signed for that input (its prevout scriptPubKey for P2PKH, -/// or the redeem/witness script for P2SH/P2WSH). `input_amounts` / `input_script_pubkeys` are the -/// spent outputs' values and scriptPubKeys for every input, in input order. +/// Per ZIP-244 §S.2g.iii (as implemented by `zcash_primitives::transaction::sighash_v5:: +/// transparent_sig_digest`, shared by v6), the per-input field committed here is the spent +/// output's **scriptPubKey** — not the redeem/witness script ("scriptCode") used to actually +/// execute the input's scriptSig. Those two coincide for P2PKH (which is all the "golden" +/// production fixture exercises), but differ for P2SH/P2WSH, where using the redeem/witness +/// script here instead produces a sighash real consensus rules reject. `input_amounts` / +/// `input_script_pubkeys` are the spent outputs' values and scriptPubKeys for every input, in +/// input order; `input_script_pubkeys[input_index]` is also the value used for this input's own +/// per-input field. pub fn compute_v6_transparent_sighash( tx: &ZcashV6Transaction, input_index: usize, - script_code: &miniscript::bitcoin::Script, input_amounts: &[i64], input_script_pubkeys: &[miniscript::bitcoin::ScriptBuf], ) -> Result<[u8; 32], ZcashV6Error> { @@ -542,14 +547,17 @@ pub fn compute_v6_transparent_sighash( let amount = *input_amounts .get(input_index) .ok_or(ZcashV6Error::UnexpectedEof)?; + let script_pubkey = input_script_pubkeys + .get(input_index) + .ok_or(ZcashV6Error::UnexpectedEof)?; - // S.2g: prevout ‖ value(8, signed LE) ‖ scriptCode(length-prefixed) ‖ nSequence(4, LE). + // S.2g: prevout ‖ value(8, signed LE) ‖ scriptPubKey(length-prefixed) ‖ nSequence(4, LE). let mut txin_data = Vec::new(); txin.previous_output .consensus_encode(&mut txin_data) .expect("vec write is infallible"); txin_data.extend_from_slice(&amount.to_le_bytes()); - script_code + script_pubkey .consensus_encode(&mut txin_data) .expect("vec write is infallible"); txin.sequence @@ -1070,37 +1078,25 @@ mod tests { let tx = sample_tx_with_inputs(); let amounts = [12_345i64]; let scripts = [ScriptBuf::from(vec![0x76u8, 0xa9, 0x14])]; - let script_code = ScriptBuf::from(vec![0x76u8, 0xa9, 0x14, 0x88, 0xac]); let shielded = compute_v6_sig_digest(&tx, &amounts, &scripts); - let transparent = - compute_v6_transparent_sighash(&tx, 0, script_code.as_script(), &amounts, &scripts) - .unwrap(); + let transparent = compute_v6_transparent_sighash(&tx, 0, &amounts, &scripts).unwrap(); // The per-input transparent sighash populates the txin component, so it must differ from // the shielded (empty-txin) digest over the same tx. assert_ne!(shielded, transparent); // Deterministic. assert_eq!( transparent, - compute_v6_transparent_sighash(&tx, 0, script_code.as_script(), &amounts, &scripts) - .unwrap() + compute_v6_transparent_sighash(&tx, 0, &amounts, &scripts).unwrap() ); - // A different script_code changes the digest. - let other_code = ScriptBuf::from(vec![0x51u8]); + // A different spent scriptPubKey changes the digest. + let other_scripts = [ScriptBuf::from(vec![0x51u8])]; assert_ne!( transparent, - compute_v6_transparent_sighash(&tx, 0, other_code.as_script(), &amounts, &scripts) - .unwrap() + compute_v6_transparent_sighash(&tx, 0, &amounts, &other_scripts).unwrap() ); // Out-of-range input index is an error, not a panic. - assert!(compute_v6_transparent_sighash( - &tx, - 9, - script_code.as_script(), - &amounts, - &scripts - ) - .is_err()); + assert!(compute_v6_transparent_sighash(&tx, 9, &amounts, &scripts).is_err()); } /// Golden oracle for [`compute_v6_transparent_sighash`]: verify the **real** ECDSA signature @@ -1152,7 +1148,6 @@ mod tests { let sighash = compute_v6_transparent_sighash( &tx, 0, - prevout_script.as_script(), &[prevout_value], std::slice::from_ref(&prevout_script), ) @@ -1167,6 +1162,98 @@ mod tests { .expect("the tx's real signature verifies against compute_v6_transparent_sighash"); } + /// Regression test for the bug where `compute_v6_transparent_sighash` hashed the redeem + /// script (scriptCode) into the ZIP-244 §S.2g.iii per-input field instead of the spent + /// output's scriptPubKey. That distinction is invisible for a P2PKH input (its scriptCode + /// *is* its scriptPubKey — see [`golden_transparent_sighash_verifies_real_signature`] + /// above), but for a P2SH input they differ, and hashing the wrong one produces a sighash + /// real consensus rules reject. + /// + /// The fixture is a real transaction — spending a 2-of-3 P2SH multisig transparent input — + /// that was built with this codebase's CLI, submitted to a live Zcash testnet (NU6.3) + /// `zebrad` node via `sendrawtransaction`, and **accepted into its mempool**: real + /// consensus-rule validation of the transparent scriptSig, not merely self-consistency + /// against this codebase's own sighash. + #[test] + fn golden_multisig_transparent_sighash_verifies_real_signature() { + use miniscript::bitcoin::script::Instruction; + use miniscript::bitcoin::secp256k1::{ecdsa::Signature, Message, PublicKey, Secp256k1}; + + let raw = hex::decode(load_zcash_fixture("v6_shield_multisig_rawtx.hex").trim()).unwrap(); + let tx = decode_v6_transaction(&raw).unwrap(); + assert_eq!(tx.transparent.input.len(), 1); + assert_eq!(tx.transparent.output.len(), 0); + + // Spent output (a synthetic 2-of-3 P2SH multisig address funded on Zcash testnet, then + // spent by this tx); ZIP-244 commits to both. + let prevout_value: i64 = 2_000_000; + let prevout_script = + ScriptBuf::from(hex::decode("a914ed68766fe37d9e2325758ed209ac78db505425a987").unwrap()); + let redeem_script = ScriptBuf::from( + hex::decode( + "5221023b4221b042fa25af6609d7e65d322fcb64c497b79ffc8f1891ea6b23d4e7d84a\ + 2102feaf8248a2f8dcc34f2e2f520201801bb88d20ab549baf47b48bc9f2f4dfcc93\ + 21030b82f01fd53e7dabe2d904938d64294e3352e9e836240af6ba2cfb9df8f837da53ae", + ) + .unwrap(), + ); + let pubkeys: Vec> = redeem_script + .instructions() + .map(|i| i.expect("valid redeem script")) + .filter_map(|i| match i { + Instruction::PushBytes(pb) => Some(pb.as_bytes().to_vec()), + Instruction::Op(_) => None, + }) + .collect(); + assert_eq!(pubkeys.len(), 3, "2-of-3 redeem script has 3 pubkeys"); + + // scriptSig = OP_0 ; the two signatures correspond to + // pubkeys[0] and pubkeys[2] (the redeem script's first and third keys). + let pushes: Vec> = tx.transparent.input[0] + .script_sig + .instructions() + .map(|i| i.expect("valid scriptSig")) + .filter_map(|i| match i { + Instruction::PushBytes(pb) if !pb.as_bytes().is_empty() => { + Some(pb.as_bytes().to_vec()) + } + _ => None, + }) + .collect(); + assert_eq!( + pushes.len(), + 3, + "OP_0 dummy, 2 sigs, redeem script (dummy excluded above)" + ); + let sig_pubkey_pairs = [(&pushes[0], &pubkeys[0]), (&pushes[1], &pubkeys[2])]; + + let sighash = compute_v6_transparent_sighash( + &tx, + 0, + &[prevout_value], + std::slice::from_ref(&prevout_script), + ) + .unwrap(); + + let secp = Secp256k1::verification_only(); + let msg = Message::from_digest(sighash); + for (sig_bytes, pubkey_bytes) in sig_pubkey_pairs { + assert_eq!( + *sig_bytes.last().unwrap(), + SIGHASH_ALL, + "signature uses SIGHASH_ALL" + ); + let mut sig = + Signature::from_der(&sig_bytes[..sig_bytes.len() - 1]).expect("DER signature"); + sig.normalize_s(); + let pk = PublicKey::from_slice(pubkey_bytes).expect("valid pubkey"); + secp.verify_ecdsa(&msg, &sig, &pk).expect( + "the real mempool-accepted multisig tx's signature verifies against \ + compute_v6_transparent_sighash", + ); + } + } + #[test] fn digest_is_deterministic() { let b = sample_bundle(); diff --git a/packages/wasm-utxo/test/fixedScript/zcashV6Transaction.ts b/packages/wasm-utxo/test/fixedScript/zcashV6Transaction.ts index 7f5104368d7..ab8fb8e6a1d 100644 --- a/packages/wasm-utxo/test/fixedScript/zcashV6Transaction.ts +++ b/packages/wasm-utxo/test/fixedScript/zcashV6Transaction.ts @@ -2,6 +2,7 @@ import * as assert from "assert"; import * as fs from "fs"; import * as path from "path"; import { fileURLToPath } from "url"; +import { ecc, script } from "@bitgo/utxo-lib"; import { ZcashV6Transaction } from "../../js/fixedScriptWallet/ZcashV6Transaction.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -45,4 +46,78 @@ describe("ZcashV6Transaction", function () { it("throws on non-v6 bytes", function () { assert.throws(() => ZcashV6Transaction.fromBytes(Buffer.from("00010203", "hex"))); }); + + /** + * Golden regression test: `transparentSighash` must hash the spent output's scriptPubKey + * into the ZIP-244 per-input digest, not the redeem/witness script ("scriptCode"). Those + * coincide for a P2PKH input (see the Rust-side `golden_transparent_sighash_verifies_real_signature` + * test), which is why a prior bug that hashed the redeem script there went uncaught until it + * was exercised against a real P2SH multisig input. + * + * The fixture is a real transaction — spending a 2-of-3 P2SH multisig transparent input into + * an Ironwood shielded output — that was built with this codebase's CLI, submitted to a live + * Zcash testnet (NU6.3) `zebrad` node via `sendrawtransaction`, and accepted into its + * mempool: real consensus-rule validation of the transparent scriptSig, not merely + * self-consistency against this codebase's own sighash. + */ + it("verifies a real mempool-accepted multisig tx's signatures against transparentSighash", function () { + const tx = ZcashV6Transaction.fromBytes( + Buffer.from(readFixture("v6_shield_multisig_rawtx.hex"), "hex"), + ); + + // Spent output (a synthetic 2-of-3 P2SH multisig address funded on Zcash testnet, then + // spent by this tx); ZIP-244 commits to both. + const prevoutValue = 2_000_000n; + const prevoutScript = Buffer.from("a914ed68766fe37d9e2325758ed209ac78db505425a987", "hex"); + const redeemScript = Buffer.from( + "5221023b4221b042fa25af6609d7e65d322fcb64c497b79ffc8f1891ea6b23d4e7d84a" + + "2102feaf8248a2f8dcc34f2e2f520201801bb88d20ab549baf47b48bc9f2f4dfcc93" + + "21030b82f01fd53e7dabe2d904938d64294e3352e9e836240af6ba2cfb9df8f837da53ae", + "hex", + ); + // scriptSig = OP_0 , as decoded from the fixture's raw bytes. + const scriptSig = Buffer.from( + "0047304402204da2cef266325268af039a5ad4992babbb7d6813ac98f4351741c89e0186e41" + + "10220441b74b62b346ed9a3e1e5b07c850d2c7acfce22b7140f2a9d02bddf221f29c50148" + + "3045022100ee66c425f9fae3e32ae866534db4cae9b8daf5fb570a3638bf426cab1893e2a" + + "8022033af0f5ccaf869703e5af54920a771a392c722588ff5660c342d5c06df726e4c014c" + + "695221023b4221b042fa25af6609d7e65d322fcb64c497b79ffc8f1891ea6b23d4e7d84a2" + + "102feaf8248a2f8dcc34f2e2f520201801bb88d20ab549baf47b48bc9f2f4dfcc9321030b" + + "82f01fd53e7dabe2d904938d64294e3352e9e836240af6ba2cfb9df8f837da53ae", + "hex", + ); + + const pubkeys = (script.decompile(redeemScript) ?? []).filter((el): el is Buffer => + Buffer.isBuffer(el), + ); + assert.strictEqual(pubkeys.length, 3, "2-of-3 redeem script has 3 pubkeys"); + + // The signatures correspond to pubkeys[0] and pubkeys[2] (the redeem script's first and + // third keys). + const scriptSigChunks = (script.decompile(scriptSig) ?? []).filter( + (el): el is Buffer => Buffer.isBuffer(el) && el.length > 0, + ); + assert.strictEqual( + scriptSigChunks.length, + 3, + "OP_0 dummy (excluded above), 2 sigs, redeem script", + ); + const sigPubkeyPairs: [Buffer, Buffer][] = [ + [scriptSigChunks[0], pubkeys[0]], + [scriptSigChunks[1], pubkeys[2]], + ]; + + const sighash = tx.transparentSighash(0, [prevoutValue], [prevoutScript]); + assert.strictEqual(sighash.length, 32); + + for (const [derSigWithHashType, pubkey] of sigPubkeyPairs) { + const { signature, hashType } = script.signature.decode(derSigWithHashType); + assert.strictEqual(hashType, 0x01, "signature uses SIGHASH_ALL"); + assert.strictEqual( + ecc.verify(sighash, pubkey, signature), + true, + "the real mempool-accepted multisig tx's signature verifies against transparentSighash", + ); + } + }); }); diff --git a/packages/wasm-utxo/test/fixtures/zcash/v6_shield_multisig_rawtx.hex b/packages/wasm-utxo/test/fixtures/zcash/v6_shield_multisig_rawtx.hex new file mode 100644 index 00000000000..ea5ae5e03f8 --- /dev/null +++ b/packages/wasm-utxo/test/fixtures/zcash/v6_shield_multisig_rawtx.hex @@ -0,0 +1,2 @@ +0600008098b684d85b16a5370000000010e640000173fed9cb726d62aa3dc5f82b42cef3eb670034c0d6ceb2c7d721f014a31dbd1e01000000fdfd000047304402204da2cef266325268af039a5ad4992babbb7d6813ac98f4351741c89e0186e4110220441b74b62b346ed9a3e1e5b07c850d2c7acfce22b7140f2a9d02bddf221f29c501483045022100ee66c425f9fae3e32ae866534db4cae9b8daf5fb570a3638bf426cab1893e2a8022033af0f5ccaf869703e5af54920a771a392c722588ff5660c342d5c06df726e4c014c695221023b4221b042fa25af6609d7e65d322fcb64c497b79ffc8f1891ea6b23d4e7d84a2102feaf8248a2f8dcc34f2e2f520201801bb88d20ab549baf47b48bc9f2f4dfcc9321030b82f01fd53e7dabe2d904938d64294e3352e9e836240af6ba2cfb9df8f837da53aefeffffff0000000001db605c0b410f456e9894e6be9b064fa5c1cae49bedb8f046fbdc7f047d68b3bbc454ebbbdbf5709d200fe6dcb821d235ec0a732f5f3ac20e341ac59946ebf90324650c64555f81a29ae210c4fa68e36773faf7a90bf429b2d6edea4b7375a53303e20f9786e7754132e11198bf10ebb7e8feca25abbe9980c431e021a179e61602f85400d16d218a411759e478587231e167d2aac44819cd4a8d4a26811eeb2cbbe301d1e3075364361e1b0c865834ea350cfcef6cefb70cacbbbeccc03b3348f20287716d27897c15c8bb9c3d836ceb3d3e908307a1873fa56254c23fc51cc6de87f7ad0f25794eb0681c585750625bd02f4c07ee6f1ee1c16f58cc95aacae85b3edbdc122e083d03453d9b9bee26956b873c8184560cdeee09d40dbff54681cbc30f7106443a9b2c65c23b22e3ec5abeec18ac6d0133e9d165a212df26953c5dd438e5c5fc040b546f57aade72d603ae6720a60d5cfa97d29f5519235703910ebc17f1d363afe189e4aa29e171ef962b7584cf6a413a47b8f76a1afe80c539d809f6c768edccca0b9b3088bbc4d342eb7a3c10f999810cb4e637814dd6228226969dbd011f0d60c3ff00008c1029db408ba07e5106e728042a91fd72d773da77276a6e8b73fcdd0de1bee4366a446fbb4806d53fe60b8d2714fd22f98c98c078bff7eb99ec92d1b48e7f07287248e73bd3e12a1d086f53a66d3b6d0c28380280555e50b50c6d5644c5c01708810522010734107cc5d4f368d8c5633ac03a09bc15c098b4d42f6e9ab7eb02749f178c76d5b545fbed054801d506a81f96add407bd7bc7e0ebdf1b73d1b85e62b666f9dfa6c16d93e6392d9623a8f3113563f916fb0e396bceb6184389557daf15e4d34074a1f2c7b1496deebdd0f32445aa9a4683215c8ab9ce8a88d7d5231843d0cbb231763ce321a7fa8d97142793f480632bf6c4bcb6d7b976462e466dcf8e72d97912ee1cd18d9008d40a395942f70b6dfea643ea81a5da8f00d802f0ba88a252c477bb60d99f00b67a55d9e7269e8e05a2c21931e76312411c6f26bfb195f24cf029a92d9c6c3faeb9f40f8e3294a3abba3803131a6dbb066e04fba63289812ae62e97f2978056fbb41bf5afa0bd09f6a32f44d90cfd3057a2ccf3ca746934e5d84a3c74072002e3ffffffffff179fa4ebcadd3006a14b0ea80380e6e14287e453fc468fa93c7f73c88f87b408fd8013315dd4c76bed18d2d96d43c4bd3acd2fc8bdcbe08e300f2e27f835703b5c649f82fd9dfd017143e8f09bf626deb79a12e382ca000c4d6151e9af7ca0ab9677a4a0a6afa0baf5432cd4febdca5659e49fd7d5f9caa5a8bd41ac6705ccf18ce937dc828fce7f134771d83174c117daaeba000cd910f546c208f6af1e3712e5ef3db70a9e22a32329b5c59565406e484035b9d9e852b621054b93c7dc1da69263b903e0f81d8caeb0def717f99ac589e490d9a87643684d63a6afed7e26f6de2b15d9268773517bcab1a44facf63330e2c04fa418819815e4359662289bce7604a7c1e920e378ed1ed1719c02d3f07599ccf3f2aa554b47a8a85d41032e2d44e091fdc43510f1f59bcd25d55b194b6910027a87cb14270ede4674122b98ef427dba7398287d7db2a87deb455e2680378c641f4496c9f73ca509dcbdcbbf666e7c024005e486a0028100e0f558a0435e77b2ef6b0f8ae96be59964aea58ee7acc4152b5fef6615aea4da55ea6dd2c601f70ecbbf6ad8e3326d408e9a1cbedd51103dc7005f67599e29f2ed29c71ed9f42f3c072abb3c91b1fc5195d49436e073ee92bc85485a0877a13d063da5b3ffd1903d15cb1029d283d1f105d25ff7fd3fc3086b2392f0c2e84fe2138472c5e75ff5d6665bd265fc2c2db30c91e7d1aaf2c6b3be3cd18f9cdff7153931750890d3717b019d1fd258de7a3f69004ad3925ef8ad7cdd1c778b039d337cb46b9071ce81fb8b4441d4268f4a6557271e15b1a7ff044958289a0da49e6d778d1632fb1a0c5263c4d7e9e4225b2dedaf5d20ba6a35b21462f40d9d386b0888ada768638d4dbd0b0c0ad450c9e1e587261dc88824c49676039ce6573bcf90f83938bb1aa9acc4f78634ef855862ea0dc0e312c0ec0ba01ba02085501452481277e4635e098a7daafa8e79798c77eda0da31a6e47ec4b7002431417d37d033603383b20cbff35271324c22d6a7e2dd04a7ceefb9996610078a3dd6c87fba091748959ac5f72a2626796e79d289b83fe1f8e04685eafb2ad03dc536152841caac1c48a6e346d0908b7e6a019071ea70b86926558efec6be1232e20719d37ea12cd8c8ee66371da77d4409a67ffe3fea2878f7bbe359301d0c6fa0fe8ec175822dc4560af38bb9d031b45ee6fbbfd4ae4abc70262f41de808613af7c5e6ef20c54c5497a1c9d5b3435521cb25d8ab644ce851f8184829ba46dfa737ff513a4b7ce81039081dc6fe19077d4f8d7ba23aad4ba1385da47e387c2fc6731e05175b9d13279a75c069a1e633202e4b750992f5f0f46e922583a2f1c1585f25e757912ef189be7d738627f19305c0be6572dc934064d1a5911458d112f57dc3b3db612c8ac2661e42fdd508ffce068b88173e7957bfaf12cb22888ee6dd22923bcfd3eb15b1e5c7ad6ef3dcfcb26fa775b756cb96265714eaa8b0d31655401eefa69c8c5a3abcda1a4bf647939d1d088bf5d44f442e3346ccd8539f5233e9c1ce3409313a3cdaa1b25f603f7ca98e52561dc08059a26b995d86c1bb1a2e0035fae6aed3d856f68a4bc5224a4d7217bba47a2b16fe0511a7f869c0b548fb148d8958175206fc6e639c09992a2b84ca3d532c410e950688caa6f641963ebe3071d202d7cb6627096f9e653d5ed5b0d6844cedebd371b8a2137f191379c9dfb0de0a783ab039d955471a9ecdf4c303a3764f2901d10b7175d5bda2604e8f6588095674ef6150928224db4f526bcee804e66973f667df72c41681669325370101b0bb16f997716b4d9ab9cf574469e9d83da382453deac499befd7812d93cedc0bed24d04f590d57b3e0767e80859ac6b05b60064cea0d3f5cad10332940e2c469d623bd4890a8dccc522605ae66eb99e47cef9a172d9e2bb426b497151d8daec4777685db2fde0d1058e2be3f25722096b9f9ae1ad82cf2350d384a2abca625c4b553f603b480780e3c0a0132129fc05e26843f277395464645dbbc00a10a3647adf2f0ac48191df252164192642ddf1ebbe6cf16b47893e2b4832413c47477f56f069d8470de6f0a87e4ae5c3b43b667a4b9b1969a01eaf9fd9aa4099e6d8fb90c28535799636978e557015027ad78ef1dc1398c0c0a297fe7b4123a8009065ad4309f35a445754dff0d7a9f31e123d51eb2086205fa8c0f0b50cf069ac293fadf9dc3db42978536dcc1e45ae3b1e687cbf5e6e3bdf07bbf584246099e87f8bf9ba2848e7b02e3837a0e4d2978e87381e789cd6cc96f1850a088c73e988c905b25ae73ad5a076d9f8f49f2340002e630f5272182f37e579886ab5804bc1583c20ac10973a7fe69fbd2646b9f1b165537cc9ef9e0e9cc075319bc1a1b8c28be0443573d7f99159f245d0ec66ffa8d9b291f785cf92fb07df652734d0a1f985a36030a8c471baa8ae6a30cd887412c65bee9b02e942264456a663c5a28e20f646bb92d47443fe4ea2a126bc9274148220add56a4e100e4bac5466c5e2d249e62997292707c8d2d6caaecf278524e81e92dfd2b2a9e8926f77cb234dd289e455decfc835d14eaa3abbf382fcda6499d3900f43d04597dfdeb4f7aedb5376dd4f99eac823e28c36dd95bcc388b26b1e308c1d5074b638a85b9a5edadd50426d872fe0cf828f124960424d4cc65122a6d417d40fcc7ec809359a4a677a1076701ba617e36c8066d84099fa27891dd10683d75326a7c018b1e28420518642962caab713549e625289d3e0fc45691fbd1fdc8c846b7c47e5fc222bde7135810512e2b33d9a1439b16f0ea57257b28b20a3caf38a5feb0804337f246c1379423a5855dcd6ee444f338bfc1a1137536d70444436603bc9912c30b76de5a5aef20c66582f9e7eedf662b2aa5d3db34237031e7ca51a886819eb294d6cf9662d927286fe881557c694a1787289c8f3b008b980980a215146b2f15551c53dc390100bc3d1754c68f17ba9deb1298c879a55c6334e0df7c78e30e9d29d5d00f107432308899099b7be7a3a7ad7c98a73b74df32a3796745d0727ebf2a6eafbcf53a3a1bc08e268413655a9267e2f642f36884ed71671ad4f9b9be596d048aa3a360000d0944c2b587e0176f0574ddf68537edfbd71f5ad4d8bf5c7469f52b6b54910fb308cb776f3b44a4f255d5c51c1c9aea63dc7d42a615f0ce2704067922365118fe44b98215fdbd6b800e42d80a59444a278ded45f3d2bd9b2ba515e81f8c9236c415eb9a1d916558a381795e52cee16e97feb9dc403bdcef22f2ee6b714a623fc57413b17eccdb3e084562efcedb65fb61f6c523a5d358166e490b56fe362735086da256d70c1c6f16057a68ae7f5e0cab79588f2266aa5d303352c9e2943b1f92f45a9fbde8e6c98d852feeaf737dfebcf124885f8642277b0d263203f5ae0e70fe2c0b9658d66045475f26f699ceb2ffeb623a16f8dfc8512085d91608e21c4c9cf5286028b137b36f175c3b42ca242a418a299be8ab09144011850f3c2a18dea874dda2a13c3cbee730918c99f0fbedef94b12b8a5245c554d50cd2144d3c3c1c7ca8c0a9a14f71eab92d629ad6debac3212b5ddcfbddaa547a75a2db0c04bf7176700077fe708f2ea70569a7935de34e3f627cd991006bfe735d3dc79f028f1aca544c2d87423a8d7e5527601bd67ff95f62a17149681c823feb0381e03daf8855ac6d76e7d3800ead2474166abf6ae267345b8273fb1fa14c258c0b662c3d0d737a03ec34e27829de4ffbe5b6a84063bc17bbea4d10d14685a834c79b1520a543f1b8ca45ff5e33f32cfee4a8550edf513b43217e13516856c229dfe11928701f409e9cc121a2957aaf8ea30fefaf33a9107fefa1d38918a95961319026fd7697432e949d005014edecc0bf4cd8fe4c7dadbb4a89e738291ff8db3bc33ddbe0b7cb47c4cbcbf1606d9c17e6cfe87e1ed524994aba6e6f59b09ba867cb20e18d3a19dbdfdf434ba33ec24456dd676b67159b0868358416cb5d0f0e5c7f02a020d06cf555eeff7375a4ac6ad6df4c698215db0c1be2d2cf52f736d4260610e5caf146a8ee2bd1272c60570b5dc93479c5c5e48a058986380409db33527c06ad03de6a5c42cb124dd71e7719fdc9952d11101929df3d1f55235821c920b707844800595adf1f8576f0773662524189814786f64d26ba6059b2a61292e059055d8d6be86f574f597097948ae304f2ce71ad75641b68adae967818fa2dc3f405545578a5cdbebc6eb5dc49eadee5482d7eba8ebd1e49341795171de48d7d4c1fe08a96ee16e181cd6b73a8483d0b54b869fd57f172959893e5573993bc2a9d289b84aa0cbeecf38f3b3e4aa95d63231e107c4ace686d2e4ecff455817ee6d8129a1a5b61dc6e5bb9c1c517ec0191e9db8503a9e1b81b0e1c5012df6fdcff182f4f0ee14560d59df31a0a2a98fd7ade298c7b846c9444ccb9c8685e7e6bec6930cd1cab65e779de4de641086c2d4f3f2f7bbc9df4eb9d1dd3fdcc566363c1ad394bfa639be345ba54bf042adfeb5629de2c682bb90b72f8cdb19d697846d0841fdc685935a4fb3665a21c279ee88fb6fc0d9960dfac4b24be1c1e6c2fd148a10abf9104759a32bd2f73479fefd2deb8c9f64642a932c8348646aaccadce3ae5013ddf2980d5b84de8a835a67af1a77a0be0de07c0c6a0a549c5c45ef0f27fc01732a2ffedbaec2f112dd2ca9d1e33044a563b2586c193fc5f052e7fa277d0630b9d189af44342e63b4c5793ac77d3e97f6ad98e3a867c1ba4cfe3327da2b8642bfb872e2827b14eaa432c4a9641d43dea60de19cf6e450d69b932f616a5673e2cfe6df2f80b0cc2d6fc302b94110f032beec47bec09e1cc322bca7d48841c7f3f5cc958c483dd1b35ed31ad74d9c5cf6cea857d5a1a18cb786cc9e7d12e424922ecd2e1e6f734dd372c698e3e28c624eb4d781cf285d54015e7658b29166f3f0c6f9e2ad6276fe855ec806ccd496da1c22cb7bfff22925b166932fc0af7ac163f9370acd9566654807de1c855fe591f2e883f2cc9d6faca2a6b688f4f0013c524edd674bd88b76d620a705c8c9b94b05ee7b95a04a9db51c617abc9859491e91155a27ea6840702b81851c02c59969e7febe47b9c5bac9ee9201ec2f0d3350f06ac76eb6421e188aaf7484e4aa55fa3420172935af44899b410d3e3923f3fa2268448ccbb6cdbe5fa8e97fdec59479e293e2edbeb4c7c4421675e6ae4b035e81c00d593f6931fa0971efe686a72f33f25d5e1d2ff7b6470142bd9cee35235192bdeef8a1b47568cf09dcaf1184679e7bf74a710db89e594945cecea05443fdd20323e169b6d661c00a63d41b25fa82357ae7d51a1ee6083aa70e6681a3380ad3516dacc0b91e490d9d8bcf9ecad0c5fb75045445bbb14d58e46c53f6132812a0485cd7f0c662e870c60282569a9435df635862492d49633ffd6696761b24b0830dc005a2b660722d750f11cc5010f1058789ae38eb0281fe471ed17c3eacb7233a8d70151030d86244ebf6c9f6847f5ec33a2c482a6eeed165522a57bc1e3843beac8bd01b34a76e9f770134d0e7226289debe3e75b16e5d5f45719b11631fc0dfe7a9507b794ccfcc1466a587ecec5212767643efc494032bcebf174205e6e2a4daa5f5ef624793fac415a79a7ce762cc6c78e45fcff8a63687098b351f6ba3db40b512e1a40d0692fc7d15db70d5cb2d125a413813615cb51f01b0f48421c3ba27c8ca015e7a197f14b709ffee91d2f730642363e7b17b8fa3213b39b581101aa7c5e303d514ec54bc45c21d54c637a75a04af80b9a7b5b2376ed45421c21016a0e0c75446e6c02e50bc387a9582ad95ab386189571905c9a7c3d464040cb0b6a20a8e889f988c0b26818ce0e5b62cb3b83895d93f53fc970cd87e9e7ff9f17388f3d00a7acbdae96895fbeef26a30a6c27053b483104063062a89fbcf1eb2078574504e41548bf978f6c5e516845e3b8569126a5d371002caa7862e227bd9e2344f824de08c8accb13cc834e96ee1ddf2f57e60c0528ff7f666efef41c3987cf4ba340cb3dfe062c04e49908f32a8a7a7e417c1c97f1beea88c630b0c4e3093312e196e01be90c8f9f31415b03dd1087affff6441af327475ccc37e2e34f150946644f04efc99ed9f05db851649c2b7c55109abaa9d026d3740d4ce42069195a9debce5f5e9c47dd1116dee410b07ce4378917871a9a212517843a90b2a0b2b09a6ef2b9dff642b97c2657b30c890ac6b7029ea3f16b94672987bc639bd6b0fb72cd5ae07079e92fa5525c5cf02df8dfe2ebea29426b0fcd30b4486a69d037c5aed4a2e9b9c9082af5c4500904ca2f1aa1e3e8856d7eb8cdef2b2ab502b70e5e963d9166e06b7e3965d6c5b19d878abe3561aa2951c62618217912962d06a35144b4a190f18e6c168674e237e75e722a57de623c616efea1f0144f420570831ebe92807dc4e2dad3599f3af284b7b13c6cb16547e65368fa0b969f568b811f6f21b6f13e95e09527fb521a59e4ab9a853ba5a7da2b2da73722371880c1b790cd668fa13dcda602757705f5c0dec085d3675af7ed9615d1de66b7726971d4a84a35026eb10e25ec433f04045dda21dffcc06b3fecb9cfd086fce948afc8b3bc1de57e8fc9f71ccf335008279d7e7582517e0f6a447b9be06e189a252f61b31fd69d6feb7837f0d8cb2f4acc6a56e64f8f08c3f5eab1f74b0f2c645419c72c9925d7c21408a3938a610668409fb7203c88255951558f0086483ae703e4c5f31239fc747461ecd7f8cbbf9553b6515edbcd46ba3f30dcdb2db257195225efa83781f59aad2a3570429286083a42cd3a9758cf236a347c1a6aa9241746248b343dc837d1811129f626a80b8a6e64619a0d1d21f290b861a5f0c12f5c880f38ca8b16ea4a515e249a633eb54c1c743d4197ff238da46d9126b24593ffd13a79a2324006a4c12f06b9b68b46896959eada1c9d473457df1d277eb474405a89eeecbfdaa01185a413c7e6fcec10fd9bc80b25ebffd94a7052a07bee14fb24b08ce3385b8a1f6d1880ca3d0c51ede29751dcef687ad2fada504530f65b5ece3cd3832951f61e7a95179c37222012015dc3326a283329b1851b8a23d965003d1785f68dcaef726ded7d712318bf66cc0266a2501771817fa0b850afa651f5f53f3e60236e136525d6795b0e61c32c102927b4a3127847b4b41891ea09dc225dd0f70b2be4dfc58c8d97d7acf7276bab64c7b799c1dfe4be8e87508555918bee9be7073c +