Browse Source

SegWit wallet support

This introduces two command line flags (-addresstype and -changetype) which control
the type of addresses/outputs created by the GUI and RPCs. Certain RPCs allow
overriding these (`getnewaddress` and `getrawchangeaddress`). Supported types
are "legacy" (P2PKH and P2SH-multisig), "p2sh-segwit" (P2SH-P2WPKH and P2SH-P2WSH-multisig),
and "bech32" (P2WPKH and P2WSH-multisig).

A few utility functions are added to the wallet to construct different address type
and to add the necessary entries to the wallet file to be compatible with earlier
versions (see `CWallet::LearnRelatedScripts`, `GetDestinationForKey`,
`GetAllDestinationsForKey`, `CWallet::AddAndGetDestinationForScript`).
0.16
Pieter Wuille 7 years ago
parent
commit
940a21932b
  1. 3
      src/qt/addresstablemodel.cpp
  2. 25
      src/qt/paymentserver.cpp
  3. 5
      src/qt/test/wallettests.cpp
  4. 12
      src/wallet/init.cpp
  5. 12
      src/wallet/rpcdump.cpp
  6. 50
      src/wallet/rpcwallet.cpp
  7. 2
      src/wallet/test/wallet_test_fixture.cpp
  8. 117
      src/wallet/wallet.cpp
  9. 45
      src/wallet/wallet.h
  10. 2
      test/functional/bip68-112-113-p2p.py
  11. 3
      test/functional/bip68-sequence.py
  12. 3
      test/functional/bumpfee.py
  13. 2
      test/functional/import-rescan.py
  14. 1
      test/functional/importmulti.py
  15. 2
      test/functional/nulldummy.py
  16. 5
      test/functional/rawtransactions.py
  17. 10
      test/functional/segwit.py
  18. 1
      test/functional/signmessages.py
  19. 1
      test/functional/test_runner.py
  20. 15
      test/functional/txn_clone.py
  21. 2
      test/functional/wallet-dump.py
  22. 11
      test/functional/wallet.py

3
src/qt/addresstablemodel.cpp

@ -384,7 +384,8 @@ QString AddressTableModel::addRow(const QString &type, const QString &label, con
return QString(); return QString();
} }
} }
strAddress = EncodeDestination(newKey.GetID()); wallet->LearnRelatedScripts(newKey, g_address_type);
strAddress = EncodeDestination(GetDestinationForKey(newKey, g_address_type));
} }
else else
{ {

25
src/qt/paymentserver.cpp

@ -636,28 +636,25 @@ void PaymentServer::fetchPaymentACK(CWallet* wallet, const SendCoinsRecipient& r
// Create a new refund address, or re-use: // Create a new refund address, or re-use:
QString account = tr("Refund from %1").arg(recipient.authenticatedMerchant); QString account = tr("Refund from %1").arg(recipient.authenticatedMerchant);
std::string strAccount = account.toStdString(); std::string strAccount = account.toStdString();
std::set<CTxDestination> refundAddresses = wallet->GetAccountAddresses(strAccount);
if (!refundAddresses.empty()) {
CScript s = GetScriptForDestination(*refundAddresses.begin());
payments::Output* refund_to = payment.add_refund_to();
refund_to->set_script(&s[0], s.size());
}
else {
CPubKey newKey; CPubKey newKey;
if (wallet->GetKeyFromPool(newKey)) { if (wallet->GetKeyFromPool(newKey)) {
CKeyID keyID = newKey.GetID(); // BIP70 requests encode the scriptPubKey directly, so we are not restricted to address
wallet->SetAddressBook(keyID, strAccount, "refund"); // types supported by the receiver. As a result, we choose the address format we also
// use for change. Despite an actual payment and not change, this is a close match:
CScript s = GetScriptForDestination(keyID); // it's the output type we use subject to privacy issues, but not restricted by what
// other software supports.
wallet->LearnRelatedScripts(newKey, g_change_type);
CTxDestination dest = GetDestinationForKey(newKey, g_change_type);
wallet->SetAddressBook(dest, strAccount, "refund");
CScript s = GetScriptForDestination(dest);
payments::Output* refund_to = payment.add_refund_to(); payments::Output* refund_to = payment.add_refund_to();
refund_to->set_script(&s[0], s.size()); refund_to->set_script(&s[0], s.size());
} } else {
else {
// This should never happen, because sending coins should have // This should never happen, because sending coins should have
// just unlocked the wallet and refilled the keypool. // just unlocked the wallet and refilled the keypool.
qWarning() << "PaymentServer::fetchPaymentACK: Error getting refund key, refund_to not set"; qWarning() << "PaymentServer::fetchPaymentACK: Error getting refund key, refund_to not set";
} }
}
int length = payment.ByteSize(); int length = payment.ByteSize();
netRequest.setHeader(QNetworkRequest::ContentLengthHeader, length); netRequest.setHeader(QNetworkRequest::ContentLengthHeader, length);

5
src/qt/test/wallettests.cpp

@ -149,6 +149,9 @@ void BumpFee(TransactionView& view, const uint256& txid, bool expectDisabled, st
// src/qt/test/test_bitcoin-qt -platform cocoa # macOS // src/qt/test/test_bitcoin-qt -platform cocoa # macOS
void TestGUI() void TestGUI()
{ {
g_address_type = OUTPUT_TYPE_P2SH_SEGWIT;
g_change_type = OUTPUT_TYPE_P2SH_SEGWIT;
// Set up wallet and chain with 105 blocks (5 mature blocks for spending). // Set up wallet and chain with 105 blocks (5 mature blocks for spending).
TestChain100Setup test; TestChain100Setup test;
for (int i = 0; i < 5; ++i) { for (int i = 0; i < 5; ++i) {
@ -161,7 +164,7 @@ void TestGUI()
wallet.LoadWallet(firstRun); wallet.LoadWallet(firstRun);
{ {
LOCK(wallet.cs_wallet); LOCK(wallet.cs_wallet);
wallet.SetAddressBook(test.coinbaseKey.GetPubKey().GetID(), "", "receive"); wallet.SetAddressBook(GetDestinationForKey(test.coinbaseKey.GetPubKey(), g_address_type), "", "receive");
wallet.AddKeyPubKey(test.coinbaseKey, test.coinbaseKey.GetPubKey()); wallet.AddKeyPubKey(test.coinbaseKey, test.coinbaseKey.GetPubKey());
} }
{ {

12
src/wallet/init.cpp

@ -16,6 +16,8 @@
std::string GetWalletHelpString(bool showDebug) std::string GetWalletHelpString(bool showDebug)
{ {
std::string strUsage = HelpMessageGroup(_("Wallet options:")); std::string strUsage = HelpMessageGroup(_("Wallet options:"));
strUsage += HelpMessageOpt("-addresstype", strprintf(_("What type of addresses to use (\"legacy\", \"p2sh-segwit\", or \"bech32\", default: \"%s\")"), FormatOutputType(OUTPUT_TYPE_DEFAULT)));
strUsage += HelpMessageOpt("-changetype", _("What type of change to use (\"legacy\", \"p2sh-segwit\", or \"bech32\", default is same as -addresstype)"));
strUsage += HelpMessageOpt("-disablewallet", _("Do not load the wallet and disable wallet RPC calls")); strUsage += HelpMessageOpt("-disablewallet", _("Do not load the wallet and disable wallet RPC calls"));
strUsage += HelpMessageOpt("-keypool=<n>", strprintf(_("Set key pool size to <n> (default: %u)"), DEFAULT_KEYPOOL_SIZE)); strUsage += HelpMessageOpt("-keypool=<n>", strprintf(_("Set key pool size to <n> (default: %u)"), DEFAULT_KEYPOOL_SIZE));
strUsage += HelpMessageOpt("-fallbackfee=<amt>", strprintf(_("A fee rate (in %s/kB) that will be used when fee estimation has insufficient data (default: %s)"), strUsage += HelpMessageOpt("-fallbackfee=<amt>", strprintf(_("A fee rate (in %s/kB) that will be used when fee estimation has insufficient data (default: %s)"),
@ -175,6 +177,16 @@ bool WalletParameterInteraction()
bSpendZeroConfChange = gArgs.GetBoolArg("-spendzeroconfchange", DEFAULT_SPEND_ZEROCONF_CHANGE); bSpendZeroConfChange = gArgs.GetBoolArg("-spendzeroconfchange", DEFAULT_SPEND_ZEROCONF_CHANGE);
fWalletRbf = gArgs.GetBoolArg("-walletrbf", DEFAULT_WALLET_RBF); fWalletRbf = gArgs.GetBoolArg("-walletrbf", DEFAULT_WALLET_RBF);
g_address_type = ParseOutputType(gArgs.GetArg("-addresstype", ""));
if (g_address_type == OUTPUT_TYPE_NONE) {
return InitError(strprintf(_("Unknown address type '%s'"), gArgs.GetArg("-addresstype", "")));
}
g_change_type = ParseOutputType(gArgs.GetArg("-changetype", ""), g_address_type);
if (g_change_type == OUTPUT_TYPE_NONE) {
return InitError(strprintf(_("Unknown change type '%s'"), gArgs.GetArg("-changetype", "")));
}
return true; return true;
} }

12
src/wallet/rpcdump.cpp

@ -131,7 +131,11 @@ UniValue importprivkey(const JSONRPCRequest& request)
CKeyID vchAddress = pubkey.GetID(); CKeyID vchAddress = pubkey.GetID();
{ {
pwallet->MarkDirty(); pwallet->MarkDirty();
pwallet->SetAddressBook(vchAddress, strLabel, "receive");
// We don't know which corresponding address will be used; label them all
for (const auto& dest : GetAllDestinationsForKey(pubkey)) {
pwallet->SetAddressBook(dest, strLabel, "receive");
}
// Don't throw error in case a key is already there // Don't throw error in case a key is already there
if (pwallet->HaveKey(vchAddress)) { if (pwallet->HaveKey(vchAddress)) {
@ -143,6 +147,7 @@ UniValue importprivkey(const JSONRPCRequest& request)
if (!pwallet->AddKeyPubKey(key, pubkey)) { if (!pwallet->AddKeyPubKey(key, pubkey)) {
throw JSONRPCError(RPC_WALLET_ERROR, "Error adding key to wallet"); throw JSONRPCError(RPC_WALLET_ERROR, "Error adding key to wallet");
} }
pwallet->LearnAllRelatedScripts(pubkey);
// whenever a key is imported, we need to scan the whole chain // whenever a key is imported, we need to scan the whole chain
pwallet->UpdateTimeFirstKey(1); pwallet->UpdateTimeFirstKey(1);
@ -433,8 +438,11 @@ UniValue importpubkey(const JSONRPCRequest& request)
LOCK2(cs_main, pwallet->cs_wallet); LOCK2(cs_main, pwallet->cs_wallet);
ImportAddress(pwallet, pubKey.GetID(), strLabel); for (const auto& dest : GetAllDestinationsForKey(pubKey)) {
ImportAddress(pwallet, dest, strLabel);
}
ImportScript(pwallet, GetScriptForRawPubKey(pubKey), strLabel, false); ImportScript(pwallet, GetScriptForRawPubKey(pubKey), strLabel, false);
pwallet->LearnAllRelatedScripts(pubKey);
if (fRescan) if (fRescan)
{ {

50
src/wallet/rpcwallet.cpp

@ -136,14 +136,15 @@ UniValue getnewaddress(const JSONRPCRequest& request)
return NullUniValue; return NullUniValue;
} }
if (request.fHelp || request.params.size() > 1) if (request.fHelp || request.params.size() > 2)
throw std::runtime_error( throw std::runtime_error(
"getnewaddress ( \"account\" )\n" "getnewaddress ( \"account\" \"address_type\" )\n"
"\nReturns a new Bitcoin address for receiving payments.\n" "\nReturns a new Bitcoin address for receiving payments.\n"
"If 'account' is specified (DEPRECATED), it is added to the address book \n" "If 'account' is specified (DEPRECATED), it is added to the address book \n"
"so payments received with the address will be credited to 'account'.\n" "so payments received with the address will be credited to 'account'.\n"
"\nArguments:\n" "\nArguments:\n"
"1. \"account\" (string, optional) DEPRECATED. The account name for the address to be linked to. If not provided, the default account \"\" is used. It can also be set to the empty string \"\" to represent the default account. The account does not need to exist, it will be created if there is no account by the given name.\n" "1. \"account\" (string, optional) DEPRECATED. The account name for the address to be linked to. If not provided, the default account \"\" is used. It can also be set to the empty string \"\" to represent the default account. The account does not need to exist, it will be created if there is no account by the given name.\n"
"2. \"address_type\" (string, optional) The address type to use. Options are \"legacy\", \"p2sh\", and \"bech32\". Default is set by -addresstype.\n"
"\nResult:\n" "\nResult:\n"
"\"address\" (string) The new bitcoin address\n" "\"address\" (string) The new bitcoin address\n"
"\nExamples:\n" "\nExamples:\n"
@ -158,6 +159,14 @@ UniValue getnewaddress(const JSONRPCRequest& request)
if (!request.params[0].isNull()) if (!request.params[0].isNull())
strAccount = AccountFromValue(request.params[0]); strAccount = AccountFromValue(request.params[0]);
OutputType output_type = g_address_type;
if (!request.params[1].isNull()) {
output_type = ParseOutputType(request.params[1].get_str(), g_address_type);
if (output_type == OUTPUT_TYPE_NONE) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Unknown address type '%s'", request.params[1].get_str()));
}
}
if (!pwallet->IsLocked()) { if (!pwallet->IsLocked()) {
pwallet->TopUpKeyPool(); pwallet->TopUpKeyPool();
} }
@ -167,11 +176,12 @@ UniValue getnewaddress(const JSONRPCRequest& request)
if (!pwallet->GetKeyFromPool(newKey)) { if (!pwallet->GetKeyFromPool(newKey)) {
throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, "Error: Keypool ran out, please call keypoolrefill first"); throw JSONRPCError(RPC_WALLET_KEYPOOL_RAN_OUT, "Error: Keypool ran out, please call keypoolrefill first");
} }
CKeyID keyID = newKey.GetID(); pwallet->LearnRelatedScripts(newKey, output_type);
CTxDestination dest = GetDestinationForKey(newKey, output_type);
pwallet->SetAddressBook(keyID, strAccount, "receive"); pwallet->SetAddressBook(dest, strAccount, "receive");
return EncodeDestination(keyID); return EncodeDestination(dest);
} }
@ -226,11 +236,13 @@ UniValue getrawchangeaddress(const JSONRPCRequest& request)
return NullUniValue; return NullUniValue;
} }
if (request.fHelp || request.params.size() > 0) if (request.fHelp || request.params.size() > 1)
throw std::runtime_error( throw std::runtime_error(
"getrawchangeaddress\n" "getrawchangeaddress ( \"address_type\" )\n"
"\nReturns a new Bitcoin address, for receiving change.\n" "\nReturns a new Bitcoin address, for receiving change.\n"
"This is for use with raw transactions, NOT normal use.\n" "This is for use with raw transactions, NOT normal use.\n"
"\nArguments:\n"
"1. \"address_type\" (string, optional) The address type to use. Options are \"legacy\", \"p2sh\", and \"bech32\". Default is set by -changetype.\n"
"\nResult:\n" "\nResult:\n"
"\"address\" (string) The address\n" "\"address\" (string) The address\n"
"\nExamples:\n" "\nExamples:\n"
@ -244,6 +256,14 @@ UniValue getrawchangeaddress(const JSONRPCRequest& request)
pwallet->TopUpKeyPool(); pwallet->TopUpKeyPool();
} }
OutputType output_type = g_change_type;
if (!request.params[0].isNull()) {
output_type = ParseOutputType(request.params[0].get_str(), g_change_type);
if (output_type == OUTPUT_TYPE_NONE) {
throw JSONRPCError(RPC_INVALID_ADDRESS_OR_KEY, strprintf("Unknown address type '%s'", request.params[0].get_str()));
}
}
CReserveKey reservekey(pwallet); CReserveKey reservekey(pwallet);
CPubKey vchPubKey; CPubKey vchPubKey;
if (!reservekey.GetReservedKey(vchPubKey, true)) if (!reservekey.GetReservedKey(vchPubKey, true))
@ -251,9 +271,10 @@ UniValue getrawchangeaddress(const JSONRPCRequest& request)
reservekey.KeepKey(); reservekey.KeepKey();
CKeyID keyID = vchPubKey.GetID(); pwallet->LearnRelatedScripts(vchPubKey, output_type);
CTxDestination dest = GetDestinationForKey(vchPubKey, output_type);
return EncodeDestination(keyID); return EncodeDestination(dest);
} }
@ -1184,11 +1205,12 @@ UniValue addmultisigaddress(const JSONRPCRequest& request)
// Construct using pay-to-script-hash: // Construct using pay-to-script-hash:
CScript inner = _createmultisig_redeemScript(pwallet, request.params); CScript inner = _createmultisig_redeemScript(pwallet, request.params);
CScriptID innerID(inner);
pwallet->AddCScript(inner); pwallet->AddCScript(inner);
pwallet->SetAddressBook(innerID, strAccount, "send"); CTxDestination dest = pwallet->AddAndGetDestinationForScript(inner, g_address_type);
return EncodeDestination(innerID);
pwallet->SetAddressBook(dest, strAccount, "send");
return EncodeDestination(dest);
} }
class Witnessifier : public boost::static_visitor<bool> class Witnessifier : public boost::static_visitor<bool>
@ -3446,8 +3468,8 @@ static const CRPCCommand commands[] =
{ "wallet", "getaccount", &getaccount, {"address"} }, { "wallet", "getaccount", &getaccount, {"address"} },
{ "wallet", "getaddressesbyaccount", &getaddressesbyaccount, {"account"} }, { "wallet", "getaddressesbyaccount", &getaddressesbyaccount, {"account"} },
{ "wallet", "getbalance", &getbalance, {"account","minconf","include_watchonly"} }, { "wallet", "getbalance", &getbalance, {"account","minconf","include_watchonly"} },
{ "wallet", "getnewaddress", &getnewaddress, {"account"} }, { "wallet", "getnewaddress", &getnewaddress, {"account","address_type"} },
{ "wallet", "getrawchangeaddress", &getrawchangeaddress, {} }, { "wallet", "getrawchangeaddress", &getrawchangeaddress, {"address_type"} },
{ "wallet", "getreceivedbyaccount", &getreceivedbyaccount, {"account","minconf"} }, { "wallet", "getreceivedbyaccount", &getreceivedbyaccount, {"account","minconf"} },
{ "wallet", "getreceivedbyaddress", &getreceivedbyaddress, {"address","minconf"} }, { "wallet", "getreceivedbyaddress", &getreceivedbyaddress, {"address","minconf"} },
{ "wallet", "gettransaction", &gettransaction, {"txid","include_watchonly"} }, { "wallet", "gettransaction", &gettransaction, {"txid","include_watchonly"} },

2
src/wallet/test/wallet_test_fixture.cpp

@ -13,6 +13,8 @@ WalletTestingSetup::WalletTestingSetup(const std::string& chainName):
bitdb.MakeMock(); bitdb.MakeMock();
bool fFirstRun; bool fFirstRun;
g_address_type = OUTPUT_TYPE_DEFAULT;
g_change_type = OUTPUT_TYPE_DEFAULT;
std::unique_ptr<CWalletDBWrapper> dbw(new CWalletDBWrapper(&bitdb, "wallet_test.dat")); std::unique_ptr<CWalletDBWrapper> dbw(new CWalletDBWrapper(&bitdb, "wallet_test.dat"));
pwalletMain = MakeUnique<CWallet>(std::move(dbw)); pwalletMain = MakeUnique<CWallet>(std::move(dbw));
pwalletMain->LoadWallet(fFirstRun); pwalletMain->LoadWallet(fFirstRun);

117
src/wallet/wallet.cpp

@ -42,6 +42,8 @@ CFeeRate payTxFee(DEFAULT_TRANSACTION_FEE);
unsigned int nTxConfirmTarget = DEFAULT_TX_CONFIRM_TARGET; unsigned int nTxConfirmTarget = DEFAULT_TX_CONFIRM_TARGET;
bool bSpendZeroConfChange = DEFAULT_SPEND_ZEROCONF_CHANGE; bool bSpendZeroConfChange = DEFAULT_SPEND_ZEROCONF_CHANGE;
bool fWalletRbf = DEFAULT_WALLET_RBF; bool fWalletRbf = DEFAULT_WALLET_RBF;
OutputType g_address_type = OUTPUT_TYPE_NONE;
OutputType g_change_type = OUTPUT_TYPE_NONE;
const char * DEFAULT_WALLET_DAT = "wallet.dat"; const char * DEFAULT_WALLET_DAT = "wallet.dat";
const uint32_t BIP32_HARDENED_KEY_LIMIT = 0x80000000; const uint32_t BIP32_HARDENED_KEY_LIMIT = 0x80000000;
@ -832,8 +834,8 @@ bool CWallet::GetAccountDestination(CTxDestination &dest, std::string strAccount
if (!account.vchPubKey.IsValid()) if (!account.vchPubKey.IsValid())
bForceNew = true; bForceNew = true;
else { else {
// Check if the current key has been used // Check if the current key has been used (TODO: check other addresses with the same key)
CScript scriptPubKey = GetScriptForDestination(account.vchPubKey.GetID()); CScript scriptPubKey = GetScriptForDestination(GetDestinationForKey(account.vchPubKey, g_address_type));
for (std::map<uint256, CWalletTx>::iterator it = mapWallet.begin(); for (std::map<uint256, CWalletTx>::iterator it = mapWallet.begin();
it != mapWallet.end() && account.vchPubKey.IsValid(); it != mapWallet.end() && account.vchPubKey.IsValid();
++it) ++it)
@ -850,11 +852,12 @@ bool CWallet::GetAccountDestination(CTxDestination &dest, std::string strAccount
if (!GetKeyFromPool(account.vchPubKey, false)) if (!GetKeyFromPool(account.vchPubKey, false))
return false; return false;
dest = account.vchPubKey.GetID(); LearnRelatedScripts(account.vchPubKey, g_address_type);
dest = GetDestinationForKey(account.vchPubKey, g_address_type);
SetAddressBook(dest, strAccount, "receive"); SetAddressBook(dest, strAccount, "receive");
walletdb.WriteAccount(strAccount, account); walletdb.WriteAccount(strAccount, account);
} else { } else {
dest = account.vchPubKey.GetID(); dest = GetDestinationForKey(account.vchPubKey, g_address_type);
} }
return true; return true;
@ -2743,7 +2746,8 @@ bool CWallet::CreateTransaction(const std::vector<CRecipient>& vecSend, CWalletT
return false; return false;
} }
scriptChange = GetScriptForDestination(vchPubKey.GetID()); LearnRelatedScripts(vchPubKey, g_change_type);
scriptChange = GetScriptForDestination(GetDestinationForKey(vchPubKey, g_change_type));
} }
CTxOut change_prototype_txout(0, scriptChange); CTxOut change_prototype_txout(0, scriptChange);
size_t change_prototype_size = GetSerializeSize(change_prototype_txout, SER_DISK, 0); size_t change_prototype_size = GetSerializeSize(change_prototype_txout, SER_DISK, 0);
@ -4136,3 +4140,106 @@ bool CWalletTx::AcceptToMemoryPool(const CAmount& nAbsurdFee, CValidationState&
fInMempool = ret; fInMempool = ret;
return ret; return ret;
} }
static const std::string OUTPUT_TYPE_STRING_LEGACY = "legacy";
static const std::string OUTPUT_TYPE_STRING_P2SH_SEGWIT = "p2sh-segwit";
static const std::string OUTPUT_TYPE_STRING_BECH32 = "bech32";
OutputType ParseOutputType(const std::string& type, OutputType default_type)
{
if (type.empty()) {
return default_type;
} else if (type == OUTPUT_TYPE_STRING_LEGACY) {
return OUTPUT_TYPE_LEGACY;
} else if (type == OUTPUT_TYPE_STRING_P2SH_SEGWIT) {
return OUTPUT_TYPE_P2SH_SEGWIT;
} else if (type == OUTPUT_TYPE_STRING_BECH32) {
return OUTPUT_TYPE_BECH32;
} else {
return OUTPUT_TYPE_NONE;
}
}
const std::string& FormatOutputType(OutputType type)
{
switch (type) {
case OUTPUT_TYPE_LEGACY: return OUTPUT_TYPE_STRING_LEGACY;
case OUTPUT_TYPE_P2SH_SEGWIT: return OUTPUT_TYPE_STRING_P2SH_SEGWIT;
case OUTPUT_TYPE_BECH32: return OUTPUT_TYPE_STRING_BECH32;
default: assert(false);
}
}
void CWallet::LearnRelatedScripts(const CPubKey& key, OutputType type)
{
if (key.IsCompressed() && (type == OUTPUT_TYPE_P2SH_SEGWIT || type == OUTPUT_TYPE_BECH32)) {
CTxDestination witdest = WitnessV0KeyHash(key.GetID());
CScript witprog = GetScriptForDestination(witdest);
// Make sure the resulting program is solvable.
assert(IsSolvable(*this, witprog));
AddCScript(witprog);
}
}
void CWallet::LearnAllRelatedScripts(const CPubKey& key)
{
// OUTPUT_TYPE_P2SH_SEGWIT always adds all necessary scripts for all types.
LearnRelatedScripts(key, OUTPUT_TYPE_P2SH_SEGWIT);
}
CTxDestination GetDestinationForKey(const CPubKey& key, OutputType type)
{
switch (type) {
case OUTPUT_TYPE_LEGACY: return key.GetID();
case OUTPUT_TYPE_P2SH_SEGWIT:
case OUTPUT_TYPE_BECH32: {
if (!key.IsCompressed()) return key.GetID();
CTxDestination witdest = WitnessV0KeyHash(key.GetID());
CScript witprog = GetScriptForDestination(witdest);
if (type == OUTPUT_TYPE_P2SH_SEGWIT) {
return CScriptID(witprog);
} else {
return witdest;
}
}
default: assert(false);
}
}
std::vector<CTxDestination> GetAllDestinationsForKey(const CPubKey& key)
{
CKeyID keyid = key.GetID();
if (key.IsCompressed()) {
CTxDestination segwit = WitnessV0KeyHash(keyid);
CTxDestination p2sh = CScriptID(GetScriptForDestination(segwit));
return std::vector<CTxDestination>{std::move(keyid), std::move(p2sh), std::move(segwit)};
} else {
return std::vector<CTxDestination>{std::move(keyid)};
}
}
CTxDestination CWallet::AddAndGetDestinationForScript(const CScript& script, OutputType type)
{
// Note that scripts over 520 bytes are not yet supported.
switch (type) {
case OUTPUT_TYPE_LEGACY:
return CScriptID(script);
case OUTPUT_TYPE_P2SH_SEGWIT:
case OUTPUT_TYPE_BECH32: {
WitnessV0ScriptHash hash;
CSHA256().Write(script.data(), script.size()).Finalize(hash.begin());
CTxDestination witdest = hash;
CScript witprog = GetScriptForDestination(witdest);
// Check if the resulting program is solvable (i.e. doesn't use an uncompressed key)
if (!IsSolvable(*this, witprog)) return CScriptID(script);
// Add the redeemscript, so that P2WSH and P2SH-P2WSH outputs are recognized as ours.
AddCScript(witprog);
if (type == OUTPUT_TYPE_BECH32) {
return witdest;
} else {
return CScriptID(witprog);
}
}
default: assert(false);
}
}

45
src/wallet/wallet.h

@ -99,6 +99,19 @@ enum WalletFeature
FEATURE_LATEST = FEATURE_COMPRPUBKEY // HD is optional, use FEATURE_COMPRPUBKEY as latest version FEATURE_LATEST = FEATURE_COMPRPUBKEY // HD is optional, use FEATURE_COMPRPUBKEY as latest version
}; };
enum OutputType
{
OUTPUT_TYPE_NONE,
OUTPUT_TYPE_LEGACY,
OUTPUT_TYPE_P2SH_SEGWIT,
OUTPUT_TYPE_BECH32,
OUTPUT_TYPE_DEFAULT = OUTPUT_TYPE_P2SH_SEGWIT
};
extern OutputType g_address_type;
extern OutputType g_change_type;
/** A key pool entry */ /** A key pool entry */
class CKeyPool class CKeyPool
@ -1129,6 +1142,26 @@ public:
* deadlock * deadlock
*/ */
void BlockUntilSyncedToCurrentChain(); void BlockUntilSyncedToCurrentChain();
/**
* Explicitly make the wallet learn the related scripts for outputs to the
* given key. This is purely to make the wallet file compatible with older
* software, as CBasicKeyStore automatically does this implicitly for all
* keys now.
*/
void LearnRelatedScripts(const CPubKey& key, OutputType);
/**
* Same as LearnRelatedScripts, but when the OutputType is not known (and could
* be anything).
*/
void LearnAllRelatedScripts(const CPubKey& key);
/**
* Get a destination of the requested type (if possible) to the specified script.
* This function will automatically add the necessary scripts to the wallet.
*/
CTxDestination AddAndGetDestinationForScript(const CScript& script, OutputType);
}; };
/** A key allocated from the key pool. */ /** A key allocated from the key pool. */
@ -1218,4 +1251,16 @@ bool CWallet::DummySignTx(CMutableTransaction &txNew, const ContainerType &coins
return true; return true;
} }
OutputType ParseOutputType(const std::string& str, OutputType default_type = OUTPUT_TYPE_DEFAULT);
const std::string& FormatOutputType(OutputType type);
/**
* Get a destination of the requested type (if possible) to the specified key.
* The caller must make sure LearnRelatedScripts has been called beforehand.
*/
CTxDestination GetDestinationForKey(const CPubKey& key, OutputType);
/** Get all destinations (potentially) supported by the wallet for the given key. */
std::vector<CTxDestination> GetAllDestinationsForKey(const CPubKey& key);
#endif // BITCOIN_WALLET_WALLET_H #endif // BITCOIN_WALLET_WALLET_H

2
test/functional/bip68-112-113-p2p.py

@ -95,7 +95,7 @@ class BIP68_112_113Test(ComparisonTestFramework):
def set_test_params(self): def set_test_params(self):
self.num_nodes = 1 self.num_nodes = 1
self.setup_clean_chain = True self.setup_clean_chain = True
self.extra_args = [['-whitelist=127.0.0.1', '-blockversion=4']] self.extra_args = [['-whitelist=127.0.0.1', '-blockversion=4', '-addresstype=legacy']]
def run_test(self): def run_test(self):
test = TestManager(self, self.options.tmpdir) test = TestManager(self, self.options.tmpdir)

3
test/functional/bip68-sequence.py

@ -362,9 +362,10 @@ class BIP68Test(BitcoinTestFramework):
block.vtx.extend([tx1, tx2, tx3]) block.vtx.extend([tx1, tx2, tx3])
block.hashMerkleRoot = block.calc_merkle_root() block.hashMerkleRoot = block.calc_merkle_root()
block.rehash() block.rehash()
add_witness_commitment(block)
block.solve() block.solve()
self.nodes[0].submitblock(ToHex(block)) self.nodes[0].submitblock(bytes_to_hex_str(block.serialize(True)))
assert_equal(self.nodes[0].getbestblockhash(), block.hash) assert_equal(self.nodes[0].getbestblockhash(), block.hash)
def activateCSV(self): def activateCSV(self):

3
test/functional/bumpfee.py

@ -194,7 +194,7 @@ def test_settxfee(rbf_node, dest_address):
requested_feerate = Decimal("0.00025000") requested_feerate = Decimal("0.00025000")
rbf_node.settxfee(requested_feerate) rbf_node.settxfee(requested_feerate)
bumped_tx = rbf_node.bumpfee(rbfid) bumped_tx = rbf_node.bumpfee(rbfid)
actual_feerate = bumped_tx["fee"] * 1000 / rbf_node.getrawtransaction(bumped_tx["txid"], True)["size"] actual_feerate = bumped_tx["fee"] * 1000 / rbf_node.getrawtransaction(bumped_tx["txid"], True)["vsize"]
# Assert that the difference between the requested feerate and the actual # Assert that the difference between the requested feerate and the actual
# feerate of the bumped transaction is small. # feerate of the bumped transaction is small.
assert_greater_than(Decimal("0.00001000"), abs(requested_feerate - actual_feerate)) assert_greater_than(Decimal("0.00001000"), abs(requested_feerate - actual_feerate))
@ -290,6 +290,7 @@ def submit_block_with_tx(node, tx):
block.vtx.append(ctx) block.vtx.append(ctx)
block.rehash() block.rehash()
block.hashMerkleRoot = block.calc_merkle_root() block.hashMerkleRoot = block.calc_merkle_root()
blocktools.add_witness_commitment(block)
block.solve() block.solve()
node.submitblock(bytes_to_hex_str(block.serialize(True))) node.submitblock(bytes_to_hex_str(block.serialize(True)))
return block return block

2
test/functional/import-rescan.py

@ -119,7 +119,7 @@ class ImportRescanTest(BitcoinTestFramework):
self.num_nodes = 2 + len(IMPORT_NODES) self.num_nodes = 2 + len(IMPORT_NODES)
def setup_network(self): def setup_network(self):
extra_args = [[] for _ in range(self.num_nodes)] extra_args = [["-addresstype=legacy"] for _ in range(self.num_nodes)]
for i, import_node in enumerate(IMPORT_NODES, 2): for i, import_node in enumerate(IMPORT_NODES, 2):
if import_node.prune: if import_node.prune:
extra_args[i] += ["-prune=1"] extra_args[i] += ["-prune=1"]

1
test/functional/importmulti.py

@ -9,6 +9,7 @@ from test_framework.util import *
class ImportMultiTest (BitcoinTestFramework): class ImportMultiTest (BitcoinTestFramework):
def set_test_params(self): def set_test_params(self):
self.num_nodes = 2 self.num_nodes = 2
self.extra_args = [["-addresstype=legacy"], ["-addresstype=legacy"]]
self.setup_clean_chain = True self.setup_clean_chain = True
def setup_network(self): def setup_network(self):

2
test/functional/nulldummy.py

@ -42,7 +42,7 @@ class NULLDUMMYTest(BitcoinTestFramework):
self.setup_clean_chain = True self.setup_clean_chain = True
# This script tests NULLDUMMY activation, which is part of the 'segwit' deployment, so we go through # This script tests NULLDUMMY activation, which is part of the 'segwit' deployment, so we go through
# normal segwit activation here (and don't use the default always-on behaviour). # normal segwit activation here (and don't use the default always-on behaviour).
self.extra_args = [['-whitelist=127.0.0.1', '-walletprematurewitness', '-vbparams=segwit:0:999999999999']] self.extra_args = [['-whitelist=127.0.0.1', '-walletprematurewitness', '-vbparams=segwit:0:999999999999', '-addresstype=legacy']]
def run_test(self): def run_test(self):
self.address = self.nodes[0].getnewaddress() self.address = self.nodes[0].getnewaddress()

5
test/functional/rawtransactions.py

@ -20,6 +20,7 @@ class RawTransactionsTest(BitcoinTestFramework):
def set_test_params(self): def set_test_params(self):
self.setup_clean_chain = True self.setup_clean_chain = True
self.num_nodes = 3 self.num_nodes = 3
self.extra_args = [["-addresstype=legacy"], ["-addresstype=legacy"], ["-addresstype=legacy"]]
def setup_network(self, split=False): def setup_network(self, split=False):
super().setup_network() super().setup_network()
@ -135,7 +136,7 @@ class RawTransactionsTest(BitcoinTestFramework):
break break
bal = self.nodes[0].getbalance() bal = self.nodes[0].getbalance()
inputs = [{ "txid" : txId, "vout" : vout['n'], "scriptPubKey" : vout['scriptPubKey']['hex']}] inputs = [{ "txid" : txId, "vout" : vout['n'], "scriptPubKey" : vout['scriptPubKey']['hex'], "amount" : vout['value']}]
outputs = { self.nodes[0].getnewaddress() : 2.19 } outputs = { self.nodes[0].getnewaddress() : 2.19 }
rawTx = self.nodes[2].createrawtransaction(inputs, outputs) rawTx = self.nodes[2].createrawtransaction(inputs, outputs)
rawTxPartialSigned = self.nodes[1].signrawtransaction(rawTx, inputs) rawTxPartialSigned = self.nodes[1].signrawtransaction(rawTx, inputs)
@ -180,7 +181,7 @@ class RawTransactionsTest(BitcoinTestFramework):
break break
bal = self.nodes[0].getbalance() bal = self.nodes[0].getbalance()
inputs = [{ "txid" : txId, "vout" : vout['n'], "scriptPubKey" : vout['scriptPubKey']['hex'], "redeemScript" : mSigObjValid['hex']}] inputs = [{ "txid" : txId, "vout" : vout['n'], "scriptPubKey" : vout['scriptPubKey']['hex'], "redeemScript" : mSigObjValid['hex'], "amount" : vout['value']}]
outputs = { self.nodes[0].getnewaddress() : 2.19 } outputs = { self.nodes[0].getnewaddress() : 2.19 }
rawTx2 = self.nodes[2].createrawtransaction(inputs, outputs) rawTx2 = self.nodes[2].createrawtransaction(inputs, outputs)
rawTxPartialSigned1 = self.nodes[1].signrawtransaction(rawTx2, inputs) rawTxPartialSigned1 = self.nodes[1].signrawtransaction(rawTx2, inputs)

10
test/functional/segwit.py

@ -78,9 +78,9 @@ class SegWitTest(BitcoinTestFramework):
self.setup_clean_chain = True self.setup_clean_chain = True
self.num_nodes = 3 self.num_nodes = 3
# This test tests SegWit both pre and post-activation, so use the normal BIP9 activation. # This test tests SegWit both pre and post-activation, so use the normal BIP9 activation.
self.extra_args = [["-walletprematurewitness", "-rpcserialversion=0", "-vbparams=segwit:0:999999999999"], self.extra_args = [["-walletprematurewitness", "-rpcserialversion=0", "-vbparams=segwit:0:999999999999", "-addresstype=legacy"],
["-blockversion=4", "-promiscuousmempoolflags=517", "-prematurewitness", "-walletprematurewitness", "-rpcserialversion=1", "-vbparams=segwit:0:999999999999"], ["-blockversion=4", "-promiscuousmempoolflags=517", "-prematurewitness", "-walletprematurewitness", "-rpcserialversion=1", "-vbparams=segwit:0:999999999999", "-addresstype=legacy"],
["-blockversion=536870915", "-promiscuousmempoolflags=517", "-prematurewitness", "-walletprematurewitness", "-vbparams=segwit:0:999999999999"]] ["-blockversion=536870915", "-promiscuousmempoolflags=517", "-prematurewitness", "-walletprematurewitness", "-vbparams=segwit:0:999999999999", "-addresstype=legacy"]]
def setup_network(self): def setup_network(self):
super().setup_network() super().setup_network()
@ -135,9 +135,9 @@ class SegWitTest(BitcoinTestFramework):
self.pubkey.append(self.nodes[i].validateaddress(newaddress)["pubkey"]) self.pubkey.append(self.nodes[i].validateaddress(newaddress)["pubkey"])
multiaddress = self.nodes[i].addmultisigaddress(1, [self.pubkey[-1]]) multiaddress = self.nodes[i].addmultisigaddress(1, [self.pubkey[-1]])
multiscript = CScript([OP_1, hex_str_to_bytes(self.pubkey[-1]), OP_1, OP_CHECKMULTISIG]) multiscript = CScript([OP_1, hex_str_to_bytes(self.pubkey[-1]), OP_1, OP_CHECKMULTISIG])
p2sh_addr = self.nodes[i].addwitnessaddress(newaddress, True) p2sh_addr = self.nodes[i].addwitnessaddress(newaddress)
bip173_addr = self.nodes[i].addwitnessaddress(newaddress, False) bip173_addr = self.nodes[i].addwitnessaddress(newaddress, False)
p2sh_ms_addr = self.nodes[i].addwitnessaddress(multiaddress, True) p2sh_ms_addr = self.nodes[i].addwitnessaddress(multiaddress)
bip173_ms_addr = self.nodes[i].addwitnessaddress(multiaddress, False) bip173_ms_addr = self.nodes[i].addwitnessaddress(multiaddress, False)
assert_equal(p2sh_addr, key_to_p2sh_p2wpkh(self.pubkey[-1])) assert_equal(p2sh_addr, key_to_p2sh_p2wpkh(self.pubkey[-1]))
assert_equal(bip173_addr, key_to_p2wpkh(self.pubkey[-1])) assert_equal(bip173_addr, key_to_p2wpkh(self.pubkey[-1]))

1
test/functional/signmessages.py

@ -11,6 +11,7 @@ class SignMessagesTest(BitcoinTestFramework):
def set_test_params(self): def set_test_params(self):
self.setup_clean_chain = True self.setup_clean_chain = True
self.num_nodes = 1 self.num_nodes = 1
self.extra_args = [["-addresstype=legacy"]]
def run_test(self): def run_test(self):
message = 'This is just a test message' message = 'This is just a test message'

1
test/functional/test_runner.py

@ -86,6 +86,7 @@ BASE_SCRIPTS= [
'mempool_resurrect_test.py', 'mempool_resurrect_test.py',
'txn_doublespend.py --mineblock', 'txn_doublespend.py --mineblock',
'txn_clone.py', 'txn_clone.py',
'txn_clone.py --segwit',
'getchaintips.py', 'getchaintips.py',
'rest.py', 'rest.py',
'mempool_spendcoinbase.py', 'mempool_spendcoinbase.py',

15
test/functional/txn_clone.py

@ -14,6 +14,8 @@ class TxnMallTest(BitcoinTestFramework):
def add_options(self, parser): def add_options(self, parser):
parser.add_option("--mineblock", dest="mine_block", default=False, action="store_true", parser.add_option("--mineblock", dest="mine_block", default=False, action="store_true",
help="Test double-spend of 1-confirmed transaction") help="Test double-spend of 1-confirmed transaction")
parser.add_option("--segwit", dest="segwit", default=False, action="store_true",
help="Test behaviour with SegWit txn (which should fail")
def setup_network(self): def setup_network(self):
# Start with split network: # Start with split network:
@ -22,6 +24,11 @@ class TxnMallTest(BitcoinTestFramework):
disconnect_nodes(self.nodes[2], 1) disconnect_nodes(self.nodes[2], 1)
def run_test(self): def run_test(self):
if self.options.segwit:
output_type="p2sh-segwit"
else:
output_type="legacy"
# All nodes should start with 1,250 BTC: # All nodes should start with 1,250 BTC:
starting_balance = 1250 starting_balance = 1250
for i in range(4): for i in range(4):
@ -31,11 +38,11 @@ class TxnMallTest(BitcoinTestFramework):
# Assign coins to foo and bar accounts: # Assign coins to foo and bar accounts:
self.nodes[0].settxfee(.001) self.nodes[0].settxfee(.001)
node0_address_foo = self.nodes[0].getnewaddress("foo") node0_address_foo = self.nodes[0].getnewaddress("foo", output_type)
fund_foo_txid = self.nodes[0].sendfrom("", node0_address_foo, 1219) fund_foo_txid = self.nodes[0].sendfrom("", node0_address_foo, 1219)
fund_foo_tx = self.nodes[0].gettransaction(fund_foo_txid) fund_foo_tx = self.nodes[0].gettransaction(fund_foo_txid)
node0_address_bar = self.nodes[0].getnewaddress("bar") node0_address_bar = self.nodes[0].getnewaddress("bar", output_type)
fund_bar_txid = self.nodes[0].sendfrom("", node0_address_bar, 29) fund_bar_txid = self.nodes[0].sendfrom("", node0_address_bar, 29)
fund_bar_tx = self.nodes[0].gettransaction(fund_bar_txid) fund_bar_tx = self.nodes[0].gettransaction(fund_bar_txid)
@ -106,6 +113,10 @@ class TxnMallTest(BitcoinTestFramework):
# Send clone and its parent to miner # Send clone and its parent to miner
self.nodes[2].sendrawtransaction(fund_foo_tx["hex"]) self.nodes[2].sendrawtransaction(fund_foo_tx["hex"])
txid1_clone = self.nodes[2].sendrawtransaction(tx1_clone["hex"]) txid1_clone = self.nodes[2].sendrawtransaction(tx1_clone["hex"])
if self.options.segwit:
assert_equal(txid1, txid1_clone)
return
# ... mine a block... # ... mine a block...
self.nodes[2].generate(1) self.nodes[2].generate(1)

2
test/functional/wallet-dump.py

@ -58,7 +58,7 @@ def read_dump(file_name, addrs, hd_master_addr_old):
class WalletDumpTest(BitcoinTestFramework): class WalletDumpTest(BitcoinTestFramework):
def set_test_params(self): def set_test_params(self):
self.num_nodes = 1 self.num_nodes = 1
self.extra_args = [["-keypool=90"]] self.extra_args = [["-keypool=90", "-addresstype=legacy"]]
def setup_network(self, split=False): def setup_network(self, split=False):
# Use 1 minute timeout because the initial getnewaddress RPC can take # Use 1 minute timeout because the initial getnewaddress RPC can take

11
test/functional/wallet.py

@ -27,6 +27,9 @@ class WalletTest(BitcoinTestFramework):
assert_fee_amount(fee, tx_size, fee_per_byte * 1000) assert_fee_amount(fee, tx_size, fee_per_byte * 1000)
return curr_balance return curr_balance
def get_vsize(self, txn):
return self.nodes[0].decoderawtransaction(txn)['vsize']
def run_test(self): def run_test(self):
# Check that there's no UTXO on none of the nodes # Check that there's no UTXO on none of the nodes
assert_equal(len(self.nodes[0].listunspent()), 0) assert_equal(len(self.nodes[0].listunspent()), 0)
@ -162,7 +165,7 @@ class WalletTest(BitcoinTestFramework):
txid = self.nodes[2].sendtoaddress(address, 10, "", "", False) txid = self.nodes[2].sendtoaddress(address, 10, "", "", False)
self.nodes[2].generate(1) self.nodes[2].generate(1)
self.sync_all([self.nodes[0:3]]) self.sync_all([self.nodes[0:3]])
node_2_bal = self.check_fee_amount(self.nodes[2].getbalance(), Decimal('84'), fee_per_byte, count_bytes(self.nodes[2].getrawtransaction(txid))) node_2_bal = self.check_fee_amount(self.nodes[2].getbalance(), Decimal('84'), fee_per_byte, self.get_vsize(self.nodes[2].getrawtransaction(txid)))
assert_equal(self.nodes[0].getbalance(), Decimal('10')) assert_equal(self.nodes[0].getbalance(), Decimal('10'))
# Send 10 BTC with subtract fee from amount # Send 10 BTC with subtract fee from amount
@ -171,14 +174,14 @@ class WalletTest(BitcoinTestFramework):
self.sync_all([self.nodes[0:3]]) self.sync_all([self.nodes[0:3]])
node_2_bal -= Decimal('10') node_2_bal -= Decimal('10')
assert_equal(self.nodes[2].getbalance(), node_2_bal) assert_equal(self.nodes[2].getbalance(), node_2_bal)
node_0_bal = self.check_fee_amount(self.nodes[0].getbalance(), Decimal('20'), fee_per_byte, count_bytes(self.nodes[2].getrawtransaction(txid))) node_0_bal = self.check_fee_amount(self.nodes[0].getbalance(), Decimal('20'), fee_per_byte, self.get_vsize(self.nodes[2].getrawtransaction(txid)))
# Sendmany 10 BTC # Sendmany 10 BTC
txid = self.nodes[2].sendmany('from1', {address: 10}, 0, "", []) txid = self.nodes[2].sendmany('from1', {address: 10}, 0, "", [])
self.nodes[2].generate(1) self.nodes[2].generate(1)
self.sync_all([self.nodes[0:3]]) self.sync_all([self.nodes[0:3]])
node_0_bal += Decimal('10') node_0_bal += Decimal('10')
node_2_bal = self.check_fee_amount(self.nodes[2].getbalance(), node_2_bal - Decimal('10'), fee_per_byte, count_bytes(self.nodes[2].getrawtransaction(txid))) node_2_bal = self.check_fee_amount(self.nodes[2].getbalance(), node_2_bal - Decimal('10'), fee_per_byte, self.get_vsize(self.nodes[2].getrawtransaction(txid)))
assert_equal(self.nodes[0].getbalance(), node_0_bal) assert_equal(self.nodes[0].getbalance(), node_0_bal)
# Sendmany 10 BTC with subtract fee from amount # Sendmany 10 BTC with subtract fee from amount
@ -187,7 +190,7 @@ class WalletTest(BitcoinTestFramework):
self.sync_all([self.nodes[0:3]]) self.sync_all([self.nodes[0:3]])
node_2_bal -= Decimal('10') node_2_bal -= Decimal('10')
assert_equal(self.nodes[2].getbalance(), node_2_bal) assert_equal(self.nodes[2].getbalance(), node_2_bal)
node_0_bal = self.check_fee_amount(self.nodes[0].getbalance(), node_0_bal + Decimal('10'), fee_per_byte, count_bytes(self.nodes[2].getrawtransaction(txid))) node_0_bal = self.check_fee_amount(self.nodes[0].getbalance(), node_0_bal + Decimal('10'), fee_per_byte, self.get_vsize(self.nodes[2].getrawtransaction(txid)))
# Test ResendWalletTransactions: # Test ResendWalletTransactions:
# Create a couple of transactions, then start up a fourth # Create a couple of transactions, then start up a fourth

Loading…
Cancel
Save