Skip to content

Commit

Permalink
Add PubKeyDestination for P2PK scripts
Browse files Browse the repository at this point in the history
P2PK scripts are not PKHash destinations, they should have their own
type.

This also results in no longer showing a p2pkh address for p2pk outputs.
However for backwards compatibility, ListCoinst will still do this
conversion.
  • Loading branch information
achow101 committed Sep 12, 2023
1 parent 1a98a51 commit 07d3bdf
Show file tree
Hide file tree
Showing 11 changed files with 85 additions and 18 deletions.
31 changes: 25 additions & 6 deletions src/addresstype.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,12 @@ bool ExtractDestination(const CScript& scriptPubKey, CTxDestination& addressRet)
switch (whichType) {
case TxoutType::PUBKEY: {
CPubKey pubKey(vSolutions[0]);
if (!pubKey.IsValid())
return false;

addressRet = PKHash(pubKey);
return true;
if (!pubKey.IsValid()) {
addressRet = CNoDestination(scriptPubKey);
} else {
addressRet = PubKeyDestination(pubKey);
}
return false;
}
case TxoutType::PUBKEYHASH: {
addressRet = PKHash(uint160(vSolutions[0]));
Expand Down Expand Up @@ -108,6 +109,11 @@ class CScriptVisitor
return dest.GetScript();
}

CScript operator()(const PubKeyDestination& dest) const
{
return CScript() << ToByteVector(dest.GetPubKey()) << OP_CHECKSIG;
}

CScript operator()(const PKHash& keyID) const
{
return CScript() << OP_DUP << OP_HASH160 << ToByteVector(keyID) << OP_EQUALVERIFY << OP_CHECKSIG;
Expand Down Expand Up @@ -138,6 +144,19 @@ class CScriptVisitor
return CScript() << CScript::EncodeOP_N(id.GetWitnessVersion()) << id.GetWitnessProgram();
}
};

class ValidDestinationVisitor
{
public:
bool operator()(const CNoDestination& dest) const { return false; }
bool operator()(const PubKeyDestination& dest) const { return false; }
bool operator()(const PKHash& dest) const { return true; }
bool operator()(const ScriptHash& dest) const { return true; }
bool operator()(const WitnessV0KeyHash& dest) const { return true; }
bool operator()(const WitnessV0ScriptHash& dest) const { return true; }
bool operator()(const WitnessV1Taproot& dest) const { return true; }
bool operator()(const WitnessUnknown& dest) const { return true; }
};
} // namespace

CScript GetScriptForDestination(const CTxDestination& dest)
Expand All @@ -146,5 +165,5 @@ CScript GetScriptForDestination(const CTxDestination& dest)
}

bool IsValidDestination(const CTxDestination& dest) {
return dest.index() != 0;
return std::visit(ValidDestinationVisitor(), dest);
}
22 changes: 18 additions & 4 deletions src/addresstype.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,19 @@ class CNoDestination {
friend bool operator<(const CNoDestination& a, const CNoDestination& b) { return a.GetScript() < b.GetScript(); }
};

struct PubKeyDestination {
private:
CPubKey m_pubkey;

public:
PubKeyDestination(const CPubKey& pubkey) : m_pubkey(pubkey) {}

const CPubKey& GetPubKey() const LIFETIMEBOUND { return m_pubkey; }

friend bool operator==(const PubKeyDestination& a, const PubKeyDestination& b) { return a.GetPubKey() == b.GetPubKey(); }
friend bool operator<(const PubKeyDestination& a, const PubKeyDestination& b) { return a.GetPubKey() < b.GetPubKey(); }
};

struct PKHash : public BaseHash<uint160>
{
PKHash() : BaseHash() {}
Expand Down Expand Up @@ -103,6 +116,7 @@ struct WitnessUnknown
/**
* A txout script categorized into standard templates.
* * CNoDestination: Optionally a script, no corresponding address.
* * PubKeyDestination: TxoutType::PUBKEY (P2PK), no corresponding address
* * PKHash: TxoutType::PUBKEYHASH destination (P2PKH address)
* * ScriptHash: TxoutType::SCRIPTHASH destination (P2SH address)
* * WitnessV0ScriptHash: TxoutType::WITNESS_V0_SCRIPTHASH destination (P2WSH address)
Expand All @@ -111,9 +125,9 @@ struct WitnessUnknown
* * WitnessUnknown: TxoutType::WITNESS_UNKNOWN destination (P2W??? address)
* A CTxDestination is the internal data type encoded in a bitcoin address
*/
using CTxDestination = std::variant<CNoDestination, PKHash, ScriptHash, WitnessV0ScriptHash, WitnessV0KeyHash, WitnessV1Taproot, WitnessUnknown>;
using CTxDestination = std::variant<CNoDestination, PubKeyDestination, PKHash, ScriptHash, WitnessV0ScriptHash, WitnessV0KeyHash, WitnessV1Taproot, WitnessUnknown>;

/** Check whether a CTxDestination is a CNoDestination. */
/** Check whether a CTxDestination corresponds to one with an address. */
bool IsValidDestination(const CTxDestination& dest);

/**
Expand All @@ -123,8 +137,8 @@ bool IsValidDestination(const CTxDestination& dest);
* is assigned to addressRet.
* For all other scripts. addressRet is assigned as a CNoDestination containing the scriptPubKey.
*
* Returns true for standard destinations - P2PK, P2PKH, P2SH, P2WPKH, P2WSH, and P2TR scripts.
* Returns false for non-standard destinations - P2PK with invalid pubkeys, P2W???, bare multisig, null data, and nonstandard scripts.
* Returns true for standard destinations with addresses - P2PKH, P2SH, P2WPKH, P2WSH, P2TR and P2W??? scripts.
* Returns false for non-standard destinations and those without addresses - P2PK, bare multisig, null data, and nonstandard scripts.
*/
bool ExtractDestination(const CScript& scriptPubKey, CTxDestination& addressRet);

Expand Down
1 change: 1 addition & 0 deletions src/key_io.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ class DestinationEncoder
}

std::string operator()(const CNoDestination& no) const { return {}; }
std::string operator()(const PubKeyDestination& pk) const { return {}; }
};

CTxDestination DecodeDestination(const std::string& str, const CChainParams& params, std::string& error_str, std::vector<int>* error_locations)
Expand Down
5 changes: 5 additions & 0 deletions src/rpc/output_script.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,11 @@ static RPCHelpMan deriveaddresses()
for (const CScript& script : scripts) {
CTxDestination dest;
if (!ExtractDestination(script, dest)) {
// ExtractDestination no longer returns true for P2PK since it doesn't have a corresponding address
// However combo will output P2PK and should just ignore that script
if (scripts.size() > 1 && std::get_if<PubKeyDestination>(&dest)) {
continue;
}
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, "Descriptor does not have a corresponding address");
}

Expand Down
5 changes: 5 additions & 0 deletions src/rpc/util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,11 @@ class DescribeAddressVisitor
return UniValue(UniValue::VOBJ);
}

UniValue operator()(const PubKeyDestination& dest) const
{
return UniValue(UniValue::VOBJ);
}

UniValue operator()(const PKHash& keyID) const
{
UniValue obj(UniValue::VOBJ);
Expand Down
9 changes: 6 additions & 3 deletions src/test/fuzz/script.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -149,13 +149,16 @@ FUZZ_TARGET(script, .init = initialize_script)
const CTxDestination tx_destination_2{ConsumeTxDestination(fuzzed_data_provider)};
const std::string encoded_dest{EncodeDestination(tx_destination_1)};
const UniValue json_dest{DescribeAddress(tx_destination_1)};
Assert(tx_destination_1 == DecodeDestination(encoded_dest));
(void)GetKeyForDestination(/*store=*/{}, tx_destination_1);
const CScript dest{GetScriptForDestination(tx_destination_1)};
const bool valid{IsValidDestination(tx_destination_1)};
Assert(dest.empty() != valid);

Assert(valid == IsValidDestinationString(encoded_dest));
if (!std::get_if<PubKeyDestination>(&tx_destination_1)) {
// Only try to round trip non-pubkey destinations since PubKeyDestination has no encoding
Assert(dest.empty() != valid);
Assert(tx_destination_1 == DecodeDestination(encoded_dest));
Assert(valid == IsValidDestinationString(encoded_dest));
}

(void)(tx_destination_1 < tx_destination_2);
if (tx_destination_1 == tx_destination_2) {
Expand Down
9 changes: 9 additions & 0 deletions src/test/fuzz/util.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,15 @@ CTxDestination ConsumeTxDestination(FuzzedDataProvider& fuzzed_data_provider) no
[&] {
tx_destination = CNoDestination{};
},
[&] {
bool compressed = fuzzed_data_provider.ConsumeBool();
CPubKey pk{ConstructPubKeyBytes(
fuzzed_data_provider,
ConsumeFixedLengthByteVector(fuzzed_data_provider, (compressed ? CPubKey::COMPRESSED_SIZE : CPubKey::SIZE)),
compressed
)};
tx_destination = PubKeyDestination{pk};
},
[&] {
tx_destination = PKHash{ConsumeUInt160(fuzzed_data_provider)};
},
Expand Down
4 changes: 2 additions & 2 deletions src/test/script_standard_tests.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,8 @@ BOOST_AUTO_TEST_CASE(script_standard_ExtractDestination)
// TxoutType::PUBKEY
s.clear();
s << ToByteVector(pubkey) << OP_CHECKSIG;
BOOST_CHECK(ExtractDestination(s, address));
BOOST_CHECK(std::get<PKHash>(address) == PKHash(pubkey));
BOOST_CHECK(!ExtractDestination(s, address));
BOOST_CHECK(std::get<PubKeyDestination>(address) == PubKeyDestination(pubkey));

// TxoutType::PUBKEYHASH
s.clear();
Expand Down
1 change: 1 addition & 0 deletions src/wallet/rpc/addresses.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -427,6 +427,7 @@ class DescribeWalletAddressVisitor
explicit DescribeWalletAddressVisitor(const SigningProvider* _provider) : provider(_provider) {}

UniValue operator()(const CNoDestination& dest) const { return UniValue(UniValue::VOBJ); }
UniValue operator()(const PubKeyDestination& dest) const { return UniValue(UniValue::VOBJ); }

UniValue operator()(const PKHash& pkhash) const
{
Expand Down
11 changes: 9 additions & 2 deletions src/wallet/spend.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -490,8 +490,15 @@ std::map<CTxDestination, std::vector<COutput>> ListCoins(const CWallet& wallet)
coins_params.skip_locked = false;
for (const COutput& coin : AvailableCoins(wallet, &coin_control, /*feerate=*/std::nullopt, coins_params).All()) {
CTxDestination address;
if ((coin.spendable || (wallet.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS) && coin.solvable)) &&
ExtractDestination(FindNonChangeParentOutput(wallet, coin.outpoint).scriptPubKey, address)) {
if ((coin.spendable || (wallet.IsWalletFlagSet(WALLET_FLAG_DISABLE_PRIVATE_KEYS) && coin.solvable))) {
if (!ExtractDestination(FindNonChangeParentOutput(wallet, coin.outpoint).scriptPubKey, address)) {
// For backwards compatibility, we convert P2PK output scripts into PKHash destinations
if (auto pk_dest = std::get_if<PubKeyDestination>(&address)) {
address = PKHash(pk_dest->GetPubKey());
} else {
continue;
}
}
result[address].emplace_back(coin);
}
}
Expand Down
5 changes: 4 additions & 1 deletion test/functional/rpc_deriveaddresses.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ def run_test(self):
assert_raises_rpc_error(-8, "Range should be greater or equal than 0", self.nodes[0].deriveaddresses, descsum_create("wpkh(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/*)"), [-1, 0])

combo_descriptor = descsum_create("combo(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK/1/1/0)")
assert_equal(self.nodes[0].deriveaddresses(combo_descriptor), ["mtfUoUax9L4tzXARpw1oTGxWyoogp52KhJ", "mtfUoUax9L4tzXARpw1oTGxWyoogp52KhJ", address, "2NDvEwGfpEqJWfybzpKPHF2XH3jwoQV3D7x"])
assert_equal(self.nodes[0].deriveaddresses(combo_descriptor), ["mtfUoUax9L4tzXARpw1oTGxWyoogp52KhJ", address, "2NDvEwGfpEqJWfybzpKPHF2XH3jwoQV3D7x"])

# P2PK does not have a valid address
assert_raises_rpc_error(-5, "Descriptor does not have a corresponding address", self.nodes[0].deriveaddresses, descsum_create("pk(tprv8ZgxMBicQKsPd7Uf69XL1XwhmjHopUGep8GuEiJDZmbQz6o58LninorQAfcKZWARbtRtfnLcJ5MQ2AtHcQJCCRUcMRvmDUjyEmNUWwx8UbK)"))

# Before #26275, bitcoind would crash when deriveaddresses was
# called with derivation index 2147483647, which is the maximum
Expand Down

0 comments on commit 07d3bdf

Please sign in to comment.