diff --git a/src/util/psbt/error.rs b/src/util/psbt/error.rs index 3d46a118..caab843b 100644 --- a/src/util/psbt/error.rs +++ b/src/util/psbt/error.rs @@ -53,7 +53,7 @@ pub enum Error { MustHaveUnsignedTx, /// Signals that there are no more key-value pairs in a key-value map. NoMorePairs, - /// Attempting to merge with a PSBT describing a different unsigned + /// Attempting to combine with a PSBT describing a different unsigned /// transaction. UnexpectedUnsignedTx { /// Expected @@ -74,9 +74,9 @@ pub enum Error { /// Hash value hash: Box<[u8]>, }, - /// Conflicting data during merge procedure: + /// Conflicting data during combine procedure: /// global extended public key has inconsistent key sources - MergeInconsistentKeySources(ExtendedPubKey), + CombineInconsistentKeySources(ExtendedPubKey), /// Serialization error in bitcoin consensus-encoded structures ConsensusEncoding, } @@ -102,7 +102,7 @@ impl fmt::Display for Error { // directly using debug forms of psbthash enums write!(f, "Preimage {:?} does not match {:?} hash {:?}", preimage, hash_type, hash ) } - Error::MergeInconsistentKeySources(ref s) => { write!(f, "merge conflict: {}", s) } + Error::CombineInconsistentKeySources(ref s) => { write!(f, "combine conflict: {}", s) } Error::ConsensusEncoding => f.write_str("bitcoin consensus or BIP-174 encoding error"), } } diff --git a/src/util/psbt/macros.rs b/src/util/psbt/macros.rs index f1bdfe6c..78c7f359 100644 --- a/src/util/psbt/macros.rs +++ b/src/util/psbt/macros.rs @@ -17,7 +17,7 @@ macro_rules! hex_psbt { ($s:expr) => { $crate::consensus::deserialize::<$crate::util::psbt::PartiallySignedTransaction>(&<$crate::prelude::Vec as $crate::hashes::hex::FromHex>::from_hex($s).unwrap()) }; } -macro_rules! merge { +macro_rules! combine { ($thing:ident, $slf:ident, $other:ident) => { if let (&None, Some($thing)) = (&$slf.$thing, $other.$thing) { $slf.$thing = Some($thing); diff --git a/src/util/psbt/map/global.rs b/src/util/psbt/map/global.rs index 4d1ec59d..b6f7d51a 100644 --- a/src/util/psbt/map/global.rs +++ b/src/util/psbt/map/global.rs @@ -15,14 +15,12 @@ use prelude::*; use io::{self, Cursor, Read}; -use core::cmp; use blockdata::transaction::Transaction; use consensus::{encode, Encodable, Decodable}; use consensus::encode::MAX_VEC_SIZE; use util::psbt::map::Map; use util::psbt::{raw, PartiallySignedTransaction}; -use util::psbt; use util::psbt::Error; use util::endian::u32_to_array_le; use util::bip32::{ExtendedPubKey, Fingerprint, DerivationPath, ChildNumber}; @@ -99,69 +97,6 @@ impl Map for PartiallySignedTransaction { Ok(rv) } - - // Keep in mind that according to BIP 174 this function must be commutative, i.e. - // A.merge(B) == B.merge(A) - fn merge(&mut self, other: Self) -> Result<(), psbt::Error> { - if self.unsigned_tx != other.unsigned_tx { - return Err(psbt::Error::UnexpectedUnsignedTx { - expected: Box::new(self.unsigned_tx.clone()), - actual: Box::new(other.unsigned_tx), - }); - } - - // BIP 174: The Combiner must remove any duplicate key-value pairs, in accordance with - // the specification. It can pick arbitrarily when conflicts occur. - - // Keeping the highest version - self.version = cmp::max(self.version, other.version); - - // Merging xpubs - for (xpub, (fingerprint1, derivation1)) in other.xpub { - match self.xpub.entry(xpub) { - btree_map::Entry::Vacant(entry) => { - entry.insert((fingerprint1, derivation1)); - }, - btree_map::Entry::Occupied(mut entry) => { - // Here in case of the conflict we select the version with algorithm: - // 1) if everything is equal we do nothing - // 2) report an error if - // - derivation paths are equal and fingerprints are not - // - derivation paths are of the same length, but not equal - // - derivation paths has different length, but the shorter one - // is not the strict suffix of the longer one - // 3) choose longest derivation otherwise - - let (fingerprint2, derivation2) = entry.get().clone(); - - if (derivation1 == derivation2 && fingerprint1 == fingerprint2) || - (derivation1.len() < derivation2.len() && derivation1[..] == derivation2[derivation2.len() - derivation1.len()..]) - { - continue - } - else if derivation2[..] == derivation1[derivation1.len() - derivation2.len()..] - { - entry.insert((fingerprint1, derivation1)); - continue - } - return Err(psbt::Error::MergeInconsistentKeySources(xpub)); - } - } - } - - self.proprietary.extend(other.proprietary); - self.unknown.extend(other.unknown); - - for (self_input, other_input) in self.inputs.iter_mut().zip(other.inputs.into_iter()) { - self_input.merge(other_input)?; - } - - for (self_output, other_output) in self.outputs.iter_mut().zip(other.outputs.into_iter()) { - self_output.merge(other_output)?; - } - - Ok(()) - } } impl PartiallySignedTransaction { diff --git a/src/util/psbt/map/input.rs b/src/util/psbt/map/input.rs index 64e731d4..2afb931e 100644 --- a/src/util/psbt/map/input.rs +++ b/src/util/psbt/map/input.rs @@ -326,6 +326,36 @@ impl Input { Ok(()) } + + /// Combines this [`Input`] with `other` `Input` (as described by BIP 174). + pub fn combine(&mut self, other: Self) { + combine!(non_witness_utxo, self, other); + + if let (&None, Some(witness_utxo)) = (&self.witness_utxo, other.witness_utxo) { + self.witness_utxo = Some(witness_utxo); + self.non_witness_utxo = None; // Clear out any non-witness UTXO when we set a witness one + } + + self.partial_sigs.extend(other.partial_sigs); + self.bip32_derivation.extend(other.bip32_derivation); + self.ripemd160_preimages.extend(other.ripemd160_preimages); + self.sha256_preimages.extend(other.sha256_preimages); + self.hash160_preimages.extend(other.hash160_preimages); + self.hash256_preimages.extend(other.hash256_preimages); + self.tap_script_sigs.extend(other.tap_script_sigs); + self.tap_scripts.extend(other.tap_scripts); + self.tap_key_origins.extend(other.tap_key_origins); + self.proprietary.extend(other.proprietary); + self.unknown.extend(other.unknown); + + combine!(redeem_script, self, other); + combine!(witness_script, self, other); + combine!(final_script_sig, self, other); + combine!(final_script_witness, self, other); + combine!(tap_key_sig, self, other); + combine!(tap_internal_key, self, other); + combine!(tap_merkle_root, self, other); + } } impl Map for Input { @@ -423,37 +453,6 @@ impl Map for Input { Ok(rv) } - - fn merge(&mut self, other: Self) -> Result<(), psbt::Error> { - merge!(non_witness_utxo, self, other); - - if let (&None, Some(witness_utxo)) = (&self.witness_utxo, other.witness_utxo) { - self.witness_utxo = Some(witness_utxo); - self.non_witness_utxo = None; // Clear out any non-witness UTXO when we set a witness one - } - - self.partial_sigs.extend(other.partial_sigs); - self.bip32_derivation.extend(other.bip32_derivation); - self.ripemd160_preimages.extend(other.ripemd160_preimages); - self.sha256_preimages.extend(other.sha256_preimages); - self.hash160_preimages.extend(other.hash160_preimages); - self.hash256_preimages.extend(other.hash256_preimages); - self.tap_script_sigs.extend(other.tap_script_sigs); - self.tap_scripts.extend(other.tap_scripts); - self.tap_key_origins.extend(other.tap_key_origins); - self.proprietary.extend(other.proprietary); - self.unknown.extend(other.unknown); - - merge!(redeem_script, self, other); - merge!(witness_script, self, other); - merge!(final_script_sig, self, other); - merge!(final_script_witness, self, other); - merge!(tap_key_sig, self, other); - merge!(tap_internal_key, self, other); - merge!(tap_merkle_root, self, other); - - Ok(()) - } } impl_psbtmap_consensus_enc_dec_oding!(Input); diff --git a/src/util/psbt/map/mod.rs b/src/util/psbt/map/mod.rs index d6b26ecc..44bffeba 100644 --- a/src/util/psbt/map/mod.rs +++ b/src/util/psbt/map/mod.rs @@ -17,7 +17,6 @@ use prelude::*; use io; use consensus::encode; -use util::psbt; use util::psbt::raw; mod global; @@ -32,9 +31,6 @@ pub(super) trait Map { /// Attempt to get all key-value pairs. fn get_pairs(&self) -> Result, io::Error>; - /// Attempt to merge with another key-value map of the same type. - fn merge(&mut self, other: Self) -> Result<(), psbt::Error>; - /// Encodes map data with bitcoin consensus encoding. fn consensus_encode_map( &self, diff --git a/src/util/psbt/map/output.rs b/src/util/psbt/map/output.rs index 3b1d7268..e03049c4 100644 --- a/src/util/psbt/map/output.rs +++ b/src/util/psbt/map/output.rs @@ -21,7 +21,6 @@ use consensus::encode; use secp256k1::XOnlyPublicKey; use util::bip32::KeySource; use secp256k1; -use util::psbt; use util::psbt::map::Map; use util::psbt::raw; use util::psbt::Error; @@ -177,6 +176,19 @@ impl Output { Ok(()) } + + /// Combines this [`Output`] with `other` `Output` (as described by BIP 174). + pub fn combine(&mut self, other: Self) { + self.bip32_derivation.extend(other.bip32_derivation); + self.proprietary.extend(other.proprietary); + self.unknown.extend(other.unknown); + self.tap_key_origins.extend(other.tap_key_origins); + + combine!(redeem_script, self, other); + combine!(witness_script, self, other); + combine!(tap_internal_key, self, other); + combine!(tap_tree, self, other); + } } impl Map for Output { @@ -223,20 +235,6 @@ impl Map for Output { Ok(rv) } - - fn merge(&mut self, other: Self) -> Result<(), psbt::Error> { - self.bip32_derivation.extend(other.bip32_derivation); - self.proprietary.extend(other.proprietary); - self.unknown.extend(other.unknown); - self.tap_key_origins.extend(other.tap_key_origins); - - merge!(redeem_script, self, other); - merge!(witness_script, self, other); - merge!(tap_internal_key, self, other); - merge!(tap_tree, self, other); - - Ok(()) - } } impl_psbtmap_consensus_enc_dec_oding!(Output); diff --git a/src/util/psbt/mod.rs b/src/util/psbt/mod.rs index e1cf4cca..5e427d96 100644 --- a/src/util/psbt/mod.rs +++ b/src/util/psbt/mod.rs @@ -19,6 +19,8 @@ //! except we define PSBTs containing non-standard SigHash types as invalid. //! +use core::cmp; + use blockdata::script::Script; use blockdata::transaction::Transaction; use consensus::{encode, Encodable, Decodable}; @@ -117,6 +119,70 @@ impl PartiallySignedTransaction { tx } + + /// Combines this [`PartiallySignedTransaction`] with `other` PSBT as described by BIP 174. + /// + /// In accordance with BIP 174 this function is commutative i.e., `A.combine(B) == B.combine(A)` + pub fn combine(&mut self, other: Self) -> Result<(), Error> { + if self.unsigned_tx != other.unsigned_tx { + return Err(Error::UnexpectedUnsignedTx { + expected: Box::new(self.unsigned_tx.clone()), + actual: Box::new(other.unsigned_tx), + }); + } + + // BIP 174: The Combiner must remove any duplicate key-value pairs, in accordance with + // the specification. It can pick arbitrarily when conflicts occur. + + // Keeping the highest version + self.version = cmp::max(self.version, other.version); + + // Merging xpubs + for (xpub, (fingerprint1, derivation1)) in other.xpub { + match self.xpub.entry(xpub) { + btree_map::Entry::Vacant(entry) => { + entry.insert((fingerprint1, derivation1)); + }, + btree_map::Entry::Occupied(mut entry) => { + // Here in case of the conflict we select the version with algorithm: + // 1) if everything is equal we do nothing + // 2) report an error if + // - derivation paths are equal and fingerprints are not + // - derivation paths are of the same length, but not equal + // - derivation paths has different length, but the shorter one + // is not the strict suffix of the longer one + // 3) choose longest derivation otherwise + + let (fingerprint2, derivation2) = entry.get().clone(); + + if (derivation1 == derivation2 && fingerprint1 == fingerprint2) || + (derivation1.len() < derivation2.len() && derivation1[..] == derivation2[derivation2.len() - derivation1.len()..]) + { + continue + } + else if derivation2[..] == derivation1[derivation1.len() - derivation2.len()..] + { + entry.insert((fingerprint1, derivation1)); + continue + } + return Err(Error::CombineInconsistentKeySources(xpub)); + } + } + } + + self.proprietary.extend(other.proprietary); + self.unknown.extend(other.unknown); + + for (self_input, other_input) in self.inputs.iter_mut().zip(other.inputs.into_iter()) { + self_input.combine(other_input); + } + + for (self_output, other_output) in self.outputs.iter_mut().zip(other.outputs.into_iter()) { + self_output.combine(other_output); + } + + Ok(()) + } } #[cfg(feature = "base64")] @@ -243,7 +309,7 @@ impl Decodable for PartiallySignedTransaction { #[cfg(test)] mod tests { - use super::PartiallySignedTransaction; + use super::*; use hashes::hex::FromHex; use hashes::{sha256, hash160, Hash, ripemd160}; @@ -1031,4 +1097,28 @@ mod tests { assert!(!rtt.proprietary.is_empty()); } + // PSBTs taken from BIP 174 test vectors. + #[test] + fn combine_psbts() { + let mut psbt1 = hex_psbt!(include_str!("../../../test_data/psbt1.hex")).unwrap(); + let psbt2 = hex_psbt!(include_str!("../../../test_data/psbt2.hex")).unwrap(); + let psbt_combined = hex_psbt!(include_str!("../../../test_data/psbt2.hex")).unwrap(); + + psbt1.combine(psbt2).expect("psbt combine to succeed"); + assert_eq!(psbt1, psbt_combined); + } + + #[test] + fn combine_psbts_commutative() { + let mut psbt1 = hex_psbt!(include_str!("../../../test_data/psbt1.hex")).unwrap(); + let mut psbt2 = hex_psbt!(include_str!("../../../test_data/psbt2.hex")).unwrap(); + + let psbt1_clone = psbt1.clone(); + let psbt2_clone = psbt2.clone(); + + psbt1.combine(psbt2_clone).expect("psbt1 combine to succeed"); + psbt2.combine(psbt1_clone).expect("psbt2 combine to succeed"); + + assert_eq!(psbt1, psbt2); + } } diff --git a/test_data/psbt1.hex b/test_data/psbt1.hex new file mode 100644 index 00000000..3bcb9179 --- /dev/null +++ b/test_data/psbt1.hex @@ -0,0 +1 @@ +70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000002202029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e887220203089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000 \ No newline at end of file diff --git a/test_data/psbt2.hex b/test_data/psbt2.hex new file mode 100644 index 00000000..47f38701 --- /dev/null +++ b/test_data/psbt2.hex @@ -0,0 +1 @@ +70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000002202029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01220202dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e887220203089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f012202023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d2010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000 \ No newline at end of file diff --git a/test_data/psbt_combined.hex b/test_data/psbt_combined.hex new file mode 100644 index 00000000..47f38701 --- /dev/null +++ b/test_data/psbt_combined.hex @@ -0,0 +1 @@ +70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000002202029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01220202dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e887220203089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f012202023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d2010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000 \ No newline at end of file