diff options
-rw-r--r-- | src/psbt.cpp | 15 | ||||
-rw-r--r-- | src/psbt.h | 290 | ||||
-rw-r--r-- | src/pubkey.cpp | 12 | ||||
-rw-r--r-- | src/pubkey.h | 19 | ||||
-rw-r--r-- | src/rpc/rawtransaction.cpp | 108 | ||||
-rw-r--r-- | src/script/keyorigin.h | 19 | ||||
-rw-r--r-- | src/script/sign.cpp | 1 | ||||
-rw-r--r-- | src/script/sign.h | 76 | ||||
-rw-r--r-- | src/script/signingprovider.h | 3 | ||||
-rw-r--r-- | src/serialize.h | 13 | ||||
-rw-r--r-- | src/test/fuzz/script_sign.cpp | 5 | ||||
-rw-r--r-- | test/functional/data/rpc_psbt.json | 9 |
12 files changed, 469 insertions, 101 deletions
diff --git a/src/psbt.cpp b/src/psbt.cpp index 6585766807..4c9b439815 100644 --- a/src/psbt.cpp +++ b/src/psbt.cpp @@ -32,6 +32,13 @@ bool PartiallySignedTransaction::Merge(const PartiallySignedTransaction& psbt) for (unsigned int i = 0; i < outputs.size(); ++i) { outputs[i].Merge(psbt.outputs[i]); } + for (auto& xpub_pair : psbt.m_xpubs) { + if (m_xpubs.count(xpub_pair.first) == 0) { + m_xpubs[xpub_pair.first] = xpub_pair.second; + } else { + m_xpubs[xpub_pair.first].insert(xpub_pair.second.begin(), xpub_pair.second.end()); + } + } unknown.insert(psbt.unknown.begin(), psbt.unknown.end()); return true; @@ -401,3 +408,11 @@ bool DecodeRawPSBT(PartiallySignedTransaction& psbt, const std::string& tx_data, } return true; } + +uint32_t PartiallySignedTransaction::GetVersion() const +{ + if (m_version != std::nullopt) { + return *m_version; + } + return 0; +} diff --git a/src/psbt.h b/src/psbt.h index 21daa050ea..690d1b3bbd 100644 --- a/src/psbt.h +++ b/src/psbt.h @@ -10,8 +10,11 @@ #include <policy/feerate.h> #include <primitives/transaction.h> #include <pubkey.h> +#include <script/keyorigin.h> #include <script/sign.h> #include <script/signingprovider.h> +#include <span.h> +#include <streams.h> #include <optional> @@ -20,6 +23,9 @@ static constexpr uint8_t PSBT_MAGIC_BYTES[5] = {'p', 's', 'b', 't', 0xff}; // Global types static constexpr uint8_t PSBT_GLOBAL_UNSIGNED_TX = 0x00; +static constexpr uint8_t PSBT_GLOBAL_XPUB = 0x01; +static constexpr uint8_t PSBT_GLOBAL_VERSION = 0xFB; +static constexpr uint8_t PSBT_GLOBAL_PROPRIETARY = 0xFC; // Input types static constexpr uint8_t PSBT_IN_NON_WITNESS_UTXO = 0x00; @@ -31,11 +37,13 @@ static constexpr uint8_t PSBT_IN_WITNESSSCRIPT = 0x05; static constexpr uint8_t PSBT_IN_BIP32_DERIVATION = 0x06; static constexpr uint8_t PSBT_IN_SCRIPTSIG = 0x07; static constexpr uint8_t PSBT_IN_SCRIPTWITNESS = 0x08; +static constexpr uint8_t PSBT_IN_PROPRIETARY = 0xFC; // Output types static constexpr uint8_t PSBT_OUT_REDEEMSCRIPT = 0x00; static constexpr uint8_t PSBT_OUT_WITNESSSCRIPT = 0x01; static constexpr uint8_t PSBT_OUT_BIP32_DERIVATION = 0x02; +static constexpr uint8_t PSBT_OUT_PROPRIETARY = 0xFC; // The separator is 0x00. Reading this in means that the unserializer can interpret it // as a 0 length key which indicates that this is the separator. The separator has no value. @@ -45,6 +53,113 @@ static constexpr uint8_t PSBT_SEPARATOR = 0x00; // to prevent reading a stream indefinitely and running out of memory. const std::streamsize MAX_FILE_SIZE_PSBT = 100000000; // 100 MiB +// PSBT version number +static constexpr uint32_t PSBT_HIGHEST_VERSION = 0; + +/** A structure for PSBT proprietary types */ +struct PSBTProprietary +{ + uint64_t subtype; + std::vector<unsigned char> identifier; + std::vector<unsigned char> key; + std::vector<unsigned char> value; + + bool operator<(const PSBTProprietary &b) const { + return key < b.key; + } + bool operator==(const PSBTProprietary &b) const { + return key == b.key; + } +}; + +// Takes a stream and multiple arguments and serializes them as if first serialized into a vector and then into the stream +// The resulting output into the stream has the total serialized length of all of the objects followed by all objects concatenated with each other. +template<typename Stream, typename... X> +void SerializeToVector(Stream& s, const X&... args) +{ + WriteCompactSize(s, GetSerializeSizeMany(s.GetVersion(), args...)); + SerializeMany(s, args...); +} + +// Takes a stream and multiple arguments and unserializes them first as a vector then each object individually in the order provided in the arguments +template<typename Stream, typename... X> +void UnserializeFromVector(Stream& s, X&... args) +{ + size_t expected_size = ReadCompactSize(s); + size_t remaining_before = s.size(); + UnserializeMany(s, args...); + size_t remaining_after = s.size(); + if (remaining_after + expected_size != remaining_before) { + throw std::ios_base::failure("Size of value was not the stated size"); + } +} + +// Deserialize an individual HD keypath to a stream +template<typename Stream> +void DeserializeHDKeypath(Stream& s, KeyOriginInfo& hd_keypath) +{ + // Read in key path + uint64_t value_len = ReadCompactSize(s); + if (value_len % 4 || value_len == 0) { + throw std::ios_base::failure("Invalid length for HD key path"); + } + + s >> hd_keypath.fingerprint; + for (unsigned int i = 4; i < value_len; i += sizeof(uint32_t)) { + uint32_t index; + s >> index; + hd_keypath.path.push_back(index); + } +} + +// Deserialize HD keypaths into a map +template<typename Stream> +void DeserializeHDKeypaths(Stream& s, const std::vector<unsigned char>& key, std::map<CPubKey, KeyOriginInfo>& hd_keypaths) +{ + // Make sure that the key is the size of pubkey + 1 + if (key.size() != CPubKey::SIZE + 1 && key.size() != CPubKey::COMPRESSED_SIZE + 1) { + throw std::ios_base::failure("Size of key was not the expected size for the type BIP32 keypath"); + } + // Read in the pubkey from key + CPubKey pubkey(key.begin() + 1, key.end()); + if (!pubkey.IsFullyValid()) { + throw std::ios_base::failure("Invalid pubkey"); + } + if (hd_keypaths.count(pubkey) > 0) { + throw std::ios_base::failure("Duplicate Key, pubkey derivation path already provided"); + } + + KeyOriginInfo keypath; + DeserializeHDKeypath(s, keypath); + + // Add to map + hd_keypaths.emplace(pubkey, std::move(keypath)); +} + +// Serialize an individual HD keypath to a stream +template<typename Stream> +void SerializeHDKeypath(Stream& s, KeyOriginInfo hd_keypath) +{ + WriteCompactSize(s, (hd_keypath.path.size() + 1) * sizeof(uint32_t)); + s << hd_keypath.fingerprint; + for (const auto& path : hd_keypath.path) { + s << path; + } +} + +// Serialize HD keypaths to a stream from a map +template<typename Stream> +void SerializeHDKeypaths(Stream& s, const std::map<CPubKey, KeyOriginInfo>& hd_keypaths, CompactSizeWriter type) +{ + for (auto keypath_pair : hd_keypaths) { + if (!keypath_pair.first.IsValid()) { + throw std::ios_base::failure("Invalid CPubKey being serialized"); + } + SerializeToVector(s, type, Span{keypath_pair.first}); + SerializeHDKeypath(s, keypath_pair.second); + } +} + /** A structure for PSBTs which contain per-input information */ struct PSBTInput { @@ -57,6 +172,7 @@ struct PSBTInput std::map<CPubKey, KeyOriginInfo> hd_keypaths; std::map<CKeyID, SigPair> partial_sigs; std::map<std::vector<unsigned char>, std::vector<unsigned char>> unknown; + std::set<PSBTProprietary> m_proprietary; std::optional<int> sighash_type; bool IsNull() const; @@ -69,55 +185,61 @@ struct PSBTInput inline void Serialize(Stream& s) const { // Write the utxo if (non_witness_utxo) { - SerializeToVector(s, PSBT_IN_NON_WITNESS_UTXO); + SerializeToVector(s, CompactSizeWriter(PSBT_IN_NON_WITNESS_UTXO)); OverrideStream<Stream> os(&s, s.GetType(), s.GetVersion() | SERIALIZE_TRANSACTION_NO_WITNESS); SerializeToVector(os, non_witness_utxo); } if (!witness_utxo.IsNull()) { - SerializeToVector(s, PSBT_IN_WITNESS_UTXO); + SerializeToVector(s, CompactSizeWriter(PSBT_IN_WITNESS_UTXO)); SerializeToVector(s, witness_utxo); } if (final_script_sig.empty() && final_script_witness.IsNull()) { // Write any partial signatures for (auto sig_pair : partial_sigs) { - SerializeToVector(s, PSBT_IN_PARTIAL_SIG, Span{sig_pair.second.first}); + SerializeToVector(s, CompactSizeWriter(PSBT_IN_PARTIAL_SIG), Span{sig_pair.second.first}); s << sig_pair.second.second; } // Write the sighash type if (sighash_type != std::nullopt) { - SerializeToVector(s, PSBT_IN_SIGHASH); + SerializeToVector(s, CompactSizeWriter(PSBT_IN_SIGHASH)); SerializeToVector(s, *sighash_type); } // Write the redeem script if (!redeem_script.empty()) { - SerializeToVector(s, PSBT_IN_REDEEMSCRIPT); + SerializeToVector(s, CompactSizeWriter(PSBT_IN_REDEEMSCRIPT)); s << redeem_script; } // Write the witness script if (!witness_script.empty()) { - SerializeToVector(s, PSBT_IN_WITNESSSCRIPT); + SerializeToVector(s, CompactSizeWriter(PSBT_IN_WITNESSSCRIPT)); s << witness_script; } // Write any hd keypaths - SerializeHDKeypaths(s, hd_keypaths, PSBT_IN_BIP32_DERIVATION); + SerializeHDKeypaths(s, hd_keypaths, CompactSizeWriter(PSBT_IN_BIP32_DERIVATION)); } // Write script sig if (!final_script_sig.empty()) { - SerializeToVector(s, PSBT_IN_SCRIPTSIG); + SerializeToVector(s, CompactSizeWriter(PSBT_IN_SCRIPTSIG)); s << final_script_sig; } // write script witness if (!final_script_witness.IsNull()) { - SerializeToVector(s, PSBT_IN_SCRIPTWITNESS); + SerializeToVector(s, CompactSizeWriter(PSBT_IN_SCRIPTWITNESS)); SerializeToVector(s, final_script_witness.stack); } + // Write proprietary things + for (const auto& entry : m_proprietary) { + s << entry.key; + s << entry.value; + } + // Write unknown things for (auto& entry : unknown) { s << entry.first; @@ -147,8 +269,9 @@ struct PSBTInput break; } - // First byte of key is the type - unsigned char type = key[0]; + // Type is compact size uint at beginning of key + SpanReader skey(s.GetType(), s.GetVersion(), key); + uint64_t type = ReadCompactSize(skey); // Do stuff based on type switch(type) { @@ -250,6 +373,20 @@ struct PSBTInput UnserializeFromVector(s, final_script_witness.stack); break; } + case PSBT_IN_PROPRIETARY: + { + PSBTProprietary this_prop; + skey >> this_prop.identifier; + this_prop.subtype = ReadCompactSize(skey); + this_prop.key = key; + + if (m_proprietary.count(this_prop) > 0) { + throw std::ios_base::failure("Duplicate Key, proprietary key already found"); + } + s >> this_prop.value; + m_proprietary.insert(this_prop); + break; + } // Unknown stuff default: if (unknown.count(key) > 0) { @@ -281,6 +418,7 @@ struct PSBTOutput CScript witness_script; std::map<CPubKey, KeyOriginInfo> hd_keypaths; std::map<std::vector<unsigned char>, std::vector<unsigned char>> unknown; + std::set<PSBTProprietary> m_proprietary; bool IsNull() const; void FillSignatureData(SignatureData& sigdata) const; @@ -292,18 +430,24 @@ struct PSBTOutput inline void Serialize(Stream& s) const { // Write the redeem script if (!redeem_script.empty()) { - SerializeToVector(s, PSBT_OUT_REDEEMSCRIPT); + SerializeToVector(s, CompactSizeWriter(PSBT_OUT_REDEEMSCRIPT)); s << redeem_script; } // Write the witness script if (!witness_script.empty()) { - SerializeToVector(s, PSBT_OUT_WITNESSSCRIPT); + SerializeToVector(s, CompactSizeWriter(PSBT_OUT_WITNESSSCRIPT)); s << witness_script; } // Write any hd keypaths - SerializeHDKeypaths(s, hd_keypaths, PSBT_OUT_BIP32_DERIVATION); + SerializeHDKeypaths(s, hd_keypaths, CompactSizeWriter(PSBT_OUT_BIP32_DERIVATION)); + + // Write proprietary things + for (const auto& entry : m_proprietary) { + s << entry.key; + s << entry.value; + } // Write unknown things for (auto& entry : unknown) { @@ -334,8 +478,9 @@ struct PSBTOutput break; } - // First byte of key is the type - unsigned char type = key[0]; + // Type is compact size uint at beginning of key + SpanReader skey(s.GetType(), s.GetVersion(), key); + uint64_t type = ReadCompactSize(skey); // Do stuff based on type switch(type) { @@ -364,6 +509,20 @@ struct PSBTOutput DeserializeHDKeypaths(s, key, hd_keypaths); break; } + case PSBT_OUT_PROPRIETARY: + { + PSBTProprietary this_prop; + skey >> this_prop.identifier; + this_prop.subtype = ReadCompactSize(skey); + this_prop.key = key; + + if (m_proprietary.count(this_prop) > 0) { + throw std::ios_base::failure("Duplicate Key, proprietary key already found"); + } + s >> this_prop.value; + m_proprietary.insert(this_prop); + break; + } // Unknown stuff default: { if (unknown.count(key) > 0) { @@ -393,11 +552,17 @@ struct PSBTOutput struct PartiallySignedTransaction { std::optional<CMutableTransaction> tx; + // We use a vector of CExtPubKey in the event that there happens to be the same KeyOriginInfos for different CExtPubKeys + // Note that this map swaps the key and values from the serialization + std::map<KeyOriginInfo, std::set<CExtPubKey>> m_xpubs; std::vector<PSBTInput> inputs; std::vector<PSBTOutput> outputs; std::map<std::vector<unsigned char>, std::vector<unsigned char>> unknown; + std::optional<uint32_t> m_version; + std::set<PSBTProprietary> m_proprietary; bool IsNull() const; + uint32_t GetVersion() const; /** Merge psbt into this. The two psbts must have the same underlying CTransaction (i.e. the * same actual Bitcoin transaction.) Returns true if the merge succeeded, false otherwise. */ @@ -422,12 +587,36 @@ struct PartiallySignedTransaction s << PSBT_MAGIC_BYTES; // unsigned tx flag - SerializeToVector(s, PSBT_GLOBAL_UNSIGNED_TX); + SerializeToVector(s, CompactSizeWriter(PSBT_GLOBAL_UNSIGNED_TX)); // Write serialized tx to a stream OverrideStream<Stream> os(&s, s.GetType(), s.GetVersion() | SERIALIZE_TRANSACTION_NO_WITNESS); SerializeToVector(os, *tx); + // Write xpubs + for (const auto& xpub_pair : m_xpubs) { + for (const auto& xpub : xpub_pair.second) { + unsigned char ser_xpub[BIP32_EXTKEY_WITH_VERSION_SIZE]; + xpub.EncodeWithVersion(ser_xpub); + // Note that the serialization swaps the key and value + // The xpub is the key (for uniqueness) while the path is the value + SerializeToVector(s, PSBT_GLOBAL_XPUB, ser_xpub); + SerializeHDKeypath(s, xpub_pair.first); + } + } + + // PSBT version + if (GetVersion() > 0) { + SerializeToVector(s, CompactSizeWriter(PSBT_GLOBAL_VERSION)); + SerializeToVector(s, *m_version); + } + + // Write proprietary things + for (const auto& entry : m_proprietary) { + s << entry.key; + s << entry.value; + } + // Write the unknown things for (auto& entry : unknown) { s << entry.first; @@ -460,6 +649,9 @@ struct PartiallySignedTransaction // Used for duplicate key detection std::set<std::vector<unsigned char>> key_lookup; + // Track the global xpubs we have already seen. Just for sanity checking + std::set<CExtPubKey> global_xpubs; + // Read global data bool found_sep = false; while(!s.empty()) { @@ -474,8 +666,9 @@ struct PartiallySignedTransaction break; } - // First byte of key is the type - unsigned char type = key[0]; + // Type is compact size uint at beginning of key + SpanReader skey(s.GetType(), s.GetVersion(), key); + uint64_t type = ReadCompactSize(skey); // Do stuff based on type switch(type) { @@ -499,6 +692,65 @@ struct PartiallySignedTransaction } break; } + case PSBT_GLOBAL_XPUB: + { + if (key.size() != BIP32_EXTKEY_WITH_VERSION_SIZE + 1) { + throw std::ios_base::failure("Size of key was not the expected size for the type global xpub"); + } + // Read in the xpub from key + CExtPubKey xpub; + xpub.DecodeWithVersion(&key.data()[1]); + if (!xpub.pubkey.IsFullyValid()) { + throw std::ios_base::failure("Invalid pubkey"); + } + if (global_xpubs.count(xpub) > 0) { + throw std::ios_base::failure("Duplicate key, global xpub already provided"); + } + global_xpubs.insert(xpub); + // Read in the keypath from stream + KeyOriginInfo keypath; + DeserializeHDKeypath(s, keypath); + + // Note that we store these swapped to make searches faster. + // Serialization uses xpub -> keypath to enqure key uniqueness + if (m_xpubs.count(keypath) == 0) { + // Make a new set to put the xpub in + m_xpubs[keypath] = {xpub}; + } else { + // Insert xpub into existing set + m_xpubs[keypath].insert(xpub); + } + break; + } + case PSBT_GLOBAL_VERSION: + { + if (m_version) { + throw std::ios_base::failure("Duplicate Key, version already provided"); + } else if (key.size() != 1) { + throw std::ios_base::failure("Global version key is more than one byte type"); + } + uint32_t v; + UnserializeFromVector(s, v); + m_version = v; + if (*m_version > PSBT_HIGHEST_VERSION) { + throw std::ios_base::failure("Unsupported version number"); + } + break; + } + case PSBT_GLOBAL_PROPRIETARY: + { + PSBTProprietary this_prop; + skey >> this_prop.identifier; + this_prop.subtype = ReadCompactSize(skey); + this_prop.key = key; + + if (m_proprietary.count(this_prop) > 0) { + throw std::ios_base::failure("Duplicate Key, proprietary key already found"); + } + s >> this_prop.value; + m_proprietary.insert(this_prop); + break; + } // Unknown stuff default: { if (unknown.count(key) > 0) { diff --git a/src/pubkey.cpp b/src/pubkey.cpp index 956ff2b34a..b7dfb6d83f 100644 --- a/src/pubkey.cpp +++ b/src/pubkey.cpp @@ -352,6 +352,18 @@ void CExtPubKey::Decode(const unsigned char code[BIP32_EXTKEY_SIZE]) { if ((nDepth == 0 && (nChild != 0 || ReadLE32(vchFingerprint) != 0)) || !pubkey.IsFullyValid()) pubkey = CPubKey(); } +void CExtPubKey::EncodeWithVersion(unsigned char code[BIP32_EXTKEY_WITH_VERSION_SIZE]) const +{ + memcpy(code, version, 4); + Encode(&code[4]); +} + +void CExtPubKey::DecodeWithVersion(const unsigned char code[BIP32_EXTKEY_WITH_VERSION_SIZE]) +{ + memcpy(version, code, 4); + Decode(&code[4]); +} + bool CExtPubKey::Derive(CExtPubKey &out, unsigned int _nChild) const { out.nDepth = nDepth + 1; CKeyID id = pubkey.GetID(); diff --git a/src/pubkey.h b/src/pubkey.h index 2453c92d92..204e96f49e 100644 --- a/src/pubkey.h +++ b/src/pubkey.h @@ -17,6 +17,7 @@ #include <vector> const unsigned int BIP32_EXTKEY_SIZE = 74; +const unsigned int BIP32_EXTKEY_WITH_VERSION_SIZE = 78; /** A reference to a CKey: the Hash160 of its serialized public key */ class CKeyID : public uint160 @@ -129,6 +130,11 @@ public: return a.vch[0] < b.vch[0] || (a.vch[0] == b.vch[0] && memcmp(a.vch, b.vch, a.size()) < 0); } + friend bool operator>(const CPubKey& a, const CPubKey& b) + { + return a.vch[0] > b.vch[0] || + (a.vch[0] == b.vch[0] && memcmp(a.vch, b.vch, a.size()) > 0); + } //! Implement serialization, as if this was a byte vector. template <typename Stream> @@ -283,6 +289,7 @@ public: }; struct CExtPubKey { + unsigned char version[4]; unsigned char nDepth; unsigned char vchFingerprint[4]; unsigned int nChild; @@ -303,8 +310,20 @@ struct CExtPubKey { return !(a == b); } + friend bool operator<(const CExtPubKey &a, const CExtPubKey &b) + { + if (a.pubkey < b.pubkey) { + return true; + } else if (a.pubkey > b.pubkey) { + return false; + } + return a.chaincode < b.chaincode; + } + void Encode(unsigned char code[BIP32_EXTKEY_SIZE]) const; void Decode(const unsigned char code[BIP32_EXTKEY_SIZE]); + void EncodeWithVersion(unsigned char code[BIP32_EXTKEY_WITH_VERSION_SIZE]) const; + void DecodeWithVersion(const unsigned char code[BIP32_EXTKEY_WITH_VERSION_SIZE]); bool Derive(CExtPubKey& out, unsigned int nChild) const; }; diff --git a/src/rpc/rawtransaction.cpp b/src/rpc/rawtransaction.cpp index 78c1596219..12ade466da 100644 --- a/src/rpc/rawtransaction.cpp +++ b/src/rpc/rawtransaction.cpp @@ -3,6 +3,7 @@ // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. +#include <base58.h> #include <chain.h> #include <coins.h> #include <consensus/amount.h> @@ -1075,6 +1076,26 @@ static RPCHelpMan decodepsbt() { {RPCResult::Type::ELISION, "", "The layout is the same as the output of decoderawtransaction."}, }}, + {RPCResult::Type::ARR, "global_xpubs", "", + { + {RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::STR, "xpub", "The extended public key this path corresponds to"}, + {RPCResult::Type::STR_HEX, "master_fingerprint", "The fingerprint of the master key"}, + {RPCResult::Type::STR, "path", "The path"}, + }}, + }}, + {RPCResult::Type::NUM, "psbt_version", "The PSBT version number. Not to be confused with the unsigned transaction version"}, + {RPCResult::Type::ARR, "proprietary", "The global proprietary map", + { + {RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::STR_HEX, "identifier", "The hex string for the proprietary identifier"}, + {RPCResult::Type::NUM, "subtype", "The number for the subtype"}, + {RPCResult::Type::STR_HEX, "key", "The hex for the key"}, + {RPCResult::Type::STR_HEX, "value", "The hex for the value"}, + }}, + }}, {RPCResult::Type::OBJ_DYN, "unknown", "The unknown global fields", { {RPCResult::Type::STR_HEX, "key", "(key-value pair) An unknown key-value pair"}, @@ -1137,6 +1158,16 @@ static RPCHelpMan decodepsbt() { {RPCResult::Type::STR_HEX, "key", "(key-value pair) An unknown key-value pair"}, }}, + {RPCResult::Type::ARR, "proprietary", "The input proprietary map", + { + {RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::STR_HEX, "identifier", "The hex string for the proprietary identifier"}, + {RPCResult::Type::NUM, "subtype", "The number for the subtype"}, + {RPCResult::Type::STR_HEX, "key", "The hex for the key"}, + {RPCResult::Type::STR_HEX, "value", "The hex for the value"}, + }}, + }}, }}, }}, {RPCResult::Type::ARR, "outputs", "", @@ -1168,6 +1199,16 @@ static RPCHelpMan decodepsbt() { {RPCResult::Type::STR_HEX, "key", "(key-value pair) An unknown key-value pair"}, }}, + {RPCResult::Type::ARR, "proprietary", "The output proprietary map", + { + {RPCResult::Type::OBJ, "", "", + { + {RPCResult::Type::STR_HEX, "identifier", "The hex string for the proprietary identifier"}, + {RPCResult::Type::NUM, "subtype", "The number for the subtype"}, + {RPCResult::Type::STR_HEX, "key", "The hex for the key"}, + {RPCResult::Type::STR_HEX, "value", "The hex for the value"}, + }}, + }}, }}, }}, {RPCResult::Type::STR_AMOUNT, "fee", /*optional=*/true, "The transaction fee paid if all UTXOs slots in the PSBT have been filled."}, @@ -1194,6 +1235,38 @@ static RPCHelpMan decodepsbt() TxToUniv(CTransaction(*psbtx.tx), uint256(), tx_univ, false); result.pushKV("tx", tx_univ); + // Add the global xpubs + UniValue global_xpubs(UniValue::VARR); + for (std::pair<KeyOriginInfo, std::set<CExtPubKey>> xpub_pair : psbtx.m_xpubs) { + for (auto& xpub : xpub_pair.second) { + std::vector<unsigned char> ser_xpub; + ser_xpub.assign(BIP32_EXTKEY_WITH_VERSION_SIZE, 0); + xpub.EncodeWithVersion(ser_xpub.data()); + + UniValue keypath(UniValue::VOBJ); + keypath.pushKV("xpub", EncodeBase58Check(ser_xpub)); + keypath.pushKV("master_fingerprint", HexStr(Span<unsigned char>(xpub_pair.first.fingerprint, xpub_pair.first.fingerprint + 4))); + keypath.pushKV("path", WriteHDKeypath(xpub_pair.first.path)); + global_xpubs.push_back(keypath); + } + } + result.pushKV("global_xpubs", global_xpubs); + + // PSBT version + result.pushKV("psbt_version", static_cast<uint64_t>(psbtx.GetVersion())); + + // Proprietary + UniValue proprietary(UniValue::VARR); + for (const auto& entry : psbtx.m_proprietary) { + UniValue this_prop(UniValue::VOBJ); + this_prop.pushKV("identifier", HexStr(entry.identifier)); + this_prop.pushKV("subtype", entry.subtype); + this_prop.pushKV("key", HexStr(entry.key)); + this_prop.pushKV("value", HexStr(entry.value)); + proprietary.push_back(this_prop); + } + result.pushKV("proprietary", proprietary); + // Unknown data UniValue unknowns(UniValue::VOBJ); for (auto entry : psbtx.unknown) { @@ -1300,6 +1373,20 @@ static RPCHelpMan decodepsbt() in.pushKV("final_scriptwitness", txinwitness); } + // Proprietary + if (!input.m_proprietary.empty()) { + UniValue proprietary(UniValue::VARR); + for (const auto& entry : input.m_proprietary) { + UniValue this_prop(UniValue::VOBJ); + this_prop.pushKV("identifier", HexStr(entry.identifier)); + this_prop.pushKV("subtype", entry.subtype); + this_prop.pushKV("key", HexStr(entry.key)); + this_prop.pushKV("value", HexStr(entry.value)); + proprietary.push_back(this_prop); + } + in.pushKV("proprietary", proprietary); + } + // Unknown data if (input.unknown.size() > 0) { UniValue unknowns(UniValue::VOBJ); @@ -1344,6 +1431,20 @@ static RPCHelpMan decodepsbt() out.pushKV("bip32_derivs", keypaths); } + // Proprietary + if (!output.m_proprietary.empty()) { + UniValue proprietary(UniValue::VARR); + for (const auto& entry : output.m_proprietary) { + UniValue this_prop(UniValue::VOBJ); + this_prop.pushKV("identifier", HexStr(entry.identifier)); + this_prop.pushKV("subtype", entry.subtype); + this_prop.pushKV("key", HexStr(entry.key)); + this_prop.pushKV("value", HexStr(entry.value)); + proprietary.push_back(this_prop); + } + out.pushKV("proprietary", proprietary); + } + // Unknown data if (output.unknown.size() > 0) { UniValue unknowns(UniValue::VOBJ); @@ -1757,6 +1858,13 @@ static RPCHelpMan joinpsbts() for (unsigned int i = 0; i < psbt.tx->vout.size(); ++i) { merged_psbt.AddOutput(psbt.tx->vout[i], psbt.outputs[i]); } + for (auto& xpub_pair : psbt.m_xpubs) { + if (merged_psbt.m_xpubs.count(xpub_pair.first) == 0) { + merged_psbt.m_xpubs[xpub_pair.first] = xpub_pair.second; + } else { + merged_psbt.m_xpubs[xpub_pair.first].insert(xpub_pair.second.begin(), xpub_pair.second.end()); + } + } merged_psbt.unknown.insert(psbt.unknown.begin(), psbt.unknown.end()); } diff --git a/src/script/keyorigin.h b/src/script/keyorigin.h index 210395d177..c779872be2 100644 --- a/src/script/keyorigin.h +++ b/src/script/keyorigin.h @@ -18,6 +18,25 @@ struct KeyOriginInfo return std::equal(std::begin(a.fingerprint), std::end(a.fingerprint), std::begin(b.fingerprint)) && a.path == b.path; } + friend bool operator<(const KeyOriginInfo& a, const KeyOriginInfo& b) + { + // Compare the fingerprints lexicographically + int fpr_cmp = memcmp(a.fingerprint, b.fingerprint, 4); + if (fpr_cmp < 0) { + return true; + } else if (fpr_cmp > 0) { + return false; + } + // Compare the sizes of the paths, shorter is "less than" + if (a.path.size() < b.path.size()) { + return true; + } else if (a.path.size() > b.path.size()) { + return false; + } + // Paths same length, compare them lexicographically + return a.path < b.path; + } + SERIALIZE_METHODS(KeyOriginInfo, obj) { READWRITE(obj.fingerprint, obj.path); } void clear() diff --git a/src/script/sign.cpp b/src/script/sign.cpp index 3f7060879c..d33c847d98 100644 --- a/src/script/sign.cpp +++ b/src/script/sign.cpp @@ -9,6 +9,7 @@ #include <key.h> #include <policy/policy.h> #include <primitives/transaction.h> +#include <script/keyorigin.h> #include <script/signingprovider.h> #include <script/standard.h> #include <uint256.h> diff --git a/src/script/sign.h b/src/script/sign.h index 50525af332..7e3d5e80e4 100644 --- a/src/script/sign.h +++ b/src/script/sign.h @@ -12,8 +12,6 @@ #include <script/interpreter.h> #include <script/keyorigin.h> #include <script/standard.h> -#include <span.h> -#include <streams.h> class CKey; class CKeyID; @@ -84,80 +82,6 @@ struct SignatureData { void MergeSignatureData(SignatureData sigdata); }; -// Takes a stream and multiple arguments and serializes them as if first serialized into a vector and then into the stream -// The resulting output into the stream has the total serialized length of all of the objects followed by all objects concatenated with each other. -template<typename Stream, typename... X> -void SerializeToVector(Stream& s, const X&... args) -{ - WriteCompactSize(s, GetSerializeSizeMany(s.GetVersion(), args...)); - SerializeMany(s, args...); -} - -// Takes a stream and multiple arguments and unserializes them first as a vector then each object individually in the order provided in the arguments -template<typename Stream, typename... X> -void UnserializeFromVector(Stream& s, X&... args) -{ - size_t expected_size = ReadCompactSize(s); - size_t remaining_before = s.size(); - UnserializeMany(s, args...); - size_t remaining_after = s.size(); - if (remaining_after + expected_size != remaining_before) { - throw std::ios_base::failure("Size of value was not the stated size"); - } -} - -// Deserialize HD keypaths into a map -template<typename Stream> -void DeserializeHDKeypaths(Stream& s, const std::vector<unsigned char>& key, std::map<CPubKey, KeyOriginInfo>& hd_keypaths) -{ - // Make sure that the key is the size of pubkey + 1 - if (key.size() != CPubKey::SIZE + 1 && key.size() != CPubKey::COMPRESSED_SIZE + 1) { - throw std::ios_base::failure("Size of key was not the expected size for the type BIP32 keypath"); - } - // Read in the pubkey from key - CPubKey pubkey(key.begin() + 1, key.end()); - if (!pubkey.IsFullyValid()) { - throw std::ios_base::failure("Invalid pubkey"); - } - if (hd_keypaths.count(pubkey) > 0) { - throw std::ios_base::failure("Duplicate Key, pubkey derivation path already provided"); - } - - // Read in key path - uint64_t value_len = ReadCompactSize(s); - if (value_len % 4 || value_len == 0) { - throw std::ios_base::failure("Invalid length for HD key path"); - } - - KeyOriginInfo keypath; - s >> keypath.fingerprint; - for (unsigned int i = 4; i < value_len; i += sizeof(uint32_t)) { - uint32_t index; - s >> index; - keypath.path.push_back(index); - } - - // Add to map - hd_keypaths.emplace(pubkey, std::move(keypath)); -} - -// Serialize HD keypaths to a stream from a map -template<typename Stream> -void SerializeHDKeypaths(Stream& s, const std::map<CPubKey, KeyOriginInfo>& hd_keypaths, uint8_t type) -{ - for (auto keypath_pair : hd_keypaths) { - if (!keypath_pair.first.IsValid()) { - throw std::ios_base::failure("Invalid CPubKey being serialized"); - } - SerializeToVector(s, type, Span{keypath_pair.first}); - WriteCompactSize(s, (keypath_pair.second.path.size() + 1) * sizeof(uint32_t)); - s << keypath_pair.second.fingerprint; - for (const auto& path : keypath_pair.second.path) { - s << path; - } - } -} - /** Produce a script signature using a generic signature creator. */ bool ProduceSignature(const SigningProvider& provider, const BaseSignatureCreator& creator, const CScript& scriptPubKey, SignatureData& sigdata); diff --git a/src/script/signingprovider.h b/src/script/signingprovider.h index fbce61c6a9..b8b3e03dd3 100644 --- a/src/script/signingprovider.h +++ b/src/script/signingprovider.h @@ -8,12 +8,11 @@ #include <key.h> #include <pubkey.h> +#include <script/keyorigin.h> #include <script/script.h> #include <script/standard.h> #include <sync.h> -struct KeyOriginInfo; - /** An interface to be implemented by keystores that support signing. */ class SigningProvider { diff --git a/src/serialize.h b/src/serialize.h index edf10440c6..873361fe9e 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -527,6 +527,19 @@ struct CompactSizeFormatter } }; +class CompactSizeWriter +{ +protected: + uint64_t n; +public: + explicit CompactSizeWriter(uint64_t n_in) : n(n_in) { } + + template<typename Stream> + void Serialize(Stream &s) const { + WriteCompactSize<Stream>(s, n); + } +}; + template<size_t Limit> struct LimitedStringFormatter { diff --git a/src/test/fuzz/script_sign.cpp b/src/test/fuzz/script_sign.cpp index 79380bd9c9..1a42179724 100644 --- a/src/test/fuzz/script_sign.cpp +++ b/src/test/fuzz/script_sign.cpp @@ -5,6 +5,7 @@ #include <chainparams.h> #include <chainparamsbase.h> #include <key.h> +#include <psbt.h> #include <pubkey.h> #include <script/keyorigin.h> #include <script/sign.h> @@ -43,7 +44,7 @@ FUZZ_TARGET_INIT(script_sign, initialize_script_sign) } catch (const std::ios_base::failure&) { } CDataStream serialized{SER_NETWORK, PROTOCOL_VERSION}; - SerializeHDKeypaths(serialized, hd_keypaths, fuzzed_data_provider.ConsumeIntegral<uint8_t>()); + SerializeHDKeypaths(serialized, hd_keypaths, CompactSizeWriter(fuzzed_data_provider.ConsumeIntegral<uint8_t>())); } { @@ -61,7 +62,7 @@ FUZZ_TARGET_INIT(script_sign, initialize_script_sign) } CDataStream serialized{SER_NETWORK, PROTOCOL_VERSION}; try { - SerializeHDKeypaths(serialized, hd_keypaths, fuzzed_data_provider.ConsumeIntegral<uint8_t>()); + SerializeHDKeypaths(serialized, hd_keypaths, CompactSizeWriter(fuzzed_data_provider.ConsumeIntegral<uint8_t>())); } catch (const std::ios_base::failure&) { } std::map<CPubKey, KeyOriginInfo> deserialized_hd_keypaths; diff --git a/test/functional/data/rpc_psbt.json b/test/functional/data/rpc_psbt.json index 0f6cd97fd8..dd24dce94d 100644 --- a/test/functional/data/rpc_psbt.json +++ b/test/functional/data/rpc_psbt.json @@ -19,6 +19,7 @@ "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", @@ -34,7 +35,11 @@ "cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQD9pQEBAAAAAAECiaPHHqtNIOA3G7ukzGmPopXJRjr6Ljl/hTPMti+VZ+UBAAAAFxYAFL4Y0VKpsBIDna89p95PUzSe7LmF/////4b4qkOnHf8USIk6UwpyN+9rRgi7st0tAXHmOuxqSJC0AQAAABcWABT+Pp7xp0XpdNkCxDVZQ6vLNL1TU/////8CAMLrCwAAAAAZdqkUhc/xCX/Z4Ai7NK9wnGIZeziXikiIrHL++E4sAAAAF6kUM5cluiHv1irHU6m80GfWx6ajnQWHAkcwRAIgJxK+IuAnDzlPVoMR3HyppolwuAJf3TskAinwf4pfOiQCIAGLONfc0xTnNMkna9b7QPZzMlvEuqFEyADS8vAtsnZcASED0uFWdJQbrUqZY3LLh+GFbTZSYG2YVi/jnF6efkE/IQUCSDBFAiEA0SuFLYXc2WHS9fSrZgZU327tzHlMDDPOXMMJ/7X85Y0CIGczio4OFyXBl/saiK9Z9R5E5CVbIBZ8hoQDHAXR8lkqASECI7cr7vCWXRC+B3jv7NYfysb3mk6haTkzgHNEZPhPKrMAAAAAAQMEAQAAAAAAAA==", "cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEA3wIAAAABJoFxNx7f8oXpN63upLN7eAAMBWbLs61kZBcTykIXG/YAAAAAakcwRAIgcLIkUSPmv0dNYMW1DAQ9TGkaXSQ18Jo0p2YqncJReQoCIAEynKnazygL3zB0DsA5BCJCLIHLRYOUV663b8Eu3ZWzASECZX0RjTNXuOD0ws1G23s59tnDjZpwq8ubLeXcjb/kzjH+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIACICAurVlmh8qAYEPtw94RbN8p1eklfBls0FXPaYyNAr8k6ZELSmumcAAACAAAAAgAIAAIAAIgIDlPYr6d8ZlSxVh3aK63aYBhrSxKJciU9H2MFitNchPQUQtKa6ZwAAAIABAACAAgAAgAA=", "cHNidP8BAFUCAAAAASeaIyOl37UfxF8iD6WLD8E+HjNCeSqF1+Ns1jM7XLw5AAAAAAD/////AaBa6gsAAAAAGXapFP/pwAYQl8w7Y28ssEYPpPxCfStFiKwAAAAAAAEBIJVe6gsAAAAAF6kUY0UgD2jRieGtwN8cTRbqjxTA2+uHIgIDsTQcy6doO2r08SOM1ul+cWfVafrEfx5I1HVBhENVvUZGMEMCIAQktY7/qqaU4VWepck7v9SokGQiQFXN8HC2dxRpRC0HAh9cjrD+plFtYLisszrWTt5g6Hhb+zqpS5m9+GFR25qaAQEEIgAgdx/RitRZZm3Unz1WTj28QvTIR3TjYK2haBao7UiNVoEBBUdSIQOxNBzLp2g7avTxI4zW6X5xZ9Vp+sR/HkjUdUGEQ1W9RiED3lXR4drIBeP4pYwfv5uUwC89uq/hJ/78pJlfJvggg71SriIGA7E0HMunaDtq9PEjjNbpfnFn1Wn6xH8eSNR1QYRDVb1GELSmumcAAACAAAAAgAQAAIAiBgPeVdHh2sgF4/iljB+/m5TALz26r+En/vykmV8m+CCDvRC0prpnAAAAgAAAAIAFAACAAAA=", - "cHNidP8BACoCAAAAAAFAQg8AAAAAABepFG6Rty1Vk+fUOR4v9E6R6YXDFkHwhwAAAAAAAA==" + "cHNidP8BACoCAAAAAAFAQg8AAAAAABepFG6Rty1Vk+fUOR4v9E6R6YXDFkHwhwAAAAAAAA==", + "cHNidP8B+wQAAAAAAQB1AgAAAAEmgXE3Ht/yhek3re6ks3t4AAwFZsuzrWRkFxPKQhcb9gAAAAAA/v///wLT3/UFAAAAABl2qRTQxZkDxbrChodg6Q/VIaRmWqdlIIisAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4ezLhMAAAEA/aUBAQAAAAABAomjxx6rTSDgNxu7pMxpj6KVyUY6+i45f4UzzLYvlWflAQAAABcWABS+GNFSqbASA52vPafeT1M0nuy5hf////+G+KpDpx3/FEiJOlMKcjfva0YIu7LdLQFx5jrsakiQtAEAAAAXFgAU/j6e8adF6XTZAsQ1WUOryzS9U1P/////AgDC6wsAAAAAGXapFIXP8Ql/2eAIuzSvcJxiGXs4l4pIiKxy/vhOLAAAABepFDOXJboh79Yqx1OpvNBn1semo50FhwJHMEQCICcSviLgJw85T1aDEdx8qaaJcLgCX907JAIp8H+KXzokAiABizjX3NMU5zTJJ2vW+0D2czJbxLqhRMgA0vLwLbJ2XAEhA9LhVnSUG61KmWNyy4fhhW02UmBtmFYv45xenn5BPyEFAkgwRQIhANErhS2F3Nlh0vX0q2YGVN9u7cx5TAwzzlzDCf+1/OWNAiBnM4qODhclwZf7GoivWfUeROQlWyAWfIaEAxwF0fJZKgEhAiO3K+7wll0Qvgd47+zWH8rG95pOoWk5M4BzRGT4TyqzAAAAAAAAAA==", + "cHNidP8BAHUCAAAAASaBcTce3/KF6Tet7qSze3gADAVmy7OtZGQXE8pCFxv2AAAAAAD+////AtPf9QUAAAAAGXapFNDFmQPFusKGh2DpD9UhpGZap2UgiKwA4fUFAAAAABepFDVF5uM7gyxHBQ8k0+65PJwDlIvHh7MuEwAD/AAAAaoF/AKqqgABqgABAP2lAQEAAAAAAQKJo8ceq00g4Dcbu6TMaY+ilclGOvouOX+FM8y2L5Vn5QEAAAAXFgAUvhjRUqmwEgOdrz2n3k9TNJ7suYX/////hviqQ6cd/xRIiTpTCnI372tGCLuy3S0BceY67GpIkLQBAAAAFxYAFP4+nvGnRel02QLENVlDq8s0vVNT/////wIAwusLAAAAABl2qRSFz/EJf9ngCLs0r3CcYhl7OJeKSIiscv74TiwAAAAXqRQzlyW6Ie/WKsdTqbzQZ9bHpqOdBYcCRzBEAiAnEr4i4CcPOU9WgxHcfKmmiXC4Al/dOyQCKfB/il86JAIgAYs419zTFOc0ySdr1vtA9nMyW8S6oUTIANLy8C2ydlwBIQPS4VZ0lButSpljcsuH4YVtNlJgbZhWL+OcXp5+QT8hBQJIMEUCIQDRK4UthdzZYdL19KtmBlTfbu3MeUwMM85cwwn/tfzljQIgZzOKjg4XJcGX+xqIr1n1HkTkJVsgFnyGhAMcBdHyWSoBIQIjtyvu8JZdEL4HeO/s1h/KxveaTqFpOTOAc0Rk+E8qswAAAAAF/AKqqgEBqwAABfwCqqoCAawA", + "cHNidP8BAFICAAAAAZ38ZijCbFiZ/hvT3DOGZb/VXXraEPYiCXPfLTht7BJ2AQAAAAD/////AfA9zR0AAAAAFgAUezoAv9wU0neVwrdJAdCdpu8TNXkAAAAATwEENYfPAto/0AiAAAAAlwSLGtBEWx7IJ1UXcnyHtOTrwYogP/oPlMAVZr046QADUbdDiH7h1A3DKmBDck8tZFmztaTXPa7I+64EcvO8Q+IM2QxqT64AAIAAAACATwEENYfPAto/0AiAAAABuQRSQnE5zXjCz/JES+NTzVhgXj5RMoXlKLQH+uP2FzUD0wpel8itvFV9rCrZp+OcFyLrrGnmaLbyZnzB1nHIPKsM2QxqT64AAIABAACAAAEBKwBlzR0AAAAAIgAgLFSGEmxJeAeagU4TcV1l82RZ5NbMre0mbQUIZFuvpjIBBUdSIQKdoSzbWyNWkrkVNq/v5ckcOrlHPY5DtTODarRWKZyIcSEDNys0I07Xz5wf6l0F1EFVeSe+lUKxYusC4ass6AIkwAtSriIGAp2hLNtbI1aSuRU2r+/lyRw6uUc9jkO1M4NqtFYpnIhxENkMak+uAACAAAAAgAAAAAAiBgM3KzQjTtfPnB/qXQXUQVV5J76VQrFi6wLhqyzoAiTACxDZDGpPrgAAgAEAAIAAAAAAACICA57/H1R6HV+S36K6evaslxpL0DukpzSwMVaiVritOh75EO3kXMUAAACAAAAAgAEAAIAA", + "cHNidP8BAJoCAAAAAljoeiG1ba8MI76OcHBFbDNvfLqlyHV5JPVFiHuyq911AAAAAAD/////g40EJ9DsZQpoqka7CwmK6kQiwHGyyng1Kgd5WdB86h0BAAAAAP////8CcKrwCAAAAAAWABTYXCtx0AYLCcmIauuBXlCZHdoSTQDh9QUAAAAAFgAUAK6pouXw+HaliN9VRuh0LR2HAI8AAAAATwEENYfPAAAAAAAAAAAAG3t93NmzqdwlifBjtWBRnFrHYkoMdmriSG1s74PiZ8ID3+4wNJ18fPeMDsRRe9iTAopsKogDQfxLmL6Kgj07xScE2QxqTwAAAAAA" ], "creator" : [ { @@ -136,4 +141,4 @@ "result" : "0200000000010258e87a21b56daf0c23be8e7070456c336f7cbaa5c8757924f545887bb2abdd7500000000da00473044022074018ad4180097b873323c0015720b3684cc8123891048e7dbcd9b55ad679c99022073d369b740e3eb53dcefa33823c8070514ca55a7dd9544f157c167913261118c01483045022100f61038b308dc1da865a34852746f015772934208c6d24454393cd99bdf2217770220056e675a675a6d0a02b85b14e5e29074d8a25a9b5760bea2816f661910a006ea01475221029583bf39ae0a609747ad199addd634fa6108559d6c5cd39b4c2183f1ab96e07f2102dab61ff49a14db6a7d02b0cd1fbb78fc4b18312b5b4e54dae4dba2fbfef536d752aeffffffff838d0427d0ec650a68aa46bb0b098aea4422c071b2ca78352a077959d07cea1d01000000232200208c2353173743b595dfb4a07b72ba8e42e3797da74e87fe7d9d7497e3b2028903ffffffff0270aaf00800000000160014d85c2b71d0060b09c9886aeb815e50991dda124d00e1f5050000000016001400aea9a2e5f0f876a588df5546e8742d1d87008f000400473044022062eb7a556107a7c73f45ac4ab5a1dddf6f7075fb1275969a7f383efff784bcb202200c05dbb7470dbf2f08557dd356c7325c1ed30913e996cd3840945db12228da5f01473044022065f45ba5998b59a27ffe1a7bed016af1f1f90d54b3aa8f7450aa5f56a25103bd02207f724703ad1edb96680b284b56d4ffcb88f7fb759eabbe08aa30f29b851383d20147522103089dc10c7ac6db54f91329af617333db388cead0c231f723379d1b99030b02dc21023add904f3d6dcf59ddb906b0dee23529b7ffb9ed50e5e86151926860221f0e7352ae00000000" } ] -}
\ No newline at end of file +} |