mirror of
https://github.com/bitcoin/bitcoin.git
synced 2025-01-10 03:47:29 -03:00
Merge bitcoin/bitcoin#17034: [BIP 174] PSBT version, proprietary, and xpub fields
81521173ba
Merge global xpubs in joinpsbts and combinepsbts (Andrew Chow)d8043ddf64
Add global xpub test vectors from BIP (Andrew Chow)35670df866
Add global_xpubs to decodepsbt (Andrew Chow)903848562e
Implement serializations for PSBT_GLOBAL_XPUB (Andrew Chow)c5c63b8e4f
Implement operator< for KeyOriginInfo and CExtPubKey (Andrew Chow)d3dbb16168
Separate individual HD Keypath serialization into separate functions (Andrew Chow)a69332fd89
Store version bytes and be able to serialize them in CExtPubKey (Andrew Chow)5fdaf6a2ad
moveonly: Move (Un)Serialize(To/From)Vector, (De)SerializeHDKeypaths to psbt module (Andrew Chow)94065cc6c5
Test for proprietary field (Andrew Chow)a4cf810174
Output proprietary type info in decodepsbt (Andrew Chow)aebe758e54
Implement PSBT proprietary type (Andrew Chow)10ba0b593d
Output psbt version in decodepsbt (Andrew Chow)df84fa99c5
Add GetVersion helper to PSBT (Andrew Chow)c3eb416b88
Implement PSBT versions (Andrew Chow)3235847473
Types are compact size uints (Andrew Chow) Pull request description: Implements the changes to BIP 174 proposed in https://github.com/bitcoin/bips/pull/849 and https://github.com/bitcoin/bips/pull/784 Implements `PSBT_GLOBAL_VERSION`, `PSBT_GLOBAL_PROPRIETARY`, `PSBT_IN_PROPRIETARY`, `PSBT_OUT_PROPRIETARY`, and `PSBT_GLOBAL_XPUB`. The `PSBT_GLOBAL_XPUB` changes are merged in from #16463. Also includes the test vectors added to BIP 174 for these fields. A number of additional changes to keypath and xpub serialization are made to support `PSBT_GLOBAL_XPUB`. ACKs for top commit: laanwj: Code review ACK81521173ba
Tree-SHA512: bd71c3f26030fc23824e76a30d3d346a753e1db224ecee163d6813348feb52d3f4cf4e739a4699e2cff381197ce2a7ea4a92a054f2c3e1db579e91e92a0945e0
This commit is contained in:
commit
50c502f54a
12 changed files with 469 additions and 101 deletions
15
src/psbt.cpp
15
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;
|
||||
}
|
||||
|
|
290
src/psbt.h
290
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) {
|
||||
|
|
|
@ -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();
|
||||
|
|
19
src/pubkey.h
19
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;
|
||||
};
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
"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",
|
||||
|
@ -34,7 +35,11 @@
|
|||
"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"
|
||||
],
|
||||
"creator" : [
|
||||
{
|
||||
|
|
Loading…
Reference in a new issue