Separate merge logic out of Map trait

Recently we (*cough* Tobin) made the `Map` trait private and neglected
to add a public API for combining together two PSBTs. Doing so broke the
`psbt` module.

Pull the merge logic out of the `Map` trait and put it in methods on
each individual type (`Input`, `Output`, `PartiallySignedTransaction`).
Doing so allows for simplification of return types since combining
inputs/outputs never errors.

Use the term 'combine' instead of 'merge' since that is the term used in
BIP 174.
This commit is contained in:
Tobin Harding 2022-02-22 13:41:20 +00:00
parent 1871c3ada9
commit 5e2449922d
No known key found for this signature in database
GPG Key ID: 40BF9E4C269D6607
10 changed files with 142 additions and 121 deletions

View File

@ -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"),
}
}

View File

@ -17,7 +17,7 @@ macro_rules! hex_psbt {
($s:expr) => { $crate::consensus::deserialize::<$crate::util::psbt::PartiallySignedTransaction>(&<$crate::prelude::Vec<u8> 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);

View File

@ -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 {

View File

@ -304,6 +304,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 {
@ -401,37 +431,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);

View File

@ -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<Vec<raw::Pair>, 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<S: io::Write>(
&self,

View File

@ -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);

View File

@ -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);
}
}

1
test_data/psbt1.hex Normal file
View File

@ -0,0 +1 @@
70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000002202029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e887220203089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000

1
test_data/psbt2.hex Normal file
View File

@ -0,0 +1 @@
70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000002202029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01220202dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e887220203089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f012202023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d2010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000

View File

@ -0,0 +1 @@
70736274ff01009a020000000258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd750000000000ffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d0100000000ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f00000000000100bb0200000001aad73931018bd25f84ae400b68848be09db706eac2ac18298babee71ab656f8b0000000048473044022058f6fc7c6a33e1b31548d481c826c015bd30135aad42cd67790dab66d2ad243b02204a1ced2604c6735b6393e5b41691dd78b00f0c5942fb9f751856faa938157dba01feffffff0280f0fa020000000017a9140fb9463421696b82c833af241c78c17ddbde493487d0f20a270100000017a91429ca74f8a08f81999428185c97b5d852e4063f6187650000002202029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01220202dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d7483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01010304010000000104475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752ae2206029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f10d90c6a4f000000800000008000000080220602dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d710d90c6a4f0000008000000080010000800001012000c2eb0b0000000017a914b7f5faf40e3d40a5a459b1db3535f2b72fa921e887220203089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f012202023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e73473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d2010103040100000001042200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903010547522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae2206023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7310d90c6a4f000000800000008003000080220603089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc10d90c6a4f00000080000000800200008000220203a9a4c37f5996d3aa25dbac6b570af0650394492942460b354753ed9eeca5877110d90c6a4f000000800000008004000080002202027f6399757d2eff55a136ad02c684b1838b6556e5f1b6b34282a94b6b5005109610d90c6a4f00000080000000800500008000