Skip to content

Commit

Permalink
Merge #28246: wallet: Use CTxDestination in CRecipient instead of jus…
Browse files Browse the repository at this point in the history
…t scriptPubKey

ad0c469 wallet: Use CTxDestination in CRecipient rather than scriptPubKey (Andrew Chow)
07d3bdf Add PubKeyDestination for P2PK scripts (Andrew Chow)
1a98a51 Allow CNoDestination to represent a raw script (Andrew Chow)
8dd0670 Make WitnessUnknown members private (Andrew Chow)

Pull request description:

  For silent payments, we want to provide a `SilentPaymentsDestination` to be used as the recipient, which requires `CRecipient` to use something other than just the `scriptPubKey` as we cannot know the output script for a silent payment prior to transaction creation. `CTxDestination` seems like the obvious place to add a `SilentPaymentsDestination` as it is our internal representation of an address.

  In order to still allow paying to arbitrary scriptPubKeys (e.g. for data carrier outputs, or the user hand crafted a raw transaction that they have given to `fundrawtransaction`), `CNoDestination` is changed to contain raw scripts.

  Additionally, P2PK scripts are now interpreted as a new `PubKeyDestination` rather than `PKHash`. This results in some things that would have given an address for P2PK scripts to no longer do so. This is arguably more correct.

  `ExtractDestination`'s behavior is slightly changed for the above. It now returns `true` for those destinations that have addresses, so P2PK scripts now result in `false`. Even though it returns false for `CNoDestination`, the script will now be included in that `CNoDestination`.

  Builds on #28244

ACKs for top commit:
  josibake:
    ACK ad0c469

Tree-SHA512: ef3f8f3c7284779d9806c77c85b21caf910a79a1f7e7f1b51abcc0d7e074f14e00abf30f625a13075e41d94dad6202c10ddff462c0ee74c2ca4aab585b145a52
  • Loading branch information
fanquake committed Sep 19, 2023
2 parents 737aac8 + ad0c469 commit 53313c4
Show file tree
Hide file tree
Showing 19 changed files with 160 additions and 90 deletions.
42 changes: 29 additions & 13 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 @@ -87,16 +88,13 @@ bool ExtractDestination(const CScript& scriptPubKey, CTxDestination& addressRet)
return true;
}
case TxoutType::WITNESS_UNKNOWN: {
WitnessUnknown unk;
unk.version = vSolutions[0][0];
std::copy(vSolutions[1].begin(), vSolutions[1].end(), unk.program);
unk.length = vSolutions[1].size();
addressRet = unk;
addressRet = WitnessUnknown{vSolutions[0][0], vSolutions[1]};
return true;
}
case TxoutType::MULTISIG:
case TxoutType::NULL_DATA:
case TxoutType::NONSTANDARD:
addressRet = CNoDestination(scriptPubKey);
return false;
} // no default case, so the compiler can warn about missing cases
assert(false);
Expand All @@ -108,7 +106,12 @@ class CScriptVisitor
public:
CScript operator()(const CNoDestination& dest) const
{
return CScript();
return dest.GetScript();
}

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

CScript operator()(const PKHash& keyID) const
Expand Down Expand Up @@ -138,9 +141,22 @@ class CScriptVisitor

CScript operator()(const WitnessUnknown& id) const
{
return CScript() << CScript::EncodeOP_N(id.version) << std::vector<unsigned char>(id.program, id.program + id.length);
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 @@ -149,5 +165,5 @@ CScript GetScriptForDestination(const CTxDestination& dest)
}

bool IsValidDestination(const CTxDestination& dest) {
return dest.index() != 0;
return std::visit(ValidDestinationVisitor(), dest);
}
83 changes: 57 additions & 26 deletions src/addresstype.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,30 @@
#include <algorithm>

class CNoDestination {
private:
CScript m_script;

public:
CNoDestination() = default;
CNoDestination(const CScript& script) : m_script(script) {}

const CScript& GetScript() const { return m_script; }

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

struct PubKeyDestination {
private:
CPubKey m_pubkey;

public:
friend bool operator==(const CNoDestination &a, const CNoDestination &b) { return true; }
friend bool operator<(const CNoDestination &a, const CNoDestination &b) { return true; }
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>
Expand Down Expand Up @@ -69,45 +90,55 @@ struct WitnessV1Taproot : public XOnlyPubKey
//! CTxDestination subtype to encode any future Witness version
struct WitnessUnknown
{
unsigned int version;
unsigned int length;
unsigned char program[40];
private:
unsigned int m_version;
std::vector<unsigned char> m_program;

public:
WitnessUnknown(unsigned int version, const std::vector<unsigned char>& program) : m_version(version), m_program(program) {}
WitnessUnknown(int version, const std::vector<unsigned char>& program) : m_version(static_cast<unsigned int>(version)), m_program(program) {}

unsigned int GetWitnessVersion() const { return m_version; }
const std::vector<unsigned char>& GetWitnessProgram() const LIFETIMEBOUND { return m_program; }

friend bool operator==(const WitnessUnknown& w1, const WitnessUnknown& w2) {
if (w1.version != w2.version) return false;
if (w1.length != w2.length) return false;
return std::equal(w1.program, w1.program + w1.length, w2.program);
if (w1.GetWitnessVersion() != w2.GetWitnessVersion()) return false;
return w1.GetWitnessProgram() == w2.GetWitnessProgram();
}

friend bool operator<(const WitnessUnknown& w1, const WitnessUnknown& w2) {
if (w1.version < w2.version) return true;
if (w1.version > w2.version) return false;
if (w1.length < w2.length) return true;
if (w1.length > w2.length) return false;
return std::lexicographical_compare(w1.program, w1.program + w1.length, w2.program, w2.program + w2.length);
if (w1.GetWitnessVersion() < w2.GetWitnessVersion()) return true;
if (w1.GetWitnessVersion() > w2.GetWitnessVersion()) return false;
return w1.GetWitnessProgram() < w2.GetWitnessProgram();
}
};

/**
* A txout script template with a specific destination. It is either:
* * CNoDestination: no destination set
* * PKHash: TxoutType::PUBKEYHASH destination (P2PKH)
* * ScriptHash: TxoutType::SCRIPTHASH destination (P2SH)
* * WitnessV0ScriptHash: TxoutType::WITNESS_V0_SCRIPTHASH destination (P2WSH)
* * WitnessV0KeyHash: TxoutType::WITNESS_V0_KEYHASH destination (P2WPKH)
* * WitnessV1Taproot: TxoutType::WITNESS_V1_TAPROOT destination (P2TR)
* * WitnessUnknown: TxoutType::WITNESS_UNKNOWN destination (P2W???)
* 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)
* * WitnessV0KeyHash: TxoutType::WITNESS_V0_KEYHASH destination (P2WPKH address)
* * WitnessV1Taproot: TxoutType::WITNESS_V1_TAPROOT destination (P2TR address)
* * 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);

/**
* Parse a standard scriptPubKey for the destination address. Assigns result to
* the addressRet parameter and returns true if successful. Currently only works for P2PK,
* P2PKH, P2SH, P2WPKH, and P2WSH scripts.
* Parse a scriptPubKey for the destination.
*
* For standard scripts that have addresses (and P2PK as an exception), a corresponding CTxDestination
* is assigned to addressRet.
* For all other scripts. addressRet is assigned as a CNoDestination containing the scriptPubKey.
*
* 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
16 changes: 7 additions & 9 deletions src/key_io.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,18 @@ class DestinationEncoder

std::string operator()(const WitnessUnknown& id) const
{
if (id.version < 1 || id.version > 16 || id.length < 2 || id.length > 40) {
const std::vector<unsigned char>& program = id.GetWitnessProgram();
if (id.GetWitnessVersion() < 1 || id.GetWitnessVersion() > 16 || program.size() < 2 || program.size() > 40) {
return {};
}
std::vector<unsigned char> data = {(unsigned char)id.version};
data.reserve(1 + (id.length * 8 + 4) / 5);
ConvertBits<8, 5, true>([&](unsigned char c) { data.push_back(c); }, id.program, id.program + id.length);
std::vector<unsigned char> data = {(unsigned char)id.GetWitnessVersion()};
data.reserve(1 + (program.size() * 8 + 4) / 5);
ConvertBits<8, 5, true>([&](unsigned char c) { data.push_back(c); }, program.begin(), program.end());
return bech32::Encode(bech32::Encoding::BECH32M, m_params.Bech32HRP(), data);
}

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 Expand Up @@ -189,11 +191,7 @@ CTxDestination DecodeDestination(const std::string& str, const CChainParams& par
return CNoDestination();
}

WitnessUnknown unk;
unk.version = version;
std::copy(data.begin(), data.end(), unk.program);
unk.length = data.size();
return unk;
return WitnessUnknown{version, data};
} else {
error_str = strprintf("Invalid padding in Bech32 data section");
return CNoDestination();
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
9 changes: 7 additions & 2 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 Expand Up @@ -303,8 +308,8 @@ class DescribeAddressVisitor
{
UniValue obj(UniValue::VOBJ);
obj.pushKV("iswitness", true);
obj.pushKV("witness_version", (int)id.version);
obj.pushKV("witness_program", HexStr({id.program, id.length}));
obj.pushKV("witness_version", id.GetWitnessVersion());
obj.pushKV("witness_program", HexStr(id.GetWitnessProgram()));
return obj;
}
};
Expand Down
2 changes: 1 addition & 1 deletion src/test/fuzz/key.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ FUZZ_TARGET(key, .init = initialize_key)
const CTxDestination tx_destination = GetDestinationForKey(pubkey, output_type);
assert(output_type == OutputType::LEGACY);
assert(IsValidDestination(tx_destination));
assert(CTxDestination{PKHash{pubkey}} == tx_destination);
assert(PKHash{pubkey} == *std::get_if<PKHash>(&tx_destination));

const CScript script_for_destination = GetScriptForDestination(tx_destination);
assert(script_for_destination.size() == 25);
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
21 changes: 13 additions & 8 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 All @@ -188,15 +197,11 @@ CTxDestination ConsumeTxDestination(FuzzedDataProvider& fuzzed_data_provider) no
tx_destination = WitnessV1Taproot{XOnlyPubKey{ConsumeUInt256(fuzzed_data_provider)}};
},
[&] {
WitnessUnknown witness_unknown{};
witness_unknown.version = fuzzed_data_provider.ConsumeIntegralInRange(2, 16);
std::vector<uint8_t> witness_unknown_program_1{fuzzed_data_provider.ConsumeBytes<uint8_t>(40)};
if (witness_unknown_program_1.size() < 2) {
witness_unknown_program_1 = {0, 0};
std::vector<unsigned char> program{ConsumeRandomLengthByteVector(fuzzed_data_provider, /*max_length=*/40)};
if (program.size() < 2) {
program = {0, 0};
}
witness_unknown.length = witness_unknown_program_1.size();
std::copy(witness_unknown_program_1.begin(), witness_unknown_program_1.end(), witness_unknown.program);
tx_destination = witness_unknown;
tx_destination = WitnessUnknown{fuzzed_data_provider.ConsumeIntegralInRange<unsigned int>(2, 16), program};
})};
Assert(call_size == std::variant_size_v<CTxDestination>);
return tx_destination;
Expand Down
9 changes: 3 additions & 6 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 Expand Up @@ -249,10 +249,7 @@ BOOST_AUTO_TEST_CASE(script_standard_ExtractDestination)
s.clear();
s << OP_1 << ToByteVector(pubkey);
BOOST_CHECK(ExtractDestination(s, address));
WitnessUnknown unk;
unk.length = 33;
unk.version = 1;
std::copy(pubkey.begin(), pubkey.end(), unk.program);
WitnessUnknown unk{1, ToByteVector(pubkey)};
BOOST_CHECK(std::get<WitnessUnknown>(address) == unk);
}

Expand Down
2 changes: 1 addition & 1 deletion src/util/message.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ MessageVerificationResult MessageVerify(
return MessageVerificationResult::ERR_PUBKEY_NOT_RECOVERED;
}

if (!(CTxDestination(PKHash(pubkey)) == destination)) {
if (!(PKHash(pubkey) == *std::get_if<PKHash>(&destination))) {
return MessageVerificationResult::ERR_NOT_SIGNED;
}

Expand Down
10 changes: 5 additions & 5 deletions src/wallet/feebumper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -257,12 +257,12 @@ Result CreateRateBumpTransaction(CWallet& wallet, const uint256& txid, const CCo
const auto& txouts = outputs.empty() ? wtx.tx->vout : outputs;
for (size_t i = 0; i < txouts.size(); ++i) {
const CTxOut& output = txouts.at(i);
CTxDestination dest;
ExtractDestination(output.scriptPubKey, dest);
if (reduce_output.has_value() ? reduce_output.value() == i : OutputIsChange(wallet, output)) {
CTxDestination change_dest;
ExtractDestination(output.scriptPubKey, change_dest);
new_coin_control.destChange = change_dest;
new_coin_control.destChange = dest;
} else {
CRecipient recipient = {output.scriptPubKey, output.nValue, false};
CRecipient recipient = {dest, output.nValue, false};
recipients.push_back(recipient);
}
new_outputs_value += output.nValue;
Expand All @@ -278,7 +278,7 @@ Result CreateRateBumpTransaction(CWallet& wallet, const uint256& txid, const CCo

// Add change as recipient with SFFO flag enabled, so fees are deduced from it.
// If the output differs from the original tx output (because the user customized it) a new change output will be created.
recipients.emplace_back(CRecipient{GetScriptForDestination(new_coin_control.destChange), new_outputs_value, /*fSubtractFeeFromAmount=*/true});
recipients.emplace_back(CRecipient{new_coin_control.destChange, new_outputs_value, /*fSubtractFeeFromAmount=*/true});
new_coin_control.destChange = CNoDestination();
}

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

0 comments on commit 53313c4

Please sign in to comment.